Files
DocumentService/DocumentService.Client
TekH 092d283a2b Refactor WorkflowsClient for unified result handling
Refactored `ExtractSwissQrCodeAsync` to `InspectSwissQrCodeAsync`, introducing `SwissQrCodeResult` to encapsulate both validation and extraction results. Removed `throwIfInvalid` parameter, ensuring validation details are always returned without exceptions. Added extension methods for file path handling to improve usability.

Updated tests to reflect the new behavior, ensuring extraction is never attempted on invalid documents and validation results are always populated. Simplified API surface by removing redundant overloads and improving stream handling. Enhanced documentation and performed general code cleanup for better maintainability.
2026-08-31 14:44:57 +02:00
..

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.1.0
API-Basis-URL: http://172.24.12.39:9393/
Swagger-Dokumentation: 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:

# NuGet-Paket herunterladen (.nupkg ist ein ZIP-Archiv)
$nupkgUrl  = "http://172.24.12.35:5000/packages/documentservice.client/1.1.0"
$nupkgPath = "$env:TEMP\DocumentService.Client.1.1.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:

# 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):

# 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)

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;
});
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)
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:

// Standard – wirft bei erneutem Configure()-Aufruf
Client.OnReconfigure = OnReconfigure.ThrowException;

// Zweiten Aufruf stillschweigend ignorieren (z. B. in Bibliotheks-Code)
Client.OnReconfigure = OnReconfigure.Ignore;
' Standard – wirft bei erneutem Configure()-Aufruf
Client.OnReconfigure = OnReconfigure.ThrowException

' Zweiten Aufruf stillschweigend ignorieren
Client.OnReconfigure = OnReconfigure.Ignore
# 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

using DocumentService.Client;

