diff --git a/DocumentService.Client/README.md b/DocumentService.Client/README.md index aa33226..ae2a9bd 100644 --- a/DocumentService.Client/README.md +++ b/DocumentService.Client/README.md @@ -1,332 +1,1075 @@ # DocumentService.Client -.NET client library for DocumentService API - supports .NET Framework 4.6.2, 4.8, and .NET 8.0. +.NET-Clientbibliothek für die DocumentService-API – unterstützt .NET Framework 4.6.2, 4.8 und .NET 8.0. -## Features +## Funktionsumfang -- **Multi-target support**: .NET Framework 4.6.2, 4.8, and .NET 8.0 -- **HttpClientFactory integration**: Proper lifecycle management and connection pooling -- **Separate clients per controller**: `IPdfValidationClient`, `IPdfAttachmentClient`, `IPdfOperationsClient`, `ISwissQrCodeClient` -- **Dual input support**: Multipart (Stream) and Base64 (byte[]) for all endpoints -- **Strongly-typed models**: Shared request/response DTOs with XML documentation +- **Multi-Target-Unterstützung**: .NET Framework 4.6.2, 4.8 und .NET 8.0 +- **Statischer Einstiegspunkt**: `Client`-Klasse – kein DI-Container erforderlich +- **Separate Clients pro Fachbereich**: `Client.Validation`, `Client.Attachment`, `Client.Operations`, `Client.SwissQrCode`, `Client.Zugferd`, `Client.Conversion` +- **Dualer Eingabe-Support**: Multipart (Stream) und Base64 (byte[]) für alle Endpunkte +- **Stark typisierte Modelle**: Gemeinsame Request/Response-DTOs mit XML-Dokumentation ## Installation -```bash -dotnet add package DocumentService.Client +Das Paket wird über einen internen NuGet-Feed bereitgestellt und ist **nicht** auf nuget.org verfügbar. + +**NuGet-Paket:** `http://172.24.12.35:5000/packages/documentservice.client/1.0.0` +**API-Basis-URL:** `http://172.24.12.39:9393/` +**Swagger-Dokumentation:** [http://172.24.12.39:9393/swagger/index.html](http://172.24.12.39:9393/swagger/index.html) + +### .NET-Projekt (Visual Studio) + +Paket über den NuGet Package Manager installieren. Dabei sicherstellen, dass der interne NuGet-Server (`http://172.24.12.35:5000/nuget`) als Paketquelle eingetragen und ausgewählt ist. + +### PowerShell (ohne .NET-Projekt) + +Für PowerShell-Skripte ohne zugehöriges .NET-Projekt muss die Assembly manuell heruntergeladen und eingebunden werden. + +**Schritt 1 – Paket herunterladen und entpacken:** + +```powershell +# NuGet-Paket herunterladen (.nupkg ist ein ZIP-Archiv) +$nupkgUrl = "http://172.24.12.35:5000/packages/documentservice.client/1.0.0" +$nupkgPath = "$env:TEMP\DocumentService.Client.1.0.0.nupkg" +$extractDir = "$env:TEMP\DocumentService.Client" + +Invoke-WebRequest -Uri $nupkgUrl -OutFile $nupkgPath + +# Entpacken (nupkg = ZIP) +Expand-Archive -Path $nupkgPath -DestinationPath $extractDir -Force ``` -## Configuration +**Schritt 2 – Assembly für die Zielplattform laden:** -### ASP.NET Core / .NET 8.0 +```powershell +# Für PowerShell 7+ (.NET 8 / net8.0) +$dllPath = Join-Path $extractDir "lib\net8.0\DocumentService.Client.dll" + +# Für Windows PowerShell 5.1 (.NET Framework 4.8 / net48) +# $dllPath = Join-Path $extractDir "lib\net48\DocumentService.Client.dll" + +Add-Type -Path $dllPath +``` + +**Schritt 3 – Abhängigkeiten laden** (falls nicht bereits im GAC / PowerShell-Runspace vorhanden): + +```powershell +# Abhängige Assemblies aus demselben lib-Verzeichnis laden +$libDir = Split-Path $dllPath -Parent +Get-ChildItem -Path $libDir -Filter "*.dll" | ForEach-Object { + try { Add-Type -Path $_.FullName } catch { <# bereits geladen #> } +} +``` + +> **Hinweis:** In der Praxis empfiehlt es sich, die extrahierten DLLs einmalig in ein festes Verzeichnis (z. B. `C:\Tools\DocumentService.Client\`) zu kopieren und `Add-Type` mit einem absoluten Pfad aufzurufen. Das verhindert wiederholte Downloads und Versionskonflikte zwischen Skript-Ausführungen. + +--- + +## Konfiguration + +Diese Dokumentation beschreibt die Nutzung über den statischen `Client`-Einstiegspunkt, der ohne DI-Container auskommt. Die Library unterstützt darüber hinaus vollständige `IServiceCollection`-Integration via `AddDocumentServiceClients()` – dies ist jedoch nicht Gegenstand dieser Dokumentation. + +### Einmalige Initialisierung (z. B. in `Program.cs` oder `App_Start`) ```csharp -using DocumentService.Client.Extensions; +using DocumentService.Client; -var builder = WebApplication.CreateBuilder(args); +// Kurzform – nur Base-URL +Client.Configure("http://172.24.12.39:9393/"); -// Register all DocumentService clients -builder.Services.AddDocumentServiceClients(options => +// Vollständige Konfiguration +Client.Configure(options => { - options.BaseUrl = "https://documentservice.example.com"; - options.Timeout = TimeSpan.FromMinutes(10); - options.MaxRetries = 3; + options.BaseUrl = "http://172.24.12.39:9393/"; + options.Timeout = TimeSpan.FromMinutes(10); + options.MaxRetries = 3; options.ThrowOnError = true; }); - -var app = builder.Build(); ``` -### .NET Framework 4.6.2 / 4.8 +```vbnet +Imports DocumentService.Client + +' Kurzform – nur Base-URL +Client.Configure("http://172.24.12.39:9393/") + +' Vollständige Konfiguration +Client.Configure(Sub(options) + options.BaseUrl = "http://172.24.12.39:9393/" + options.Timeout = TimeSpan.FromMinutes(10) + options.MaxRetries = 3 + options.ThrowOnError = True +End Sub) +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +# Kurzform – nur Base-URL +[DocumentService.Client.Client]::Configure("http://172.24.12.39:9393/") + +# Vollständige Konfiguration +[DocumentService.Client.Client]::Configure([Action[DocumentService.Client.Configuration.DocumentServiceClientOptions]] { + param($options) + $options.BaseUrl = "http://172.24.12.39:9393/" + $options.Timeout = [TimeSpan]::FromMinutes(10) + $options.MaxRetries = 3 + $options.ThrowOnError = $true +}) +``` + +`Client.Configure()` ist **idempotent im Sinne der Initialisierung**: Der interne Service Provider wird beim ersten Zugriff lazy gebaut und danach nicht mehr verändert. Ein zweiter `Configure()`-Aufruf wird standardmäßig mit einer `InvalidOperationException` abgelehnt. Dieses Verhalten ist über `Client.OnReconfigure` steuerbar: ```csharp -using DocumentService.Client.Extensions; -using Microsoft.Extensions.DependencyInjection; +// Standard – wirft bei erneutem Configure()-Aufruf +Client.OnReconfigure = OnReconfigure.ThrowException; -var services = new ServiceCollection(); - -services.AddDocumentServiceClients(options => -{ - options.BaseUrl = "https://documentservice.example.com"; - options.Timeout = TimeSpan.FromMinutes(10); -}); - -var serviceProvider = services.BuildServiceProvider(); +// Zweiten Aufruf stillschweigend ignorieren (z. B. in Bibliotheks-Code) +Client.OnReconfigure = OnReconfigure.Ignore; ``` -## Usage Examples +```vbnet +' Standard – wirft bei erneutem Configure()-Aufruf +Client.OnReconfigure = OnReconfigure.ThrowException -### 1. PDF Validation - -```csharp -using DocumentService.Client.Interfaces; - -public class PdfService -{ - private readonly IPdfValidationClient _validationClient; - - public PdfService(IPdfValidationClient validationClient) - { - _validationClient = validationClient; - } - - public async Task ValidatePdfAsync(Stream pdfStream) - { - // Option 1: From Stream (multipart) - var result = await _validationClient.ValidatePdfAsync(pdfStream); - - Console.WriteLine($"Pages: {result.PageCount}"); - Console.WriteLine($"Version: {result.PdfVersion}"); - Console.WriteLine($"Encrypted: {result.IsEncrypted}"); - } - - public async Task ValidatePdfFromBytesAsync(byte[] pdfBytes) - { - // Option 2: From byte array (Base64 JSON) - var result = await _validationClient.ValidatePdfAsync(pdfBytes); - - Console.WriteLine($"File Size: {result.FileSizeBytes} bytes"); - } - - public async Task ValidatePdfAAsync(string filePath) - { - using var stream = File.OpenRead(filePath); - var result = await _validationClient.ValidatePdfAAsync(stream); - - Console.WriteLine($"Valid PDF/A: {result.IsValid}"); - Console.WriteLine($"PDF/A Version: {result.PdfAVersion}"); - - if (result.Errors.Any()) - { - Console.WriteLine("Errors:"); - foreach (var error in result.Errors) - { - Console.WriteLine($" - {error}"); - } - } - } -} +' Zweiten Aufruf stillschweigend ignorieren +Client.OnReconfigure = OnReconfigure.Ignore ``` -### 2. PDF Attachments +```powershell +# Standard – wirft bei erneutem Configure()-Aufruf +[DocumentService.Client.Client]::OnReconfigure = [DocumentService.Client.Models.ValueObjects.OnReconfigure]::ThrowException -```csharp -using DocumentService.Client.Interfaces; -using DocumentService.Client.Extensions; // For ToBase64StringAsync, ToBytesAsync - -public class AttachmentService -{ - private readonly IPdfAttachmentClient _attachmentClient; - - public AttachmentService(IPdfAttachmentClient attachmentClient) - { - _attachmentClient = attachmentClient; - } - - public async Task CheckAttachmentsAsync(byte[] pdfBytes) - { - var result = await _attachmentClient.CheckAttachmentsAsync(pdfBytes); - - Console.WriteLine($"Has Attachments: {result.HasAttachments}"); - Console.WriteLine($"Attachment Count: {result.AttachmentCount}"); - - foreach (var attachment in result.Attachments) - { - Console.WriteLine($" - {attachment.FileName} ({attachment.Size} bytes)"); - } - } - - public async Task ExtractAttachmentsAsync(Stream pdfStream, string outputPath) - { - // Returns ZIP file as Stream (memory efficient!) - using var zipStream = await _attachmentClient.ExtractAttachmentsAsync(pdfStream); - - // Option 1: Save directly to file - using var fileStream = File.Create(outputPath); - await zipStream.CopyToAsync(fileStream); - - Console.WriteLine($"Attachments extracted to: {outputPath}"); - } - - public async Task ExtractAttachmentsToBase64Async(byte[] pdfBytes) - { - // Returns ZIP as Stream - using var zipStream = await _attachmentClient.ExtractAttachmentsAsync(pdfBytes); - - // Option 2: Convert to Base64 using extension method - string base64Zip = await zipStream.ToBase64StringAsync(); - - Console.WriteLine($"ZIP as Base64: {base64Zip.Substring(0, 50)}..."); - } - - public async Task ExtractAttachmentsToBytesAsync(Stream pdfStream) - { - // Returns ZIP as Stream - using var zipStream = await _attachmentClient.ExtractAttachmentsAsync(pdfStream); - - // Option 3: Convert to byte array using extension method - byte[] zipBytes = await zipStream.ToBytesAsync(); - - Console.WriteLine($"ZIP size: {zipBytes.Length} bytes"); - } -} +# Zweiten Aufruf stillschweigend ignorieren +[DocumentService.Client.Client]::OnReconfigure = [DocumentService.Client.Models.ValueObjects.OnReconfigure]::Ignore ``` -### 3. PDF Operations (Merge, Annotate, Stamp) +### Verfügbare Client-Eigenschaften -```csharp -using DocumentService.Client.Interfaces; -using DocumentService.Client.Models.Requests; -using DocumentService.Client.Models.ValueObjects; -using DocumentService.Client.Extensions; // For Stream extensions +| Eigenschaft | Interface | Fachbereich | +|---|---|---| +| `Client.Validation` | `IPdfValidationClient` | PDF-Validierung | +| `Client.Attachment` | `IPdfAttachmentClient` | PDF-Anhänge | +| `Client.Operations` | `IPdfOperationsClient` | PDF-Operationen | +| `Client.SwissQrCode` | `ISwissQrCodeClient` | Schweizer QR-Code | +| `Client.Zugferd` | `IZugferdClient` | ZUGFeRD-Rechnungen | +| `Client.Conversion` | `IPdfConversionClient` | PDF-Konvertierung *(geplant)* | -public class OperationsService -{ - private readonly IPdfOperationsClient _operationsClient; +Jeder Zugriff auf eine dieser Eigenschaften erstellt einen neuen DI-Scope. Clients sind **nicht** für die dauerhafte Speicherung als Felder gedacht – jeder Aufruf holt sich eine frische Instanz. - public OperationsService(IPdfOperationsClient operationsClient) - { - _operationsClient = operationsClient; - } +> **PowerShell-Hinweis:** PowerShell unterstützt `async/await` nicht nativ. Alle asynchronen Methoden werden über `.GetAwaiter().GetResult()` synchron blockierend aufgerufen. Für lang laufende Operationen (z. B. große PDFs) empfiehlt sich die Ausführung in einem eigenen Thread via `[System.Threading.Tasks.Task]::Run({...}).GetAwaiter().GetResult()`. - // MERGE - public async Task MergePdfsAsync(List pdfPaths) - { - var streams = pdfPaths.Select(File.OpenRead).ToList(); - - // Returns merged PDF as Stream - var mergedStream = await _operationsClient.MergeAsync( - streams, - pageRanges: new List { "1-2", null, "3,5" } // Optional - ); - - foreach (var stream in streams) stream.Dispose(); - - return mergedStream; // Caller responsible for disposing - } +--- - public async Task MergePdfsToFileAsync(List pdfPaths, string outputPath) - { - using var mergedStream = await MergePdfsAsync(pdfPaths); - - // Save to file - using var fileStream = File.Create(outputPath); - await mergedStream.CopyToAsync(fileStream); - } +## Konfigurationsoptionen - // ANNOTATE - public async Task AddHighlightAsync(byte[] pdfBytes) - { - var request = new AddAnnotationBase64Request - { - Base64Pdf = Convert.ToBase64String(pdfBytes), - AnnotationType = AnnotationType.TextMarkup, - PageNumber = 1, - X1 = 100, - Y1 = 200, - Width = 150, - Height = 20, - Color = "FFFF00", // Yellow - TextMarkupStyle = TextMarkupStyle.Highlight, - Origin = AnnotationOrigin.TopLeft - }; - - // Returns Stream - convert to bytes - using var annotatedStream = await _operationsClient.AnnotateAsync(pdfBytes, request); - return await annotatedStream.ToBytesAsync(); - } - - // STAMP - public async Task AddStampAsync(Stream pdfStream) - { - var request = new AddStampBase64Request - { - Base64Pdf = string.Empty, // Will be filled by client - StampType = StampType.Text, - X = 300, - Y = 50, - Text = "CONFIDENTIAL", - FontName = "Arial", - FontSize = 24, - Color = "FF0000", // Red - Opacity = 0.5, - Rotation = 45, - Placement = StampPlacement.Foreground, - Origin = AnnotationOrigin.BottomLeft - }; - - // Returns stamped PDF as Stream - return await _operationsClient.StampAsync(pdfStream, request); - } -} -``` - -### 4. Swiss QR Code Extraction - -```csharp -using DocumentService.Client.Interfaces; - -public class QrCodeService -{ - private readonly ISwissQrCodeClient _qrCodeClient; - - public QrCodeService(ISwissQrCodeClient qrCodeClient) - { - _qrCodeClient = qrCodeClient; - } - - public async Task ExtractQrCodeAsync(byte[] pdfBytes) - { - // Get parsed Bill object - var result = await _qrCodeClient.ExtractSwissQrCodeAsync(pdfBytes, raw: false); - - Console.WriteLine($"Bill: {result.Bill}"); - } - - public async Task ExtractRawQrCodeAsync(Stream pdfStream) - { - // Get raw QR text lines - var result = await _qrCodeClient.ExtractSwissQrCodeAsync(pdfStream, raw: true); - - Console.WriteLine("Raw QR Lines:"); - foreach (var line in result.RawLines) - { - Console.WriteLine($" {line}"); - } - } -} -``` - -## API Endpoints - -| **Client** | **Method** | **API Endpoint** | **Description** | +| Eigenschaft | Typ | Standardwert | Beschreibung | |---|---|---|---| -| **IPdfValidationClient** | `ValidatePdfAsync()` | `POST /api/pdf/validation/validate` | Validates PDF and returns metadata | -| **IPdfValidationClient** | `ValidatePdfAAsync()` | `POST /api/pdf/validation/validate-pdfa` | Validates PDF/A conformance | -| **IPdfAttachmentClient** | `CheckAttachmentsAsync()` | `POST /api/pdf/attachments/check` | Checks for embedded attachments | -| **IPdfAttachmentClient** | `ExtractAttachmentsAsync()` | `POST /api/pdf/attachments/extract` | Extracts attachments as ZIP | -| **IPdfOperationsClient** | `MergeAsync()` | `POST /api/pdf/operations/merge` | Merges multiple PDFs | -| **IPdfOperationsClient** | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | Adds annotations (highlight, notes, etc.) | -| **IPdfOperationsClient** | `StampAsync()` | `POST /api/pdf/operations/stamp` | Adds text/image stamps | -| **ISwissQrCodeClient** | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/qr-code/extract-swiss` | Extracts Swiss QR Code data | +| `BaseUrl` | `string` | `"http://localhost:5000"` | Basis-URL der DocumentService-API | +| `Timeout` | `TimeSpan` | `TimeSpan.FromMinutes(5)` | HTTP-Anfrage-Timeout | +| `MaxRetries` | `int` | `3` | Maximale Wiederholungsversuche bei transienten Fehlern | +| `ThrowOnError` | `bool` | `true` | Exception bei HTTP 4xx/5xx-Antworten | -## Error Handling +--- + +## Verwendungsbeispiele + +Alle Beispiele nutzen ausschließlich den statischen `Client`-Einstiegspunkt. Voraussetzung ist eine einmalige Konfiguration wie oben beschrieben. + +--- + +### 1. PDF-Validierung ```csharp +using DocumentService.Client; + +// Variante A: Stream (multipart/form-data – empfohlen für große Dateien) +using var stream = File.OpenRead("rechnung.pdf"); +var result = await Client.Validation.ValidatePdfAsync(stream); + +Console.WriteLine($"Seiten: {result.PageCount}"); +Console.WriteLine($"PDF-Version: {result.PdfVersion}"); +Console.WriteLine($"Verschlüsselt: {result.IsEncrypted}"); +Console.WriteLine($"Anhänge: {result.AttachmentCount}"); +Console.WriteLine($"Dateigröße: {result.FileSizeMB:F2} MB"); + +// Variante B: byte[] (Base64 JSON) +byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf"); +var result2 = await Client.Validation.ValidatePdfAsync(pdfBytes); +``` + +```vbnet +Imports DocumentService.Client + +' Variante A: Stream +Dim stream As New System.IO.FileStream("rechnung.pdf", System.IO.FileMode.Open) +Dim result = Await Client.Validation.ValidatePdfAsync(stream) +stream.Dispose() + +Console.WriteLine($"Seiten: {result.PageCount}") +Console.WriteLine($"PDF-Version: {result.PdfVersion}") +Console.WriteLine($"Verschlüsselt: {result.IsEncrypted}") +Console.WriteLine($"Anhänge: {result.AttachmentCount}") +Console.WriteLine($"Dateigröße: {result.FileSizeMB:F2} MB") + +' Variante B: byte[] +Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf") +Dim result2 = Await Client.Validation.ValidatePdfAsync(pdfBytes) +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +# Variante A: Stream +$stream = [System.IO.File]::OpenRead("rechnung.pdf") +$result = [DocumentService.Client.Client]::Validation.ValidatePdfAsync($stream).GetAwaiter().GetResult() +$stream.Dispose() + +Write-Host "Seiten: $($result.PageCount)" +Write-Host "PDF-Version: $($result.PdfVersion)" +Write-Host "Verschluesselt:$($result.IsEncrypted)" +Write-Host "Anhaenge: $($result.AttachmentCount)" +Write-Host "Dateigroesse: $($result.FileSizeMB.ToString('F2')) MB" + +# Variante B: byte[] +$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf") +$result2 = [DocumentService.Client.Client]::Validation.ValidatePdfAsync($pdfBytes).GetAwaiter().GetResult() +``` + +--- + +#### PDF/A-Konformitätsprüfung + +```csharp +using DocumentService.Client; + +using var stream = File.OpenRead("archiv.pdf"); +var result = await Client.Validation.ValidatePdfAAsync(stream); + +Console.WriteLine($"PDF/A-konform: {result.IsValid}"); +Console.WriteLine($"PDF/A-Version: {result.PdfAVersion}"); +Console.WriteLine($"Seiten: {result.PageCount}"); + +if (result.Errors.Count > 0) +{ + Console.WriteLine("Konformitätsfehler:"); + foreach (var error in result.Errors) + Console.WriteLine($" - {error}"); +} + +if (result.Warnings.Count > 0) +{ + Console.WriteLine("Warnungen:"); + foreach (var warning in result.Warnings) + Console.WriteLine($" - {warning}"); +} +``` + +```vbnet +Imports DocumentService.Client + +Dim stream As New System.IO.FileStream("archiv.pdf", System.IO.FileMode.Open) +Dim result = Await Client.Validation.ValidatePdfAAsync(stream) +stream.Dispose() + +Console.WriteLine($"PDF/A-konform: {result.IsValid}") +Console.WriteLine($"PDF/A-Version: {result.PdfAVersion}") +Console.WriteLine($"Seiten: {result.PageCount}") + +If result.Errors.Count > 0 Then + Console.WriteLine("Konformitätsfehler:") + For Each [error] In result.Errors + Console.WriteLine($" - {[error]}") + Next +End If + +If result.Warnings.Count > 0 Then + Console.WriteLine("Warnungen:") + For Each warning In result.Warnings + Console.WriteLine($" - {warning}") + Next +End If +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$stream = [System.IO.File]::OpenRead("archiv.pdf") +$result = [DocumentService.Client.Client]::Validation.ValidatePdfAAsync($stream).GetAwaiter().GetResult() +$stream.Dispose() + +Write-Host "PDF/A-konform: $($result.IsValid)" +Write-Host "PDF/A-Version: $($result.PdfAVersion)" +Write-Host "Seiten: $($result.PageCount)" + +if ($result.Errors.Count -gt 0) { + Write-Host "Konformitaetsfehler:" + foreach ($err in $result.Errors) { Write-Host " - $err" } +} + +if ($result.Warnings.Count -gt 0) { + Write-Host "Warnungen:" + foreach ($warn in $result.Warnings) { Write-Host " - $warn" } +} +``` + +--- + +### 2. PDF-Anhänge + +#### Anhänge erkennen + +```csharp +using DocumentService.Client; + +byte[] pdfBytes = await File.ReadAllBytesAsync("dokument.pdf"); +var check = await Client.Attachment.CheckAttachmentsAsync(pdfBytes); + +Console.WriteLine($"Hat Anhänge: {check.HasAttachments}"); +Console.WriteLine($"Anzahl: {check.AttachmentCount}"); + +foreach (var attachment in check.Attachments) + Console.WriteLine($" {attachment.FileName} ({attachment.Size} Bytes, {attachment.MimeType})"); +``` + +```vbnet +Imports DocumentService.Client + +Dim pdfBytes = System.IO.File.ReadAllBytes("dokument.pdf") +Dim check = Await Client.Attachment.CheckAttachmentsAsync(pdfBytes) + +Console.WriteLine($"Hat Anhänge: {check.HasAttachments}") +Console.WriteLine($"Anzahl: {check.AttachmentCount}") + +For Each attachment In check.Attachments + Console.WriteLine($" {attachment.FileName} ({attachment.Size} Bytes, {attachment.MimeType})") +Next +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$pdfBytes = [System.IO.File]::ReadAllBytes("dokument.pdf") +$check = [DocumentService.Client.Client]::Attachment.CheckAttachmentsAsync($pdfBytes).GetAwaiter().GetResult() + +Write-Host "Hat Anhaenge: $($check.HasAttachments)" +Write-Host "Anzahl: $($check.AttachmentCount)" + +foreach ($att in $check.Attachments) { + Write-Host " $($att.FileName) ($($att.Size) Bytes, $($att.MimeType))" +} +``` + +#### Anhänge extrahieren + +Rückgabe: `Dictionary` – Schlüssel = ursprünglicher Dateiname im PDF, Wert = Dateiinhalt als `MemoryStream`. Der Aufrufer ist für die Freigabe der Streams verantwortlich. + +```csharp +using DocumentService.Client; + +using var pdfStream = File.OpenRead("dokument_mit_anhaengen.pdf"); +var files = await Client.Attachment.ExtractAttachmentsAsync(pdfStream); + +foreach (var (fileName, fileStream) in files) +{ + await using var fs = fileStream; + var outputPath = Path.Combine("output", fileName); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); + await using var output = File.Create(outputPath); + await fs.CopyToAsync(output); + Console.WriteLine($"Extrahiert: {outputPath}"); +} +``` + +```vbnet +Imports DocumentService.Client + +Dim pdfStream As New System.IO.FileStream("dokument_mit_anhaengen.pdf", System.IO.FileMode.Open) +Dim files = Await Client.Attachment.ExtractAttachmentsAsync(pdfStream) +pdfStream.Dispose() + +For Each entry In files + Dim outputPath = System.IO.Path.Combine("output", entry.Key) + System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outputPath)) + Using fs = entry.Value + Using output = System.IO.File.Create(outputPath) + Await fs.CopyToAsync(output) + End Using + End Using + Console.WriteLine($"Extrahiert: {outputPath}") +Next +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$pdfStream = [System.IO.File]::OpenRead("dokument_mit_anhaengen.pdf") +$files = [DocumentService.Client.Client]::Attachment.ExtractAttachmentsAsync($pdfStream).GetAwaiter().GetResult() +$pdfStream.Dispose() + +New-Item -ItemType Directory -Force -Path "output" | Out-Null + +foreach ($entry in $files.GetEnumerator()) { + $outputPath = Join-Path "output" $entry.Key + $dir = Split-Path $outputPath -Parent + if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } + + $outFile = [System.IO.File]::Create($outputPath) + $entry.Value.CopyTo($outFile) + $outFile.Dispose() + $entry.Value.Dispose() + + Write-Host "Extrahiert: $outputPath" +} +``` + +> **Hinweis – `AddAttachmentsAsync`:** Das Einbetten von Anhängen in PDF/A-3-Dokumente ist für eine zukünftige Version geplant. Die Methode ist bereits im Interface definiert, jedoch als `[Obsolete]` markiert und wirft zur Laufzeit eine `NotImplementedException`. Der API-Endpunkt `POST /api/pdf/attachments/add` ist noch nicht implementiert. + +--- + +### 3. PDF-Operationen + +#### Zusammenführen + +```csharp +using DocumentService.Client; + +// pageRanges ist optional – null bedeutet "alle Seiten dieses Dokuments" +// Seitenbereich-Syntax: "1-3" = Seiten 1 bis 3, "2,4,6" = einzelne Seiten +var streams = new List +{ + File.OpenRead("teil1.pdf"), + File.OpenRead("teil2.pdf"), + File.OpenRead("teil3.pdf") +}; + +using var mergedStream = await Client.Operations.MergeAsync( + streams, + pageRanges: new List { "1-2", null, "3,5" } +); + +foreach (var s in streams) await s.DisposeAsync(); + +await using var output = File.Create("zusammengefuehrt.pdf"); +await mergedStream.CopyToAsync(output); +``` + +```vbnet +Imports DocumentService.Client + +Dim streams As New List(Of System.IO.Stream) From { + System.IO.File.OpenRead("teil1.pdf"), + System.IO.File.OpenRead("teil2.pdf"), + System.IO.File.OpenRead("teil3.pdf") +} +Dim pageRanges As New List(Of String) From {"1-2", Nothing, "3,5"} + +Dim mergedStream = Await Client.Operations.MergeAsync(streams, pageRanges) + +For Each s In streams + s.Dispose() +Next + +Using output = System.IO.File.Create("zusammengefuehrt.pdf") + Await mergedStream.CopyToAsync(output) +End Using +mergedStream.Dispose() +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$streams = [System.Collections.Generic.List[System.IO.Stream]]::new() +$streams.Add([System.IO.File]::OpenRead("teil1.pdf")) +$streams.Add([System.IO.File]::OpenRead("teil2.pdf")) +$streams.Add([System.IO.File]::OpenRead("teil3.pdf")) + +$pageRanges = [System.Collections.Generic.List[string]]::new() +$pageRanges.Add("1-2") +$pageRanges.Add($null) +$pageRanges.Add("3,5") + +$mergedStream = [DocumentService.Client.Client]::Operations.MergeAsync($streams, $pageRanges).GetAwaiter().GetResult() + +foreach ($s in $streams) { $s.Dispose() } + +$output = [System.IO.File]::Create("zusammengefuehrt.pdf") +$mergedStream.CopyTo($output) +$output.Dispose() +$mergedStream.Dispose() +``` + +#### Annotieren + +```csharp +using DocumentService.Client; +using DocumentService.Application.Common.DTOs.Requests; +using DocumentService.Domain.Models.ValueObjects; + +byte[] pdfBytes = await File.ReadAllBytesAsync("dokument.pdf"); + +var request = new AddAnnotationBase64Request +{ + Base64Pdf = string.Empty, // wird intern vom Client befüllt + AnnotationType = AnnotationType.TextMarkup, + PageNumber = 1, + X1 = 100, + Y1 = 200, + Width = 300, + Height = 20, + Color = "FFFF00", // Gelb (RGB-Hex) + TextMarkupStyle = TextMarkupStyle.Highlight, + Origin = AnnotationOrigin.TopLeft // Y-Achse von oben (UI-Koordinaten) +}; + +using var annotatedStream = await Client.Operations.AnnotateAsync(pdfBytes, request); +await using var output = File.Create("annotiert.pdf"); +await annotatedStream.CopyToAsync(output); +``` + +```vbnet +Imports DocumentService.Client +Imports DocumentService.Application.Common.DTOs.Requests +Imports DocumentService.Domain.Models.ValueObjects + +Dim pdfBytes = System.IO.File.ReadAllBytes("dokument.pdf") + +Dim request As New AddAnnotationBase64Request With { + .Base64Pdf = String.Empty, + .AnnotationType = AnnotationType.TextMarkup, + .PageNumber = 1, + .X1 = 100, + .Y1 = 200, + .Width = 300, + .Height = 20, + .Color = "FFFF00", + .TextMarkupStyle = TextMarkupStyle.Highlight, + .Origin = AnnotationOrigin.TopLeft +} + +Dim annotatedStream = Await Client.Operations.AnnotateAsync(pdfBytes, request) +Using output = System.IO.File.Create("annotiert.pdf") + Await annotatedStream.CopyToAsync(output) +End Using +annotatedStream.Dispose() +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$pdfBytes = [System.IO.File]::ReadAllBytes("dokument.pdf") + +$request = [DocumentService.Application.Common.DTOs.Requests.AddAnnotationBase64Request]::new() +$request.Base64Pdf = "" +$request.AnnotationType = [DocumentService.Domain.Models.ValueObjects.AnnotationType]::TextMarkup +$request.PageNumber = 1 +$request.X1 = 100 +$request.Y1 = 200 +$request.Width = 300 +$request.Height = 20 +$request.Color = "FFFF00" +$request.TextMarkupStyle = [DocumentService.Domain.Models.ValueObjects.TextMarkupStyle]::Highlight +$request.Origin = [DocumentService.Domain.Models.ValueObjects.AnnotationOrigin]::TopLeft + +$annotatedStream = [DocumentService.Client.Client]::Operations.AnnotateAsync($pdfBytes, $request).GetAwaiter().GetResult() +$output = [System.IO.File]::Create("annotiert.pdf") +$annotatedStream.CopyTo($output) +$output.Dispose() +$annotatedStream.Dispose() +``` + +**Verfügbare Annotationstypen:** + +| `AnnotationType` | Beschreibung | +|---|---| +| `TextMarkup` | Hervorhebung, Unterstreichung oder Durchstreichung (`TextMarkupStyle`) | +| `FreeText` | Freitextkommentar direkt auf der Seite | +| `StickyNote` | Klebezettel (Popup-Kommentar) | +| `Circle` | Kreismarkierung | +| `Square` | Rechteckmarkierung | + +**Koordinatenursprung (`AnnotationOrigin`):** + +| Wert | Bedeutung | +|---|---| +| `TopLeft` | Y-Achse von oben – entspricht UI-Koordinaten (z. B. aus einem Viewer) | +| `BottomLeft` | Y-Achse von unten – natives PDF-Koordinatensystem | + +#### Stempel + +```csharp +using DocumentService.Client; +using DocumentService.Application.Common.DTOs.Requests; +using DocumentService.Domain.Models.ValueObjects; + +// Textstempel diagonal auf alle Seiten +using var pdfStream = File.OpenRead("dokument.pdf"); + +var request = new AddStampBase64Request +{ + Base64Pdf = string.Empty, // wird intern vom Client befüllt + StampType = StampType.Text, + Text = "VERTRAULICH", + FontName = "Arial", + FontSize = 36, + Color = "FF0000", // Rot + Opacity = 0.4, // 40% Transparenz + Rotation = 45, // Grad gegen den Uhrzeigersinn + X = 150, + Y = 400, + Placement = StampPlacement.Foreground, // über dem Inhalt + Origin = AnnotationOrigin.BottomLeft + // PageNumbers = null → alle Seiten +}; + +using var stampedStream = await Client.Operations.StampAsync(pdfStream, request); +await using var output = File.Create("gestempelt.pdf"); +await stampedStream.CopyToAsync(output); +``` + +```vbnet +Imports DocumentService.Client +Imports DocumentService.Application.Common.DTOs.Requests +Imports DocumentService.Domain.Models.ValueObjects + +Dim pdfStream As New System.IO.FileStream("dokument.pdf", System.IO.FileMode.Open) + +Dim request As New AddStampBase64Request With { + .Base64Pdf = String.Empty, + .StampType = StampType.Text, + .Text = "VERTRAULICH", + .FontName = "Arial", + .FontSize = 36, + .Color = "FF0000", + .Opacity = 0.4, + .Rotation = 45, + .X = 150, + .Y = 400, + .Placement = StampPlacement.Foreground, + .Origin = AnnotationOrigin.BottomLeft +} + +Dim stampedStream = Await Client.Operations.StampAsync(pdfStream, request) +pdfStream.Dispose() + +Using output = System.IO.File.Create("gestempelt.pdf") + Await stampedStream.CopyToAsync(output) +End Using +stampedStream.Dispose() +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$pdfStream = [System.IO.File]::OpenRead("dokument.pdf") + +$request = [DocumentService.Application.Common.DTOs.Requests.AddStampBase64Request]::new() +$request.Base64Pdf = "" +$request.StampType = [DocumentService.Domain.Models.ValueObjects.StampType]::Text +$request.Text = "VERTRAULICH" +$request.FontName = "Arial" +$request.FontSize = 36 +$request.Color = "FF0000" +$request.Opacity = 0.4 +$request.Rotation = 45 +$request.X = 150 +$request.Y = 400 +$request.Placement = [DocumentService.Domain.Models.ValueObjects.StampPlacement]::Foreground +$request.Origin = [DocumentService.Domain.Models.ValueObjects.AnnotationOrigin]::BottomLeft + +$stampedStream = [DocumentService.Client.Client]::Operations.StampAsync($pdfStream, $request).GetAwaiter().GetResult() +$pdfStream.Dispose() + +$output = [System.IO.File]::Create("gestempelt.pdf") +$stampedStream.CopyTo($output) +$output.Dispose() +$stampedStream.Dispose() +``` + +```csharp +// Vordefinierter Stempel +var request = new AddStampBase64Request +{ + Base64Pdf = string.Empty, + StampType = StampType.Predefined, + PredefinedStamp = PredefinedStampType.Approved, + X = 400, + Y = 700, + Placement = StampPlacement.Foreground, + Origin = AnnotationOrigin.BottomLeft +}; +``` + +```vbnet +' Vordefinierter Stempel +Dim request As New AddStampBase64Request With { + .Base64Pdf = String.Empty, + .StampType = StampType.Predefined, + .PredefinedStamp = PredefinedStampType.Approved, + .X = 400, + .Y = 700, + .Placement = StampPlacement.Foreground, + .Origin = AnnotationOrigin.BottomLeft +} +``` + +```powershell +# Vordefinierter Stempel +$request = [DocumentService.Application.Common.DTOs.Requests.AddStampBase64Request]::new() +$request.Base64Pdf = "" +$request.StampType = [DocumentService.Domain.Models.ValueObjects.StampType]::Predefined +$request.PredefinedStamp = [DocumentService.Domain.Models.ValueObjects.PredefinedStampType]::Approved +$request.X = 400 +$request.Y = 700 +$request.Placement = [DocumentService.Domain.Models.ValueObjects.StampPlacement]::Foreground +$request.Origin = [DocumentService.Domain.Models.ValueObjects.AnnotationOrigin]::BottomLeft +``` + +**Verfügbare Stempeltypen:** + +| `StampType` | Beschreibung | +|---|---| +| `Text` | Eigener Textstempel mit freier Formatierung | +| `Image` | Bildstempel – `Base64Image` als PNG/JPEG in Base64 | +| `Predefined` | Vorgefertigter Stempel: `Confidential`, `Approved`, `Draft`, `Void`, `ForReview` | + +--- + +### 4. Schweizer QR-Code-Extraktion + +```csharp +using DocumentService.Client; + +// Geparste Rechnungsstruktur gemäß Swiss QR Bill Standard 2.0 +byte[] pdfBytes = await File.ReadAllBytesAsync("qr-rechnung.pdf"); +var result = await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw: false); + +Console.WriteLine($"Betrag: {result.Bill?.Amount} {result.Bill?.Currency}"); +Console.WriteLine($"Konto: {result.Bill?.Account}"); +Console.WriteLine($"Empfänger: {result.Bill?.Creditor?.Name}, {result.Bill?.Creditor?.Town}"); +Console.WriteLine($"Referenz: {result.Bill?.Reference}"); +Console.WriteLine($"Mitteilung:{result.Bill?.UnstructuredMessage}"); +``` + +```vbnet +Imports DocumentService.Client + +Dim pdfBytes = System.IO.File.ReadAllBytes("qr-rechnung.pdf") +Dim result = Await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfBytes, raw:=False) + +Console.WriteLine($"Betrag: {result.Bill?.Amount} {result.Bill?.Currency}") +Console.WriteLine($"Konto: {result.Bill?.Account}") +Console.WriteLine($"Empfänger: {result.Bill?.Creditor?.Name}, {result.Bill?.Creditor?.Town}") +Console.WriteLine($"Referenz: {result.Bill?.Reference}") +Console.WriteLine($"Mitteilung:{result.Bill?.UnstructuredMessage}") +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$pdfBytes = [System.IO.File]::ReadAllBytes("qr-rechnung.pdf") +$result = [DocumentService.Client.Client]::SwissQrCode.ExtractSwissQrCodeAsync($pdfBytes, $false).GetAwaiter().GetResult() + +Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)" +Write-Host "Konto: $($result.Bill.Account)" +Write-Host "Empfaenger:$($result.Bill.Creditor.Name), $($result.Bill.Creditor.Town)" +Write-Host "Referenz: $($result.Bill.Reference)" +Write-Host "Mitteilung:$($result.Bill.UnstructuredMessage)" +``` + +--- + +#### Rohe QR-Textzeilen + +```csharp +// Ohne Parsing – direkt aus dem QR-Code-Inhalt +using var pdfStream = File.OpenRead("qr-rechnung.pdf"); +var result = await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfStream, raw: true); + +foreach (var line in result.RawLines) + Console.WriteLine(line); +``` + +```vbnet +Dim pdfStream As New System.IO.FileStream("qr-rechnung.pdf", System.IO.FileMode.Open) +Dim result = Await Client.SwissQrCode.ExtractSwissQrCodeAsync(pdfStream, raw:=True) +pdfStream.Dispose() + +For Each line In result.RawLines + Console.WriteLine(line) +Next +``` + +```powershell +$pdfStream = [System.IO.File]::OpenRead("qr-rechnung.pdf") +$result = [DocumentService.Client.Client]::SwissQrCode.ExtractSwissQrCodeAsync($pdfStream, $true).GetAwaiter().GetResult() +$pdfStream.Dispose() + +foreach ($line in $result.RawLines) { Write-Host $line } +``` + +> **Pflichtanforderung laut Standard:** Der QR-Code muss sich auf der **letzten Seite** des PDFs befinden. PDFs, bei denen der QR-Code auf einer anderen Seite liegt, werden nicht erkannt. + +--- + +### 5. ZUGFeRD-Rechnungen + +#### ZUGFeRD-Einbettung prüfen + +```csharp +using DocumentService.Client; + +byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf"); +var check = await Client.Zugferd.HasZugferdAsync(pdfBytes); + +Console.WriteLine($"Enthält ZUGFeRD: {check.HasZugferd}"); + +if (check.HasZugferd) +{ + Console.WriteLine($"XML-Dateiname: {check.ZugferdFileName}"); + Console.WriteLine($"Dateigröße: {check.ZugferdFileSize} Bytes"); + Console.WriteLine($"MIME-Typ: {check.ZugferdMimeType}"); +} +``` + +```vbnet +Imports DocumentService.Client + +Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf") +Dim check = Await Client.Zugferd.HasZugferdAsync(pdfBytes) + +Console.WriteLine($"Enthält ZUGFeRD: {check.HasZugferd}") + +If check.HasZugferd Then + Console.WriteLine($"XML-Dateiname: {check.ZugferdFileName}") + Console.WriteLine($"Dateigröße: {check.ZugferdFileSize} Bytes") + Console.WriteLine($"MIME-Typ: {check.ZugferdMimeType}") +End If +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf") +$check = [DocumentService.Client.Client]::Zugferd.HasZugferdAsync($pdfBytes).GetAwaiter().GetResult() + +Write-Host "Enthaelt ZUGFeRD: $($check.HasZugferd)" + +if ($check.HasZugferd) { + Write-Host "XML-Dateiname: $($check.ZugferdFileName)" + Write-Host "Dateigroesse: $($check.ZugferdFileSize) Bytes" + Write-Host "MIME-Typ: $($check.ZugferdMimeType)" +} +``` + +#### ZUGFeRD-XML als Stream extrahieren + +```csharp +using var pdfStream = File.OpenRead("eingangsrechnung.pdf"); +using var xmlStream = await Client.Zugferd.ExtractZugferdAsync(pdfStream); + +await using var output = File.Create("factur-x.xml"); +await xmlStream.CopyToAsync(output); +Console.WriteLine("ZUGFeRD-XML gespeichert."); +``` + +```vbnet +Dim pdfStream As New System.IO.FileStream("eingangsrechnung.pdf", System.IO.FileMode.Open) +Dim xmlStream = Await Client.Zugferd.ExtractZugferdAsync(pdfStream) +pdfStream.Dispose() + +Using output = System.IO.File.Create("factur-x.xml") + Await xmlStream.CopyToAsync(output) +End Using +xmlStream.Dispose() +Console.WriteLine("ZUGFeRD-XML gespeichert.") +``` + +```powershell +$pdfStream = [System.IO.File]::OpenRead("eingangsrechnung.pdf") +$xmlStream = [DocumentService.Client.Client]::Zugferd.ExtractZugferdAsync($pdfStream).GetAwaiter().GetResult() +$pdfStream.Dispose() + +$output = [System.IO.File]::Create("factur-x.xml") +$xmlStream.CopyTo($output) +$output.Dispose() +$xmlStream.Dispose() +Write-Host "ZUGFeRD-XML gespeichert." +``` + +#### ZUGFeRD-XML als strukturiertes Ergebnis + +```csharp +byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf"); +var result = await Client.Zugferd.ExtractZugferdAsResultAsync(pdfBytes); + +Console.WriteLine($"Dateiname: {result.FileName}"); +Console.WriteLine($"Größe: {result.FileSize} Bytes"); +Console.WriteLine($"XML-Inhalt: {result.XmlContent[..500]}..."); + +// XML weiterverarbeiten, z. B. mit System.Xml.Linq +var doc = XDocument.Parse(result.XmlContent); +``` + +```vbnet +Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf") +Dim result = Await Client.Zugferd.ExtractZugferdAsResultAsync(pdfBytes) + +Console.WriteLine($"Dateiname: {result.FileName}") +Console.WriteLine($"Größe: {result.FileSize} Bytes") +Console.WriteLine($"XML-Inhalt: {result.XmlContent.Substring(0, 500)}...") + +' XML weiterverarbeiten +Dim doc = System.Xml.Linq.XDocument.Parse(result.XmlContent) +``` + +```powershell +$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf") +$result = [DocumentService.Client.Client]::Zugferd.ExtractZugferdAsResultAsync($pdfBytes).GetAwaiter().GetResult() + +Write-Host "Dateiname: $($result.FileName)" +Write-Host "Groesse: $($result.FileSize) Bytes" +Write-Host "XML-Inhalt: $($result.XmlContent.Substring(0, 500))..." + +# XML weiterverarbeiten +$doc = [System.Xml.Linq.XDocument]::Parse($result.XmlContent) +``` + +--- + +### 6. PDF-Konvertierung *(geplant – noch nicht verfügbar)* + +`IPdfConversionClient` ist vollständig definiert und über `Client.Conversion` erreichbar. Alle Methoden sind jedoch als `[Obsolete]` markiert und werfen zur Laufzeit `NotImplementedException`, da die API-Endpunkte serverseitig noch nicht implementiert sind. + +**Geplante Endpunkte (Phase 3):** +- `POST /api/pdf/conversion/to-pdfa` – beliebiges PDF in PDF/A konvertieren +- `POST /api/pdf/conversion/from-pdfa` – PDF/A-Einschränkungen entfernen + +```csharp +// NICHT VERWENDEN – wirft NotImplementedException +#pragma warning disable CS0618 +using var pdfAStream = await Client.Conversion.ConvertToPdfAAsync(pdfStream, pdfALevel: "PDF/A-3b"); +#pragma warning restore CS0618 +``` + +```vbnet +' NICHT VERWENDEN – wirft NotImplementedException +#Disable Warning BC40000 +Dim pdfAStream = Await Client.Conversion.ConvertToPdfAAsync(pdfStream, pdfALevel:="PDF/A-3b") +#Enable Warning BC40000 +``` + +```powershell +# NICHT VERWENDEN – wirft NotImplementedException +$pdfAStream = [DocumentService.Client.Client]::Conversion.ConvertToPdfAAsync($pdfStream, "PDF/A-3b").GetAwaiter().GetResult() +``` + +Sobald die Endpunkte auf dem Server bereitgestellt werden, entfällt das `[Obsolete]`-Attribut und die Methoden sind ohne weiteren Anpassungsbedarf auf Clientseite verwendbar. + +--- + +## API-Endpunkte (Übersicht) + +| **Client-Eigenschaft** | **Methode** | **API-Endpunkt** | **Status** | **Beschreibung** | +|---|---|---|---|---| +| `Client.Validation` | `ValidatePdfAsync()` | `POST /api/pdf/validation/validate` | ✅ Verfügbar | PDF validieren, Metadaten abrufen | +| `Client.Validation` | `ValidatePdfAAsync()` | `POST /api/pdf/validation/validate-pdfa` | ✅ Verfügbar | PDF/A-Konformität prüfen | +| `Client.Attachment` | `CheckAttachmentsAsync()` | `POST /api/pdf/attachments/check` | ✅ Verfügbar | Eingebettete Anhänge erkennen | +| `Client.Attachment` | `ExtractAttachmentsAsync()` | `POST /api/pdf/attachments/extract` | ✅ Verfügbar | Anhänge extrahieren als `Dictionary` | +| `Client.Attachment` | `AddAttachmentsAsync()` | `POST /api/pdf/attachments/add` | ⏳ Geplant | Anhänge in PDF/A-3 einbetten | +| `Client.Operations` | `MergeAsync()` | `POST /api/pdf/operations/merge` | ✅ Verfügbar | Mehrere PDFs zusammenführen | +| `Client.Operations` | `AnnotateAsync()` | `POST /api/pdf/operations/annotate` | ✅ Verfügbar | Annotationen hinzufügen | +| `Client.Operations` | `StampAsync()` | `POST /api/pdf/operations/stamp` | ✅ Verfügbar | Text-/Bildstempel hinzufügen | +| `Client.SwissQrCode` | `ExtractSwissQrCodeAsync()` | `POST /api/pdf/qr-code/extract-swiss` | ✅ Verfügbar | Schweizer QR-Code-Daten extrahieren | +| `Client.Zugferd` | `HasZugferdAsync()` | `POST /api/pdf/zugferd/has-zugferd` | ✅ Verfügbar | ZUGFeRD-Einbettung prüfen | +| `Client.Zugferd` | `ExtractZugferdAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML als Stream | +| `Client.Zugferd` | `ExtractZugferdAsResultAsync()` | `POST /api/pdf/zugferd/extract` | ✅ Verfügbar | ZUGFeRD-XML mit Metadaten als Objekt | +| `Client.Conversion` | `ConvertToPdfAAsync()` | `POST /api/pdf/conversion/to-pdfa` | ⏳ Geplant | PDF zu PDF/A konvertieren | +| `Client.Conversion` | `ConvertFromPdfAAsync()` | `POST /api/pdf/conversion/from-pdfa` | ⏳ Geplant | PDF/A-Einschränkungen aufheben | + +> **⏳ Geplant:** Der Client-Code ist vorhanden und kompiliert fehlerfrei. Ein Aufruf zur Laufzeit wirft jedoch `NotImplementedException`, da der serverseitige Endpunkt noch nicht existiert. Die Methoden sind mit `[Obsolete]` markiert, sodass der Compiler eine Build-Warnung ausgibt. + +--- + +## Fehlerbehandlung + +```csharp +using DocumentService.Client; + try { - var result = await validationClient.ValidatePdfAsync(pdfBytes); + using var stream = File.OpenRead("dokument.pdf"); + var result = await Client.Validation.ValidatePdfAsync(stream); + Console.WriteLine($"Seiten: {result.PageCount}"); +} +catch (InvalidOperationException ex) when (ex.Message.Contains("not configured")) +{ + // Client.Configure() wurde nicht aufgerufen + Console.WriteLine("Client ist nicht initialisiert."); } catch (HttpRequestException ex) { - Console.WriteLine($"HTTP error: {ex.Message}"); + // Netzwerkfehler oder HTTP 4xx/5xx-Antwort vom Server + Console.WriteLine($"HTTP-Fehler: {ex.Message}"); } catch (InvalidOperationException ex) { - Console.WriteLine($"API returned null: {ex.Message}"); + // API hat null zurückgegeben (unerwartetes Serververhalten) + Console.WriteLine($"Unerwartete API-Antwort: {ex.Message}"); +} +catch (NotImplementedException ex) +{ + // Geplanter Endpunkt aufgerufen – noch nicht auf dem Server verfügbar + Console.WriteLine($"Endpunkt noch nicht implementiert: {ex.Message}"); } ``` -## License +```vbnet +Imports DocumentService.Client -Copyright © 2026 Digital Data GmbH. All rights reserved. +Try + Dim stream As New System.IO.FileStream("dokument.pdf", System.IO.FileMode.Open) + Dim result = Await Client.Validation.ValidatePdfAsync(stream) + stream.Dispose() + Console.WriteLine($"Seiten: {result.PageCount}") +Catch ex As InvalidOperationException When ex.Message.Contains("not configured") + Console.WriteLine("Client ist nicht initialisiert.") +Catch ex As System.Net.Http.HttpRequestException + Console.WriteLine($"HTTP-Fehler: {ex.Message}") +Catch ex As InvalidOperationException + Console.WriteLine($"Unerwartete API-Antwort: {ex.Message}") +Catch ex As NotImplementedException + Console.WriteLine($"Endpunkt noch nicht implementiert: {ex.Message}") +End Try +``` + +```powershell +Add-Type -Path "DocumentService.Client.dll" + +try { + $stream = [System.IO.File]::OpenRead("dokument.pdf") + $result = [DocumentService.Client.Client]::Validation.ValidatePdfAsync($stream).GetAwaiter().GetResult() + $stream.Dispose() + Write-Host "Seiten: $($result.PageCount)" +} +catch [System.InvalidOperationException] { + if ($_.Exception.Message -like "*not configured*") { + Write-Host "Client ist nicht initialisiert." + } else { + Write-Host "Unerwartete API-Antwort: $($_.Exception.Message)" + } +} +catch [System.Net.Http.HttpRequestException] { + Write-Host "HTTP-Fehler: $($_.Exception.Message)" +} +catch [System.NotImplementedException] { + Write-Host "Endpunkt noch nicht implementiert: $($_.Exception.Message)" +} +``` + +--- + +## Lizenz + +Copyright © 2026 Digital Data GmbH. Alle Rechte vorbehalten. diff --git a/DocumentService.Client/STREAM_API_DESIGN.md b/DocumentService.Client/STREAM_API_DESIGN.md deleted file mode 100644 index 9416ae6..0000000 --- a/DocumentService.Client/STREAM_API_DESIGN.md +++ /dev/null @@ -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 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` | -| **IPdfAttachmentClient** | `AddAttachmentsAsync()` | `Task` | -| **IPdfOperationsClient** | `MergeAsync()` | `Task` | -| **IPdfOperationsClient** | `AnnotateAsync()` | `Task` | -| **IPdfOperationsClient** | `StampAsync()` | `Task` | - -**Query Methods** (Metadata only): -- `IPdfValidationClient.ValidatePdfAsync()` ? `Task` -- `IPdfAttachmentClient.CheckAttachmentsAsync()` ? `Task` -- `ISwissQrCodeClient.ExtractSwissQrCodeAsync()` ? `Task` - ---- - -## ?? 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 diff --git a/DocumentService.sln b/DocumentService.sln index e029e76..098d184 100644 --- a/DocumentService.sln +++ b/DocumentService.sln @@ -46,8 +46,8 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Debug|Any CPU.Build.0 = Release|Any CPU {626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {626E2002-8EC1-4FF9-A9A6-51D5809E26FC}.Release|Any CPU.Build.0 = Release|Any CPU {3899542A-D6B3-5FF3-2493-093DE5BB0CEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU