using DocumentOperator.Application.AddAnnotation; using DocumentOperator.Application.MergePdfs; using DocumentOperator.Domain.Common.Exceptions; using DocumentOperator.Domain.Models.ValueObjects; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DocumentOperator.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) { var command = new AddAnnotationCommand { PdfStream = request.File.OpenReadStream(), AnnotationType = request.AnnotationType, PageNumber = request.PageNumber, Rectangle = (request.X1, request.Y1, request.X2, request.Y2), Content = request.Content, Author = request.Author, Color = request.Color, TextMarkupStyle = request.TextMarkupStyle }; 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); } var annotationCommand = new AddAnnotationCommand { PdfStream = pdfStream, AnnotationType = command.AnnotationType, PageNumber = command.PageNumber, Rectangle = (command.X1, command.Y1, command.X2, command.Y2), Content = command.Content, Author = command.Author, Color = command.Color, TextMarkupStyle = command.TextMarkupStyle }; byte[] annotatedPdf = await mediator.Send(annotationCommand, cancellationToken); // Cleanup stream pdfStream.Dispose(); return File(annotatedPdf, "application/pdf", "annotated.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 /// public required double X1 { get; set; } /// /// Rectangle Y1 coordinate /// public required double Y1 { get; set; } /// /// Rectangle X2 coordinate /// public required double X2 { get; set; } /// /// Rectangle Y2 coordinate /// public required double Y2 { 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; } } /// /// 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 /// /// 100.0 public required double X1 { get; init; } /// /// Rectangle Y1 coordinate /// /// 100.0 public required double Y1 { get; init; } /// /// Rectangle X2 coordinate /// /// 200.0 public required double X2 { get; init; } /// /// Rectangle Y2 coordinate /// /// 120.0 public required double Y2 { 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; } }