// Variante A: byte[]
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
var result = await Client.Workflows.ExtractSwissQrCodeAsync(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.ExtractSwissQrCodeAsync(stream);

// Variante C: Dateipfad
var result3 = await Client.Workflows.ExtractSwissQrCodeAsync("rechnung.pdf");
Imports DocumentService.Client

' Variante A: byte[]
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
Dim result = Await Client.Workflows.ExtractSwissQrCodeAsync(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.ExtractSwissQrCodeAsync("rechnung.pdf")
Add-Type -Path "DocumentService.Client.dll"

# Variante A: byte[]
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
$result = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeAsync($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.ExtractSwissQrCodeAsync("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.

// Exception (Standard) – geeignet, wenn ungültige PDFs ein Fehlerfall sind
var result = await Client.Workflows.ExtractSwissQrCodeAsync(pdfBytes);

// Null zurückgeben – geeignet für Batch-Verarbeitung o. Ä.
var result = await Client.Workflows.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid: false);
if (result is null)
{
    Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.");
}
' Exception (Standard)
Dim result = Await Client.Workflows.ExtractSwissQrCodeAsync(pdfBytes)

' Null zurückgeben
Dim result2 = Await Client.Workflows.ExtractSwissQrCodeAsync(pdfBytes, throwIfInvalid:=False)
If result2 Is Nothing Then
    Console.WriteLine("Dokument ungültig oder verschlüsselt – übersprungen.")
End If
# Exception (Standard)
$result = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeAsync($pdfBytes).GetAwaiter().GetResult()

# Null zurückgeben ($false = throwIfInvalid:false)
$result2 = [DocumentService.Client.Client]::Workflows.ExtractSwissQrCodeAsync($pdfBytes, $false, $false).GetAwaiter().GetResult()
if ($null -eq $result2) {
    Write-Host "Dokument ungueltig oder verschluesselt – uebersprungen."
}

Signatur: ExtractSwissQrCodeAsync(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).


1. PDF-Validierung

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.ValidateAsync(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.ValidateAsync(pdfBytes);
Imports DocumentService.Client

' Variante A: Stream
Dim stream As New System.IO.FileStream("rechnung.pdf", System.IO.FileMode.Open)
Dim result = Await Client.Validation.ValidateAsync(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.ValidateAsync(pdfBytes)
Add-Type -Path "DocumentService.Client.dll"

# Variante A: Stream
$stream = [System.IO.File]::OpenRead("rechnung.pdf")
$result = [DocumentService.Client.Client]::Validation.ValidateAsync($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.ValidateAsync($pdfBytes).GetAwaiter().GetResult()

PDF/A-Konformitätsprüfung

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}");
}
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
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

using DocumentService.Client;

byte[] pdfBytes = await File.ReadAllBytesAsync("dokument.pdf");
var check = await Client.Attachment.CheckAsync(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})");
Imports DocumentService.Client

Dim pdfBytes = System.IO.File.ReadAllBytes("dokument.pdf")
Dim check = Await Client.Attachment.CheckAsync(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
Add-Type -Path "DocumentService.Client.dll"

$pdfBytes = [System.IO.File]::ReadAllBytes("dokument.pdf")
$check = [DocumentService.Client.Client]::Attachment.CheckAsync($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<string, Stream> – Schlüssel = ursprünglicher Dateiname im PDF, Wert = Dateiinhalt als MemoryStream. Der Aufrufer ist für die Freigabe der Streams verantwortlich.

using DocumentService.Client;

using var pdfStream = File.OpenRead("dokument_mit_anhaengen.pdf");
var files = await Client.Attachment.ExtractAsync(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}");
}
Imports DocumentService.Client

Dim pdfStream As New System.IO.FileStream("dokument_mit_anhaengen.pdf", System.IO.FileMode.Open)
Dim files = Await Client.Attachment.ExtractAsync(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
Add-Type -Path "DocumentService.Client.dll"

$pdfStream = [System.IO.File]::OpenRead("dokument_mit_anhaengen.pdf")
$files = [DocumentService.Client.Client]::Attachment.ExtractAsync($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 – AddAsync: 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

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<Stream>
{
    File.OpenRead("teil1.pdf"),
    File.OpenRead("teil2.pdf"),
    File.OpenRead("teil3.pdf")
};

using var mergedStream = await Client.Operations.MergeAsync(
    streams,
    pageRanges: new List<string?> { "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);
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()
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

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);
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()
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

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);
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()
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()
// 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
};
' 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
}
# 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

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.ExtractAsync(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}");
Imports DocumentService.Client

Dim pdfBytes = System.IO.File.ReadAllBytes("qr-rechnung.pdf")
Dim result = Await Client.SwissQrCode.ExtractAsync(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}")
Add-Type -Path "DocumentService.Client.dll"

$pdfBytes = [System.IO.File]::ReadAllBytes("qr-rechnung.pdf")
$result = [DocumentService.Client.Client]::SwissQrCode.ExtractAsync($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

// Ohne Parsing – direkt aus dem QR-Code-Inhalt
using var pdfStream = File.OpenRead("qr-rechnung.pdf");
var result = await Client.SwissQrCode.ExtractAsync(pdfStream, raw: true);

foreach (var line in result.RawLines)
    Console.WriteLine(line);
Dim pdfStream As New System.IO.FileStream("qr-rechnung.pdf", System.IO.FileMode.Open)
Dim result = Await Client.SwissQrCode.ExtractAsync(pdfStream, raw:=True)
pdfStream.Dispose()

For Each line In result.RawLines
    Console.WriteLine(line)
Next
$pdfStream = [System.IO.File]::OpenRead("qr-rechnung.pdf")
$result = [DocumentService.Client.Client]::SwissQrCode.ExtractAsync($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

using DocumentService.Client;

byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf");
var check = await Client.Zugferd.CheckAsync(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}");
}
Imports DocumentService.Client

Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf")
Dim check = Await Client.Zugferd.CheckAsync(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
Add-Type -Path "DocumentService.Client.dll"

$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf")
$check = [DocumentService.Client.Client]::Zugferd.CheckAsync($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

using var pdfStream = File.OpenRead("eingangsrechnung.pdf");
using var xmlStream = await Client.Zugferd.ExtractAsync(pdfStream);

await using var output = File.Create("factur-x.xml");
await xmlStream.CopyToAsync(output);
Console.WriteLine("ZUGFeRD-XML gespeichert.");
Dim pdfStream As New System.IO.FileStream("eingangsrechnung.pdf", System.IO.FileMode.Open)
Dim xmlStream = Await Client.Zugferd.ExtractAsync(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.")
$pdfStream = [System.IO.File]::OpenRead("eingangsrechnung.pdf")
$xmlStream = [DocumentService.Client.Client]::Zugferd.ExtractAsync($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

byte[] pdfBytes = await File.ReadAllBytesAsync("eingangsrechnung.pdf");
var result = await Client.Zugferd.ExtractAsResultAsync(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);
Dim pdfBytes = System.IO.File.ReadAllBytes("eingangsrechnung.pdf")
Dim result = Await Client.Zugferd.ExtractAsResultAsync(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)
$pdfBytes = [System.IO.File]::ReadAllBytes("eingangsrechnung.pdf")
$result = [DocumentService.Client.Client]::Zugferd.ExtractAsResultAsync($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
// NICHT VERWENDEN – wirft NotImplementedException
#pragma warning disable CS0618
using var pdfAStream = await Client.Conversion.ToPdfAAsync(pdfStream, pdfALevel: "PDF/A-3b");
#pragma warning restore CS0618
' NICHT VERWENDEN – wirft NotImplementedException
#Disable Warning BC40000
Dim pdfAStream = Await Client.Conversion.ToPdfAAsync(pdfStream, pdfALevel:="PDF/A-3b")
#Enable Warning BC40000
# NICHT VERWENDEN – wirft NotImplementedException
$pdfAStream = [DocumentService.Client.Client]::Conversion.ToPdfAAsync($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 ValidateAsync() 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 CheckAsync() POST /api/pdf/attachments/check ✅ Verfügbar Eingebettete Anhänge erkennen
Client.Attachment ExtractAsync() POST /api/pdf/attachments/extract ✅ Verfügbar Anhänge extrahieren als Dictionary<string, Stream>
Client.Attachment AddAsync() 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 ExtractAsync() POST /api/pdf/qr-code/extract-swiss ✅ Verfügbar Schweizer QR-Code-Daten extrahieren
Client.Zugferd CheckAsync() POST /api/pdf/zugferd/has-zugferd ✅ Verfügbar ZUGFeRD-Einbettung prüfen
Client.Zugferd ExtractAsync() POST /api/pdf/zugferd/extract ✅ Verfügbar ZUGFeRD-XML als Stream
Client.Zugferd ExtractAsResultAsync() POST /api/pdf/zugferd/extract ✅ Verfügbar ZUGFeRD-XML mit Metadaten als Objekt
Client.Conversion ToPdfAAsync() POST /api/pdf/conversion/to-pdfa ⏳ Geplant PDF zu PDF/A konvertieren
Client.Conversion FromPdfAAsync() POST /api/pdf/conversion/from-pdfa ⏳ Geplant PDF/A-Einschränkungen aufheben
Client.Workflows ExtractSwissQrCodeAsync() — ✅ 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

using DocumentService.Client;

try
{
    using var stream = File.OpenRead("dokument.pdf");
    var result = await Client.Validation.ValidateAsync(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}");
}
Imports DocumentService.Client

Try
    Dim stream As New System.IO.FileStream("dokument.pdf", System.IO.FileMode.Open)
    Dim result = Await Client.Validation.ValidateAsync(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
Add-Type -Path "DocumentService.Client.dll"

try {
    $stream = [System.IO.File]::OpenRead("dokument.pdf")
    $result = [DocumentService.Client.Client]::Validation.ValidateAsync($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.