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:
@@ -1,5 +1,6 @@
|
|||||||
using DocumentOperator.Application.Common.DTOs;
|
using DocumentOperator.Application.Common.DTOs;
|
||||||
using DocumentOperator.Application.ValidatePdf.Queries;
|
using DocumentOperator.Application.ValidatePdf.Queries;
|
||||||
|
using DocumentOperator.Application.ValidatePdfA.Queries;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@@ -76,4 +77,68 @@ public class PdfValidationController(IMediator Mediator) : ControllerBase
|
|||||||
|
|
||||||
return Ok(result);
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IFormFile to byte array
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await file.CopyToAsync(memoryStream, cancellationToken);
|
||||||
|
byte[] pdfBytes = memoryStream.ToArray();
|
||||||
|
|
||||||
|
// Direct pass-through to MediatR
|
||||||
|
var query = new ValidatePdfAQuery { PdfBytes = pdfBytes };
|
||||||
|
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="query">PDF as Base64 string</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] ValidatePdfAQuery query,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Direct pass-through to MediatR
|
||||||
|
var result = await Mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; } = [];
|
||||||
|
}
|
||||||
@@ -13,4 +13,14 @@ public interface IPdfProcessor
|
|||||||
/// Thrown when PDF is corrupted or cannot be processed
|
/// Thrown when PDF is corrupted or cannot be processed
|
||||||
/// </exception>
|
/// </exception>
|
||||||
Task<PdfMetadata> ValidateAsync(byte[] pdfBytes);
|
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);
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,10 @@ public class MappingProfile : Profile
|
|||||||
// PdfMetadata -> PdfValidationResult
|
// PdfMetadata -> PdfValidationResult
|
||||||
CreateMap<PdfMetadata, PdfValidationResult>();
|
CreateMap<PdfMetadata, PdfValidationResult>();
|
||||||
|
|
||||||
|
// PdfAMetadata -> PdfAValidationResult
|
||||||
|
CreateMap<PdfAMetadata, PdfAValidationResult>()
|
||||||
|
.ForMember(dest => dest.FileSize, opt => opt.MapFrom(src => src.FileSizeBytes));
|
||||||
|
|
||||||
// SwissQrCodeData -> SwissQrCodeDataDto
|
// SwissQrCodeData -> SwissQrCodeDataDto
|
||||||
CreateMap<SwissQrCodeData, SwissQrCodeDataDto>();
|
CreateMap<SwissQrCodeData, SwissQrCodeDataDto>();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
47
DocumentOperator.Domain/Models/ValueObjects/PdfAMetadata.cs
Normal file
47
DocumentOperator.Domain/Models/ValueObjects/PdfAMetadata.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF/A validation metadata including conformance level and validation errors/warnings
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PdfAMetadata
|
||||||
|
{
|
||||||
|
public bool IsValid { get; }
|
||||||
|
public string PdfVersion { get; }
|
||||||
|
public int PageCount { get; }
|
||||||
|
public long FileSizeBytes { get; }
|
||||||
|
public bool Encrypted { get; }
|
||||||
|
public string? PdfAVersion { get; }
|
||||||
|
public bool PdfACompliant { get; }
|
||||||
|
public IReadOnlyList<string> Errors { get; }
|
||||||
|
public IReadOnlyList<string> Warnings { get; }
|
||||||
|
|
||||||
|
// Computed property
|
||||||
|
public double FileSizeMB => FileSizeBytes / 1024.0 / 1024.0;
|
||||||
|
|
||||||
|
public PdfAMetadata(
|
||||||
|
bool isValid,
|
||||||
|
string pdfVersion,
|
||||||
|
int pageCount,
|
||||||
|
long fileSizeBytes,
|
||||||
|
bool encrypted,
|
||||||
|
string? pdfaVersion,
|
||||||
|
bool pdfaCompliant,
|
||||||
|
IReadOnlyList<string> errors,
|
||||||
|
IReadOnlyList<string> warnings)
|
||||||
|
{
|
||||||
|
IsValid = isValid;
|
||||||
|
PdfVersion = pdfVersion;
|
||||||
|
PageCount = pageCount;
|
||||||
|
FileSizeBytes = fileSizeBytes;
|
||||||
|
Encrypted = encrypted;
|
||||||
|
PdfAVersion = pdfaVersion;
|
||||||
|
PdfACompliant = pdfaCompliant;
|
||||||
|
Errors = errors;
|
||||||
|
Warnings = warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"PDF/A: {PdfAVersion ?? "None"}, {PageCount} pages, {FileSizeMB:F2} MB, Compliant: {PdfACompliant}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using DevExpress.Pdf;
|
using DevExpress.Pdf;
|
||||||
using DocumentOperator.Application.Common.Interfaces;
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
using DocumentOperator.Domain.Common.Exceptions;
|
using DocumentOperator.Domain.Common.Exceptions;
|
||||||
|
using DocumentOperator.Domain.Models.ValueObjects;
|
||||||
|
|
||||||
namespace DocumentOperator.Infrastructure.Services.PdfProcessing;
|
namespace DocumentOperator.Infrastructure.Services.PdfProcessing;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
|||||||
/// <param name="pdfBytes">PDF content as byte array</param>
|
/// <param name="pdfBytes">PDF content as byte array</param>
|
||||||
/// <returns>PDF metadata (page count, file size, version, etc.)</returns>
|
/// <returns>PDF metadata (page count, file size, version, etc.)</returns>
|
||||||
/// <exception cref="PdfProcessingException">Thrown when PDF is invalid or null</exception>
|
/// <exception cref="PdfProcessingException">Thrown when PDF is invalid or null</exception>
|
||||||
public async Task<DocumentOperator.Domain.Models.ValueObjects.PdfMetadata> ValidateAsync(byte[] pdfBytes)
|
public async Task<Domain.Models.ValueObjects.PdfMetadata> ValidateAsync(byte[] pdfBytes)
|
||||||
{
|
{
|
||||||
// 1. Input Validation (Defensive Programming)
|
// 1. Input Validation (Defensive Programming)
|
||||||
if (pdfBytes == null)
|
if (pdfBytes == null)
|
||||||
@@ -46,8 +47,8 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
|||||||
// We scan PDF raw data for "/EmbeddedFiles" and parse the name tree to get count.
|
// We scan PDF raw data for "/EmbeddedFiles" and parse the name tree to get count.
|
||||||
var (hasAttachments, attachmentCount) = DetectEmbeddedFiles(pdfBytes);
|
var (hasAttachments, attachmentCount) = DetectEmbeddedFiles(pdfBytes);
|
||||||
|
|
||||||
// 4. Create and return PdfMetadata Value Object (fully qualified name!)
|
// 4. Create and return PdfMetadata Value Object
|
||||||
return new DocumentOperator.Domain.Models.ValueObjects.PdfMetadata(
|
return new Domain.Models.ValueObjects.PdfMetadata(
|
||||||
pageCount: pageCount,
|
pageCount: pageCount,
|
||||||
fileSizeBytes: pdfBytes.Length,
|
fileSizeBytes: pdfBytes.Length,
|
||||||
pdfVersion: pdfVersion,
|
pdfVersion: pdfVersion,
|
||||||
@@ -64,6 +65,126 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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="PdfProcessingException">Thrown when PDF is invalid or null</exception>
|
||||||
|
public async Task<PdfAMetadata> ValidatePdfAAsync(byte[] pdfBytes)
|
||||||
|
{
|
||||||
|
// 1. Input Validation
|
||||||
|
if (pdfBytes == null)
|
||||||
|
{
|
||||||
|
throw new PdfProcessingException("PDF bytes cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfBytes.Length == 0)
|
||||||
|
{
|
||||||
|
throw new PdfProcessingException("PDF bytes cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. Load PDF with DevExpress Document API
|
||||||
|
using var processor = new PdfDocumentProcessor();
|
||||||
|
processor.LoadDocument(new MemoryStream(pdfBytes));
|
||||||
|
|
||||||
|
var document = processor.Document;
|
||||||
|
|
||||||
|
// 3. Extract basic metadata
|
||||||
|
int pageCount = document.Pages.Count;
|
||||||
|
string pdfVersion = document.Version.ToString();
|
||||||
|
|
||||||
|
// 4. Check encryption (scan PDF raw data for /Encrypt keyword)
|
||||||
|
bool encrypted = DetectEncryption(pdfBytes);
|
||||||
|
|
||||||
|
// 5. Check PDF/A conformance (scan PDF raw data for PDF/A identifier)
|
||||||
|
var (isPdfACompliant, pdfaVersion) = DetectPdfAConformance(pdfBytes);
|
||||||
|
|
||||||
|
// 6. Collect errors and warnings
|
||||||
|
var errors = new List<string>();
|
||||||
|
var warnings = new List<string>();
|
||||||
|
|
||||||
|
// If encrypted, PDF/A compliance is not possible
|
||||||
|
if (encrypted && isPdfACompliant)
|
||||||
|
{
|
||||||
|
errors.Add("PDF/A documents cannot be encrypted");
|
||||||
|
isPdfACompliant = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic PDF/A validation checks
|
||||||
|
if (isPdfACompliant)
|
||||||
|
{
|
||||||
|
// Add generic warning for manual verification
|
||||||
|
warnings.Add("Manual verification recommended: All fonts must be embedded");
|
||||||
|
warnings.Add("Manual verification recommended: No JavaScript or multimedia content");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Determine overall validity
|
||||||
|
bool isValid = errors.Count == 0;
|
||||||
|
|
||||||
|
// 8. Create and return PdfAMetadata
|
||||||
|
return new PdfAMetadata(
|
||||||
|
isValid: isValid,
|
||||||
|
pdfVersion: pdfVersion,
|
||||||
|
pageCount: pageCount,
|
||||||
|
fileSizeBytes: pdfBytes.Length,
|
||||||
|
encrypted: encrypted,
|
||||||
|
pdfaVersion: pdfaVersion,
|
||||||
|
pdfaCompliant: isPdfACompliant,
|
||||||
|
errors: errors,
|
||||||
|
warnings: warnings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not PdfProcessingException)
|
||||||
|
{
|
||||||
|
throw new PdfProcessingException(
|
||||||
|
$"Failed to validate PDF/A: {ex.Message}",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects if PDF is encrypted by scanning for /Encrypt keyword.
|
||||||
|
/// </summary>
|
||||||
|
private static bool DetectEncryption(byte[] pdfBytes)
|
||||||
|
{
|
||||||
|
string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes);
|
||||||
|
return pdfText.Contains("/Encrypt", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects PDF/A conformance level by scanning PDF metadata.
|
||||||
|
/// PDF/A documents contain an XMP metadata stream with pdfaid:conformance and pdfaid:part.
|
||||||
|
/// </summary>
|
||||||
|
private static (bool isPdfACompliant, string? pdfaVersion) DetectPdfAConformance(byte[] pdfBytes)
|
||||||
|
{
|
||||||
|
string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes);
|
||||||
|
|
||||||
|
// Look for PDF/A identifier in XMP metadata
|
||||||
|
// Example: <pdfaid:part>1</pdfaid:part><pdfaid:conformance>B</pdfaid:conformance>
|
||||||
|
if (pdfText.Contains("pdfaid:part", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// Try to extract part and conformance level
|
||||||
|
var partMatch = System.Text.RegularExpressions.Regex.Match(pdfText, @"pdfaid:part>(\d+)</pdfaid:part");
|
||||||
|
var conformanceMatch = System.Text.RegularExpressions.Regex.Match(pdfText, @"pdfaid:conformance>([ABU])</pdfaid:conformance");
|
||||||
|
|
||||||
|
if (partMatch.Success && conformanceMatch.Success)
|
||||||
|
{
|
||||||
|
string part = partMatch.Groups[1].Value; // "1", "2", "3"
|
||||||
|
string conformance = conformanceMatch.Groups[1].Value; // "A", "B", "U"
|
||||||
|
string pdfaVersion = $"PDF/A-{part}{conformance.ToLower()}";
|
||||||
|
return (true, pdfaVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found pdfaid:part but couldn't parse details
|
||||||
|
return (true, "PDF/A (unknown level)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Detects embedded files in PDF by scanning raw PDF data for /EmbeddedFiles keyword
|
/// Detects embedded files in PDF by scanning raw PDF data for /EmbeddedFiles keyword
|
||||||
/// and parsing the name tree to count attachments.
|
/// and parsing the name tree to count attachments.
|
||||||
|
|||||||
Reference in New Issue
Block a user