Files
DocumentService/DocumentService.Client/README.md
TekH 189622c476 Refactor: Standardize method names across clients
Renamed methods across multiple client classes for consistency,
shortening and aligning naming conventions (e.g., `CheckAttachmentsAsync` → `CheckAsync`, `ValidatePdfAsync` → `ValidateAsync`).

Updated corresponding interfaces, unit tests, and documentation
to reflect the new method names. Standardized method signatures
to support both `Stream` and `byte[]` overloads consistently.

Improved error handling in tests and updated examples in the
README for batch processing and validation scenarios. Enhanced
API endpoint overview for clarity.
2026-08-31 03:40:43 +02:00

41 KiB
Raw Blame History

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

.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.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:

# 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.ExtractAsync(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.ExtractAsync(stream);

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

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

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

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

' Null zurückgeben
Dim result2 = Await Client.Workflows.ExtractAsync(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.ExtractAsync($pdfBytes).GetAwaiter().GetResult()

# Null zurückgeben ($false = throwIfInvalid:false)
$result2 = [DocumentService.Client.Client]::Workflows.ExtractAsync($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 ExtractSwissQrCodeAsync() 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 ExtractSwissQrCodeAsync() 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 ExtractSwissQrCodeAsync() 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.