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
This commit is contained in:
@@ -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
|
||||
/// <response code="200">PDFs merged successfully - returns merged PDF</response>
|
||||
/// <response code="400">Invalid input (fewer than 2 files, corrupted PDF)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[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
|
||||
/// <response code="200">PDFs merged successfully - returns merged PDF</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, fewer than 2 files, corrupted PDF, invalid page range)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[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<IActionResult> MergeFromBase64(
|
||||
[FromBody] MergePdfsRequest request,
|
||||
[FromBody] MergePdfsBase64Request request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64[] to MemoryStream[]
|
||||
List<Stream> streams = new();
|
||||
List<Stream> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an annotation to a PDF document.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="request">Multipart form data containing PDF file and annotation parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Annotated PDF file</returns>
|
||||
/// <response code="200">Annotation added successfully - returns annotated PDF</response>
|
||||
/// <response code="400">Invalid input (invalid page number, missing required parameters)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[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<IActionResult> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an annotation to a PDF document.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="command">Command containing all annotation parameters (including Base64 PDF)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Annotated PDF file</returns>
|
||||
/// <response code="200">Annotation added successfully - returns annotated PDF</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, invalid page number, missing required parameters)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[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<IActionResult> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF merge operation
|
||||
/// Request DTO for multipart/form-data annotation operation
|
||||
/// </summary>
|
||||
public record MergePdfsRequest
|
||||
public class AddAnnotationMultipartRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF file to annotate
|
||||
/// </summary>
|
||||
public required IFormFile File { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of annotation (TextMarkup, FreeText, StickyNote, Circle, Square)
|
||||
/// </summary>
|
||||
public required AnnotationType AnnotationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Target page number (1-indexed)
|
||||
/// </summary>
|
||||
public required int PageNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X1 coordinate
|
||||
/// </summary>
|
||||
public required double X1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y1 coordinate
|
||||
/// </summary>
|
||||
public required double Y1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X2 coordinate
|
||||
/// </summary>
|
||||
public required double X2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y2 coordinate
|
||||
/// </summary>
|
||||
public required double Y2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation content (required for FreeText/StickyNote)
|
||||
/// </summary>
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Author name (optional)
|
||||
/// </summary>
|
||||
public string? Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hex color (6 digits, e.g., "FF0000" for red)
|
||||
/// </summary>
|
||||
public string? Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Markup style (Highlight/Underline/Strikeout, required for TextMarkup)
|
||||
/// </summary>
|
||||
public TextMarkupStyle? TextMarkupStyle { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF merge operation (API layer only - converts to MergePdfsCommand)
|
||||
/// </summary>
|
||||
public record MergePdfsBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Array of Base64-encoded PDF files (minimum 2 required)
|
||||
/// </summary>
|
||||
/// <example>["JVBERi0xLjQK...", "JVBERi0xLjQK..."]</example>
|
||||
public List<string> Base64Pdfs { get; init; } = new();
|
||||
public required List<string> Base64Pdfs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional page ranges per PDF (null = all pages).
|
||||
@@ -118,3 +268,75 @@ public record MergePdfsRequest
|
||||
/// <example>["1-2", "1,3,5", null]</example>
|
||||
public List<string?>? PageRanges { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF annotation (API layer only - converts to AddAnnotationCommand)
|
||||
/// </summary>
|
||||
public record AddAnnotationBase64Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded PDF file
|
||||
/// </summary>
|
||||
/// <example>"JVBERi0xLjQK..."</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of annotation to add
|
||||
/// </summary>
|
||||
/// <example>TextMarkup</example>
|
||||
public required AnnotationType AnnotationType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target page number (1-indexed)
|
||||
/// </summary>
|
||||
/// <example>1</example>
|
||||
public required int PageNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X1 coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double X1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y1 coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double Y1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X2 coordinate
|
||||
/// </summary>
|
||||
/// <example>200.0</example>
|
||||
public required double X2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y2 coordinate
|
||||
/// </summary>
|
||||
/// <example>120.0</example>
|
||||
public required double Y2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation content (required for FreeText and StickyNote)
|
||||
/// </summary>
|
||||
/// <example>"Important text to highlight"</example>
|
||||
public string? Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Author name (optional)
|
||||
/// </summary>
|
||||
/// <example>"John Doe"</example>
|
||||
public string? Author { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hex color (6 digits, e.g., "FF0000" for red). Optional - defaults vary by annotation type.
|
||||
/// </summary>
|
||||
/// <example>"FFFF00"</example>
|
||||
public string? Color { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Text markup style (Highlight, Underline, or Strikeout). Required for TextMarkup annotations.
|
||||
/// </summary>
|
||||
/// <example>Highlight</example>
|
||||
public TextMarkupStyle? TextMarkupStyle { get; init; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user