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