Add PDF attachment and PDF/A conversion features

Introduced endpoints for embedding attachments in PDFs and converting
between standard PDFs and PDF/A formats. Added `PdfAttachmentController`
and `PdfConversionController` with multipart/form-data and Base64-based
support. Implemented commands, handlers, and validators for these
operations.

Extended `IPdfProcessor` with methods for adding attachments and
PDF/A conversion. Partially implemented functionality in
`DevExpressPdfProcessor`, including `ConvertFromPdfAAsync`.

Added `attachment.xml` and `withoutAttachment.pdf` as resources for
testing. Marked endpoints as `[Obsolete]` to indicate incomplete
implementation. Improved validation and error handling for commands.
This commit is contained in:
2026-07-30 13:43:55 +02:00
parent a242458d2f
commit a6694bfce7
9 changed files with 876 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
using DocumentOperator.Application.AddAttachments;
using DocumentOperator.Application.CheckPdfAttachments.Queries; using DocumentOperator.Application.CheckPdfAttachments.Queries;
using DocumentOperator.Application.Common.DTOs; using DocumentOperator.Application.Common.DTOs;
using DocumentOperator.Application.ExtractPdfAttachments; using DocumentOperator.Application.ExtractPdfAttachments;
@@ -157,6 +158,135 @@ public class PdfAttachmentController(IMediator mediator) : ControllerBase
// Return ZIP file // Return ZIP file
return File(zipBytes, "application/zip", "attachments.zip"); return File(zipBytes, "application/zip", "attachments.zip");
} }
/// <summary>
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
/// Supports multipart/form-data file upload.
/// </summary>
/// <param name="pdfFile">The PDF file to add attachments to</param>
/// <param name="attachmentFiles">Files to embed as attachments (one or more)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>PDF with embedded attachments</returns>
/// <response code="200">Attachments added successfully - returns PDF</response>
/// <response code="400">Invalid input (file missing, not a PDF, or no attachments provided)</response>
/// <response code="500">Internal server error during PDF processing</response>
[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<IActionResult> AddAttachmentsFromFile(
IFormFile pdfFile,
List<IFormFile> 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<AttachmentFile>();
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");
}
/// <summary>
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
/// Supports Base64-encoded PDF and attachments via JSON payload.
/// </summary>
/// <param name="request">Request containing Base64-encoded PDF and attachments</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>PDF with embedded attachments</returns>
/// <response code="200">Attachments added successfully - returns PDF</response>
/// <response code="400">Invalid input (Base64 format error, not a PDF, or no attachments provided)</response>
/// <response code="500">Internal server error during PDF processing</response>
[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<IActionResult> 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<AttachmentFile>();
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");
}
} }
/// <summary> /// <summary>
@@ -182,3 +312,44 @@ public record ExtractPdfAttachmentsRequest
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example> /// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; } public required string Base64Pdf { get; init; }
} }
/// <summary>
/// Request DTO for Base64-encoded PDF with attachments to add
/// </summary>
public record AddAttachmentsRequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// List of attachments to embed
/// </summary>
public required List<AttachmentRequestDto> Attachments { get; init; }
}
/// <summary>
/// DTO for attachment file in request
/// </summary>
public record AttachmentRequestDto
{
/// <summary>
/// File name (e.g., "invoice.xml", "document.pdf")
/// </summary>
/// <example>factur-x.xml</example>
public required string FileName { get; init; }
/// <summary>
/// File content encoded as Base64 string
/// </summary>
/// <example>PD94bWwgdmVyc2lvbj0iMS4wIj8+...</example>
public required string Base64Content { get; init; }
/// <summary>
/// MIME type (optional, e.g., "application/xml")
/// </summary>
/// <example>application/xml</example>
public string? MimeType { get; init; }
}

View File

@@ -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;
/// <summary>
/// Controller for PDF conversion operations (PDF ? PDF/A)
/// </summary>
[ApiController]
[Route("api/pdf/conversion")]
[Obsolete("This endpoint is not implemented yet.")]
public class PdfConversionController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Converts a standard PDF to PDF/A format.
/// Supports multipart/form-data file upload.
/// </summary>
/// <param name="file">The PDF file to convert</param>
/// <param name="pdfALevel">Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>PDF/A compliant document</returns>
/// <response code="200">PDF converted to PDF/A successfully</response>
/// <response code="400">Invalid input (file missing, not a PDF, or invalid PDF/A level)</response>
/// <response code="500">Internal server error during PDF processing</response>
[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<IActionResult> 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");
}
/// <summary>
/// Converts a standard PDF to PDF/A format.
/// Supports Base64-encoded PDF via JSON payload.
/// </summary>
/// <param name="request">Request containing Base64-encoded PDF and PDF/A level</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>PDF/A compliant document</returns>
/// <response code="200">PDF converted to PDF/A successfully</response>
/// <response code="400">Invalid input (Base64 format error, not a PDF, or invalid PDF/A level)</response>
/// <response code="500">Internal server error during PDF processing</response>
[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<IActionResult> 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");
}
/// <summary>
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
/// Supports multipart/form-data file upload.
/// </summary>
/// <param name="file">The PDF/A file to convert</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Standard PDF document</returns>
/// <response code="200">PDF/A converted to standard PDF successfully</response>
/// <response code="400">Invalid input (file missing, not a PDF)</response>
/// <response code="500">Internal server error during PDF processing</response>
[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<IActionResult> 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");
}
/// <summary>
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
/// Supports Base64-encoded PDF via JSON payload.
/// </summary>
/// <param name="request">Request containing Base64-encoded PDF/A</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Standard PDF document</returns>
/// <response code="200">PDF/A converted to standard PDF successfully</response>
/// <response code="400">Invalid input (Base64 format error, not a PDF)</response>
/// <response code="500">Internal server error during PDF processing</response>
[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<IActionResult> 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");
}
}
/// <summary>
/// Request DTO for converting PDF to PDF/A
/// </summary>
public record ConvertToPdfARequest
{
/// <summary>
/// PDF document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
/// <summary>
/// Target PDF/A level (default: "PDF/A-3b")
/// </summary>
/// <example>PDF/A-3b</example>
public string? PdfALevel { get; init; }
}
/// <summary>
/// Request DTO for converting PDF/A to PDF
/// </summary>
public record ConvertFromPdfARequest
{
/// <summary>
/// PDF/A document encoded as Base64 string
/// </summary>
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
public required string Base64Pdf { get; init; }
}

