using DocumentService.Application.AddAnnotation; using DocumentService.Application.AddStamp; using DocumentService.Application.MergePdfs; using DocumentService.Domain.Common.Exceptions; using DocumentService.Domain.Models.ValueObjects; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DocumentService.API.Controllers; /// /// Controller for PDF operations (merge, stamp, annotate). /// [ApiController] [Route("api/pdf/operations")] public class PdfOperationsController(IMediator mediator) : ControllerBase { /// /// Merges multiple PDF files into a single PDF. /// Supports multipart/form-data file upload. /// /// PDF files to merge (minimum 2 required) /// Cancellation token /// Merged PDF file /// PDFs merged successfully - returns merged PDF /// Invalid input (fewer than 2 files, corrupted PDF) /// Internal server error during PDF processing [HttpPost("merge", Name = "MergeFromFiles")] [Consumes("multipart/form-data")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task MergeFromFiles( [FromForm] List files, CancellationToken cancellationToken) { // Convert IFormFile[] to Stream[] (use OpenReadStream directly - no buffering) var streams = files.Select(f => f.OpenReadStream()).ToList(); // Send command to MediatR (no page ranges for now - multipart binding is complex) var command = new MergePdfsCommand { PdfStreams = streams, PageRanges = null }; byte[] mergedPdf = await mediator.Send(command, cancellationToken); // Return merged PDF return File(mergedPdf, "application/pdf", "merged.pdf"); } /// /// Merges multiple PDF files into a single PDF. /// Supports Base64-encoded PDFs via JSON payload. /// /// Request containing Base64-encoded PDFs and optional page ranges /// Cancellation token /// Merged PDF file /// PDFs merged successfully - returns merged PDF /// Invalid input (Base64 format error, fewer than 2 files, corrupted PDF, invalid page range) /// Internal server error during PDF processing [HttpPost("merge", Name = "MergeFromBase64")] [Consumes("application/json")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task MergeFromBase64( [FromBody] MergePdfsBase64Request request, CancellationToken cancellationToken) { // Convert Base64[] to MemoryStream[] List streams = []; try { foreach (var base64Pdf in request.Base64Pdfs) { byte[] pdfBytes = Convert.FromBase64String(base64Pdf); streams.Add(new MemoryStream(pdfBytes)); } } catch (FormatException ex) { // Dispose opened streams on error foreach (var stream in streams) stream.Dispose(); throw new BadRequestException("Invalid Base64 format: " + ex.Message); } var command = new MergePdfsCommand { PdfStreams = streams, PageRanges = request.PageRanges }; byte[] mergedPdf = await mediator.Send(command, cancellationToken); // Cleanup streams (important for MemoryStreams we created) foreach (var stream in streams) stream.Dispose(); return File(mergedPdf, "application/pdf", "merged.pdf"); } /// /// Adds an annotation to a PDF document. /// Supports multipart/form-data file upload. /// /// Multipart form data containing PDF file and annotation parameters /// Cancellation token /// Annotated PDF file /// Annotation added successfully - returns annotated PDF /// Invalid input (invalid page number, missing required parameters) /// Internal server error during PDF processing [HttpPost("annotate", Name = "AnnotateFromFile")] [Consumes("multipart/form-data")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task AnnotateFromFile( [FromForm] AddAnnotationMultipartRequest request, CancellationToken cancellationToken) { // Calculate X2, Y2 from Width/Height if provided double x2 = request.X2 ?? request.X1 + (request.Width ?? throw new BadRequestException("Either X2 or Width must be provided")); double y2 = request.Y2 ?? request.Y1 + (request.Height ?? throw new BadRequestException("Either Y2 or Height must be provided")); var command = new AddAnnotationCommand { PdfStream = request.File.OpenReadStream(), AnnotationType = request.AnnotationType, PageNumber = request.PageNumber, Rectangle = (request.X1, request.Y1, x2, y2), Content = request.Content, Author = request.Author, Color = request.Color, TextMarkupStyle = request.TextMarkupStyle, Origin = request.Origin }; byte[] annotatedPdf = await mediator.Send(command, cancellationToken); return File(annotatedPdf, "application/pdf", "annotated.pdf"); } /// /// Adds an annotation to a PDF document. /// Supports Base64-encoded PDF via JSON payload. /// /// Command containing all annotation parameters (including Base64 PDF) /// Cancellation token /// Annotated PDF file /// Annotation added successfully - returns annotated PDF /// Invalid input (Base64 format error, invalid page number, missing required parameters) /// Internal server error during PDF processing [HttpPost("annotate", Name = "AnnotateFromBase64")] [Consumes("application/json")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task AnnotateFromBase64( [FromBody] AddAnnotationBase64Command command, CancellationToken cancellationToken) { // Convert Base64 to MemoryStream Stream pdfStream; try { byte[] pdfBytes = Convert.FromBase64String(command.Base64Pdf); pdfStream = new MemoryStream(pdfBytes); } catch (FormatException ex) { throw new BadRequestException("Invalid Base64 format: " + ex.Message); } // Calculate X2, Y2 from Width/Height if provided double x2 = command.X2 ?? command.X1 + (command.Width ?? throw new BadRequestException("Either X2 or Width must be provided")); double y2 = command.Y2 ?? command.Y1 + (command.Height ?? throw new BadRequestException("Either Y2 or Height must be provided")); var annotationCommand = new AddAnnotationCommand { PdfStream = pdfStream, AnnotationType = command.AnnotationType, PageNumber = command.PageNumber, Rectangle = (command.X1, command.Y1, x2, y2), Content = command.Content, Author = command.Author, Color = command.Color, TextMarkupStyle = command.TextMarkupStyle, Origin = command.Origin }; byte[] annotatedPdf = await mediator.Send(annotationCommand, cancellationToken); // Cleanup stream pdfStream.Dispose(); return File(annotatedPdf, "application/pdf", "annotated.pdf"); } /// /// Adds a stamp (text, image, or predefined) to PDF pages. /// Supports multipart/form-data file upload. /// /// Multipart form data containing PDF file and stamp parameters /// Cancellation token /// Stamped PDF file /// Stamp added successfully - returns stamped PDF /// Invalid input (invalid page number, missing required parameters, invalid image format) /// Internal server error during PDF processing [HttpPost("stamp", Name = "AddStampFromFile")] [Consumes("multipart/form-data")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task AddStampFromFile( [FromForm] AddStampMultipartRequest request, CancellationToken cancellationToken) { // Convert ImageFile to byte[] if provided byte[]? imageBytes = null; if (request.ImageFile != null) { using var ms = new MemoryStream(); await request.ImageFile.CopyToAsync(ms, cancellationToken); imageBytes = ms.ToArray(); } var command = new AddStampCommand { PdfStream = request.File.OpenReadStream(), StampType = request.StampType, PageNumbers = request.PageNumbers, Position = (request.X, request.Y), Size = request.Width.HasValue && request.Height.HasValue ? (request.Width.Value, request.Height.Value) : null, Origin = request.Origin, Text = request.Text, FontName = request.FontName, FontSize = request.FontSize, Color = request.Color, Opacity = request.Opacity, Rotation = request.Rotation, Placement = request.Placement, ImageBytes = imageBytes, PredefinedType = request.PredefinedType }; byte[] stampedPdf = await mediator.Send(command, cancellationToken); return File(stampedPdf, "application/pdf", "stamped.pdf"); } /// /// Adds a stamp (text, image, or predefined) to PDF pages. /// Supports Base64-encoded PDF and image via JSON payload. /// /// Request containing Base64-encoded PDF, stamp parameters, and optional Base64 image /// Cancellation token /// Stamped PDF file /// Stamp added successfully - returns stamped PDF /// Invalid input (Base64 format error, invalid page number, missing required parameters) /// Internal server error during PDF processing [HttpPost("stamp", Name = "AddStampFromBase64")] [Consumes("application/json")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task AddStampFromBase64( [FromBody] AddStampBase64Request request, CancellationToken cancellationToken) { // Convert Base64 PDF to MemoryStream Stream pdfStream; try { byte[] pdfBytes = Convert.FromBase64String(request.Base64Pdf); pdfStream = new MemoryStream(pdfBytes); } catch (FormatException ex) { throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message); } // Convert Base64 image to byte[] if provided byte[]? imageBytes = null; if (!string.IsNullOrWhiteSpace(request.Base64Image)) { try { imageBytes = Convert.FromBase64String(request.Base64Image); } catch (FormatException ex) { pdfStream.Dispose(); throw new BadRequestException("Invalid Base64 image format: " + ex.Message); } } var command = new AddStampCommand { PdfStream = pdfStream, StampType = request.StampType, PageNumbers = request.PageNumbers, Position = (request.X, request.Y), Size = request.Width.HasValue && request.Height.HasValue ? (request.Width.Value, request.Height.Value) : null, Origin = request.Origin, Text = request.Text, FontName = request.FontName, FontSize = request.FontSize, Color = request.Color, Opacity = request.Opacity, Rotation = request.Rotation, Placement = request.Placement, ImageBytes = imageBytes, PredefinedType = request.PredefinedType }; byte[] stampedPdf = await mediator.Send(command, cancellationToken); // Cleanup stream pdfStream.Dispose(); return File(stampedPdf, "application/pdf", "stamped.pdf"); } } /// /// Request DTO for multipart/form-data annotation operation /// public class AddAnnotationMultipartRequest { /// /// PDF file to annotate /// public required IFormFile File { get; set; } /// /// Type of annotation (TextMarkup, FreeText, StickyNote, Circle, Square) /// public required AnnotationType AnnotationType { get; set; } /// /// Target page number (1-indexed) /// public required int PageNumber { get; set; } /// /// Rectangle X1 coordinate (left) /// public required double X1 { get; set; } /// /// Rectangle Y1 coordinate (top or bottom depending on Origin) /// public required double Y1 { get; set; } /// /// Rectangle X2 coordinate (right). Optional if Width is provided. /// public double? X2 { get; set; } /// /// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided. /// public double? Y2 { get; set; } /// /// Rectangle width. Alternative to X2 (X2 = X1 + Width). Optional if X2 is provided. /// public double? Width { get; set; } /// /// Rectangle height. Alternative to Y2 (Y2 = Y1 + Height). Optional if Y2 is provided. /// public double? Height { get; set; } /// /// Annotation content (required for FreeText/StickyNote) /// public string? Content { get; set; } /// /// Author name (optional) /// public string? Author { get; set; } /// /// Hex color (6 digits, e.g., "FF0000" for red) /// public string? Color { get; set; } /// /// Markup style (Highlight/Underline/Strikeout, required for TextMarkup) /// public TextMarkupStyle? TextMarkupStyle { get; set; } /// /// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft /// public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft; } /// /// Request DTO for Base64-encoded PDF merge operation (API layer only - converts to MergePdfsCommand) /// public record MergePdfsBase64Request { /// /// Array of Base64-encoded PDF files (minimum 2 required) /// /// ["JVBERi0xLjQK...", "JVBERi0xLjQK..."] public required List Base64Pdfs { get; init; } /// /// Optional page ranges per PDF (null = all pages). /// Format: "1-3,5" means pages 1, 2, 3, and 5. /// If provided, array length must match Base64Pdfs length. /// /// ["1-2", "1,3,5", null] public List? PageRanges { get; init; } } /// /// Request DTO for Base64-encoded PDF annotation (API layer only - converts to AddAnnotationCommand) /// public record AddAnnotationBase64Command { /// /// Base64-encoded PDF file /// /// "JVBERi0xLjQK..." public required string Base64Pdf { get; init; } /// /// Type of annotation to add /// /// TextMarkup public required AnnotationType AnnotationType { get; init; } /// /// Target page number (1-indexed) /// /// 1 public required int PageNumber { get; init; } /// /// Rectangle X1 coordinate (left) /// /// 100.0 public required double X1 { get; init; } /// /// Rectangle Y1 coordinate (top or bottom depending on Origin) /// /// 100.0 public required double Y1 { get; init; } /// /// Rectangle X2 coordinate (right). Optional if Width is provided. /// /// 200.0 public double? X2 { get; init; } /// /// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided. /// /// 120.0 public double? Y2 { get; init; } /// /// Rectangle width. Alternative to X2 (X2 = X1 + Width). Optional if X2 is provided. /// /// 100.0 public double? Width { get; init; } /// /// Rectangle height. Alternative to Y2 (Y2 = Y1 + Height). Optional if Y2 is provided. /// /// 20.0 public double? Height { get; init; } /// /// Annotation content (required for FreeText and StickyNote) /// /// "Important text to highlight" public string? Content { get; init; } /// /// Author name (optional) /// /// "John Doe" public string? Author { get; init; } /// /// Hex color (6 digits, e.g., "FF0000" for red). Optional - defaults vary by annotation type. /// /// "FFFF00" public string? Color { get; init; } /// /// Text markup style (Highlight, Underline, or Strikeout). Required for TextMarkup annotations. /// /// Highlight public TextMarkupStyle? TextMarkupStyle { get; init; } /// /// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft /// /// BottomLeft public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft; } /// /// Request DTO for multipart/form-data stamp operation /// public class AddStampMultipartRequest { /// /// PDF file to stamp /// public required IFormFile File { get; set; } /// /// Type of stamp (Text, Image, or Predefined) /// public required StampType StampType { get; set; } /// /// Target page numbers (1-indexed). Null or empty = all pages. /// /// [1, 3, 5] public int[]? PageNumbers { get; set; } /// /// Stamp position X coordinate /// /// 100.0 public required double X { get; set; } /// /// Stamp position Y coordinate /// /// 100.0 public required double Y { get; set; } /// /// Stamp width (optional, auto-size for images if not specified) /// /// 200.0 public double? Width { get; set; } /// /// Stamp height (optional, auto-size for images if not specified) /// /// 50.0 public double? Height { get; set; } /// /// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft /// /// BottomLeft public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft; /// /// Text content (required for Text stamps) /// /// "CONFIDENTIAL" public string? Text { get; set; } /// /// Font name (default: Arial) /// /// "Arial" public string? FontName { get; set; } /// /// Font size in points (default: 12) /// /// 24.0 public double? FontSize { get; set; } /// /// Hex color (6 digits, e.g., "FF0000" for red, default: "000000") /// /// "FF0000" public string? Color { get; set; } /// /// Opacity (0.0 = transparent, 1.0 = opaque, default: 0.5) /// /// 0.5 public double? Opacity { get; set; } /// /// Rotation angle in degrees (0-360, default: 0) /// /// 45.0 public double? Rotation { get; set; } /// /// Stamp placement (Foreground = on top, Background = watermark effect, default: Foreground) /// /// Foreground public StampPlacement Placement { get; set; } = StampPlacement.Foreground; /// /// Image file (required for Image stamps, PNG/JPEG) /// public IFormFile? ImageFile { get; set; } /// /// Predefined stamp type (required for Predefined stamps) /// /// Confidential public PredefinedStampType? PredefinedType { get; set; } } /// /// Request DTO for Base64-encoded PDF stamp operation (API layer only - converts to AddStampCommand) /// public record AddStampBase64Request { /// /// Base64-encoded PDF file /// /// "JVBERi0xLjQK..." public required string Base64Pdf { get; init; } /// /// Type of stamp (Text, Image, or Predefined) /// /// Text public required StampType StampType { get; init; } /// /// Target page numbers (1-indexed). Null or empty = all pages. /// /// [1, 3, 5] public int[]? PageNumbers { get; init; } /// /// Stamp position X coordinate /// /// 100.0 public required double X { get; init; } /// /// Stamp position Y coordinate /// /// 100.0 public required double Y { get; init; } /// /// Stamp width (optional, auto-size for images if not specified) /// /// 200.0 public double? Width { get; init; } /// /// Stamp height (optional, auto-size for images if not specified) /// /// 50.0 public double? Height { get; init; } /// /// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft /// /// BottomLeft public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft; /// /// Text content (required for Text stamps) /// /// "CONFIDENTIAL" public string? Text { get; init; } /// /// Font name (default: Arial) /// /// "Arial" public string? FontName { get; init; } /// /// Font size in points (default: 12) /// /// 24.0 public double? FontSize { get; init; } /// /// Hex color (6 digits, e.g., "FF0000" for red, default: "000000") /// /// "FF0000" public string? Color { get; init; } /// /// Opacity (0.0 = transparent, 1.0 = opaque, default: 0.5) /// /// 0.5 public double? Opacity { get; init; } /// /// Rotation angle in degrees (0-360, default: 0) /// /// 45.0 public double? Rotation { get; init; } /// /// Stamp placement (Foreground = on top, Background = watermark effect, default: Foreground) /// /// Foreground public StampPlacement Placement { get; init; } = StampPlacement.Foreground; /// /// Base64-encoded image (required for Image stamps, PNG/JPEG) /// /// "iVBORw0KGgoAAAANSUhEUgAA..." public string? Base64Image { get; init; } /// /// Predefined stamp type (required for Predefined stamps) /// /// Confidential public PredefinedStampType? PredefinedType { get; init; } }