feat: Add PDF/A validation endpoint (Feature 3)

Domain layer:

  - PdfAMetadata value object (isValid, pdfVersion, pageCount, encrypted, pdfaVersion, pdfaCompliant, errors, warnings)

Infrastructure layer:

  - IPdfProcessor.ValidatePdfAAsync() interface method

  - DevExpressPdfProcessor.ValidatePdfAAsync() implementation

  - DetectEncryption() - scans PDF raw data for /Encrypt keyword

  - DetectPdfAConformance() - parses XMP metadata (pdfaid:part, pdfaid:conformance)

  - Validation: encrypted PDF cannot be PDF/A compliant

Application layer:

  - ValidatePdfAQuery + ValidatePdfAQueryHandler (co-located)

  - ValidatePdfAQueryValidator (FluentValidation: PdfBytes XOR Base64Pdf)

  - PdfAValidationResult DTO

  - AutoMapper: PdfAMetadata -> PdfAValidationResult

API layer:

  - PdfValidationController.ValidatePdfAFromFile() (multipart/form-data)

  - PdfValidationController.ValidatePdfAFromBase64() (application/json)

  - XML documentation with response codes

Result:

  - POST /api/pdf/validation/validate-pdfa (both multipart and JSON)

  - Returns: conformance level, errors, warnings

  - Build: 0 errors, 4 warnings (DevExpress eval)

  - Tests: 20/20 passing

Next: Integration tests + Swagger test case
This commit is contained in:
2026-07-09 12:59:24 +02:00
parent 1ff7cbea11
commit cd50d45bd5
8 changed files with 365 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
namespace DocumentOperator.Application.Common.DTOs;
/// <summary>
/// PDF/A validation result including conformance level and validation errors/warnings
/// </summary>
public record PdfAValidationResult
{
/// <summary>
/// Whether the PDF is valid (no errors)
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// PDF version (e.g., "1.4", "1.7")
/// </summary>
public string PdfVersion { get; init; } = string.Empty;
/// <summary>
/// Number of pages in the PDF
/// </summary>
public int PageCount { get; init; }
/// <summary>
/// File size in bytes
/// </summary>
public long FileSize { get; init; }
/// <summary>
/// Whether the PDF is encrypted
/// </summary>
public bool Encrypted { get; init; }
/// <summary>
/// PDF/A version (e.g., "PDF/A-1b", "PDF/A-2a", "PDF/A-3u") or null if not PDF/A compliant
/// </summary>
public string? PdfAVersion { get; init; }
/// <summary>
/// Whether the PDF conforms to PDF/A standard
/// </summary>
public bool PdfACompliant { get; init; }
/// <summary>
/// Validation errors (e.g., "PDF/A documents cannot be encrypted")
/// </summary>
public IReadOnlyList<string> Errors { get; init; } = [];
/// <summary>
/// Validation warnings (e.g., "Manual verification recommended: All fonts must be embedded")
/// </summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
}

View File

@@ -13,4 +13,14 @@ public interface IPdfProcessor
/// Thrown when PDF is corrupted or cannot be processed
/// </exception>
Task<PdfMetadata> ValidateAsync(byte[] pdfBytes);
/// <summary>
/// Validates a PDF/A document and checks conformance level.
/// </summary>
/// <param name="pdfBytes">PDF content as byte array</param>
/// <returns>PDF/A metadata including conformance level and validation errors/warnings</returns>
/// <exception cref="Domain.Common.Exceptions.PdfProcessingException">
/// Thrown when PDF is corrupted or cannot be processed
/// </exception>
Task<PdfAMetadata> ValidatePdfAAsync(byte[] pdfBytes);
}

View File

@@ -15,6 +15,10 @@ public class MappingProfile : Profile
// PdfMetadata -> PdfValidationResult
CreateMap<PdfMetadata, PdfValidationResult>();
// PdfAMetadata -> PdfAValidationResult
CreateMap<PdfAMetadata, PdfAValidationResult>()
.ForMember(dest => dest.FileSize, opt => opt.MapFrom(src => src.FileSizeBytes));
// SwissQrCodeData -> SwissQrCodeDataDto
CreateMap<SwissQrCodeData, SwissQrCodeDataDto>();