View File

@@ -0,0 +1,90 @@
using DocumentOperator.Application.Common.Interfaces;
using FluentValidation;
using MediatR;
namespace DocumentOperator.Application.AddAttachments;
/// <summary>
/// Command to add one or more attachments to a PDF document (supports PDF/A-3)
/// </summary>
public record AddAttachmentsCommand : IRequest<byte[]>
{
/// <summary>
/// PDF document stream. Must be positioned at the beginning (Position = 0).
/// </summary>
public required Stream PdfStream { get; init; }
/// <summary>
/// List of attachments to embed (filename, content, optional MIME type)
/// </summary>
public required IReadOnlyList<AttachmentFile> Attachments { get; init; }
}
/// <summary>
/// Represents a file to be attached to a PDF
/// </summary>
public record AttachmentFile
{
/// <summary>
/// File name (e.g., "invoice.xml", "document.pdf")
/// </summary>
public required string FileName { get; init; }
/// <summary>
/// File content as byte array
/// </summary>
public required byte[] Content { get; init; }
/// <summary>
/// MIME type (optional, e.g., "application/xml", "application/pdf")
/// If not provided, will be inferred from file extension
/// </summary>
public string? MimeType { get; init; }
}
/// <summary>
/// Handler for AddAttachmentsCommand
/// </summary>
public class AddAttachmentsCommandHandler(IPdfProcessor pdfProcessor)
: IRequestHandler<AddAttachmentsCommand, byte[]>
{
public async Task<byte[]> 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);
}
}
/// <summary>
/// Validator for AddAttachmentsCommand
/// </summary>
public class AddAttachmentsCommandValidator : AbstractValidator<AddAttachmentsCommand>
{
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");
});
}
}

View File

@@ -145,4 +145,47 @@ public interface IPdfProcessor
Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground, Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground,
byte[]? imageBytes = null, byte[]? imageBytes = null,
Domain.Models.ValueObjects.PredefinedStampType? predefinedType = null); Domain.Models.ValueObjects.PredefinedStampType? predefinedType = null);
/// <summary>
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <param name="attachments">List of files to embed (filename, content, optional MIME type)</param>
/// <returns>PDF with embedded attachments as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid, or attachments list is empty
/// </exception>
Task<byte[]> AddAttachmentsAsync(
Stream pdfStream,
IReadOnlyList<(string FileName, byte[] Content, string? MimeType)> attachments);
/// <summary>
/// Converts a standard PDF to PDF/A format.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <param name="pdfALevel">Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")</param>
/// <returns>PDF/A compliant document as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid or PDF/A level is unsupported
/// </exception>
Task<byte[]> ConvertToPdfAAsync(Stream pdfStream, string pdfALevel);
/// <summary>
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
/// </summary>
/// <param name="pdfStream">
/// PDF/A document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <returns>Standard PDF document as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid
/// </exception>
Task<byte[]> ConvertFromPdfAAsync(Stream pdfStream);
} }

View File

@@ -0,0 +1,41 @@
using DocumentOperator.Application.Common.Interfaces;
using FluentValidation;
using MediatR;
namespace DocumentOperator.Application.ConvertFromPdfA;
/// <summary>
/// Command to convert a PDF/A document to a standard PDF (removes PDF/A restrictions)
/// </summary>
public record ConvertFromPdfACommand : IRequest<byte[]>
{
/// <summary>
/// PDF/A document stream. Must be positioned at the beginning (Position = 0).
/// </summary>
public required Stream PdfStream { get; init; }
}
/// <summary>
/// Handler for ConvertFromPdfACommand
/// </summary>
public class ConvertFromPdfACommandHandler(IPdfProcessor pdfProcessor)
: IRequestHandler<ConvertFromPdfACommand, byte[]>
{
public async Task<byte[]> Handle(ConvertFromPdfACommand request, CancellationToken cancellationToken)
{
return await pdfProcessor.ConvertFromPdfAAsync(request.PdfStream);
}
}
/// <summary>
/// Validator for ConvertFromPdfACommand
/// </summary>
public class ConvertFromPdfACommandValidator : AbstractValidator<ConvertFromPdfACommand>
{
public ConvertFromPdfACommandValidator()
{
RuleFor(x => x.PdfStream)
.NotNull()
.WithMessage("PDF stream is required");
}
}

