Updated `DocumentService.Client.csproj` to version 1.2.0, reflecting new features and improvements. Enhanced `README.md` with detailed examples for the new `InspectSwissQrCodeAsync` method, showcasing two approaches (TryExtract and direct null-check) for accessing QR code data. Added PowerShell-specific notes and improved code formatting for better readability. Clarified that `TryExtract` is an optional helper method.
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. Validierung und Extraktion laufen in einem einzigen Aufruf – das Ergebnis enthält immer beide Informationen.
Für Anwendungsfälle wie den eParser (Swiss QR Bill aus Eingangsrechnungen lesen) ist dies der empfohlene Einstiegspunkt.
Swiss QR Code – Validierung und Extraktion in einem Schritt
InspectSwissQrCodeAsync gibt immer ein SwissQrCodeResult zurück, das sowohl das Validierungsergebnis als auch den extrahierten QR-Code enthält. Für den Zugriff auf den QR-Code stehen zwei gleichwertige Varianten zur Verfügung:
using DocumentService.Client;
using DocumentService.Client.Models.Results;
byte[] pdfBytes = await File.ReadAllBytesAsync("rechnung.pdf");
SwissQrCodeResult result = await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes);
// Validierungsergebnis ist immer verfügbar
Console.WriteLine($"Gültig: {result.Validation.IsValid}");
Console.WriteLine($"Seiten: {result.Validation.PageCount}");
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}");
// Variante A: TryExtract – lesbarerer Code, kein expliziter Null-Check notwendig
// (optionale Hilfsmethode; Variante B ist gleichwertig)
if (result.TryExtract(out var qrCode))
{
Console.WriteLine($"Betrag: {qrCode.Bill?.Amount} {qrCode.Bill?.Currency}");
Console.WriteLine($"Empfänger: {qrCode.Bill?.Creditor?.Name}");
}
else
{
Console.WriteLine("Kein Swiss QR Code gefunden oder Dokument ungültig.");
}
// Variante B: direkter Null-Check auf QrCode
if (result.QrCode is not null)
{
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}");
}
Imports DocumentService.Client
Imports DocumentService.Client.Models.Results
Dim pdfBytes = System.IO.File.ReadAllBytes("rechnung.pdf")
Dim result As SwissQrCodeResult = Await Client.Workflows.InspectSwissQrCodeAsync(pdfBytes)
Console.WriteLine($"Gültig: {result.Validation.IsValid}")
Console.WriteLine($"Seiten: {result.Validation.PageCount}")
Console.WriteLine($"Verschlüsselt: {result.Validation.IsEncrypted}")
' Variante A: TryExtract
Dim qrCode As DocumentService.Application.Common.DTOs.SwissQrCodeExtractionResult = Nothing
If result.TryExtract(qrCode) Then
Console.WriteLine($"Betrag: {qrCode.Bill?.Amount} {qrCode.Bill?.Currency}")
Console.WriteLine($"Empfänger: {qrCode.Bill?.Creditor?.Name}")
Else
Console.WriteLine("Kein Swiss QR Code gefunden oder Dokument ungültig.")
End If
' Variante B: direkter Null-Check
If result.QrCode IsNot Nothing Then
Console.WriteLine($"Betrag: {result.QrCode.Bill?.Amount} {result.QrCode.Bill?.Currency}")
End If
Add-Type -Path "DocumentService.Client.dll"
$pdfBytes = [System.IO.File]::ReadAllBytes("rechnung.pdf")
$result = [DocumentService.Client.Client]::Workflows.InspectSwissQrCodeAsync($pdfBytes).GetAwaiter().GetResult()
Write-Host "Gueltig: $($result.Validation.IsValid)"
Write-Host "Seiten: $($result.Validation.PageCount)"
Write-Host "Verschluesselt:$($result.Validation.IsEncrypted)"
# TryExtract ist in PowerShell nicht direkt nutzbar (out-Parameter).
# Direkter Null-Check ist hier die einzige Option:
if ($null -ne $result.QrCode) {
Write-Host "Betrag: $($result.QrCode.Bill.Amount) $($result.QrCode.Bill.Currency)"
Write-Host "Empfaenger:$($result.QrCode.Bill.Creditor.Name)"
} else {
Write-Host "Kein Swiss QR Code gefunden oder Dokument ungueltig."
}
TryExtract– Hinweis: Die Methode ist eine optionale Lesbarkeits-Hilfe und nicht zwingend erforderlich. Der direkte Null-Check aufresult.QrCodeist gleichwertig. GibtTryExtractfalsezurück, ist derout-ParameterqrCodenull– er darf in diesem Fall nicht verwendet werden.
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 |
InspectSwissQrCodeAsync() |
— | ✅ Verfügbar | Validierung + Swiss QR Code Extraktion; gibt immer SwissQrCodeResult zurück, wirft nie |
⏳ 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.