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