View File

@@ -0,0 +1,59 @@
using DocumentOperator.Application.Common.Interfaces;
using FluentValidation;
using MediatR;
namespace DocumentOperator.Application.ConvertToPdfA;
/// <summary>
/// Command to convert a standard PDF to PDF/A format
/// </summary>
public record ConvertToPdfACommand : IRequest<byte[]>
{
/// <summary>
/// PDF document stream. Must be positioned at the beginning (Position = 0).
/// </summary>
public required Stream PdfStream { get; init; }
/// <summary>
/// Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")
/// </summary>
public required string PdfALevel { get; init; }
}
/// <summary>
/// Handler for ConvertToPdfACommand
/// </summary>
public class ConvertToPdfACommandHandler(IPdfProcessor pdfProcessor)
: IRequestHandler<ConvertToPdfACommand, byte[]>
{
public async Task<byte[]> Handle(ConvertToPdfACommand request, CancellationToken cancellationToken)
{
return await pdfProcessor.ConvertToPdfAAsync(request.PdfStream, request.PdfALevel);
}
}
/// <summary>
/// Validator for ConvertToPdfACommand
/// </summary>
public class ConvertToPdfACommandValidator : AbstractValidator<ConvertToPdfACommand>
{
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)}");
}
}

View File

@@ -1093,4 +1093,113 @@ public class DevExpressPdfProcessor : IPdfProcessor
} }
#endregion #endregion
#region Add Attachments (Phase 2)
/// <summary>
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
/// </summary>
public async Task<byte[]> 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)
/// <summary>
/// Converts a standard PDF to PDF/A format.
/// </summary>
public async Task<byte[]> 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.");
}
/// <summary>
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
/// </summary>
public async Task<byte[]> 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
} }

View File

@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 ../../../schemas/UN_CEFACT/CrossIndustryInvoice_100pD16B.xsd">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>2021_10</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20210924</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Project management</ram:Name>
<ram:Description/>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>500.000000</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">2.00</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>1000.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Consulting</ram:Name>
<ram:Description/>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount>40.000000</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">5.00</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:BuyerReference>139877</ram:BuyerReference>
<ram:SellerTradeParty>
<ram:Name>Webware Internet Solutions GmbH</ram:Name>
<ram:SpecifiedLegalOrganization>
<ram:ID>HRB 15635</ram:ID>
</ram:SpecifiedLegalOrganization>
<ram:DefinedTradeContact>
<ram:PersonName>John Doe</ram:PersonName>
<ram:TelephoneUniversalCommunication>
<ram:CompleteNumber>+49(0)561-560123456</ram:CompleteNumber>
</ram:TelephoneUniversalCommunication>
<ram:EmailURIUniversalCommunication>
<ram:URIID>johndoe@webware24.de</ram:URIID>
</ram:EmailURIUniversalCommunication>
</ram:DefinedTradeContact>
<ram:PostalTradeAddress>
<ram:PostcodeCode>34130</ram:PostcodeCode>
<ram:LineOne>Teichstr. 14-16</ram:LineOne>
<ram:CityName>Kassel</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:URIUniversalCommunication>
<ram:URIID schemeID="9930">DE279247134</ram:URIID>
</ram:URIUniversalCommunication>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="FC">262/481/0918</ram:ID>
</ram:SpecifiedTaxRegistration>
<ram:SpecifiedTaxRegistration>
<ram:ID schemeID="VA">DE279247134</ram:ID>
</ram:SpecifiedTaxRegistration>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Agoratech</ram:Name>
<ram:PostalTradeAddress>
<ram:PostcodeCode>34130</ram:PostcodeCode>
<ram:LineOne>Teichstr. 14-16</ram:LineOne>
<ram:CityName>Kassel</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
<ram:URIUniversalCommunication>
<ram:URIID schemeID="9930">DE319642369</ram:URIID>
</ram:URIUniversalCommunication>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
<ram:ActualDeliverySupplyChainEvent>
<ram:OccurrenceDateTime>
<udt:DateTimeString format="102">20211101</udt:DateTimeString>
</ram:OccurrenceDateTime>
</ram:ActualDeliverySupplyChainEvent>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>42</ram:TypeCode>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID/>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID/>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount>228</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>1200</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount>1200</ram:LineTotalAmount>
<ram:ChargeTotalAmount>0</ram:ChargeTotalAmount>
<ram:AllowanceTotalAmount>0</ram:AllowanceTotalAmount>
<ram:TaxBasisTotalAmount>1200.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">228.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount>1428.00</ram:GrandTotalAmount>
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
<ram:DuePayableAmount>1428.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>