Updated PHASENPLAN.md and ROADMAP.md to reflect a complete restructuring of the development plan and added a "Bugfix: Attachment Detection" entry. Implemented attachment detection in DevExpressPdfProcessor.cs using the new `DetectEmbeddedFiles` method, which scans raw PDF data for the `/EmbeddedFiles` keyword. Updated the `hasAttachments` property to use this method and set `attachmentCount` to `-1` when attachments are detected. Added a new test, `ValidateAsync_PdfWithoutAttachments_ReturnsNoAttachments`, in DevExpressPdfProcessorTests.cs to verify that PDFs without attachments are correctly identified. Included a note about future testing for PDFs with attachments using ZUGFeRD files. All related tests are passing (12/12 green).
94 lines
4.0 KiB
C#
94 lines
4.0 KiB
C#
using DevExpress.Pdf;
|
|
using DocumentOperator.Application.Common.Interfaces;
|
|
using DocumentOperator.Domain.Common.Exceptions;
|
|
|
|
namespace DocumentOperator.Infrastructure.Services.PdfProcessing;
|
|
|
|
/// <summary>
|
|
/// PDF processor implementation using DevExpress.Pdf library.
|
|
/// Handles PDF validation and metadata extraction.
|
|
/// </summary>
|
|
public class DevExpressPdfProcessor : IPdfProcessor
|
|
{
|
|
/// <summary>
|
|
/// Validates a PDF document and returns metadata.
|
|
/// </summary>
|
|
/// <param name="pdfBytes">PDF content as byte array</param>
|
|
/// <returns>PDF metadata (page count, file size, version, etc.)</returns>
|
|
/// <exception cref="PdfProcessingException">Thrown when PDF is invalid or null</exception>
|
|
public async Task<DocumentOperator.Domain.Models.ValueObjects.PdfMetadata> 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 use a simple PDF raw data scan for "/EmbeddedFiles" keyword.
|
|
// This is a pragmatic approach until Feature 2 (ExtractAttachments) is implemented.
|
|
bool hasAttachments = DetectEmbeddedFiles(pdfBytes);
|
|
int attachmentCount = hasAttachments ? -1 : 0; // -1 = "has attachments, count unknown"
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects embedded files in PDF by scanning raw PDF data for /EmbeddedFiles keyword.
|
|
/// This is a pragmatic approach as DevExpress PdfDocument API doesn't expose EmbeddedFiles directly.
|
|
/// </summary>
|
|
/// <param name="pdfBytes">PDF raw bytes</param>
|
|
/// <returns>True if PDF contains /EmbeddedFiles keyword in proper context, false otherwise</returns>
|
|
private static bool DetectEmbeddedFiles(byte[] pdfBytes)
|
|
{
|
|
// PDF embedded files are declared in the document catalog:
|
|
// /Names << /EmbeddedFiles << /Names [...] >> >>
|
|
// We search for the pattern "/Names" followed by "/EmbeddedFiles"
|
|
|
|
string pdfText = System.Text.Encoding.ASCII.GetString(pdfBytes);
|
|
|
|
// Look for the specific PDF dictionary pattern: /Names and /EmbeddedFiles
|
|
// This is more precise than just searching for /EmbeddedFiles alone
|
|
int namesIndex = pdfText.IndexOf("/Names", StringComparison.Ordinal);
|
|
if (namesIndex == -1)
|
|
return false;
|
|
|
|
// Check if /EmbeddedFiles appears after /Names within reasonable distance (< 1000 chars)
|
|
int embeddedFilesIndex = pdfText.IndexOf("/EmbeddedFiles", namesIndex, Math.Min(1000, pdfText.Length - namesIndex), StringComparison.Ordinal);
|
|
return embeddedFilesIndex > namesIndex;
|
|
}
|
|
}
|