using DevExpress.Pdf; using DocumentOperator.Application.Common.Interfaces; using DocumentOperator.Domain.Common.Exceptions; namespace DocumentOperator.Infrastructure.Services.PdfProcessing; /// /// PDF processor implementation using DevExpress.Pdf library. /// Handles PDF validation and metadata extraction. /// public class DevExpressPdfProcessor : IPdfProcessor { /// /// Validates a PDF document and returns metadata. /// /// PDF content as byte array /// PDF metadata (page count, file size, version, etc.) /// Thrown when PDF is invalid or null public async Task ValidateAsync(byte[] pdfBytes) { // 1. Input Validation (Defensive Programming) 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 (PdfDocumentProcessor) using var processor = new PdfDocumentProcessor(); processor.LoadDocument(new MemoryStream(pdfBytes)); // 3. Extract metadata var document = processor.Document; int pageCount = document.Pages.Count; string pdfVersion = document.Version.ToString(); // z.B. "1.4", "1.7" // Attachments (embedded files) // DevExpress PdfDocument API doesn't expose EmbeddedFiles directly. // We scan PDF raw data for "/EmbeddedFiles" and parse the name tree to get count. var (hasAttachments, attachmentCount) = DetectEmbeddedFiles(pdfBytes); // 4. Create and return PdfMetadata Value Object (fully qualified name!) return new DocumentOperator.Domain.Models.ValueObjects.PdfMetadata( pageCount: pageCount, fileSizeBytes: pdfBytes.Length, pdfVersion: pdfVersion, hasAttachments: hasAttachments, attachmentCount: attachmentCount ); } catch (Exception ex) when (ex is not PdfProcessingException) { // Wrap DevExpress exceptions in our domain exception throw new PdfProcessingException( $"Failed to validate PDF: {ex.Message}", ex); } } /// /// Detects embedded files in PDF by scanning raw PDF data for /EmbeddedFiles keyword /// and parsing the name tree to count attachments. /// This is a pragmatic approach as DevExpress PdfDocument API doesn't expose EmbeddedFiles directly. /// /// PDF raw bytes /// Tuple: (hasAttachments, attachmentCount) private static (bool hasAttachments, int attachmentCount) DetectEmbeddedFiles(byte[] pdfBytes) { // PDF embedded files are declared in the document catalog: // /Names << /EmbeddedFiles << /Names [...] >> >> // The /Names array contains pairs: [name1, filespec1, name2, filespec2, ...] string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes); // Search for /EmbeddedFiles in the context of /Names dictionary // Must appear after a /Names keyword to be valid int searchStart = 0; while (true) { // Find next occurrence of /EmbeddedFiles int embeddedFilesIndex = pdfText.IndexOf("/EmbeddedFiles", searchStart, StringComparison.Ordinal); if (embeddedFilesIndex == -1) return (false, 0); // Not found // Check if there's a /Names keyword BEFORE this /EmbeddedFiles // within a reasonable distance (e.g., within the same PDF object, max 5000 chars back) int contextStart = Math.Max(0, embeddedFilesIndex - 5000); string contextBefore = pdfText.Substring(contextStart, embeddedFilesIndex - contextStart); // Look for /Names in the context before /EmbeddedFiles int lastNamesIndex = contextBefore.LastIndexOf("/Names", StringComparison.Ordinal); if (lastNamesIndex != -1) { // Found /Names before /EmbeddedFiles - this is likely a valid embedded files declaration // Now try to parse the /Names array int namesArrayStart = pdfText.IndexOf("/Names", embeddedFilesIndex, StringComparison.Ordinal); if (namesArrayStart == -1) return (true, 0); // Has EmbeddedFiles but can't count int arrayStart = pdfText.IndexOf('[', namesArrayStart); if (arrayStart == -1) return (true, 0); // Has EmbeddedFiles but can't count int arrayEnd = pdfText.IndexOf(']', arrayStart); if (arrayEnd == -1) return (true, 0); // Has EmbeddedFiles but can't count // Extract array content and count entries string arrayContent = pdfText.Substring(arrayStart + 1, arrayEnd - arrayStart - 1); // Count object references in array // The /Names array contains pairs: (filename) objectReference (filename) objectReference ... // Each object reference (pattern: "123 0 R") points to one embedded file // So the number of object references = number of attachments int objectCount = System.Text.RegularExpressions.Regex.Matches(arrayContent, @"\d+ \d+ R").Count; return (true, Math.Max(1, objectCount)); // At least 1 if EmbeddedFiles found } // This /EmbeddedFiles was not in the right context, search for next occurrence searchStart = embeddedFilesIndex + 1; } } }