refactor: Update DevExpressPdfProcessor to use Stream parameters
- All 3 methods (ValidateAsync, ValidatePdfAAsync, CheckAttachmentsAsync) now accept Stream - Add ArgumentNullException.ThrowIfNull() checks for null streams - Implement fast-path optimization: reuse MemoryStream buffer when possible - Implement slow-path fallback: copy stream to byte[] for DevExpress API compatibility - Fix namespace collision: use fully qualified Application.Common.DTOs.PdfMetadata
This commit is contained in:
@@ -1,150 +1,216 @@
|
|||||||
using DevExpress.Pdf;
|
using DevExpress.Pdf;
|
||||||
|
using DocumentOperator.Application.Common.DTOs;
|
||||||
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PDF processor implementation using DevExpress.Pdf library.
|
/// PDF processor implementation using DevExpress.Pdf library.
|
||||||
/// Handles PDF validation and metadata extraction.
|
/// Handles PDF validation, metadata extraction, and attachment operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DevExpressPdfProcessor : IPdfProcessor
|
public class DevExpressPdfProcessor : IPdfProcessor
|
||||||
{
|
{
|
||||||
|
#region PDF Validation
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a PDF document and returns metadata.
|
/// Validates a PDF document and returns metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pdfBytes">PDF content as byte array</param>
|
/// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</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="BadRequestException">Thrown when stream is empty or invalid</exception>
|
||||||
public async Task<Domain.Models.ValueObjects.PdfMetadata> ValidateAsync(byte[] pdfBytes)
|
public async Task<Application.Common.DTOs.PdfMetadata> ValidateAsync(Stream pdfStream)
|
||||||
{
|
{
|
||||||
// 1. Input Validation (Defensive Programming)
|
// 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
|
||||||
|
// (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
|
// 3. Load PDF with DevExpress Document API
|
||||||
{
|
pdfStream.Position = 0;
|
||||||
// 2. Load PDF with DevExpress Document API (PdfDocumentProcessor)
|
using var processor = new PdfDocumentProcessor();
|
||||||
using var processor = new PdfDocumentProcessor();
|
processor.LoadDocument(pdfStream);
|
||||||
processor.LoadDocument(new MemoryStream(pdfBytes));
|
|
||||||
|
|
||||||
// 3. Extract metadata
|
// 4. Extract metadata
|
||||||
var document = processor.Document;
|
var document = processor.Document;
|
||||||
|
|
||||||
int pageCount = document.Pages.Count;
|
int pageCount = document.Pages.Count;
|
||||||
string pdfVersion = document.Version.ToString(); // z.B. "1.4", "1.7"
|
string pdfVersion = document.Version.ToString(); // e.g., "1.4", "1.7"
|
||||||
|
|
||||||
// Attachments (embedded files)
|
// Attachments (embedded files)
|
||||||
// DevExpress PdfDocument API doesn't expose EmbeddedFiles directly.
|
// 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.
|
// 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
|
// 5. Create and return PdfMetadata DTO
|
||||||
return new Domain.Models.ValueObjects.PdfMetadata(
|
return new Application.Common.DTOs.PdfMetadata(
|
||||||
pageCount: pageCount,
|
pageCount: pageCount,
|
||||||
fileSizeBytes: pdfBytes.Length,
|
fileSizeBytes: pdfBytes.Length,
|
||||||
pdfVersion: pdfVersion,
|
pdfVersion: pdfVersion,
|
||||||
hasAttachments: hasAttachments,
|
hasAttachments: hasAttachments,
|
||||||
attachmentCount: attachmentCount
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates a PDF/A document and checks conformance level.
|
/// Validates a PDF/A document and checks conformance level.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pdfBytes">PDF content as byte array</param>
|
/// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</param>
|
||||||
/// <returns>PDF/A metadata including conformance level and validation errors/warnings</returns>
|
/// <returns>PDF/A metadata including conformance level and validation errors/warnings</returns>
|
||||||
/// <exception cref="PdfProcessingException">Thrown when PDF is invalid or null</exception>
|
/// <exception cref="BadRequestException">Thrown when stream is empty or invalid</exception>
|
||||||
public async Task<PdfAMetadata> ValidatePdfAAsync(byte[] pdfBytes)
|
public async Task<PdfAMetadata> ValidatePdfAAsync(Stream pdfStream)
|
||||||
{
|
{
|
||||||
// 1. Input Validation
|
// 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<string>();
|
||||||
|
var warnings = new List<string>();
|
||||||
|
|
||||||
|
// If encrypted, PDF/A compliance is not possible
|
||||||
|
if (encrypted && isPdfACompliant)
|
||||||
{
|
{
|
||||||
// 2. Load PDF with DevExpress Document API
|
errors.Add("PDF/A documents cannot be encrypted");
|
||||||
using var processor = new PdfDocumentProcessor();
|
isPdfACompliant = false;
|
||||||
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)
|
|
||||||
|
// Basic PDF/A validation checks
|
||||||
|
if (isPdfACompliant)
|
||||||
{
|
{
|
||||||
throw new PdfProcessingException(
|
// Add generic warning for manual verification
|
||||||
$"Failed to validate PDF/A: {ex.Message}",
|
warnings.Add("Manual verification recommended: All fonts must be embedded");
|
||||||
ex);
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for embedded files (attachments) in a PDF document and returns detailed metadata.
|
||||||
|
/// Uses DevExpress PdfDocument.FileAttachments collection to retrieve attachment details.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pdfStream">PDF content as stream (caller is responsible for disposal)</param>
|
||||||
|
/// <returns>Attachment information including count, file names, MIME types, and sizes</returns>
|
||||||
|
/// <exception cref="BadRequestException">Thrown when stream is empty</exception>
|
||||||
|
public async Task<AttachmentInfo> 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<PdfFileAttachment>)
|
||||||
|
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
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Detects if PDF is encrypted by scanning for /Encrypt keyword.
|
/// Detects if PDF is encrypted by scanning for /Encrypt keyword.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -252,4 +318,6 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
|||||||
searchStart = embeddedFilesIndex + 1;
|
searchStart = embeddedFilesIndex + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user