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.
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 undAdd-Typemit 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/awaitnicht 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: trueliefert 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 eineNotImplementedException. Der API-EndpunktPOST /api/pdf/attachments/addist 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 konvertierenPOST /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.