From 61b11fc2160eeae124e84680e5db9180ef25feb4 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 21 Jul 2026 14:38:14 +0200 Subject: [PATCH] feat(annotation): Add Origin/Width/Height to API endpoints - Update AddAnnotationFromFile/AddAnnotationFromBase64 endpoints - Add Origin parameter to multipart/JSON request DTOs - Add Width/Height as alternative to X2/Y2 in requests - Calculate X2/Y2 from Width/Height if provided - XML documentation updated with new parameters --- .../Controllers/PdfOperationsController.cs | 418 +++++++++++++++++- 1 file changed, 402 insertions(+), 16 deletions(-) diff --git a/DocumentOperator.API/Controllers/PdfOperationsController.cs b/DocumentOperator.API/Controllers/PdfOperationsController.cs index 40940c4..fc75f1a 100644 --- a/DocumentOperator.API/Controllers/PdfOperationsController.cs +++ b/DocumentOperator.API/Controllers/PdfOperationsController.cs @@ -1,4 +1,5 @@ using DocumentOperator.Application.AddAnnotation; +using DocumentOperator.Application.AddStamp; using DocumentOperator.Application.MergePdfs; using DocumentOperator.Domain.Common.Exceptions; using DocumentOperator.Domain.Models.ValueObjects; @@ -119,16 +120,21 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase [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, request.X2, request.Y2), + Rectangle = (request.X1, request.Y1, x2, y2), Content = request.Content, Author = request.Author, Color = request.Color, - TextMarkupStyle = request.TextMarkupStyle + TextMarkupStyle = request.TextMarkupStyle, + Origin = request.Origin }; byte[] annotatedPdf = await mediator.Send(command, cancellationToken); @@ -167,16 +173,21 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase 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, command.X2, command.Y2), + Rectangle = (command.X1, command.Y1, x2, y2), Content = command.Content, Author = command.Author, Color = command.Color, - TextMarkupStyle = command.TextMarkupStyle + TextMarkupStyle = command.TextMarkupStyle, + Origin = command.Origin }; byte[] annotatedPdf = await mediator.Send(annotationCommand, cancellationToken); @@ -186,6 +197,135 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase 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"); + } } /// @@ -209,24 +349,34 @@ public class AddAnnotationMultipartRequest public required int PageNumber { get; set; } /// - /// Rectangle X1 coordinate + /// Rectangle X1 coordinate (left) /// public required double X1 { get; set; } /// - /// Rectangle Y1 coordinate + /// Rectangle Y1 coordinate (top or bottom depending on Origin) /// public required double Y1 { get; set; } /// - /// Rectangle X2 coordinate + /// Rectangle X2 coordinate (right). Optional if Width is provided. /// - public required double X2 { get; set; } + public double? X2 { get; set; } /// - /// Rectangle Y2 coordinate + /// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided. /// - public required double Y2 { get; set; } + 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) @@ -247,6 +397,11 @@ public class AddAnnotationMultipartRequest /// 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; } /// @@ -293,28 +448,40 @@ public record AddAnnotationBase64Command public required int PageNumber { get; init; } /// - /// Rectangle X1 coordinate + /// Rectangle X1 coordinate (left) /// /// 100.0 public required double X1 { get; init; } /// - /// Rectangle Y1 coordinate + /// Rectangle Y1 coordinate (top or bottom depending on Origin) /// /// 100.0 public required double Y1 { get; init; } /// - /// Rectangle X2 coordinate + /// Rectangle X2 coordinate (right). Optional if Width is provided. /// /// 200.0 - public required double X2 { get; init; } + public double? X2 { get; init; } /// - /// Rectangle Y2 coordinate + /// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided. /// /// 120.0 - public required double Y2 { get; init; } + 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) @@ -339,4 +506,223 @@ public record AddAnnotationBase64Command /// /// 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; } }