# DocumentService.Client .NET-Clientbibliothek für die DocumentService-API – unterstützt .NET Framework 4.6.2, 4.8 und .NET 8.0. ## Funktionsumfang - **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 - **Workflows**: `Client.Workflows` – kombinierte Operationen mit vorgelagerter Validierung (z. B. Swiss QR Code mit automatischer PDF-Prüfung) ## Installation 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 ``` **Schritt 2 – Assembly für die Zielplattform laden:** ```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; // Kurzform – nur Base-URL Client.Configure("http://172.24.12.39:9393/"); // Vollständige Konfiguration Client.Configure(options => { options.BaseUrl = "http://172.24.12.39:9393/"; options.Timeout = TimeSpan.FromMinutes(10); options.MaxRetries = 3; options.ThrowOnError = true; }); ``` ```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 // Standard – wirft bei erneutem Configure()-Aufruf Client.OnReconfigure = OnReconfigure.ThrowException; // Zweiten Aufruf stillschweigend ignorieren (z. B. in Bibliotheks-Code) Client.OnReconfigure = OnReconfigure.Ignore; ``` ```vbnet ' Standard – wirft bei erneutem Configure()-Aufruf Client.OnReconfigure = OnReconfigure.ThrowException ' Zweiten Aufruf stillschweigend ignorieren Client.OnReconfigure = OnReconfigure.Ignore ``` ```powershell # Standard – wirft bei erneutem Configure()-Aufruf [DocumentService.Client.Client]::OnReconfigure = [DocumentService.Client.Models.ValueObjects.OnReconfigure]::ThrowException # Zweiten Aufruf stillschweigend ignorieren [DocumentService.Client.Client]::OnReconfigure = [DocumentService.Client.Models.ValueObjects.OnReconfigure]::Ignore ``` ### Verfügbare Client-Eigenschaften | 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)* | | `Client.Workflows` | `IWorkflowsClient` | Kombinierte Operationen | 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. > **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()`. --- ## Konfigurationsoptionen | Eigenschaft | Typ | Standardwert | Beschreibung | |---|---|---|---| | `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 | --- ## Verwendungsbeispiele Alle Beispiele nutzen ausschließlich den statischen `Client`-Einstiegspunkt. Voraussetzung ist eine einmalige Konfiguration wie oben beschrieben. --- ### Workflows – Kombinierte Operationen `Client.Workflows` fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. Die Validierung läuft dabei immer zuerst – die Folgeaktion wird nur bei einem gültigen Dokument ausgeführt. > Für Anwendungsfälle wie den **eParser** (Swiss QR Bill aus Eingangsrechnungen lesen) ist dies der empfohlene Einstiegspunkt. #### Swiss QR Code – mit vorgelagerter Validierung ```csharp using DocumentService.Client; // Variante A: byte[] byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf"); var result = await Client.Workflows.ExtractSwissQrCodeValidatedAsync(pdfBytes); Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}"); Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}"); // Variante B: Stream using var stream = File.OpenRead("rechnung.pdf"); var result2 = await Client.Workflows.ExtractSwissQrCodeValidatedAsync(stream); // Variante C: Dateipfad var result3 = await Client.Workflows.ExtractSwissQrCodeValidatedAsync("rechnung.pdf"); ``` ```vbnet Imports DocumentService.Client ' Variante A: byte[] Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf") Dim result = Await Client.Workflows.ExtractSwissQrCodeValidatedAsync(pdfBytes) Console.WriteLine($"Betrag: {result?.Bill?.Amount} {result?.Bill?.Currency}") Console.WriteLine($"Empfänger: {result?.Bill?.Creditor?.Name}") ' Variante B: Dateipfad Dim result2 = Await Client.Workflows.ExtractSwissQrCodeValidatedAsync("rechnung.pdf") ``` ```powershell Add-Type -Path "DocumentService.Client.dll" # Variante A: byte[] $pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf") $result = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeValidatedAsync($pdfBytes).GetAwaiter().GetResult() Write-Host "Betrag: $($result.Bill.Amount) $($result.Bill.Currency)" Write-Host "Empfaenger:$($result.Bill.Creditor.Name)" # Variante B: Dateipfad $result2 = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeValidatedAsync("rechnung.pdf").GetAwaiter().GetResult() ``` **Fehlerbehandlung – `throwIfInvalid`:** Standardmäßig wirft die Methode eine `InvalidOperationException`, wenn das PDF ungültig ist. Mit `throwIfInvalid: false` wird stattdessen `null` zurückgegeben – geeignet, wenn ungültige Dokumente kein Ausnahmefall sind und ohne try/catch behandelt werden sollen. ```csharp // Exception (Standard) – geeignet, wenn ungültige PDFs ein Fehlerfall sind var result = await Client.Workflows.ExtractSwissQrCodeValidatedAsync(pdfBytes); // Null zurückgeben – geeignet für Batch-Verarbeitung o. Ä. var result = await Client.Workflows.ExtractSwissQrCodeValidatedAsync(pdfBytes, throwIfInvalid: false); if (result is null) { Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen."); } ``` ```vbnet ' Exception (Standard) Dim result = Await Client.Workflows.ExtractSwissQrCodeValidatedAsync(pdfBytes) ' Null zurückgeben Dim result2 = Await Client.Workflows.ExtractSwissQrCodeValidatedAsync(pdfBytes, throwIfInvalid:=False) If result2 Is Nothing Then Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.") End If ``` ```powershell # Exception (Standard) $result = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeValidatedAsync($pdfBytes).GetAwaiter().GetResult() # Null zurückgeben ($false = throwIfInvalid:false) $result2 = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeValidatedAsync($pdfBytes, $false, $false).GetAwaiter().GetResult() if ($null -eq $result2) { Write-Host "Dokument ungueltig oder verschluesselt – uebersprungen." } ``` > **Signatur:** `ExtractSwissQrCodeValidatedAsync(pdfBytes / Stream / filePath, raw = false, throwIfInvalid = true, ct = default)` > `raw: true` liefert die Rohtextzeilen des QR-Codes ohne Parsing zurück (siehe [Schweizer QR-Code-Extraktion](#4-schweizer-qr-code-extraktion)). --- ### 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 | | `Client.Workflows` | `ExtractSwissQrCodeValidatedAsync()` | — | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion in einem Schritt | > **⏳ 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 { 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) { // Netzwerkfehler oder HTTP 4xx/5xx-Antwort vom Server Console.WriteLine($"HTTP-Fehler: {ex.Message}"); } catch (InvalidOperationException ex) { // 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}"); } ``` ```vbnet Imports DocumentService.Client 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.