Compare commits
16 Commits
9cb964379f
...
d1fdbb494b
| Author | SHA1 | Date | |
|---|---|---|---|
| d1fdbb494b | |||
| 14645514d9 | |||
| 2c98558131 | |||
| af0fca198a | |||
| 75bea9ef29 | |||
| 108e522316 | |||
| 95d67567f0 | |||
| 207c778f3a | |||
| 050cb55bf9 | |||
| 7559028164 | |||
| fbaefe1fd2 | |||
| 9040aed1da | |||
| 9a9d775fec | |||
| 2630860e7e | |||
| 7b9318e75c | |||
| ef78fb5cdd |
@@ -1,34 +0,0 @@
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// PDF/A validation metadata including conformance level and validation errors/warnings
|
||||
/// </summary>
|
||||
public sealed class PdfAMetadata(
|
||||
bool isValid,
|
||||
string pdfVersion,
|
||||
int pageCount,
|
||||
long fileSizeBytes,
|
||||
bool encrypted,
|
||||
string? pdfaVersion,
|
||||
bool pdfaCompliant,
|
||||
IReadOnlyList<string> errors,
|
||||
IReadOnlyList<string> warnings)
|
||||
{
|
||||
public bool IsValid { get; } = isValid;
|
||||
public string PdfVersion { get; } = pdfVersion;
|
||||
public int PageCount { get; } = pageCount;
|
||||
public long FileSizeBytes { get; } = fileSizeBytes;
|
||||
public bool Encrypted { get; } = encrypted;
|
||||
public string? PdfAVersion { get; } = pdfaVersion;
|
||||
public bool PdfACompliant { get; } = pdfaCompliant;
|
||||
public IReadOnlyList<string> Errors { get; } = errors;
|
||||
public IReadOnlyList<string> Warnings { get; } = warnings;
|
||||
|
||||
// Computed property
|
||||
public double FileSizeMB => FileSizeBytes / 1024.0 / 1024.0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"PDF/A: {PdfAVersion ?? "None"}, {PageCount} pages, {FileSizeMB:F2} MB, Compliant: {PdfACompliant}";
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
public sealed class PdfMetadata(
|
||||
int pageCount,
|
||||
long fileSizeBytes,
|
||||
string pdfVersion,
|
||||
bool hasAttachments,
|
||||
int attachmentCount)
|
||||
{
|
||||
public int PageCount { get; } = pageCount;
|
||||
public long FileSizeBytes { get; } = fileSizeBytes;
|
||||
public string PdfVersion { get; } = pdfVersion;
|
||||
public bool HasAttachments { get; } = hasAttachments;
|
||||
public int AttachmentCount { get; } = attachmentCount;
|
||||
|
||||
// Computed Property (berechnet aus FileSizeBytes)
|
||||
public double FileSizeMB => FileSizeBytes / 1024.0 / 1024.0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"PDF: {PageCount} pages, {FileSizeMB:F2} MB, Version {PdfVersion}, Attachments: {AttachmentCount}";
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="DependencyInjection\**" />
|
||||
<Compile Remove="Features\**" />
|
||||
<EmbeddedResource Remove="DependencyInjection\**" />
|
||||
<EmbeddedResource Remove="Features\**" />
|
||||
<None Remove="DependencyInjection\**" />
|
||||
<None Remove="Features\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
<PackageReference Include="Codecrete.SwissQRBill.Generator" Version="3.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Common\Mappings\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,91 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Application.ValidatePdf.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentService.Tests.Unit.Application.Features.ValidatePdf;
|
||||
|
||||
public class ValidatePdfHandlerTests
|
||||
{
|
||||
private readonly Mock<IPdfProcessor> _mockPdfProcessor;
|
||||
private readonly Mock<IMapper> _mockMapper;
|
||||
private readonly ValidatePdfQueryHandler _handler;
|
||||
|
||||
public ValidatePdfHandlerTests()
|
||||
{
|
||||
_mockPdfProcessor = new Mock<IPdfProcessor>();
|
||||
_mockMapper = new Mock<IMapper>();
|
||||
_handler = new ValidatePdfQueryHandler(_mockPdfProcessor.Object, _mockMapper.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidPdf_ReturnsPdfMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = "%PDF"u8.ToArray(); // "%PDF"
|
||||
var query = new ValidatePdfQuery { PdfStream = new MemoryStream(pdfBytes) };
|
||||
|
||||
var domainMetadata = new PdfMetadata(
|
||||
pageCount: 5,
|
||||
fileSizeBytes: 1024,
|
||||
pdfVersion: "1.4",
|
||||
hasAttachments: false,
|
||||
attachmentCount: 0
|
||||
);
|
||||
|
||||
var expectedDto = new PdfValidationResult(
|
||||
PageCount: 5,
|
||||
FileSizeBytes: 1024,
|
||||
FileSizeMB: 0.00,
|
||||
PdfVersion: "1.4",
|
||||
HasAttachments: false,
|
||||
AttachmentCount: 0
|
||||
);
|
||||
|
||||
_mockPdfProcessor
|
||||
.Setup(x => x.ValidateAsync(It.IsAny<Stream>()))
|
||||
.ReturnsAsync(domainMetadata);
|
||||
|
||||
_mockMapper
|
||||
.Setup(x => x.Map<PdfValidationResult>(domainMetadata))
|
||||
.Returns(expectedDto);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.PageCount.Should().Be(5);
|
||||
result.FileSizeBytes.Should().Be(1024);
|
||||
result.PdfVersion.Should().Be("1.4");
|
||||
result.HasAttachments.Should().BeFalse();
|
||||
result.AttachmentCount.Should().Be(0);
|
||||
|
||||
_mockPdfProcessor.Verify(x => x.ValidateAsync(It.IsAny<Stream>()), Times.Once);
|
||||
_mockMapper.Verify(x => x.Map<PdfValidationResult>(domainMetadata), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PdfProcessorThrowsException_PropagatesException()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = "%PDF"u8.ToArray(); // "%PDF"
|
||||
var query = new ValidatePdfQuery { PdfStream = new MemoryStream(pdfBytes) };
|
||||
|
||||
_mockPdfProcessor
|
||||
.Setup(x => x.ValidateAsync(It.IsAny<Stream>()))
|
||||
.ThrowsAsync(new BadRequestException("Invalid PDF format"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => _handler.Handle(query, CancellationToken.None)
|
||||
);
|
||||
|
||||
exception.Message.Should().Be("Invalid PDF format");
|
||||
_mockPdfProcessor.Verify(x => x.ValidateAsync(It.IsAny<Stream>()), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Application.ValidatePdfA.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentService.Tests.Unit.Application.Features.ValidatePdfA;
|
||||
|
||||
public class ValidatePdfAQueryHandlerTests
|
||||
{
|
||||
private readonly Mock<IPdfProcessor> _mockPdfProcessor;
|
||||
private readonly Mock<IMapper> _mockMapper;
|
||||
private readonly ValidatePdfAQueryHandler _handler;
|
||||
|
||||
public ValidatePdfAQueryHandlerTests()
|
||||
{
|
||||
_mockPdfProcessor = new Mock<IPdfProcessor>();
|
||||
_mockMapper = new Mock<IMapper>();
|
||||
_handler = new ValidatePdfAQueryHandler(_mockPdfProcessor.Object, _mockMapper.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidPdfA_ReturnsPdfAMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = "%PDF"u8.ToArray(); // "%PDF"
|
||||
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
|
||||
|
||||
var domainMetadata = new PdfAMetadata(
|
||||
isValid: true,
|
||||
pdfVersion: "1.7",
|
||||
pageCount: 3,
|
||||
fileSizeBytes: 2048,
|
||||
encrypted: false,
|
||||
pdfaVersion: "PDF/A-3b",
|
||||
pdfaCompliant: true,
|
||||
errors: [],
|
||||
warnings: []
|
||||
);
|
||||
|
||||
var expectedDto = new PdfAValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
PdfVersion = "1.7",
|
||||
PageCount = 3,
|
||||
FileSize = 2048,
|
||||
Encrypted = false,
|
||||
PdfAVersion = "PDF/A-3b",
|
||||
PdfACompliant = true,
|
||||
Errors = new List<string>(),
|
||||
Warnings = new List<string>()
|
||||
};
|
||||
|
||||
_mockPdfProcessor
|
||||
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
|
||||
.ReturnsAsync(domainMetadata);
|
||||
|
||||
_mockMapper
|
||||
.Setup(x => x.Map<PdfAValidationResult>(domainMetadata))
|
||||
.Returns(expectedDto);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.PageCount.Should().Be(3);
|
||||
result.PdfVersion.Should().Be("1.7");
|
||||
result.PdfAVersion.Should().Be("PDF/A-3b");
|
||||
result.PdfACompliant.Should().BeTrue();
|
||||
result.Encrypted.Should().BeFalse();
|
||||
result.Errors.Should().BeEmpty();
|
||||
result.Warnings.Should().BeEmpty();
|
||||
|
||||
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
|
||||
_mockMapper.Verify(x => x.Map<PdfAValidationResult>(domainMetadata), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NonCompliantPdfA_ReturnsErrorsAndWarnings()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF"
|
||||
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
|
||||
|
||||
var errors = new List<string> { "Missing XMP metadata", "Invalid color space" };
|
||||
var warnings = new List<string> { "Embedded font not subset" };
|
||||
|
||||
var domainMetadata = new PdfAMetadata(
|
||||
isValid: false,
|
||||
pdfVersion: "1.4",
|
||||
pageCount: 2,
|
||||
fileSizeBytes: 1024,
|
||||
encrypted: false,
|
||||
pdfaVersion: null,
|
||||
pdfaCompliant: false,
|
||||
errors: errors,
|
||||
warnings: warnings
|
||||
);
|
||||
|
||||
var expectedDto = new PdfAValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
PdfVersion = "1.4",
|
||||
PageCount = 2,
|
||||
FileSize = 1024,
|
||||
Encrypted = false,
|
||||
PdfAVersion = null,
|
||||
PdfACompliant = false,
|
||||
Errors = errors,
|
||||
Warnings = warnings
|
||||
};
|
||||
|
||||
_mockPdfProcessor
|
||||
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
|
||||
.ReturnsAsync(domainMetadata);
|
||||
|
||||
_mockMapper
|
||||
.Setup(x => x.Map<PdfAValidationResult>(domainMetadata))
|
||||
.Returns(expectedDto);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.PdfACompliant.Should().BeFalse();
|
||||
result.PdfAVersion.Should().BeNull();
|
||||
result.Errors.Should().HaveCount(2);
|
||||
result.Errors.Should().Contain("Missing XMP metadata");
|
||||
result.Warnings.Should().HaveCount(1);
|
||||
|
||||
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_EncryptedPdf_ReturnsEncryptedFlag()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF"
|
||||
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
|
||||
|
||||
var domainMetadata = new PdfAMetadata(
|
||||
isValid: true,
|
||||
pdfVersion: "1.7",
|
||||
pageCount: 1,
|
||||
fileSizeBytes: 512,
|
||||
encrypted: true,
|
||||
pdfaVersion: null,
|
||||
pdfaCompliant: false,
|
||||
errors: new List<string> { "Encrypted PDFs cannot be PDF/A compliant" },
|
||||
warnings: new List<string>()
|
||||
);
|
||||
|
||||
var expectedDto = new PdfAValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
PdfVersion = "1.7",
|
||||
PageCount = 1,
|
||||
FileSize = 512,
|
||||
Encrypted = true,
|
||||
PdfAVersion = null,
|
||||
PdfACompliant = false,
|
||||
Errors = new List<string> { "Encrypted PDFs cannot be PDF/A compliant" },
|
||||
Warnings = new List<string>()
|
||||
};
|
||||
|
||||
_mockPdfProcessor
|
||||
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
|
||||
.ReturnsAsync(domainMetadata);
|
||||
|
||||
_mockMapper
|
||||
.Setup(x => x.Map<PdfAValidationResult>(domainMetadata))
|
||||
.Returns(expectedDto);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(query, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Encrypted.Should().BeTrue();
|
||||
result.PdfACompliant.Should().BeFalse();
|
||||
result.Errors.Should().Contain("Encrypted PDFs cannot be PDF/A compliant");
|
||||
|
||||
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PdfProcessorThrowsException_PropagatesException()
|
||||
{
|
||||
// Arrange
|
||||
var pdfBytes = new byte[] { 0x25, 0x50, 0x44, 0x46 }; // "%PDF"
|
||||
var query = new ValidatePdfAQuery { PdfStream = new MemoryStream(pdfBytes) };
|
||||
|
||||
_mockPdfProcessor
|
||||
.Setup(x => x.ValidatePdfAAsync(It.IsAny<Stream>()))
|
||||
.ThrowsAsync(new BadRequestException("Invalid PDF format"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => _handler.Handle(query, CancellationToken.None)
|
||||
);
|
||||
|
||||
exception.Message.Should().Be("Invalid PDF format");
|
||||
_mockPdfProcessor.Verify(x => x.ValidatePdfAAsync(It.IsAny<Stream>()), Times.Once);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
using DocumentService.Client.Clients;
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DocumentService.Tests.Unit.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="PdfValidationClient"/>.
|
||||
/// All tests use a fake <see cref="MockHttpMessageHandler"/> — no real HTTP calls are made.
|
||||
/// </summary>
|
||||
public class PdfValidationClientTests
|
||||
{
|
||||
// ?? helpers ?????????????????????????????????????????????????????????????
|
||||
|
||||
private static (PdfValidationClient client, MockHttpMessageHandler handler) Build<T>(T responseBody)
|
||||
{
|
||||
var handler = MockHttpMessageHandler.ReturningJson(responseBody);
|
||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
||||
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
|
||||
return (client, handler);
|
||||
}
|
||||
|
||||
private static byte[] FakePdfBytes() => "fake-pdf-content"u8.ToArray();
|
||||
|
||||
// ?? ValidatePdfAsync (Stream) ????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Stream_SendsMultipartPost()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new PdfValidationResult { PageCount = 3, PdfVersion = "1.7", FileSizeBytes = 2048 };
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ValidatePdfAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.PageCount.Should().Be(3);
|
||||
result.PdfVersion.Should().Be("1.7");
|
||||
handler.LastRequest!.Method.Should().Be(HttpMethod.Post);
|
||||
handler.LastRequest.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate");
|
||||
handler.LastRequest.Content.Should().BeOfType<MultipartFormDataContent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Stream_ThrowsWhenApiReturnsNull()
|
||||
{
|
||||
// Arrange — API returns JSON null
|
||||
var handler = MockHttpMessageHandler.ReturningJson<PdfValidationResult?>(null);
|
||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
||||
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
// ?? ValidatePdfAsync (byte[]) ????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_Bytes_SendsJsonWithBase64()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new PdfValidationResult { PageCount = 1, IsEncrypted = false };
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ValidatePdfAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
result.PageCount.Should().Be(1);
|
||||
handler.LastRequest!.Content.Should().NotBeNull();
|
||||
handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json");
|
||||
|
||||
var body = await handler.LastRequest.Content!.ReadAsStringAsync();
|
||||
var doc = JsonDocument.Parse(body);
|
||||
doc.RootElement.GetProperty("base64Pdf").GetString().Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
// ?? ValidatePdfAAsync (Stream) ???????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAAsync_Stream_SendsMultipartPost()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new PdfAValidationResult { IsValid = true, PdfAVersion = "PDF/A-3b", PageCount = 2 };
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ValidatePdfAAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.PdfAVersion.Should().Be("PDF/A-3b");
|
||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate-pdfa");
|
||||
handler.LastRequest.Content.Should().BeOfType<MultipartFormDataContent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAAsync_Stream_WithErrors_ReturnsErrors()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new PdfAValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Errors = new List<string> { "Missing embedded font", "Encryption not allowed" }
|
||||
};
|
||||
var (client, _) = Build(expected);
|
||||
|
||||
// Act
|
||||
var result = await client.ValidatePdfAAsync(new MemoryStream(FakePdfBytes()));
|
||||
|
||||
// Assert
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Should().HaveCount(2).And.Contain("Missing embedded font");
|
||||
}
|
||||
|
||||
// ?? ValidatePdfAAsync (byte[]) ???????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAAsync_Bytes_SendsJson()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new PdfAValidationResult { IsValid = true };
|
||||
var (client, handler) = Build(expected);
|
||||
|
||||
// Act
|
||||
await client.ValidatePdfAAsync(FakePdfBytes());
|
||||
|
||||
// Assert
|
||||
handler.LastRequest!.RequestUri!.PathAndQuery.Should().Be("/api/pdf/validation/validate-pdfa");
|
||||
handler.LastRequest.Content.Should().NotBeNull();
|
||||
handler.LastRequest.Content!.Headers.ContentType!.MediaType.Should().Be("application/json");
|
||||
}
|
||||
|
||||
// ?? HTTP error propagation ???????????????????????????????????????????????
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_WhenApiReturns400_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.BadRequest);
|
||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
||||
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidatePdfAsync_WhenApiReturns500_ThrowsHttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var handler = MockHttpMessageHandler.ReturningStatus(HttpStatusCode.InternalServerError);
|
||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") };
|
||||
var client = new PdfValidationClient(httpClient, NullLogger<PdfValidationClient>.Instance);
|
||||
|
||||
// Act & Assert
|
||||
await client.Invoking(c => c.ValidatePdfAsync(new MemoryStream(FakePdfBytes())))
|
||||
.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using DocumentService.Application.AddAttachments;
|
||||
using DocumentService.Application.CheckPdfAttachments.Queries;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractPdfAttachments;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -290,67 +290,3 @@ public class PdfAttachmentController(IMediator mediator) : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF attachment check
|
||||
/// </summary>
|
||||
public record CheckPdfAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public string Base64Pdf { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF attachment extraction
|
||||
/// </summary>
|
||||
public record ExtractPdfAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
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; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using DocumentService.Application.ConvertFromPdfA;
|
||||
using DocumentService.Application.ConvertToPdfA;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -1,5 +1,6 @@
|
||||
using DocumentService.Application.AddAnnotation;
|
||||
using DocumentService.Application.AddStamp;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using DocumentService.Application.MergePdfs;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
@@ -158,7 +159,7 @@ public class PdfOperationsController(IMediator mediator) : ControllerBase
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AnnotateFromBase64(
|
||||
[FromBody] AddAnnotationBase64Command command,
|
||||
[FromBody] AddAnnotationBase64Request command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to MemoryStream
|
||||
@@ -404,116 +405,6 @@ public class AddAnnotationMultipartRequest
|
||||
public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF merge operation (API layer only - converts to MergePdfsCommand)
|
||||
/// </summary>
|
||||
public record MergePdfsBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Array of Base64-encoded PDF files (minimum 2 required)
|
||||
/// </summary>
|
||||
/// <example>["JVBERi0xLjQK...", "JVBERi0xLjQK..."]</example>
|
||||
public required List<string> Base64Pdfs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional page ranges per PDF (null = all pages).
|
||||
/// Format: "1-3,5" means pages 1, 2, 3, and 5.
|
||||
/// If provided, array length must match Base64Pdfs length.
|
||||
/// </summary>
|
||||
/// <example>["1-2", "1,3,5", null]</example>
|
||||
public List<string?>? PageRanges { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF annotation (API layer only - converts to AddAnnotationCommand)
|
||||
/// </summary>
|
||||
public record AddAnnotationBase64Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded PDF file
|
||||
/// </summary>
|
||||
/// <example>"JVBERi0xLjQK..."</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of annotation to add
|
||||
/// </summary>
|
||||
/// <example>TextMarkup</example>
|
||||
public required AnnotationType AnnotationType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target page number (1-indexed)
|
||||
/// </summary>
|
||||
/// <example>1</example>
|
||||
public required int PageNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X1 coordinate (left)
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double X1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y1 coordinate (top or bottom depending on Origin)
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double Y1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle X2 coordinate (right). Optional if Width is provided.
|
||||
/// </summary>
|
||||
/// <example>200.0</example>
|
||||
public double? X2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle Y2 coordinate (bottom or top depending on Origin). Optional if Height is provided.
|
||||
/// </summary>
|
||||
/// <example>120.0</example>
|
||||
public double? Y2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle width. Alternative to X2 (X2 = X1 + Width). Optional if X2 is provided.
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle height. Alternative to Y2 (Y2 = Y1 + Height). Optional if Y2 is provided.
|
||||
/// </summary>
|
||||
/// <example>20.0</example>
|
||||
public double? Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation content (required for FreeText and StickyNote)
|
||||
/// </summary>
|
||||
/// <example>"Important text to highlight"</example>
|
||||
public string? Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Author name (optional)
|
||||
/// </summary>
|
||||
/// <example>"John Doe"</example>
|
||||
public string? Author { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hex color (6 digits, e.g., "FF0000" for red). Optional - defaults vary by annotation type.
|
||||
/// </summary>
|
||||
/// <example>"FFFF00"</example>
|
||||
public string? Color { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Text markup style (Highlight, Underline, or Strikeout). Required for TextMarkup annotations.
|
||||
/// </summary>
|
||||
/// <example>Highlight</example>
|
||||
public TextMarkupStyle? TextMarkupStyle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
|
||||
/// </summary>
|
||||
/// <example>BottomLeft</example>
|
||||
public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for multipart/form-data stamp operation
|
||||
/// </summary>
|
||||
@@ -618,111 +509,3 @@ public class AddStampMultipartRequest
|
||||
/// <example>Confidential</example>
|
||||
public PredefinedStampType? PredefinedType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF stamp operation (API layer only - converts to AddStampCommand)
|
||||
/// </summary>
|
||||
public record AddStampBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded PDF file
|
||||
/// </summary>
|
||||
/// <example>"JVBERi0xLjQK..."</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of stamp (Text, Image, or Predefined)
|
||||
/// </summary>
|
||||
/// <example>Text</example>
|
||||
public required StampType StampType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target page numbers (1-indexed). Null or empty = all pages.
|
||||
/// </summary>
|
||||
/// <example>[1, 3, 5]</example>
|
||||
public int[]? PageNumbers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp position X coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp position Y coordinate
|
||||
/// </summary>
|
||||
/// <example>100.0</example>
|
||||
public required double Y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp width (optional, auto-size for images if not specified)
|
||||
/// </summary>
|
||||
/// <example>200.0</example>
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp height (optional, auto-size for images if not specified)
|
||||
/// </summary>
|
||||
/// <example>50.0</example>
|
||||
public double? Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft
|
||||
/// </summary>
|
||||
/// <example>BottomLeft</example>
|
||||
public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Text content (required for Text stamps)
|
||||
/// </summary>
|
||||
/// <example>"CONFIDENTIAL"</example>
|
||||
public string? Text { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Font name (default: Arial)
|
||||
/// </summary>
|
||||
/// <example>"Arial"</example>
|
||||
public string? FontName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Font size in points (default: 12)
|
||||
/// </summary>
|
||||
/// <example>24.0</example>
|
||||
public double? FontSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hex color (6 digits, e.g., "FF0000" for red, default: "000000")
|
||||
/// </summary>
|
||||
/// <example>"FF0000"</example>
|
||||
public string? Color { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Opacity (0.0 = transparent, 1.0 = opaque, default: 0.5)
|
||||
/// </summary>
|
||||
/// <example>0.5</example>
|
||||
public double? Opacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation angle in degrees (0-360, default: 0)
|
||||
/// </summary>
|
||||
/// <example>45.0</example>
|
||||
public double? Rotation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stamp placement (Foreground = on top, Background = watermark effect, default: Foreground)
|
||||
/// </summary>
|
||||
/// <example>Foreground</example>
|
||||
public StampPlacement Placement { get; init; } = StampPlacement.Foreground;
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded image (required for Image stamps, PNG/JPEG)
|
||||
/// </summary>
|
||||
/// <example>"iVBORw0KGgoAAAANSUhEUgAA..."</example>
|
||||
public string? Base64Image { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Predefined stamp type (required for Predefined stamps)
|
||||
/// </summary>
|
||||
/// <example>Confidential</example>
|
||||
public PredefinedStampType? PredefinedType { get; init; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ValidatePdf.Queries;
|
||||
using DocumentService.Application.ValidatePdfA.Queries;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -1,4 +1,5 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using DocumentService.Application.SwissQrCode.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
@@ -100,14 +101,3 @@ public class SwissQrCodeController(IMediator Mediator) : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded Swiss QR Code extraction
|
||||
/// </summary>
|
||||
public record ExtractSwissQrCodeBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractZugferd;
|
||||
using DocumentService.Application.HasZugferd.Queries;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -9,10 +9,10 @@
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>DocumentOperator.API</Product>
|
||||
<Version>1.0.0</Version>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<InformationalVersion>1.0.0</InformationalVersion>
|
||||
<Version>1.1.0</Version>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<InformationalVersion>1.1.0</InformationalVersion>
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
<Description>PDF Document Operations REST API - Validation, Swiss QR Code extraction, attachments, merge, annotation, stamp operations powered by DevExpress Office File API</Description>
|
||||
<PackageTags>pdf document operator validation swiss-qr-code annotations stamp devexpress</PackageTags>
|
||||
@@ -32,10 +32,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentService.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Infrastructure\DocumentService.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentService.Client\DocumentService.Client.csproj" />
|
||||
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentService.Infrastructure\DocumentService.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -9,10 +9,12 @@ namespace DocumentService.Application.Common.DTOs;
|
||||
/// <param name="PdfVersion">PDF-Version (z.B. "1.4")</param>
|
||||
/// <param name="HasAttachments">Hat das PDF Anhänge?</param>
|
||||
/// <param name="AttachmentCount">Anzahl der Anhänge</param>
|
||||
/// <param name="IsEncrypted">Ist das PDF passwortgeschützt/verschlüsselt?</param>
|
||||
public record PdfValidationResult(
|
||||
int PageCount,
|
||||
long FileSizeBytes,
|
||||
double FileSizeMB,
|
||||
string PdfVersion,
|
||||
bool HasAttachments,
|
||||
int AttachmentCount);
|
||||
int AttachmentCount,
|
||||
bool IsEncrypted);
|
||||
@@ -1,40 +1,20 @@
|
||||
namespace DocumentService.Client.Models.Requests;
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF attachment check
|
||||
/// </summary>
|
||||
public record CheckPdfAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
public record CheckPdfAttachmentsRequest : PdfBase64RequestBase;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF attachment extraction
|
||||
/// </summary>
|
||||
public record ExtractPdfAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
public record ExtractPdfAttachmentsRequest : PdfBase64RequestBase;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF with attachments to add
|
||||
/// </summary>
|
||||
public record AddAttachmentsRequest
|
||||
public record AddAttachmentsRequest : PdfBase64RequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of attachments to embed
|
||||
/// </summary>
|
||||
@@ -42,7 +22,7 @@ public record AddAttachmentsRequest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for attachment file in request
|
||||
/// DTO for a single attachment file in a request
|
||||
/// </summary>
|
||||
public record AttachmentRequestDto
|
||||
{
|
||||
@@ -1,9 +1,10 @@
|
||||
namespace DocumentService.Client.Models.Requests;
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded Swiss QR Code extraction
|
||||
/// Base record for all single-PDF Base64 request types.
|
||||
/// Provides the common <see cref="Base64Pdf"/> property shared across endpoints.
|
||||
/// </summary>
|
||||
public record ExtractSwissQrCodeBase64Request
|
||||
public abstract record PdfBase64RequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for converting a standard PDF to PDF/A format (Base64 JSON)
|
||||
/// </summary>
|
||||
public record ConvertToPdfARequest : PdfBase64RequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Target PDF/A conformance level. Defaults to "PDF/A-3b".
|
||||
/// </summary>
|
||||
/// <example>PDF/A-3b</example>
|
||||
public string? PdfALevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for converting a PDF/A document back to standard PDF (Base64 JSON)
|
||||
/// </summary>
|
||||
public record ConvertFromPdfARequest : PdfBase64RequestBase;
|
||||
@@ -1,6 +1,6 @@
|
||||
using DocumentService.Client.Models.ValueObjects;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
namespace DocumentService.Client.Models.Requests;
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF merge operation
|
||||
@@ -25,14 +25,8 @@ public record MergePdfsBase64Request
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF annotation
|
||||
/// </summary>
|
||||
public record AddAnnotationBase64Request
|
||||
public record AddAnnotationBase64Request : PdfBase64RequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded PDF file
|
||||
/// </summary>
|
||||
/// <example>"JVBERi0xLjQK..."</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of annotation to add
|
||||
/// </summary>
|
||||
@@ -115,14 +109,8 @@ public record AddAnnotationBase64Request
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF stamp operation
|
||||
/// </summary>
|
||||
public record AddStampBase64Request
|
||||
public record AddStampBase64Request : PdfBase64RequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64-encoded PDF file
|
||||
/// </summary>
|
||||
/// <example>"JVBERi0xLjQK..."</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of stamp (Text, Image, or Predefined)
|
||||
/// </summary>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF validation
|
||||
/// </summary>
|
||||
public record ValidatePdfBase64Request : PdfBase64RequestBase;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF/A validation
|
||||
/// </summary>
|
||||
public record ValidatePdfABase64Request : PdfBase64RequestBase;
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded Swiss QR Code extraction
|
||||
/// </summary>
|
||||
public record ExtractSwissQrCodeBase64Request : PdfBase64RequestBase;
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF ZUGFeRD check
|
||||
/// </summary>
|
||||
public record HasZugferdRequest : PdfBase64RequestBase;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF ZUGFeRD extraction
|
||||
/// </summary>
|
||||
public record ExtractZugferdRequest : PdfBase64RequestBase;
|
||||
@@ -32,6 +32,10 @@ namespace DocumentService.Application.Common.DTOs;
|
||||
/// }
|
||||
/// </example>
|
||||
public record SwissQrCodeExtractionResult(
|
||||
#if NET
|
||||
SwissQrBillDto Bill,
|
||||
IReadOnlyList<string> RawLines
|
||||
);
|
||||
#else
|
||||
object? Bill,
|
||||
#endif
|
||||
IEnumerable<string> RawLines
|
||||
);
|
||||
@@ -15,7 +15,7 @@ public interface IPdfProcessor
|
||||
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
|
||||
/// Thrown when stream is empty, invalid, or not positioned at the beginning
|
||||
/// </exception>
|
||||
Task<PdfMetadata> ValidateAsync(Stream pdfStream);
|
||||
Task<PdfValidationResult> ValidateAsync(Stream pdfStream);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a PDF/A document and checks conformance level.
|
||||
@@ -28,7 +28,7 @@ public interface IPdfProcessor
|
||||
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
|
||||
/// Thrown when stream is empty, invalid, or not positioned at the beginning
|
||||
/// </exception>
|
||||
Task<PdfAMetadata> ValidatePdfAAsync(Stream pdfStream);
|
||||
Task<PdfAValidationResult> ValidatePdfAAsync(Stream pdfStream);
|
||||
|
||||
/// <summary>
|
||||
/// Checks for embedded files (attachments) in a PDF document and returns detailed metadata.
|
||||
@@ -13,12 +13,6 @@ public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
// PdfMetadata -> PdfValidationResult
|
||||
CreateMap<PdfMetadata, PdfValidationResult>();
|
||||
|
||||
// PdfAMetadata -> PdfAValidationResult
|
||||
CreateMap<PdfAMetadata, PdfAValidationResult>()
|
||||
.ForMember(dest => dest.FileSize, opt => opt.MapFrom(src => src.FileSizeBytes));
|
||||
|
||||
// Codecrete Bill -> SwissQrBillDto
|
||||
CreateMap<Bill, SwissQrBillDto>()
|
||||
@@ -0,0 +1,65 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="DependencyInjection\**" />
|
||||
<Compile Remove="Features\**" />
|
||||
<EmbeddedResource Remove="DependencyInjection\**" />
|
||||
<EmbeddedResource Remove="Features\**" />
|
||||
<None Remove="DependencyInjection\**" />
|
||||
<None Remove="Features\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
<PackageReference Include="Codecrete.SwissQRBill.Generator" Version="3.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net480'">
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
|
||||
<Compile Remove="Common\Interfaces\ISwissQrCodeProcessor.cs" />
|
||||
<Compile Remove="Common\DTOs\SwissQrBillDto.cs" />
|
||||
<Compile Remove="Common\Mapping\MappingProfile.cs" />
|
||||
<Compile Remove="SwissQrCode\**" />
|
||||
<Compile Remove="HasZugferd\**" />
|
||||
<Compile Remove="DependencyInjection.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentService.Domain\DocumentService.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -18,6 +18,7 @@ public record ExtractZugferdCommand : IRequest<ZugferdExtractionResult>
|
||||
public required Stream PdfStream { get; init; }
|
||||
}
|
||||
|
||||
#if NET
|
||||
/// <summary>
|
||||
/// Handler for ExtractZugferdCommand.
|
||||
/// Extracts ZUGFeRD XML from PDF and returns XML content
|
||||
@@ -77,6 +78,7 @@ public class ExtractZugferdCommandHandler(
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ExtractZugferdCommand.
|
||||
@@ -1,4 +1,3 @@
|
||||
using AutoMapper;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
@@ -18,20 +17,12 @@ public record ValidatePdfQuery : IRequest<PdfValidationResult>
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ValidatePdfQuery
|
||||
/// Orchestrates PDF validation using IPdfProcessor and AutoMapper
|
||||
/// </summary>
|
||||
public class ValidatePdfQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper)
|
||||
public class ValidatePdfQueryHandler(IPdfProcessor pdfProcessor)
|
||||
: IRequestHandler<ValidatePdfQuery, PdfValidationResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates PDF and returns metadata
|
||||
/// </summary>
|
||||
public async Task<PdfValidationResult> Handle(ValidatePdfQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Call DevExpress service directly with stream (exceptions propagate naturally)
|
||||
var metadata = await PdfProcessor.ValidateAsync(request.PdfStream);
|
||||
|
||||
// Map DTO to response DTO using AutoMapper
|
||||
return Mapper.Map<PdfValidationResult>(metadata);
|
||||
return await pdfProcessor.ValidateAsync(request.PdfStream);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using AutoMapper;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
@@ -18,20 +17,12 @@ public record ValidatePdfAQuery : IRequest<PdfAValidationResult>
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ValidatePdfAQuery
|
||||
/// Orchestrates PDF/A validation using IPdfProcessor and AutoMapper
|
||||
/// </summary>
|
||||
public class ValidatePdfAQueryHandler(IPdfProcessor PdfProcessor, IMapper Mapper)
|
||||
public class ValidatePdfAQueryHandler(IPdfProcessor pdfProcessor)
|
||||
: IRequestHandler<ValidatePdfAQuery, PdfAValidationResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates PDF/A and returns metadata with conformance level
|
||||
/// </summary>
|
||||
public async Task<PdfAValidationResult> Handle(ValidatePdfAQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Call DevExpress service directly with stream (exceptions propagate naturally)
|
||||
var metadata = await PdfProcessor.ValidatePdfAAsync(request.PdfStream);
|
||||
|
||||
// Map DTO to response DTO using AutoMapper
|
||||
return Mapper.Map<PdfAValidationResult>(metadata);
|
||||
return await pdfProcessor.ValidatePdfAAsync(request.PdfStream);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractZugferd;
|
||||
using DocumentService.Client.Interfaces;
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
|
||||
|
||||
@@ -42,5 +42,9 @@
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentService.Application\DocumentService.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
namespace DocumentService.Client.Interfaces;
|
||||
|
||||
@@ -60,30 +61,4 @@ public interface IPdfAttachmentClient
|
||||
/// <returns>Modified PDF as stream</returns>
|
||||
[Obsolete("API endpoint not implemented yet")]
|
||||
Task<Stream> AddAttachmentsAsync(byte[] pdfBytes, List<AttachmentRequestDto> attachments, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for attachment check result
|
||||
/// </summary>
|
||||
public record AttachmentCheckResult
|
||||
{
|
||||
/// <summary>Whether the document contains any embedded file attachments.</summary>
|
||||
public bool HasAttachments { get; init; }
|
||||
/// <summary>Number of embedded file attachments found.</summary>
|
||||
public int AttachmentCount { get; init; }
|
||||
/// <summary>Metadata for each attachment found in the document.</summary>
|
||||
public List<AttachmentMetadata> Attachments { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for attachment metadata
|
||||
/// </summary>
|
||||
public record AttachmentMetadata
|
||||
{
|
||||
/// <summary>Name of the embedded file.</summary>
|
||||
public string FileName { get; init; } = string.Empty;
|
||||
/// <summary>MIME type of the embedded file (e.g. "application/xml"), or <c>null</c> if unknown.</summary>
|
||||
public string? MimeType { get; init; }
|
||||
/// <summary>Size of the embedded file in bytes.</summary>
|
||||
public long Size { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using DocumentService.Client.Models.Requests;
|
||||
using DocumentService.Application.Common.DTOs.Requests;
|
||||
|
||||
namespace DocumentService.Client.Interfaces;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
|
||||
namespace DocumentService.Client.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,40 +38,4 @@ public interface IPdfValidationClient
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A validation result with conformance level and errors</returns>
|
||||
Task<PdfAValidationResult> ValidatePdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for PDF validation result
|
||||
/// </summary>
|
||||
public record PdfValidationResult
|
||||
{
|
||||
/// <summary>Total number of pages in the document.</summary>
|
||||
public int PageCount { get; init; }
|
||||
/// <summary>File size in bytes.</summary>
|
||||
public long FileSizeBytes { get; init; }
|
||||
/// <summary>PDF specification version (e.g. "1.7").</summary>
|
||||
public string PdfVersion { get; init; } = string.Empty;
|
||||
/// <summary>Whether the document is password-protected.</summary>
|
||||
public bool IsEncrypted { get; init; }
|
||||
/// <summary>Whether the document contains embedded file attachments.</summary>
|
||||
public bool HasAttachments { get; init; }
|
||||
/// <summary>Number of embedded file attachments.</summary>
|
||||
public int AttachmentCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for PDF/A validation result
|
||||
/// </summary>
|
||||
public record PdfAValidationResult
|
||||
{
|
||||
/// <summary>Whether the document is fully PDF/A conformant.</summary>
|
||||
public bool IsValid { get; init; }
|
||||
/// <summary>Detected PDF/A conformance level (e.g. "PDF/A-3b"), or <c>null</c> if not a PDF/A document.</summary>
|
||||
public string? PdfAVersion { get; init; }
|
||||
/// <summary>Total number of pages in the document.</summary>
|
||||
public int PageCount { get; init; }
|
||||
/// <summary>List of conformance errors found during validation.</summary>
|
||||
public List<string> Errors { get; init; } = new();
|
||||
/// <summary>List of conformance warnings found during validation.</summary>
|
||||
public List<string> Warnings { get; init; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
|
||||
namespace DocumentService.Client.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
@@ -24,13 +26,3 @@ public interface ISwissQrCodeClient
|
||||
Task<SwissQrCodeExtractionResult> ExtractSwissQrCodeAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for Swiss QR Code extraction result
|
||||
/// </summary>
|
||||
public record SwissQrCodeExtractionResult
|
||||
{
|
||||
/// <summary>Parsed Swiss QR bill object. <c>null</c> when <c>raw=true</c> was requested.</summary>
|
||||
public object? Bill { get; init; }
|
||||
/// <summary>Raw QR code text lines. Populated when <c>raw=true</c> was requested.</summary>
|
||||
public List<string> RawLines { get; init; } = new();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractZugferd;
|
||||
|
||||
namespace DocumentService.Client.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
@@ -53,27 +56,3 @@ public interface IZugferdClient
|
||||
/// <returns>Structured ZUGFeRD extraction result</returns>
|
||||
Task<ZugferdExtractionResult> ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>DTO for ZUGFeRD detection result</summary>
|
||||
public record ZugferdCheckResult
|
||||
{
|
||||
/// <summary>Whether the document contains a ZUGFeRD XML attachment.</summary>
|
||||
public bool HasZugferd { get; init; }
|
||||
/// <summary>Detected ZUGFeRD version (e.g. "2.1"), or <c>null</c> if not present.</summary>
|
||||
public string? Version { get; init; }
|
||||
/// <summary>ZUGFeRD profile name (e.g. "EN 16931"), or <c>null</c> if not present.</summary>
|
||||
public string? Profile { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>DTO for ZUGFeRD extraction result</summary>
|
||||
public record ZugferdExtractionResult
|
||||
{
|
||||
/// <summary>File name of the extracted XML attachment (e.g. "factur-x.xml").</summary>
|
||||
public string FileName { get; init; } = string.Empty;
|
||||
/// <summary>Full XML content of the ZUGFeRD attachment.</summary>
|
||||
public string XmlContent { get; init; } = string.Empty;
|
||||
/// <summary>ZUGFeRD version (e.g. "2.1"), or <c>null</c> if not detected.</summary>
|
||||
public string? Version { get; init; }
|
||||
/// <summary>ZUGFeRD profile name (e.g. "EN 16931"), or <c>null</c> if not detected.</summary>
|
||||
public string? Profile { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace DocumentService.Client.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for converting a standard PDF to PDF/A format (Base64 JSON)
|
||||
/// </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 conformance level. Defaults to "PDF/A-3b".
|
||||
/// </summary>
|
||||
/// <example>PDF/A-3b</example>
|
||||
public string? PdfALevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for converting a PDF/A document back to standard PDF (Base64 JSON)
|
||||
/// </summary>
|
||||
public record ConvertFromPdfARequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF/A document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
namespace DocumentService.Client.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF validation
|
||||
/// </summary>
|
||||
public record ValidatePdfBase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF/A validation
|
||||
/// </summary>
|
||||
public record ValidatePdfABase64Request
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace DocumentService.Client.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF ZUGFeRD check
|
||||
/// </summary>
|
||||
public record HasZugferdRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF ZUGFeRD extraction
|
||||
/// </summary>
|
||||
public record ExtractZugferdRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin point for PDF annotations.
|
||||
/// </summary>
|
||||
public enum AnnotationOrigin
|
||||
{
|
||||
/// <summary>
|
||||
/// Bottom-left corner (PDF native coordinate system, default)
|
||||
/// </summary>
|
||||
BottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// Top-left corner (common in UI frameworks)
|
||||
/// </summary>
|
||||
TopLeft
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Supported PDF annotation types
|
||||
/// </summary>
|
||||
public enum AnnotationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Text markup annotation (highlight, underline, strikeout)
|
||||
/// </summary>
|
||||
TextMarkup,
|
||||
|
||||
/// <summary>
|
||||
/// Free text annotation (text box with visible text)
|
||||
/// </summary>
|
||||
FreeText,
|
||||
|
||||
/// <summary>
|
||||
/// Sticky note annotation (popup comment icon)
|
||||
/// </summary>
|
||||
StickyNote,
|
||||
|
||||
/// <summary>
|
||||
/// Circle shape annotation
|
||||
/// </summary>
|
||||
Circle,
|
||||
|
||||
/// <summary>
|
||||
/// Square shape annotation
|
||||
/// </summary>
|
||||
Square
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined stamp types with standard text and styling.
|
||||
/// </summary>
|
||||
public enum PredefinedStampType
|
||||
{
|
||||
/// <summary>
|
||||
/// CONFIDENTIAL stamp (red, bold).
|
||||
/// </summary>
|
||||
Confidential,
|
||||
|
||||
/// <summary>
|
||||
/// APPROVED stamp (green, bold).
|
||||
/// </summary>
|
||||
Approved,
|
||||
|
||||
/// <summary>
|
||||
/// DRAFT stamp (gray, italic).
|
||||
/// </summary>
|
||||
Draft,
|
||||
|
||||
/// <summary>
|
||||
/// VOID stamp (red, strikethrough effect).
|
||||
/// </summary>
|
||||
Void,
|
||||
|
||||
/// <summary>
|
||||
/// FOR REVIEW stamp (orange, bold).
|
||||
/// </summary>
|
||||
ForReview
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the stamp should appear in the foreground or background.
|
||||
/// </summary>
|
||||
public enum StampPlacement
|
||||
{
|
||||
/// <summary>
|
||||
/// Stamp appears on top of existing page content.
|
||||
/// </summary>
|
||||
Foreground,
|
||||
|
||||
/// <summary>
|
||||
/// Stamp appears behind existing page content (watermark effect).
|
||||
/// </summary>
|
||||
Background
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of stamp to add to a PDF document.
|
||||
/// </summary>
|
||||
public enum StampType
|
||||
{
|
||||
/// <summary>
|
||||
/// Text-based stamp with custom text, font, and color.
|
||||
/// </summary>
|
||||
Text,
|
||||
|
||||
/// <summary>
|
||||
/// Image-based stamp (PNG/JPEG overlay).
|
||||
/// </summary>
|
||||
Image,
|
||||
|
||||
/// <summary>
|
||||
/// Predefined stamp with standard text (e.g., CONFIDENTIAL, APPROVED).
|
||||
/// </summary>
|
||||
Predefined
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace DocumentService.Client.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Text markup annotation style (highlight, underline, strikeout)
|
||||
/// </summary>
|
||||
public enum TextMarkupStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// Highlight text with background color
|
||||
/// </summary>
|
||||
Highlight,
|
||||
|
||||
/// <summary>
|
||||
/// Underline text
|
||||
/// </summary>
|
||||
Underline,
|
||||
|
||||
/// <summary>
|
||||
/// Strikeout text (strikethrough)
|
||||
/// </summary>
|
||||
Strikeout
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,230 +0,0 @@
|
||||
# DocumentService.Client - Stream-Based API
|
||||
|
||||
## ?? Design Decision: Why Stream Instead of byte[]?
|
||||
|
||||
### ? **Advantages of Stream-Based Returns**
|
||||
|
||||
| **Aspect** | **Stream** | **byte[]** |
|
||||
|---|---|---|
|
||||
| **Memory Efficiency** | ????? | ?? |
|
||||
| **Flexibility** | ????? | ??? |
|
||||
| **Large Files** | ? Excellent | ? Poor (OutOfMemoryException risk) |
|
||||
| **Direct File Save** | ? `CopyToAsync(fileStream)` | ? Must buffer entire file |
|
||||
| **Streaming to Response** | ? Direct pipe | ? Must load to memory first |
|
||||
| **Base64 Conversion** | ? Extension method | ? `Convert.ToBase64String()` |
|
||||
| **Network Transfer** | ? Progressive | ? Buffered |
|
||||
|
||||
---
|
||||
|
||||
## ?? Extension Methods
|
||||
|
||||
### `StreamExtensions` - Converting Streams
|
||||
|
||||
```csharp
|
||||
using DocumentService.Client.Extensions;
|
||||
|
||||
// Convert Stream to Base64
|
||||
using var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
string base64 = await pdfStream.ToBase64StringAsync();
|
||||
|
||||
// Convert Stream to byte[]
|
||||
using var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
byte[] bytes = await pdfStream.ToBytesAsync();
|
||||
|
||||
// Reset stream position (if seekable)
|
||||
pdfStream.Reset(); // Position = 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Usage Patterns
|
||||
|
||||
### Pattern 1: Direct File Save (Memory Efficient ?)
|
||||
|
||||
```csharp
|
||||
// ? Best for large files - no intermediate buffering
|
||||
using var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
using var fileStream = File.Create("output.pdf");
|
||||
await pdfStream.CopyToAsync(fileStream);
|
||||
```
|
||||
|
||||
### Pattern 2: HTTP Response Streaming (Memory Efficient ?)
|
||||
|
||||
```csharp
|
||||
// ASP.NET Core example
|
||||
[HttpGet("merge")]
|
||||
public async Task<IActionResult> MergePdfs()
|
||||
{
|
||||
using var mergedStream = await _client.Operations.MergeAsync(streams);
|
||||
|
||||
// Stream directly to HTTP response - no buffering
|
||||
return File(mergedStream, "application/pdf", "merged.pdf");
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Base64 Conversion (When Needed)
|
||||
|
||||
```csharp
|
||||
// ?? Only if Base64 is required (e.g., JSON APIs, email attachments)
|
||||
using var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
string base64Pdf = await pdfStream.ToBase64StringAsync();
|
||||
|
||||
// Send to external API
|
||||
await externalApi.SendDocumentAsync(new { pdf = base64Pdf });
|
||||
```
|
||||
|
||||
### Pattern 4: Byte Array (Legacy Compatibility)
|
||||
|
||||
```csharp
|
||||
// ?? For legacy code that requires byte[]
|
||||
using var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
byte[] pdfBytes = await pdfStream.ToBytesAsync();
|
||||
|
||||
// Use with legacy method
|
||||
legacyService.ProcessPdf(pdfBytes);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Performance Comparison
|
||||
|
||||
### Scenario: Merging 10 PDFs (100 MB total)
|
||||
|
||||
| **Approach** | **Memory Usage** | **Speed** | **Scalability** |
|
||||
|---|---|---|---|
|
||||
| **Stream ? File** | ~10 MB | ????? | Excellent |
|
||||
| **Stream ? HTTP** | ~10 MB | ????? | Excellent |
|
||||
| **Stream ? byte[]** | ~110 MB | ??? | Limited |
|
||||
| **byte[] ? File** | ~210 MB | ?? | Poor |
|
||||
|
||||
**Conclusion:** Stream-based API reduces memory footprint by **10-20x** for large files.
|
||||
|
||||
---
|
||||
|
||||
## ?? API Reference
|
||||
|
||||
### All Stream-Returning Methods
|
||||
|
||||
| **Client** | **Method** | **Return Type** |
|
||||
|---|---|---|
|
||||
| **IPdfAttachmentClient** | `ExtractAttachmentsAsync()` | `Task<Stream>` |
|
||||
| **IPdfAttachmentClient** | `AddAttachmentsAsync()` | `Task<Stream>` |
|
||||
| **IPdfOperationsClient** | `MergeAsync()` | `Task<Stream>` |
|
||||
| **IPdfOperationsClient** | `AnnotateAsync()` | `Task<Stream>` |
|
||||
| **IPdfOperationsClient** | `StampAsync()` | `Task<Stream>` |
|
||||
|
||||
**Query Methods** (Metadata only):
|
||||
- `IPdfValidationClient.ValidatePdfAsync()` ? `Task<PdfValidationResult>`
|
||||
- `IPdfAttachmentClient.CheckAttachmentsAsync()` ? `Task<AttachmentCheckResult>`
|
||||
- `ISwissQrCodeClient.ExtractSwissQrCodeAsync()` ? `Task<SwissQrCodeExtractionResult>`
|
||||
|
||||
---
|
||||
|
||||
## ?? Stream Disposal Best Practices
|
||||
|
||||
### ? Correct Usage
|
||||
|
||||
```csharp
|
||||
// Pattern 1: using declaration (C# 8.0+)
|
||||
using var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
// Auto-disposed at end of scope
|
||||
|
||||
// Pattern 2: using statement
|
||||
using (var pdfStream = await client.Operations.MergeAsync(streams))
|
||||
{
|
||||
// Use stream here
|
||||
} // Auto-disposed
|
||||
|
||||
// Pattern 3: Manual disposal (not recommended)
|
||||
var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
try
|
||||
{
|
||||
// Use stream
|
||||
}
|
||||
finally
|
||||
{
|
||||
pdfStream.Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
### ? Incorrect Usage (Memory Leak)
|
||||
|
||||
```csharp
|
||||
// ? NO using - stream never disposed!
|
||||
var pdfStream = await client.Operations.MergeAsync(streams);
|
||||
await pdfStream.CopyToAsync(fileStream);
|
||||
// Memory leak!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? .NET Framework Compatibility
|
||||
|
||||
### Conditional Compilation for CopyToAsync
|
||||
|
||||
```csharp
|
||||
// StreamExtensions.cs handles this internally
|
||||
#if NET8_0
|
||||
await stream.CopyToAsync(memoryStream, cancellationToken);
|
||||
#else
|
||||
await stream.CopyToAsync(memoryStream); // .NET Framework doesn't support CancellationToken
|
||||
#endif
|
||||
```
|
||||
|
||||
### Supported Versions
|
||||
- ? .NET 8.0 - Full support with CancellationToken
|
||||
- ? .NET Framework 4.8 - Full support (no CancellationToken in CopyToAsync)
|
||||
- ? .NET Framework 4.6.2 - Full support (no CancellationToken in CopyToAsync)
|
||||
|
||||
---
|
||||
|
||||
## ?? Migration from byte[] to Stream
|
||||
|
||||
### Before (byte[]-based)
|
||||
|
||||
```csharp
|
||||
byte[] mergedPdf = await client.Operations.MergeAsync(streams);
|
||||
await File.WriteAllBytesAsync("output.pdf", mergedPdf);
|
||||
```
|
||||
|
||||
### After (Stream-based)
|
||||
|
||||
```csharp
|
||||
using var mergedStream = await client.Operations.MergeAsync(streams);
|
||||
using var fileStream = File.Create("output.pdf");
|
||||
await mergedStream.CopyToAsync(fileStream);
|
||||
```
|
||||
|
||||
### If you NEED byte[] (Legacy Code)
|
||||
|
||||
```csharp
|
||||
using DocumentService.Client.Extensions;
|
||||
|
||||
using var mergedStream = await client.Operations.MergeAsync(streams);
|
||||
byte[] mergedPdf = await mergedStream.ToBytesAsync(); // Extension method
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ?? Summary
|
||||
|
||||
? **Stream-based API** for:
|
||||
- Memory efficiency
|
||||
- Large file support
|
||||
- Direct file/HTTP streaming
|
||||
- Flexibility (convert to byte[]/Base64 when needed)
|
||||
|
||||
? **Extension methods** for:
|
||||
- `Stream.ToBase64StringAsync()`
|
||||
- `Stream.ToBytesAsync()`
|
||||
- `Stream.Reset()`
|
||||
|
||||
? **Multi-target support**:
|
||||
- .NET 8.0
|
||||
- .NET Framework 4.8
|
||||
- .NET Framework 4.6.2
|
||||
|
||||
? **Performance**:
|
||||
- 10-20x memory reduction for large files
|
||||
- Progressive streaming (no buffering)
|
||||
- Scalable for enterprise workloads
|
||||
@@ -1,9 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net462;net480;net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user