diff --git a/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs b/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs
index e688818..12b231d 100644
--- a/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs
+++ b/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs
@@ -1,150 +1,216 @@
using DevExpress.Pdf;
+using DocumentOperator.Application.Common.DTOs;
using DocumentOperator.Application.Common.Interfaces;
using DocumentOperator.Domain.Common.Exceptions;
-using DocumentOperator.Domain.Models.ValueObjects;
namespace DocumentOperator.Infrastructure.Services.PdfProcessing;
///
/// PDF processor implementation using DevExpress.Pdf library.
-/// Handles PDF validation and metadata extraction.
+/// Handles PDF validation, metadata extraction, and attachment operations.
///
public class DevExpressPdfProcessor : IPdfProcessor
{
+ #region PDF Validation
///
/// Validates a PDF document and returns metadata.
///
- /// PDF content as byte array
+ /// PDF content as stream (caller is responsible for disposal)
/// PDF metadata (page count, file size, version, etc.)
- /// Thrown when PDF is invalid or null
- public async Task ValidateAsync(byte[] pdfBytes)
+ /// Thrown when stream is empty or invalid
+ public async Task ValidateAsync(Stream pdfStream)
{
- // 1. Input Validation (Defensive Programming)
- if (pdfBytes == null)
+ // 1. Input Validation
+ ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
+
+ if (pdfStream.Length == 0)
{
- throw new PdfProcessingException("PDF bytes cannot be null");
+ throw new BadRequestException("PDF stream cannot be empty");
}
- if (pdfBytes.Length == 0)
+ // 2. Read stream to byte array for raw data analysis
+ // (DevExpress needs byte[] for some operations like attachment detection)
+ byte[] pdfBytes;
+ if (pdfStream is MemoryStream ms && ms.TryGetBuffer(out var buffer))
{
- throw new PdfProcessingException("PDF bytes cannot be empty");
+ // Fast path: reuse MemoryStream buffer
+ pdfBytes = buffer.Array!;
+ }
+ else
+ {
+ // Slow path: copy stream to byte array
+ pdfStream.Position = 0;
+ using var memoryStream = new MemoryStream();
+ await pdfStream.CopyToAsync(memoryStream);
+ pdfBytes = memoryStream.ToArray();
}
- try
- {
- // 2. Load PDF with DevExpress Document API (PdfDocumentProcessor)
- using var processor = new PdfDocumentProcessor();
- processor.LoadDocument(new MemoryStream(pdfBytes));
+ // 3. Load PDF with DevExpress Document API
+ pdfStream.Position = 0;
+ using var processor = new PdfDocumentProcessor();
+ processor.LoadDocument(pdfStream);
- // 3. Extract metadata
- var document = processor.Document;
+ // 4. Extract metadata
+ var document = processor.Document;
- int pageCount = document.Pages.Count;
- string pdfVersion = document.Version.ToString(); // z.B. "1.4", "1.7"
+ int pageCount = document.Pages.Count;
+ string pdfVersion = document.Version.ToString(); // e.g., "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);
+ // Attachments (embedded files)
+ // DevExpress PdfDocument API doesn't expose EmbeddedFiles directly in old API.
+ // 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
- return new 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);
- }
+ // 5. Create and return PdfMetadata DTO
+ return new Application.Common.DTOs.PdfMetadata(
+ pageCount: pageCount,
+ fileSizeBytes: pdfBytes.Length,
+ pdfVersion: pdfVersion,
+ hasAttachments: hasAttachments,
+ attachmentCount: attachmentCount
+ );
}
///
/// Validates a PDF/A document and checks conformance level.
///
- /// PDF content as byte array
+ /// PDF content as stream (caller is responsible for disposal)
/// PDF/A metadata including conformance level and validation errors/warnings
- /// Thrown when PDF is invalid or null
- public async Task ValidatePdfAAsync(byte[] pdfBytes)
+ /// Thrown when stream is empty or invalid
+ public async Task ValidatePdfAAsync(Stream pdfStream)
{
// 1. Input Validation
- if (pdfBytes == null)
+ ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
+
+ if (pdfStream.Length == 0)
{
- throw new PdfProcessingException("PDF bytes cannot be null");
+ throw new BadRequestException("PDF stream cannot be empty");
}
- if (pdfBytes.Length == 0)
+ // 2. Read stream to byte array for raw data analysis
+ byte[] pdfBytes;
+ if (pdfStream is MemoryStream ms && ms.TryGetBuffer(out var buffer))
{
- throw new PdfProcessingException("PDF bytes cannot be empty");
+ pdfBytes = buffer.Array!;
+ }
+ else
+ {
+ pdfStream.Position = 0;
+ using var memoryStream = new MemoryStream();
+ await pdfStream.CopyToAsync(memoryStream);
+ pdfBytes = memoryStream.ToArray();
}
- try
+ // 3. Load PDF with DevExpress Document API
+ pdfStream.Position = 0;
+ using var processor = new PdfDocumentProcessor();
+ processor.LoadDocument(pdfStream);
+
+ var document = processor.Document;
+
+ // 4. Extract basic metadata
+ int pageCount = document.Pages.Count;
+ string pdfVersion = document.Version.ToString();
+
+ // 5. Check encryption (scan PDF raw data for /Encrypt keyword)
+ bool encrypted = DetectEncryption(pdfBytes);
+
+ // 6. Check PDF/A conformance (scan PDF raw data for PDF/A identifier)
+ var (isPdfACompliant, pdfaVersion) = DetectPdfAConformance(pdfBytes);
+
+ // 7. Collect errors and warnings
+ var errors = new List();
+ var warnings = new List();
+
+ // If encrypted, PDF/A compliance is not possible
+ if (encrypted && isPdfACompliant)
{
- // 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();
- var warnings = new List();
-
- // 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
- );
+ errors.Add("PDF/A documents cannot be encrypted");
+ isPdfACompliant = false;
}
- catch (Exception ex) when (ex is not PdfProcessingException)
+
+ // Basic PDF/A validation checks
+ if (isPdfACompliant)
{
- throw new PdfProcessingException(
- $"Failed to validate PDF/A: {ex.Message}",
- ex);
+ // 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");
}
+
+ // 8. Determine overall validity
+ bool isValid = errors.Count == 0;
+
+ // 9. Create and return PdfAMetadata DTO
+ return new PdfAMetadata(
+ isValid: isValid,
+ pdfVersion: pdfVersion,
+ pageCount: pageCount,
+ fileSizeBytes: pdfBytes.Length,
+ encrypted: encrypted,
+ pdfaVersion: pdfaVersion,
+ pdfaCompliant: isPdfACompliant,
+ errors: errors,
+ warnings: warnings
+ );
}
+ #endregion
+
+ #region Attachment Operations
+
+ ///
+ /// Checks for embedded files (attachments) in a PDF document and returns detailed metadata.
+ /// Uses DevExpress PdfDocument.FileAttachments collection to retrieve attachment details.
+ ///
+ /// PDF content as stream (caller is responsible for disposal)
+ /// Attachment information including count, file names, MIME types, and sizes
+ /// Thrown when stream is empty
+ public async Task CheckAttachmentsAsync(Stream pdfStream)
+ {
+ // 1. Input Validation
+ ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
+
+ if (pdfStream.Length == 0)
+ {
+ throw new BadRequestException("PDF stream cannot be empty");
+ }
+
+ // 2. Load PDF with DevExpress Document API (exceptions propagate naturally)
+ pdfStream.Position = 0;
+ using var processor = new PdfDocumentProcessor();
+ processor.LoadDocument(pdfStream);
+
+ var document = processor.Document;
+
+ // 3. Extract attachment details using DevExpress FileAttachments collection
+ var fileAttachments = document.FileAttachments;
+
+ // 4. No attachments case (FileAttachments is IEnumerable)
+ if (fileAttachments == null || !fileAttachments.Any())
+ {
+ return AttachmentInfo.Empty;
+ }
+
+ // 5. Map DevExpress PdfFileAttachment to our DTO AttachmentMetadata
+ var attachments = fileAttachments.Select(devExpressAttachment =>
+ new AttachmentMetadata(
+ fileName: devExpressAttachment.FileName ?? "unnamed",
+ mimeType: devExpressAttachment.MimeType ?? "application/octet-stream",
+ sizeBytes: devExpressAttachment.Size
+ )).ToList();
+
+ // 6. Return AttachmentInfo DTO
+ return new AttachmentInfo(
+ hasAttachments: true,
+ attachmentCount: attachments.Count,
+ attachments: attachments.AsReadOnly()
+ );
+ }
+
+ #endregion
+
+ #region Private Helpers
+
///
/// Detects if PDF is encrypted by scanning for /Encrypt keyword.
///
@@ -252,4 +318,6 @@ public class DevExpressPdfProcessor : IPdfProcessor
searchStart = embeddedFilesIndex + 1;
}
}
+
+ #endregion
}