From d4107f6f89c63600f3665fc3d92ecdce047106d7 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 21 Jul 2026 12:28:43 +0200 Subject: [PATCH] feat(api): Add annotation endpoints to PdfOperationsController - Add POST /api/pdf/operations/annotate (multipart/form-data) - Add POST /api/pdf/operations/annotate (application/json with Base64) - Create AddAnnotationMultipartRequest DTO (wrapper for 10+ form parameters) - Create AddAnnotationBase64Command DTO (Base64 PDF + annotation parameters) - Add unique operation names (AnnotateFromFile, AnnotateFromBase64) for Swagger - Rename MergePdfsRequest -> MergePdfsBase64Request for clarity - Add Name attributes to merge endpoints (MergeFromFiles, MergeFromBase64) to fix Swagger conflict - Base64 FormatException wrapped in BadRequestException --- .../Controllers/PdfOperationsController.cs | 236 +++++++++++++++++- 1 file changed, 229 insertions(+), 7 deletions(-) diff --git a/DocumentOperator.API/Controllers/PdfOperationsController.cs b/DocumentOperator.API/Controllers/PdfOperationsController.cs index 663572a..40940c4 100644 --- a/DocumentOperator.API/Controllers/PdfOperationsController.cs +++ b/DocumentOperator.API/Controllers/PdfOperationsController.cs @@ -1,5 +1,7 @@ +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; @@ -23,7 +25,7 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase /// PDFs merged successfully - returns merged PDF /// Invalid input (fewer than 2 files, corrupted PDF) /// Internal server error during PDF processing - [HttpPost("merge")] + [HttpPost("merge", Name = "MergeFromFiles")] [Consumes("multipart/form-data")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] @@ -58,17 +60,17 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase /// 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")] + [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] MergePdfsRequest request, + [FromBody] MergePdfsBase64Request request, CancellationToken cancellationToken) { // Convert Base64[] to MemoryStream[] - List streams = new(); + List streams = []; try { foreach (var base64Pdf in request.Base64Pdfs) @@ -97,18 +99,166 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase 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 Base64-encoded PDF merge operation +/// Request DTO for multipart/form-data annotation operation /// -public record MergePdfsRequest +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 List Base64Pdfs { get; init; } = new(); + public required List Base64Pdfs { get; init; } /// /// Optional page ranges per PDF (null = all pages). @@ -118,3 +268,75 @@ public record MergePdfsRequest /// ["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; } +}