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