diff --git a/DocumentOperator.API/Controllers/PdfAttachmentController.cs b/DocumentOperator.API/Controllers/PdfAttachmentController.cs index 0b03274..73a5aa5 100644 --- a/DocumentOperator.API/Controllers/PdfAttachmentController.cs +++ b/DocumentOperator.API/Controllers/PdfAttachmentController.cs @@ -1,3 +1,4 @@ +using DocumentOperator.Application.AddAttachments; using DocumentOperator.Application.CheckPdfAttachments.Queries; using DocumentOperator.Application.Common.DTOs; using DocumentOperator.Application.ExtractPdfAttachments; @@ -157,6 +158,135 @@ public class PdfAttachmentController(IMediator mediator) : ControllerBase // Return ZIP file return File(zipBytes, "application/zip", "attachments.zip"); } + + /// + /// Embeds one or more files as attachments in a PDF document (supports PDF/A-3). + /// Supports multipart/form-data file upload. + /// + /// The PDF file to add attachments to + /// Files to embed as attachments (one or more) + /// Cancellation token + /// PDF with embedded attachments + /// Attachments added successfully - returns PDF + /// Invalid input (file missing, not a PDF, or no attachments provided) + /// Internal server error during PDF processing + [Obsolete("This endpoint is not implemented yet.")] + [HttpPost("add")] + [Consumes("multipart/form-data")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task AddAttachmentsFromFile( + IFormFile pdfFile, + List attachmentFiles, + CancellationToken cancellationToken) + { + if (pdfFile == null || pdfFile.Length == 0) + { + throw new BadRequestException("PDF file is required"); + } + + if (attachmentFiles == null || attachmentFiles.Count == 0) + { + throw new BadRequestException("At least one attachment file is required"); + } + + // Use IFormFile stream directly (no intermediate byte[] conversion) + using var pdfStream = pdfFile.OpenReadStream(); + + // Convert attachment files to AttachmentFile records + var attachments = new List(); + foreach (var file in attachmentFiles) + { + using var ms = new MemoryStream(); + await file.CopyToAsync(ms, cancellationToken); + + attachments.Add(new AttachmentFile + { + FileName = file.FileName, + Content = ms.ToArray(), + MimeType = file.ContentType + }); + } + + // Send command to MediatR + var command = new AddAttachmentsCommand + { + PdfStream = pdfStream, + Attachments = attachments + }; + byte[] resultPdf = await mediator.Send(command, cancellationToken); + + // Return PDF with attachments + return File(resultPdf, "application/pdf", "with-attachments.pdf"); + } + + /// + /// Embeds one or more files as attachments in a PDF document (supports PDF/A-3). + /// Supports Base64-encoded PDF and attachments via JSON payload. + /// + /// Request containing Base64-encoded PDF and attachments + /// Cancellation token + /// PDF with embedded attachments + /// Attachments added successfully - returns PDF + /// Invalid input (Base64 format error, not a PDF, or no attachments provided) + /// Internal server error during PDF processing + [Obsolete("This endpoint is not implemented yet.")] + [HttpPost("add")] + [Consumes("application/json")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task AddAttachmentsFromBase64( + [FromBody] AddAttachmentsRequest request, + CancellationToken cancellationToken) + { + // Convert Base64 PDF to stream + byte[] pdfBytes; + try + { + pdfBytes = Convert.FromBase64String(request.Base64Pdf); + } + catch (FormatException ex) + { + throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message); + } + + using var pdfStream = new MemoryStream(pdfBytes); + + // Convert Base64 attachments to AttachmentFile records + var attachments = new List(); + foreach (var att in request.Attachments) + { + byte[] attBytes; + try + { + attBytes = Convert.FromBase64String(att.Base64Content); + } + catch (FormatException ex) + { + throw new BadRequestException($"Invalid Base64 format for attachment '{att.FileName}': " + ex.Message); + } + + attachments.Add(new AttachmentFile + { + FileName = att.FileName, + Content = attBytes, + MimeType = att.MimeType + }); + } + + // Send command to MediatR + var command = new AddAttachmentsCommand + { + PdfStream = pdfStream, + Attachments = attachments + }; + byte[] resultPdf = await mediator.Send(command, cancellationToken); + + // Return PDF with attachments + return File(resultPdf, "application/pdf", "with-attachments.pdf"); + } } /// @@ -182,3 +312,44 @@ public record ExtractPdfAttachmentsRequest /// JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c... public required string Base64Pdf { get; init; } } + +/// +/// Request DTO for Base64-encoded PDF with attachments to add +/// +public record AddAttachmentsRequest +{ + /// + /// PDF document encoded as Base64 string + /// + /// JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c... + public required string Base64Pdf { get; init; } + + /// + /// List of attachments to embed + /// + public required List Attachments { get; init; } +} + +/// +/// DTO for attachment file in request +/// +public record AttachmentRequestDto +{ + /// + /// File name (e.g., "invoice.xml", "document.pdf") + /// + /// factur-x.xml + public required string FileName { get; init; } + + /// + /// File content encoded as Base64 string + /// + /// PD94bWwgdmVyc2lvbj0iMS4wIj8+... + public required string Base64Content { get; init; } + + /// + /// MIME type (optional, e.g., "application/xml") + /// + /// application/xml + public string? MimeType { get; init; } +} diff --git a/DocumentOperator.API/Controllers/PdfConversionController.cs b/DocumentOperator.API/Controllers/PdfConversionController.cs new file mode 100644 index 0000000..786e283 --- /dev/null +++ b/DocumentOperator.API/Controllers/PdfConversionController.cs @@ -0,0 +1,211 @@ +using DocumentOperator.Application.ConvertFromPdfA; +using DocumentOperator.Application.ConvertToPdfA; +using DocumentOperator.Domain.Common.Exceptions; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace DocumentOperator.API.Controllers; + +/// +/// Controller for PDF conversion operations (PDF ? PDF/A) +/// +[ApiController] +[Route("api/pdf/conversion")] +[Obsolete("This endpoint is not implemented yet.")] +public class PdfConversionController(IMediator mediator) : ControllerBase +{ + /// + /// Converts a standard PDF to PDF/A format. + /// Supports multipart/form-data file upload. + /// + /// The PDF file to convert + /// Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b") + /// Cancellation token + /// PDF/A compliant document + /// PDF converted to PDF/A successfully + /// Invalid input (file missing, not a PDF, or invalid PDF/A level) + /// Internal server error during PDF processing + [Obsolete("This endpoint is not implemented yet.")] + [HttpPost("to-pdfa")] + [Consumes("multipart/form-data")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task ConvertToPdfAFromFile( + IFormFile file, + [FromQuery] string pdfALevel = "PDF/A-3b", + CancellationToken cancellationToken = default) + { + if (file == null || file.Length == 0) + { + throw new BadRequestException("PDF file is required"); + } + + // Use IFormFile stream directly (no intermediate byte[] conversion) + using var pdfStream = file.OpenReadStream(); + + // Send command to MediatR + var command = new ConvertToPdfACommand + { + PdfStream = pdfStream, + PdfALevel = pdfALevel + }; + byte[] resultPdf = await mediator.Send(command, cancellationToken); + + // Return PDF/A file + return File(resultPdf, "application/pdf", "converted-pdfa.pdf"); + } + + /// + /// Converts a standard PDF to PDF/A format. + /// Supports Base64-encoded PDF via JSON payload. + /// + /// Request containing Base64-encoded PDF and PDF/A level + /// Cancellation token + /// PDF/A compliant document + /// PDF converted to PDF/A successfully + /// Invalid input (Base64 format error, not a PDF, or invalid PDF/A level) + /// Internal server error during PDF processing + [Obsolete("This endpoint is not implemented yet.")] + [HttpPost("to-pdfa")] + [Consumes("application/json")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task ConvertToPdfAFromBase64( + [FromBody] ConvertToPdfARequest request, + CancellationToken cancellationToken = default) + { + // Convert Base64 PDF to stream + byte[] pdfBytes; + try + { + pdfBytes = Convert.FromBase64String(request.Base64Pdf); + } + catch (FormatException ex) + { + throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message); + } + + using var pdfStream = new MemoryStream(pdfBytes); + + // Send command to MediatR + var command = new ConvertToPdfACommand + { + PdfStream = pdfStream, + PdfALevel = request.PdfALevel ?? "PDF/A-3b" + }; + byte[] resultPdf = await mediator.Send(command, cancellationToken); + + // Return PDF/A file + return File(resultPdf, "application/pdf", "converted-pdfa.pdf"); + } + + /// + /// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions). + /// Supports multipart/form-data file upload. + /// + /// The PDF/A file to convert + /// Cancellation token + /// Standard PDF document + /// PDF/A converted to standard PDF successfully + /// Invalid input (file missing, not a PDF) + /// Internal server error during PDF processing + [Obsolete("This endpoint is not implemented yet.")] + [HttpPost("from-pdfa")] + [Consumes("multipart/form-data")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task ConvertFromPdfAFromFile( + IFormFile file, + CancellationToken cancellationToken = default) + { + if (file == null || file.Length == 0) + { + throw new BadRequestException("PDF/A file is required"); + } + + // Use IFormFile stream directly (no intermediate byte[] conversion) + using var pdfStream = file.OpenReadStream(); + + // Send command to MediatR + var command = new ConvertFromPdfACommand { PdfStream = pdfStream }; + byte[] resultPdf = await mediator.Send(command, cancellationToken); + + // Return standard PDF file + return File(resultPdf, "application/pdf", "converted-pdf.pdf"); + } + + /// + /// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions). + /// Supports Base64-encoded PDF via JSON payload. + /// + /// Request containing Base64-encoded PDF/A + /// Cancellation token + /// Standard PDF document + /// PDF/A converted to standard PDF successfully + /// Invalid input (Base64 format error, not a PDF) + /// Internal server error during PDF processing + [Obsolete("This endpoint is not implemented yet.")] + [HttpPost("from-pdfa")] + [Consumes("application/json")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task ConvertFromPdfAFromBase64( + [FromBody] ConvertFromPdfARequest request, + CancellationToken cancellationToken = default) + { + // Convert Base64 PDF to stream + byte[] pdfBytes; + try + { + pdfBytes = Convert.FromBase64String(request.Base64Pdf); + } + catch (FormatException ex) + { + throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message); + } + + using var pdfStream = new MemoryStream(pdfBytes); + + // Send command to MediatR + var command = new ConvertFromPdfACommand { PdfStream = pdfStream }; + byte[] resultPdf = await mediator.Send(command, cancellationToken); + + // Return standard PDF file + return File(resultPdf, "application/pdf", "converted-pdf.pdf"); + } +} + +/// +/// Request DTO for converting PDF to PDF/A +/// +public record ConvertToPdfARequest +{ + /// + /// PDF document encoded as Base64 string + /// + /// JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c... + public required string Base64Pdf { get; init; } + + /// + /// Target PDF/A level (default: "PDF/A-3b") + /// + /// PDF/A-3b + public string? PdfALevel { get; init; } +} + +/// +/// Request DTO for converting PDF/A to PDF +/// +public record ConvertFromPdfARequest +{ + /// + /// PDF/A document encoded as Base64 string + /// + /// JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c... + public required string Base64Pdf { get; init; } +} diff --git a/DocumentOperator.Application/AddAttachments/AddAttachmentsCommand.cs b/DocumentOperator.Application/AddAttachments/AddAttachmentsCommand.cs new file mode 100644 index 0000000..604d909 --- /dev/null +++ b/DocumentOperator.Application/AddAttachments/AddAttachmentsCommand.cs @@ -0,0 +1,90 @@ +using DocumentOperator.Application.Common.Interfaces; +using FluentValidation; +using MediatR; + +namespace DocumentOperator.Application.AddAttachments; + +/// +/// Command to add one or more attachments to a PDF document (supports PDF/A-3) +/// +public record AddAttachmentsCommand : IRequest +{ + /// + /// PDF document stream. Must be positioned at the beginning (Position = 0). + /// + public required Stream PdfStream { get; init; } + + /// + /// List of attachments to embed (filename, content, optional MIME type) + /// + public required IReadOnlyList Attachments { get; init; } +} + +/// +/// Represents a file to be attached to a PDF +/// +public record AttachmentFile +{ + /// + /// File name (e.g., "invoice.xml", "document.pdf") + /// + public required string FileName { get; init; } + + /// + /// File content as byte array + /// + public required byte[] Content { get; init; } + + /// + /// MIME type (optional, e.g., "application/xml", "application/pdf") + /// If not provided, will be inferred from file extension + /// + public string? MimeType { get; init; } +} + +/// +/// Handler for AddAttachmentsCommand +/// +public class AddAttachmentsCommandHandler(IPdfProcessor pdfProcessor) + : IRequestHandler +{ + public async Task Handle(AddAttachmentsCommand request, CancellationToken cancellationToken) + { + // Convert to tuple list for IPdfProcessor + var attachmentTuples = request.Attachments + .Select(a => (a.FileName, a.Content, a.MimeType)) + .ToList(); + + return await pdfProcessor.AddAttachmentsAsync(request.PdfStream, attachmentTuples); + } +} + +/// +/// Validator for AddAttachmentsCommand +/// +public class AddAttachmentsCommandValidator : AbstractValidator +{ + public AddAttachmentsCommandValidator() + { + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PDF stream is required"); + + RuleFor(x => x.Attachments) + .NotNull() + .NotEmpty() + .WithMessage("At least one attachment is required"); + + RuleForEach(x => x.Attachments).ChildRules(attachment => + { + attachment.RuleFor(a => a.FileName) + .NotEmpty() + .WithMessage("Attachment file name is required"); + + attachment.RuleFor(a => a.Content) + .NotNull() + .NotEmpty() + .WithMessage("Attachment content is required"); + }); + } +} diff --git a/DocumentOperator.Application/Common/Interfaces/IPdfProcessor.cs b/DocumentOperator.Application/Common/Interfaces/IPdfProcessor.cs index ed7c1e3..e1c23da 100644 --- a/DocumentOperator.Application/Common/Interfaces/IPdfProcessor.cs +++ b/DocumentOperator.Application/Common/Interfaces/IPdfProcessor.cs @@ -145,4 +145,47 @@ public interface IPdfProcessor Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground, byte[]? imageBytes = null, Domain.Models.ValueObjects.PredefinedStampType? predefinedType = null); + + /// + /// Embeds one or more files as attachments in a PDF document (supports PDF/A-3). + /// + /// + /// PDF document stream. Must be readable and positioned at the beginning (Position = 0). + /// Non-seekable streams are supported. Caller is responsible for disposal. + /// + /// List of files to embed (filename, content, optional MIME type) + /// PDF with embedded attachments as byte array + /// + /// Thrown when stream is empty/invalid, or attachments list is empty + /// + Task AddAttachmentsAsync( + Stream pdfStream, + IReadOnlyList<(string FileName, byte[] Content, string? MimeType)> attachments); + + /// + /// Converts a standard PDF to PDF/A format. + /// + /// + /// PDF document stream. Must be readable and positioned at the beginning (Position = 0). + /// Non-seekable streams are supported. Caller is responsible for disposal. + /// + /// Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b") + /// PDF/A compliant document as byte array + /// + /// Thrown when stream is empty/invalid or PDF/A level is unsupported + /// + Task ConvertToPdfAAsync(Stream pdfStream, string pdfALevel); + + /// + /// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions). + /// + /// + /// PDF/A document stream. Must be readable and positioned at the beginning (Position = 0). + /// Non-seekable streams are supported. Caller is responsible for disposal. + /// + /// Standard PDF document as byte array + /// + /// Thrown when stream is empty/invalid + /// + Task ConvertFromPdfAAsync(Stream pdfStream); } \ No newline at end of file diff --git a/DocumentOperator.Application/ConvertFromPdfA/ConvertFromPdfACommand.cs b/DocumentOperator.Application/ConvertFromPdfA/ConvertFromPdfACommand.cs new file mode 100644 index 0000000..1ccfb2c --- /dev/null +++ b/DocumentOperator.Application/ConvertFromPdfA/ConvertFromPdfACommand.cs @@ -0,0 +1,41 @@ +using DocumentOperator.Application.Common.Interfaces; +using FluentValidation; +using MediatR; + +namespace DocumentOperator.Application.ConvertFromPdfA; + +/// +/// Command to convert a PDF/A document to a standard PDF (removes PDF/A restrictions) +/// +public record ConvertFromPdfACommand : IRequest +{ + /// + /// PDF/A document stream. Must be positioned at the beginning (Position = 0). + /// + public required Stream PdfStream { get; init; } +} + +/// +/// Handler for ConvertFromPdfACommand +/// +public class ConvertFromPdfACommandHandler(IPdfProcessor pdfProcessor) + : IRequestHandler +{ + public async Task Handle(ConvertFromPdfACommand request, CancellationToken cancellationToken) + { + return await pdfProcessor.ConvertFromPdfAAsync(request.PdfStream); + } +} + +/// +/// Validator for ConvertFromPdfACommand +/// +public class ConvertFromPdfACommandValidator : AbstractValidator +{ + public ConvertFromPdfACommandValidator() + { + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PDF stream is required"); + } +} diff --git a/DocumentOperator.Application/ConvertToPdfA/ConvertToPdfACommand.cs b/DocumentOperator.Application/ConvertToPdfA/ConvertToPdfACommand.cs new file mode 100644 index 0000000..4d4e533 --- /dev/null +++ b/DocumentOperator.Application/ConvertToPdfA/ConvertToPdfACommand.cs @@ -0,0 +1,59 @@ +using DocumentOperator.Application.Common.Interfaces; +using FluentValidation; +using MediatR; + +namespace DocumentOperator.Application.ConvertToPdfA; + +/// +/// Command to convert a standard PDF to PDF/A format +/// +public record ConvertToPdfACommand : IRequest +{ + /// + /// PDF document stream. Must be positioned at the beginning (Position = 0). + /// + public required Stream PdfStream { get; init; } + + /// + /// Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b") + /// + public required string PdfALevel { get; init; } +} + +/// +/// Handler for ConvertToPdfACommand +/// +public class ConvertToPdfACommandHandler(IPdfProcessor pdfProcessor) + : IRequestHandler +{ + public async Task Handle(ConvertToPdfACommand request, CancellationToken cancellationToken) + { + return await pdfProcessor.ConvertToPdfAAsync(request.PdfStream, request.PdfALevel); + } +} + +/// +/// Validator for ConvertToPdfACommand +/// +public class ConvertToPdfACommandValidator : AbstractValidator +{ + private static readonly string[] ValidPdfALevels = + { + "PDF/A-1b", "PDF/A-1a", + "PDF/A-2b", "PDF/A-2u", "PDF/A-2a", + "PDF/A-3b", "PDF/A-3u", "PDF/A-3a" + }; + + public ConvertToPdfACommandValidator() + { + RuleFor(x => x.PdfStream) + .NotNull() + .WithMessage("PDF stream is required"); + + RuleFor(x => x.PdfALevel) + .NotEmpty() + .WithMessage("PDF/A level is required") + .Must(level => ValidPdfALevels.Contains(level, StringComparer.OrdinalIgnoreCase)) + .WithMessage($"Invalid PDF/A level. Valid values: {string.Join(", ", ValidPdfALevels)}"); + } +} diff --git a/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs b/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs index cca97fc..80f140c 100644 --- a/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs +++ b/DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs @@ -1093,4 +1093,113 @@ public class DevExpressPdfProcessor : IPdfProcessor } #endregion + + #region Add Attachments (Phase 2) + + /// + /// Embeds one or more files as attachments in a PDF document (supports PDF/A-3). + /// + public async Task AddAttachmentsAsync( + Stream pdfStream, + IReadOnlyList<(string FileName, byte[] Content, string? MimeType)> attachments) + { + // 1. Validate input + ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream)); + ArgumentNullException.ThrowIfNull(attachments, nameof(attachments)); + + if (pdfStream.Length == 0) + throw new BadRequestException("PDF stream cannot be empty"); + + if (pdfStream.Position != 0) + throw new BadRequestException("PDF stream must be at position 0"); + + if (attachments.Count == 0) + throw new BadRequestException("At least one attachment is required"); + + // TODO: Implement AddFileAttachment using DevExpress.Pdf low-level API + // Current limitation: DevExpress.Pdf.PdfDocumentProcessor doesn't directly support adding attachments + // Workaround options: + // 1. Use PdfDocumentProcessor.Document to manipulate PDF structure directly (advanced) + // 2. Use third-party library for this specific operation + // 3. Wait for DevExpress API update + + throw new NotImplementedException( + "Add attachments feature is not yet implemented. " + + "DevExpress.Pdf high-level API doesn't directly support adding file attachments. " + + "This requires low-level PDF structure manipulation."); + } + + private string InferMimeType(string fileName) + { + string extension = Path.GetExtension(fileName).ToLowerInvariant(); + return extension switch + { + ".xml" => "application/xml", + ".pdf" => "application/pdf", + ".json" => "application/json", + ".txt" => "text/plain", + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + _ => "application/octet-stream" + }; + } + + #endregion + + #region PDF Conversion (Phase 3) + + /// + /// Converts a standard PDF to PDF/A format. + /// + public async Task ConvertToPdfAAsync(Stream pdfStream, string pdfALevel) + { + // 1. Validate input + ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream)); + + if (pdfStream.Length == 0) + throw new BadRequestException("PDF stream cannot be empty"); + + if (pdfStream.Position != 0) + throw new BadRequestException("PDF stream must be at position 0"); + + if (string.IsNullOrWhiteSpace(pdfALevel)) + throw new BadRequestException("PDF/A level is required"); + + // TODO: Implement PDF to PDF/A conversion using DevExpress + // Current limitation: DevExpress.Pdf.PdfDocumentProcessor doesn't directly support PDF/A conversion + // Requires using specialized PDF/A conversion libraries or low-level PDF manipulation + + throw new NotImplementedException( + $"PDF to PDF/A conversion ({pdfALevel}) is not yet implemented. " + + "DevExpress.Pdf high-level API doesn't directly support PDF/A conversion. " + + "This requires specialized PDF/A conversion logic."); + } + + /// + /// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions). + /// + public async Task ConvertFromPdfAAsync(Stream pdfStream) + { + // 1. Validate input + ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream)); + + if (pdfStream.Length == 0) + throw new BadRequestException("PDF stream cannot be empty"); + + if (pdfStream.Position != 0) + throw new BadRequestException("PDF stream must be at position 0"); + + // 2. Load PDF/A + using var processor = new PdfDocumentProcessor(); + processor.LoadDocument(pdfStream); + + // 3. Save as standard PDF + // DevExpress SaveDocument without special options creates standard PDF + using var outputStream = new MemoryStream(); + processor.SaveDocument(outputStream); + + return await Task.FromResult(outputStream.ToArray()); + } + + #endregion } diff --git a/DocumentOperator.Tests/TestData/Pdfs/attachment.xml b/DocumentOperator.Tests/TestData/Pdfs/attachment.xml new file mode 100644 index 0000000..ea78e6d --- /dev/null +++ b/DocumentOperator.Tests/TestData/Pdfs/attachment.xml @@ -0,0 +1,152 @@ + + + + + urn:cen.eu:en16931:2017 + + + + 2021_10 + 380 + + 20210924 + + + + + + 1 + + + Project management + + + + + 500.000000 + + + + 2.00 + + + + VAT + S + 19.00 + + + 1000.00 + + + + + + 2 + + + Consulting + + + + + 40.000000 + + + + 5.00 + + + + VAT + S + 19.00 + + + 200.00 + + + + + 139877 + + Webware Internet Solutions GmbH + + HRB 15635 + + + John Doe + + +49(0)561-560123456 + + + johndoe@webware24.de + + + + 34130 + Teichstr. 14-16 + Kassel + DE + + + DE279247134 + + + 262/481/0918 + + + DE279247134 + + + + Agoratech + + 34130 + Teichstr. 14-16 + Kassel + DE + + + DE319642369 + + + + + + + 20211101 + + + + + EUR + + 42 + + + + + + + + + 228 + VAT + 1200 + S + 19.00 + + + 1200 + 0 + 0 + 1200.00 + 228.00 + 1428.00 + 0.00 + 1428.00 + + + + diff --git a/DocumentOperator.Tests/TestData/Pdfs/withoutAttachment.pdf b/DocumentOperator.Tests/TestData/Pdfs/withoutAttachment.pdf new file mode 100644 index 0000000..eef028a Binary files /dev/null and b/DocumentOperator.Tests/TestData/Pdfs/withoutAttachment.pdf differ