using DocumentOperator.Application.MergePdfs; using DocumentOperator.Domain.Common.Exceptions; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DocumentOperator.API.Controllers; /// /// Controller for PDF operations (merge, stamp, annotate). /// [ApiController] [Route("api/pdf/operations")] public class PdfOperationsController(IMediator mediator) : ControllerBase { /// /// Merges multiple PDF files into a single PDF. /// Supports multipart/form-data file upload. /// /// PDF files to merge (minimum 2 required) /// Cancellation token /// Merged PDF file /// PDFs merged successfully - returns merged PDF /// Invalid input (fewer than 2 files, corrupted PDF) /// Internal server error during PDF processing [HttpPost("merge")] [Consumes("multipart/form-data")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task MergeFromFiles( [FromForm] List files, CancellationToken cancellationToken) { // Convert IFormFile[] to Stream[] (use OpenReadStream directly - no buffering) var streams = files.Select(f => f.OpenReadStream()).ToList(); // Send command to MediatR (no page ranges for now - multipart binding is complex) var command = new MergePdfsCommand { PdfStreams = streams, PageRanges = null }; byte[] mergedPdf = await mediator.Send(command, cancellationToken); // Return merged PDF return File(mergedPdf, "application/pdf", "merged.pdf"); } /// /// Merges multiple PDF files into a single PDF. /// Supports Base64-encoded PDFs via JSON payload. /// /// Request containing Base64-encoded PDFs and optional page ranges /// Cancellation token /// Merged PDF file /// 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")] [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, CancellationToken cancellationToken) { // Convert Base64[] to MemoryStream[] List streams = new(); try { foreach (var base64Pdf in request.Base64Pdfs) { byte[] pdfBytes = Convert.FromBase64String(base64Pdf); streams.Add(new MemoryStream(pdfBytes)); } } catch (FormatException ex) { // Dispose opened streams on error foreach (var stream in streams) stream.Dispose(); throw new BadRequestException("Invalid Base64 format: " + ex.Message); } var command = new MergePdfsCommand { PdfStreams = streams, PageRanges = request.PageRanges }; byte[] mergedPdf = await mediator.Send(command, cancellationToken); // Cleanup streams (important for MemoryStreams we created) foreach (var stream in streams) stream.Dispose(); return File(mergedPdf, "application/pdf", "merged.pdf"); } } /// /// Request DTO for Base64-encoded PDF merge operation /// public record MergePdfsRequest { /// /// Array of Base64-encoded PDF files (minimum 2 required) /// /// ["JVBERi0xLjQK...", "JVBERi0xLjQK..."] public List Base64Pdfs { get; init; } = new(); /// /// Optional page ranges per PDF (null = all pages). /// Format: "1-3,5" means pages 1, 2, 3, and 5. /// If provided, array length must match Base64Pdfs length. /// /// ["1-2", "1,3,5", null] public List? PageRanges { get; init; } }