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,45 @@
using AutoMapper;
using DocumentOperator.Application.Common.DTOs;
using DocumentOperator.Application.Common.Interfaces;
using MediatR;
namespace DocumentOperator.Application.ValidatePdfA.Queries;
/// <summary>
/// Query for PDF/A validation (supports both byte array and Base64 input)
/// </summary>
public record ValidatePdfAQuery : IRequest<PdfAValidationResult>
{
/// <summary>
/// PDF as byte array (direct upload)
/// </summary>
public byte[]? PdfBytes { get; init; }
/// <summary>
/// PDF as Base64 string (API clients)
/// </summary>
public string? Base64Pdf { get; init; }
}
/// <summary>
/// Handler for ValidatePdfAQuery
/// Orchestrates PDF/A validation using IPdfProcessor and AutoMapper
/// </summary>
public class ValidatePdfAQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper)
: IRequestHandler<ValidatePdfAQuery, PdfAValidationResult>
{
/// <summary>
/// Validates PDF/A and returns metadata with conformance level
/// </summary>
public async Task<PdfAValidationResult> Handle(ValidatePdfAQuery request, CancellationToken cancellationToken)
{
// Use byte[] if available, otherwise convert Base64
byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!);
// Call DevExpress service (can throw PdfProcessingException)
var metadata = await PdfProcessor.ValidatePdfAAsync(pdfBytes);
// Map domain entity to DTO using AutoMapper
return Mapper.Map<PdfAValidationResult>(metadata);
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
namespace DocumentOperator.Application.ValidatePdfA.Validators;
/// <summary>
/// Validator for ValidatePdfAQuery
/// Ensures exactly ONE input format is provided (PdfBytes XOR Base64Pdf)
/// </summary>
public class ValidatePdfAQueryValidator : AbstractValidator<Queries.ValidatePdfAQuery>
{
public ValidatePdfAQueryValidator()
{
RuleFor(x => x)
.Must(x => (x.PdfBytes != null && x.PdfBytes.Length > 0) ^
!string.IsNullOrWhiteSpace(x.Base64Pdf))
.WithMessage("Either PdfBytes or Base64Pdf must be provided, but not both");
}
}