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; }
}