Refactor: Rename DocumentOperator to DocumentService
Updated project and solution files to rename and restructure `DocumentOperator` to `DocumentService`. Modified project references in `DocumentService.API.csproj`, `DocumentService.Application.csproj`, and `DocumentService.Infrastructure.csproj` to reflect the new naming convention. Updated `DocumentService.sln` to remove old project references, add new ones, and adjust configuration and nested project mappings accordingly. This change aligns the project structure with the new naming convention.
This commit is contained in:
356
DocumentService.API/Controllers/PdfAttachmentController.cs
Normal file
356
DocumentService.API/Controllers/PdfAttachmentController.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using DocumentService.Application.AddAttachments;
|
||||
using DocumentService.Application.CheckPdfAttachments.Queries;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractPdfAttachments;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for PDF attachment operations (detection, extraction, embedding)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/attachments")]
|
||||
[Produces("application/json")]
|
||||
public class PdfAttachmentController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a PDF contains embedded files (attachments) and returns their metadata.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to check for attachments</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Attachment check result with metadata for all found attachments</returns>
|
||||
/// <response code="200">PDF successfully checked - returns attachment details</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or corrupted)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("check")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(AttachmentCheckResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CheckAttachmentsFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send query to MediatR (ValidationBehavior runs automatically)
|
||||
var query = new CheckPdfAttachmentsQuery { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a PDF contains embedded files (attachments) and returns their metadata.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Attachment check result with metadata for all found attachments</returns>
|
||||
/// <response code="200">PDF successfully checked - returns attachment details</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or corrupted)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("check")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(AttachmentCheckResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> CheckAttachmentsFromBase64(
|
||||
[FromBody] CheckPdfAttachmentsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send query to MediatR (ValidationBehavior runs automatically)
|
||||
var query = new CheckPdfAttachmentsQuery { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all embedded files from a PDF and returns them as a ZIP archive.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to extract attachments from</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZIP archive containing all extracted attachments</returns>
|
||||
/// <response code="200">Attachments extracted successfully - returns ZIP file</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or corrupted)</response>
|
||||
/// <response code="404">PDF contains no attachments</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("extract")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractAttachmentsFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ExtractPdfAttachmentsCommand { PdfStream = pdfStream };
|
||||
byte[] zipBytes = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return ZIP file
|
||||
return File(zipBytes, "application/zip", "attachments.zip");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all embedded files from a PDF and returns them as a ZIP archive.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZIP archive containing all extracted attachments</returns>
|
||||
/// <response code="200">Attachments extracted successfully - returns ZIP file</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or corrupted)</response>
|
||||
/// <response code="404">PDF contains no attachments</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("extract")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractAttachmentsFromBase64(
|
||||
[FromBody] ExtractPdfAttachmentsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ExtractPdfAttachmentsCommand { PdfStream = pdfStream };
|
||||
byte[] zipBytes = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return ZIP file
|
||||
return File(zipBytes, "application/zip", "attachments.zip");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="pdfFile">The PDF file to add attachments to</param>
|
||||
/// <param name="attachmentFiles">Files to embed as attachments (one or more)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF with embedded attachments</returns>
|
||||
/// <response code="200">Attachments added successfully - returns PDF</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or no attachments provided)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("add")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AddAttachmentsFromFile(
|
||||
IFormFile pdfFile,
|
||||
List<IFormFile> attachmentFiles,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (pdfFile == null || pdfFile.Length == 0)
|
||||
{
|
||||
throw new BadRequestException("PDF file is required");
|
||||
}
|
||||
|
||||
if (attachmentFiles == null || attachmentFiles.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("At least one attachment file is required");
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = pdfFile.OpenReadStream();
|
||||
|
||||
// Convert attachment files to AttachmentFile records
|
||||
var attachments = new List<AttachmentFile>();
|
||||
foreach (var file in attachmentFiles)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms, cancellationToken);
|
||||
|
||||
attachments.Add(new AttachmentFile
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Content = ms.ToArray(),
|
||||
MimeType = file.ContentType
|
||||
});
|
||||
}
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new AddAttachmentsCommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
Attachments = attachments
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF with attachments
|
||||
return File(resultPdf, "application/pdf", "with-attachments.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
|
||||
/// Supports Base64-encoded PDF and attachments via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF and attachments</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF with embedded attachments</returns>
|
||||
/// <response code="200">Attachments added successfully - returns PDF</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or no attachments provided)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("add")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AddAttachmentsFromBase64(
|
||||
[FromBody] AddAttachmentsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 PDF to stream
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Convert Base64 attachments to AttachmentFile records
|
||||
var attachments = new List<AttachmentFile>();
|
||||
foreach (var att in request.Attachments)
|
||||
{
|
||||
byte[] attBytes;
|
||||
try
|
||||
{
|
||||
attBytes = Convert.FromBase64String(att.Base64Content);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException($"Invalid Base64 format for attachment '{att.FileName}': " + ex.Message);
|
||||
}
|
||||
|
||||
attachments.Add(new AttachmentFile
|
||||
{
|
||||
FileName = att.FileName,
|
||||
Content = attBytes,
|
||||
MimeType = att.MimeType
|
||||
});
|
||||
}
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new AddAttachmentsCommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
Attachments = attachments
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF with attachments
|
||||
return File(resultPdf, "application/pdf", "with-attachments.pdf");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF attachment check
|
||||
/// </summary>
|
||||
public record CheckPdfAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public string Base64Pdf { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF attachment extraction
|
||||
/// </summary>
|
||||
public record ExtractPdfAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF with attachments to add
|
||||
/// </summary>
|
||||
public record AddAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of attachments to embed
|
||||
/// </summary>
|
||||
public required List<AttachmentRequestDto> Attachments { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for attachment file in request
|
||||
/// </summary>
|
||||
public record AttachmentRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// File name (e.g., "invoice.xml", "document.pdf")
|
||||
/// </summary>
|
||||
/// <example>factur-x.xml</example>
|
||||
public required string FileName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File content encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>PD94bWwgdmVyc2lvbj0iMS4wIj8+...</example>
|
||||
public required string Base64Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME type (optional, e.g., "application/xml")
|
||||
/// </summary>
|
||||
/// <example>application/xml</example>
|
||||
public string? MimeType { get; init; }
|
||||
}
|
||||
182
DocumentService.API/Controllers/PdfConversionController.cs
Normal file
182
DocumentService.API/Controllers/PdfConversionController.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using DocumentService.Application.ConvertFromPdfA;
|
||||
using DocumentService.Application.ConvertToPdfA;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for PDF conversion operations (PDF ? PDF/A)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/conversion")]
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
public class PdfConversionController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a standard PDF to PDF/A format.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to convert</param>
|
||||
/// <param name="pdfALevel">Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A compliant document</returns>
|
||||
/// <response code="200">PDF converted to PDF/A successfully</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or invalid PDF/A level)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("to-pdfa")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertToPdfAFromFile(
|
||||
IFormFile file,
|
||||
[FromQuery] string pdfALevel = "PDF/A-3b",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new BadRequestException("PDF file is required");
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertToPdfACommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
PdfALevel = pdfALevel
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF/A file
|
||||
return File(resultPdf, "application/pdf", "converted-pdfa.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a standard PDF to PDF/A format.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF and PDF/A level</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A compliant document</returns>
|
||||
/// <response code="200">PDF converted to PDF/A successfully</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or invalid PDF/A level)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("to-pdfa")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertToPdfAFromBase64(
|
||||
[FromBody] ConvertToPdfARequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 PDF to stream
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertToPdfACommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
PdfALevel = request.PdfALevel ?? "PDF/A-3b"
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF/A file
|
||||
return File(resultPdf, "application/pdf", "converted-pdfa.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF/A file to convert</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Standard PDF document</returns>
|
||||
/// <response code="200">PDF/A converted to standard PDF successfully</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("from-pdfa")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertFromPdfAFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new BadRequestException("PDF/A file is required");
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertFromPdfACommand { PdfStream = pdfStream };
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return standard PDF file
|
||||
return File(resultPdf, "application/pdf", "converted-pdf.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF/A</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Standard PDF document</returns>
|
||||
/// <response code="200">PDF/A converted to standard PDF successfully</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("from-pdfa")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertFromPdfAFromBase64(
|
||||
[FromBody] ConvertFromPdfARequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 PDF to stream
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertFromPdfACommand { PdfStream = pdfStream };
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return standard PDF file
|
||||
return File(resultPdf, "application/pdf", "converted-pdf.pdf");
|
||||
}
|
||||
}
|
||||
728
DocumentService.API/Controllers/PdfOperationsController.cs
Normal file
728
DocumentService.API/Controllers/PdfOperationsController.cs
Normal file
@@ -0,0 +1,728 @@
|
||||
using DocumentService.Application.AddAnnotation;
|
||||
using DocumentService.Application.AddStamp;
|
||||
using DocumentService.Application.MergePdfs;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for PDF operations (merge, stamp, annotate).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/operations")]
|
||||
public class PdfOperationsController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Merges multiple PDF files into a single PDF.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="files">PDF files to merge (minimum 2 required)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Merged PDF file</returns>
|
||||
/// <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", Name = "MergeFromFiles")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> MergeFromFiles(
|
||||
[FromForm] List<IFormFile> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple PDF files into a single PDF.
|
||||
/// Supports Base64-encoded PDFs via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDFs and optional page ranges</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Merged PDF file</returns>
|
||||
/// <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", 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] MergePdfsBase64Request request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64[] to MemoryStream[]
|
||||
List<Stream> streams = [];
|
||||
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");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// 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, x2, y2),
|
||||
Content = request.Content,
|
||||
Author = request.Author,
|
||||
Color = request.Color,
|
||||
TextMarkupStyle = request.TextMarkupStyle,
|
||||
Origin = request.Origin
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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, x2, y2),
|
||||
Content = command.Content,
|
||||
Author = command.Author,
|
||||
Color = command.Color,
|
||||
TextMarkupStyle = command.TextMarkupStyle,
|
||||
Origin = command.Origin
|
||||
};
|
||||
|
||||
byte[] annotatedPdf = await mediator.Send(annotationCommand, cancellationToken);
|
||||
|
||||
// Cleanup stream
|
||||
pdfStream.Dispose();
|
||||
|
||||
return File(annotatedPdf, "application/pdf", "annotated.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stamp (text, image, or predefined) to PDF pages.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="request">Multipart form data containing PDF file and stamp parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Stamped PDF file</returns>
|
||||
/// <response code="200">Stamp added successfully - returns stamped PDF</response>
|
||||
/// <response code="400">Invalid input (invalid page number, missing required parameters, invalid image format)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[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<IActionResult> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stamp (text, image, or predefined) to PDF pages.
|
||||
/// Supports Base64-encoded PDF and image via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF, stamp parameters, and optional Base64 image</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Stamped PDF file</returns>
|
||||
/// <response code="200">Stamp added successfully - returns stamped 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("stamp", Name = "AddStampFromBase64")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> 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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for multipart/form-data annotation operation
|
||||
/// </summary>
|
||||
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 (left)
|
||||
/// </summary>
|
||||
public required double X1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y1 coordinate (top or bottom depending on Origin)
|
||||
/// </summary>
|
||||
public required double Y1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X2 coordinate (right). Optional if Width is provided.
|
||||
/// </summary>
|
||||
public double? X2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided.
|
||||
/// </summary>
|
||||
public double? Y2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle width. Alternative to X2 (X2 = X1 + Width). Optional if X2 is provided.
|
||||
/// </summary>
|
||||
public double? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle height. Alternative to Y2 (Y2 = Y1 + Height). Optional if Y2 is provided.
|
||||
/// </summary>
|
||||
public double? Height { 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>
|
||||
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
|
||||
/// </summary>
|
||||
public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft;
|
||||
}
|
||||
|
||||
/// <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 required List<string> Base64Pdfs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <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 (left)
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double X1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y1 coordinate (top or bottom depending on Origin)
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double Y1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X2 coordinate (right). Optional if Width is provided.
|
||||
/// </summary>
|
||||
/// <example>200.0</example>
|
||||
public double? X2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided.
|
||||
/// </summary>
|
||||
/// <example>120.0</example>
|
||||
public double? Y2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle width. Alternative to X2 (X2 = X1 + Width). Optional if X2 is provided.
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle height. Alternative to Y2 (Y2 = Y1 + Height). Optional if Y2 is provided.
|
||||
/// </summary>
|
||||
/// <example>20.0</example>
|
||||
public double? Height { 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
|
||||
/// </summary>
|
||||
/// <example>BottomLeft</example>
|
||||
public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for multipart/form-data stamp operation
|
||||
/// </summary>
|
||||
public class AddStampMultipartRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF file to stamp
|
||||
/// </summary>
|
||||
public required IFormFile File { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of stamp (Text, Image, or Predefined)
|
||||
/// </summary>
|
||||
public required StampType StampType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Target page numbers (1-indexed). Null or empty = all pages.
|
||||
/// </summary>
|
||||
/// <example>[1, 3, 5]</example>
|
||||
public int[]? PageNumbers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp position X coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp position Y coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp width (optional, auto-size for images if not specified)
|
||||
/// </summary>
|
||||
/// <example>200.0</example>
|
||||
public double? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp height (optional, auto-size for images if not specified)
|
||||
/// </summary>
|
||||
/// <example>50.0</example>
|
||||
public double? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
|
||||
/// </summary>
|
||||
/// <example>BottomLeft</example>
|
||||
public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Text content (required for Text stamps)
|
||||
/// </summary>
|
||||
/// <example>"CONFIDENTIAL"</example>
|
||||
public string? Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Font name (default: Arial)
|
||||
/// </summary>
|
||||
/// <example>"Arial"</example>
|
||||
public string? FontName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Font size in points (default: 12)
|
||||
/// </summary>
|
||||
/// <example>24.0</example>
|
||||
public double? FontSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hex color (6 digits, e.g., "FF0000" for red, default: "000000")
|
||||
/// </summary>
|
||||
/// <example>"FF0000"</example>
|
||||
public string? Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Opacity (0.0 = transparent, 1.0 = opaque, default: 0.5)
|
||||
/// </summary>
|
||||
/// <example>0.5</example>
|
||||
public double? Opacity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation angle in degrees (0-360, default: 0)
|
||||
/// </summary>
|
||||
/// <example>45.0</example>
|
||||
public double? Rotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp placement (Foreground = on top, Background = watermark effect, default: Foreground)
|
||||
/// </summary>
|
||||
/// <example>Foreground</example>
|
||||
public StampPlacement Placement { get; set; } = StampPlacement.Foreground;
|
||||
|
||||
/// <summary>
|
||||
/// Image file (required for Image stamps, PNG/JPEG)
|
||||
/// </summary>
|
||||
public IFormFile? ImageFile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Predefined stamp type (required for Predefined stamps)
|
||||
/// </summary>
|
||||
/// <example>Confidential</example>
|
||||
public PredefinedStampType? PredefinedType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF stamp operation (API layer only - converts to AddStampCommand)
|
||||
/// </summary>
|
||||
public record AddStampBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded PDF file
|
||||
/// </summary>
|
||||
/// <example>"JVBERi0xLjQK..."</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of stamp (Text, Image, or Predefined)
|
||||
/// </summary>
|
||||
/// <example>Text</example>
|
||||
public required StampType StampType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target page numbers (1-indexed). Null or empty = all pages.
|
||||
/// </summary>
|
||||
/// <example>[1, 3, 5]</example>
|
||||
public int[]? PageNumbers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp position X coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp position Y coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double Y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp width (optional, auto-size for images if not specified)
|
||||
/// </summary>
|
||||
/// <example>200.0</example>
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp height (optional, auto-size for images if not specified)
|
||||
/// </summary>
|
||||
/// <example>50.0</example>
|
||||
public double? Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
|
||||
/// </summary>
|
||||
/// <example>BottomLeft</example>
|
||||
public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Text content (required for Text stamps)
|
||||
/// </summary>
|
||||
/// <example>"CONFIDENTIAL"</example>
|
||||
public string? Text { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Font name (default: Arial)
|
||||
/// </summary>
|
||||
/// <example>"Arial"</example>
|
||||
public string? FontName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Font size in points (default: 12)
|
||||
/// </summary>
|
||||
/// <example>24.0</example>
|
||||
public double? FontSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hex color (6 digits, e.g., "FF0000" for red, default: "000000")
|
||||
/// </summary>
|
||||
/// <example>"FF0000"</example>
|
||||
public string? Color { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Opacity (0.0 = transparent, 1.0 = opaque, default: 0.5)
|
||||
/// </summary>
|
||||
/// <example>0.5</example>
|
||||
public double? Opacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation angle in degrees (0-360, default: 0)
|
||||
/// </summary>
|
||||
/// <example>45.0</example>
|
||||
public double? Rotation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp placement (Foreground = on top, Background = watermark effect, default: Foreground)
|
||||
/// </summary>
|
||||
/// <example>Foreground</example>
|
||||
public StampPlacement Placement { get; init; } = StampPlacement.Foreground;
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded image (required for Image stamps, PNG/JPEG)
|
||||
/// </summary>
|
||||
/// <example>"iVBORw0KGgoAAAANSUhEUgAA..."</example>
|
||||
public string? Base64Image { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Predefined stamp type (required for Predefined stamps)
|
||||
/// </summary>
|
||||
/// <example>Confidential</example>
|
||||
public PredefinedStampType? PredefinedType { get; init; }
|
||||
}
|
||||
170
DocumentService.API/Controllers/PdfValidationController.cs
Normal file
170
DocumentService.API/Controllers/PdfValidationController.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ValidatePdf.Queries;
|
||||
using DocumentService.Application.ValidatePdfA.Queries;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// PDF validation operations
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/validation")]
|
||||
[Produces("application/json")]
|
||||
public class PdfValidationController(IMediator Mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a PDF document and returns metadata (multipart/form-data)
|
||||
/// </summary>
|
||||
/// <param name="file">PDF file to validate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF metadata (page count, file size, PDF version, attachments)</returns>
|
||||
/// <response code="200">PDF is valid, metadata returned</response>
|
||||
/// <response code="400">Invalid PDF or file format</response>
|
||||
/// <response code="500">Internal server error during validation</response>
|
||||
[HttpPost("validate")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(PdfValidationResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ValidateFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid file",
|
||||
Detail = "File is required and cannot be empty",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Direct pass-through to MediatR
|
||||
var query = new ValidatePdfQuery { PdfStream = pdfStream };
|
||||
var result = await Mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a PDF document and returns metadata (Base64 JSON)
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF metadata (page count, file size, PDF version, attachments)</returns>
|
||||
/// <response code="200">PDF is valid, metadata returned</response>
|
||||
/// <response code="400">Invalid PDF or Base64 format</response>
|
||||
/// <response code="500">Internal server error during validation</response>
|
||||
[HttpPost("validate")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(PdfValidationResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ValidateFromBase64(
|
||||
[FromBody] ValidatePdfBase64Request request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Direct pass-through to MediatR
|
||||
var query = new ValidatePdfQuery { PdfStream = pdfStream };
|
||||
var result = await Mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a PDF/A document and checks conformance level (multipart/form-data)
|
||||
/// </summary>
|
||||
/// <param name="file">PDF file to validate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A metadata (conformance level, errors, warnings)</returns>
|
||||
/// <response code="200">PDF/A validation completed, results returned</response>
|
||||
/// <response code="400">Invalid PDF or file format</response>
|
||||
/// <response code="500">Internal server error during validation</response>
|
||||
[HttpPost("validate-pdfa")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(PdfAValidationResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ValidatePdfAFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid file",
|
||||
Detail = "File is required and cannot be empty",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Direct pass-through to MediatR
|
||||
var query = new ValidatePdfAQuery { PdfStream = pdfStream };
|
||||
var result = await Mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a PDF/A document and checks conformance level (Base64 JSON)
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A metadata (conformance level, errors, warnings)</returns>
|
||||
/// <response code="200">PDF/A validation completed, results returned</response>
|
||||
/// <response code="400">Invalid PDF or Base64 format</response>
|
||||
/// <response code="500">Internal server error during validation</response>
|
||||
[HttpPost("validate-pdfa")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(PdfAValidationResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ValidatePdfAFromBase64(
|
||||
[FromBody] ValidatePdfABase64Request request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Direct pass-through to MediatR
|
||||
var query = new ValidatePdfAQuery { PdfStream = pdfStream };
|
||||
var result = await Mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
113
DocumentService.API/Controllers/SwissQrCodeController.cs
Normal file
113
DocumentService.API/Controllers/SwissQrCodeController.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.SwissQrCode.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Swiss QR Code extraction operations
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/qr-code")]
|
||||
[Produces("application/json")]
|
||||
public class SwissQrCodeController(IMediator Mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts Swiss QR Code from the last page of a PDF document (multipart/form-data)
|
||||
/// </summary>
|
||||
/// <param name="file">PDF file containing Swiss QR Code</param>
|
||||
/// <param name="raw">If true, returns raw QR text lines instead of parsed Bill object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.)</returns>
|
||||
/// <response code="200">Swiss QR Code extracted successfully</response>
|
||||
/// <response code="400">Invalid PDF or file format</response>
|
||||
/// <response code="404">No Swiss QR Code found on the last page</response>
|
||||
/// <response code="500">Internal server error during extraction</response>
|
||||
[HttpPost("extract-swiss")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(SwissQrCodeExtractionResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractFromFile(
|
||||
IFormFile file,
|
||||
[FromQuery] bool raw = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (file.Length == 0)
|
||||
return BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid file",
|
||||
Detail = "File is required and cannot be empty",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Direct pass-through to MediatR
|
||||
var query = new ExtractSwissQrCodeQuery
|
||||
{
|
||||
PdfStream = pdfStream
|
||||
};
|
||||
var result = await Mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(raw ? result.RawLines : result.Bill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts Swiss QR Code from the last page of a PDF document (Base64 JSON)
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="raw">If true, returns raw QR text lines instead of parsed Bill object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.)</returns>
|
||||
/// <response code="200">Swiss QR Code extracted successfully</response>
|
||||
/// <response code="400">Invalid PDF or Base64 format</response>
|
||||
/// <response code="404">No Swiss QR Code found on the last page</response>
|
||||
/// <response code="500">Internal server error during extraction</response>
|
||||
[HttpPost("extract-swiss")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(SwissQrCodeExtractionResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractFromBase64(
|
||||
[FromBody] ExtractSwissQrCodeBase64Request request,
|
||||
[FromQuery] bool raw = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Direct pass-through to MediatR
|
||||
var query = new ExtractSwissQrCodeQuery { PdfStream = pdfStream };
|
||||
var result = await Mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(raw ? result.RawLines : result.Bill);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded Swiss QR Code extraction
|
||||
/// </summary>
|
||||
public record ExtractSwissQrCodeBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
179
DocumentService.API/Controllers/ZugferdController.cs
Normal file
179
DocumentService.API/Controllers/ZugferdController.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractZugferd;
|
||||
using DocumentService.Application.HasZugferd.Queries;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for ZUGFeRD operations (detection, extraction)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/zugferd")]
|
||||
[Produces("application/json")]
|
||||
public class ZugferdController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a PDF contains ZUGFeRD XML attachment.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to check for ZUGFeRD</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD check result with metadata</returns>
|
||||
/// <response code="200">PDF successfully checked - returns ZUGFeRD status</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or corrupted)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("has-zugferd")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(ZugferdCheckResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> HasZugferdFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send query to MediatR (ValidationBehavior runs automatically)
|
||||
var query = new HasZugferdQuery { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a PDF contains ZUGFeRD XML attachment.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD check result with metadata</returns>
|
||||
/// <response code="200">PDF successfully checked - returns ZUGFeRD status</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or corrupted)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("has-zugferd")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(ZugferdCheckResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> HasZugferdFromBase64(
|
||||
[FromBody] HasZugferdRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send query to MediatR (ValidationBehavior runs automatically)
|
||||
var query = new HasZugferdQuery { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts ZUGFeRD XML from a PDF document.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to extract ZUGFeRD from</param>
|
||||
/// <param name="asFile">if true, 'file' (returns XML file directly); otherwise output format: 'json' (default, returns metadata + XML content)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD XML content and metadata (JSON) or XML file (application/xml)</returns>
|
||||
/// <response code="200">ZUGFeRD XML extracted successfully</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or corrupted)</response>
|
||||
/// <response code="404">PDF contains no ZUGFeRD XML</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("extract")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(ZugferdExtractionResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractZugferdFromFile(
|
||||
IFormFile file,
|
||||
[FromQuery] bool asFile = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ExtractZugferdCommand { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return as file or JSON based on format parameter
|
||||
if (asFile)
|
||||
{
|
||||
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(result.XmlContent);
|
||||
return File(xmlBytes, "application/xml", result.FileName);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts ZUGFeRD XML from a PDF document.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="format">Output format: 'json' (default, returns metadata + XML content) or 'file' (returns XML file directly)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD XML content and metadata (JSON) or XML file (application/xml)</returns>
|
||||
/// <response code="200">ZUGFeRD XML extracted successfully</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or corrupted)</response>
|
||||
/// <response code="404">PDF contains no ZUGFeRD XML</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("extract")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(ZugferdExtractionResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractZugferdFromBase64(
|
||||
[FromBody] ExtractZugferdRequest request,
|
||||
[FromQuery] string format = "json",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ExtractZugferdCommand { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return as file or JSON based on format parameter
|
||||
if (format.Equals("file", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(result.XmlContent);
|
||||
return File(xmlBytes, "application/xml", result.FileName);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user