Files
DocumentService/DocumentService.Client
TekH 5e7652ef88 Refactor and rename to DocumentService
Renamed namespaces and projects from DocumentOperator to DocumentService, updating all references accordingly. Refactored PdfSignatureAppearanceDto and related enums to use properties with init accessors for improved clarity. Removed and re-added IPkcs11SelfTestStatusProvider with updated namespace. Simplified dependency injection by removing DocumentOperatorApplication registration. Updated appsettings.json with a specific AssetsRootPath. Incremented version numbers in project files for a new release. Introduced PdfSignatureClient and IPdfSignatureClient for PDF digital-signing operations. Updated project files to use newer Microsoft.Extensions package versions. Removed all DocumentOperator references, completing the transition to DocumentService. Added new DTOs and enums for PDF signature options and appearance.
2026-10-25 18:49:08 +01:00
..

DocumentService.Client

.NET-Clientbibliothek für die DocumentService-API – unterstützt .NET Framework 4.6.2, 4.8 und .NET 8.0.

Funktionsumfang

  • Multi-Target-Unterstützung: .NET Framework 4.6.2, 4.8 und .NET 8.0
  • Statischer Einstiegspunkt: Client-Klasse – kein DI-Container erforderlich
  • Separate Clients pro Fachbereich: Client.Validation, Client.Attachment, Client.Operations, Client.SwissQrCode, Client.Zugferd, Client.Conversion
  • Dualer Eingabe-Support: Multipart (Stream) und Base64 (byte[]) für alle Endpunkte
  • Stark typisierte Modelle: Gemeinsame Request/Response-DTOs mit XML-Dokumentation
  • Workflows: Client.Workflows – kombinierte Operationen mit vorgelagerter Validierung (z. B. Swiss QR Code mit automatischer PDF-Prüfung)

Installation

Das Paket wird über einen internen NuGet-Feed bereitgestellt und ist nicht auf nuget.org verfügbar.

NuGet-Paket: http://172.24.12.35:5000/packages/documentservice.client/1.1.0
API-Basis-URL: http://172.24.12.39:9393/
Swagger-Dokumentation: http://172.24.12.39:9393/swagger/index.html

.NET-Projekt (Visual Studio)

Paket über den NuGet Package Manager installieren. Dabei sicherstellen, dass der interne NuGet-Server (http://172.24.12.35:5000/nuget) als Paketquelle eingetragen und ausgewählt ist.

PowerShell (ohne .NET-Projekt)

Für PowerShell-Skripte ohne zugehöriges .NET-Projekt muss die Assembly manuell heruntergeladen und eingebunden werden.

Schritt 1 – Paket herunterladen und entpacken:

# NuGet-Paket herunterladen (.nupkg ist ein ZIP-Archiv)
$nupkgUrl  = "http://172.24.12.35:5000/packages/documentservice.client/1.1.0"
$nupkgPath = "$env:TEMP\DocumentService.Client.1.1.0.nupkg"
$extractDir = "$env:TEMP\DocumentService.Client"

Invoke-WebRequest -Uri $nupkgUrl -OutFile $nupkgPath

# Entpacken (nupkg = ZIP)
Expand-Archive -Path $nupkgPath -DestinationPath $extractDir -Force

Schritt 2 – Assembly für die Zielplattform laden:

# Für PowerShell 7+ (.NET 8 / net8.0)
$dllPath = Join-Path $extractDir "lib\net8.0\DocumentService.Client.dll"

# Für Windows PowerShell 5.1 (.NET Framework 4.8 / net48)
# $dllPath = Join-Path $extractDir "lib\net48\DocumentService.Client.dll"

Add-Type -Path $dllPath

Schritt 3 – Abhängigkeiten laden (falls nicht bereits im GAC / PowerShell-Runspace vorhanden):

# Abhängige Assemblies aus demselben lib-Verzeichnis laden
$libDir = Split-Path $dllPath -Parent
Get-ChildItem -Path $libDir -Filter "*.dll" | ForEach-Object {
    try { Add-Type -Path $_.FullName } catch { <# bereits geladen #> }
}

Hinweis: In der Praxis empfiehlt es sich, die extrahierten DLLs einmalig in ein festes Verzeichnis (z. B. C:\Tools\DocumentService.Client\) zu kopieren und Add-Type mit einem absoluten Pfad aufzurufen. Das verhindert wiederholte Downloads und Versionskonflikte zwischen Skript-Ausführungen.


Konfiguration

Diese Dokumentation beschreibt die Nutzung über den statischen Client-Einstiegspunkt, der ohne DI-Container auskommt. Die Library unterstützt darüber hinaus vollständige IServiceCollection-Integration via AddDocumentServiceClients() – dies ist jedoch nicht Gegenstand dieser Dokumentation.

Einmalige Initialisierung (z. B. in Program.cs oder App_Start)

using DocumentService.Client;

// Kurzform – nur Base-URL
Client.Configure("http://172.24.12.39:9393/");

// Vollständige Konfiguration
Client.Configure(options =>
{
    options.BaseUrl      = "http://172.24.12.39:9393/";
    options.Timeout      = TimeSpan.FromMinutes(10);
    options.MaxRetries   = 3;
    options.ThrowOnError = true;
});
Imports DocumentService.Client

' Kurzform – nur Base-URL
Client.Configure("http://172.24.12.39:9393/")

' Vollständige Konfiguration
Client.Configure(Sub(options)
    options.BaseUrl      = "http://172.24.12.39:9393/"
    options.Timeout      = TimeSpan.FromMinutes(10)
    options.MaxRetries   = 3
    options.ThrowOnError = True
End Sub)
Add-Type -Path "DocumentService.Client.dll"

# Kurzform – nur Base-URL
[DocumentService.Client.Client]::Configure("http://172.24.12.39:9393/")

# Vollständige Konfiguration
[DocumentService.Client.Client]::Configure([Action[DocumentService.Client.Configuration.DocumentServiceClientOptions]] {
    param($options)
    $options.BaseUrl      = "http://172.24.12.39:9393/"
    $options.Timeout      = [TimeSpan]::FromMinutes(10)
    $options.MaxRetries   = 3
    $options.ThrowOnError = $true
})

Client.Configure() ist idempotent im Sinne der Initialisierung: Der interne Service Provider wird beim ersten Zugriff lazy gebaut und danach nicht mehr verändert. Ein zweiter Configure()-Aufruf wird standardmäßig mit einer InvalidOperationException abgelehnt. Dieses Verhalten ist über Client.OnReconfigure steuerbar:

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

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

' Zweiten Aufruf stillschweigend ignorieren
Client.OnReconfigure = OnReconfigure.Ignore
# Standard – wirft bei erneutem Configure()-Aufruf
[DocumentService.Client.Client]::OnReconfigure = [DocumentService.Client.Models.ValueObjects.OnReconfigure]::ThrowException

# Zweiten Aufruf stillschweigend ignorieren
[DocumentService.Client.Client]::OnReconfigure = [DocumentService.Client.Models.ValueObjects.OnReconfigure]::Ignore

Verfügbare Client-Eigenschaften

Eigenschaft Interface Fachbereich
Client.Validation IPdfValidationClient PDF-Validierung
Client.Attachment IPdfAttachmentClient PDF-Anhänge
Client.Operations IPdfOperationsClient PDF-Operationen
Client.SwissQrCode ISwissQrCodeClient Schweizer QR-Code
Client.Zugferd IZugferdClient ZUGFeRD-Rechnungen
Client.Conversion IPdfConversionClient PDF-Konvertierung (geplant)
Client.Workflows IWorkflowsClient Kombinierte Operationen

Jeder Zugriff auf eine dieser Eigenschaften erstellt einen neuen DI-Scope. Clients sind nicht für die dauerhafte Speicherung als Felder gedacht – jeder Aufruf holt sich eine frische Instanz.

PowerShell-Hinweis: PowerShell unterstützt async/await nicht nativ. Alle asynchronen Methoden werden über .GetAwaiter().GetResult() synchron blockierend aufgerufen. Für lang laufende Operationen (z. B. große PDFs) empfiehlt sich die Ausführung in einem eigenen Thread via [System.Threading.Tasks.Task]::Run({...}).GetAwaiter().GetResult().


Konfigurationsoptionen

Eigenschaft Typ Standardwert Beschreibung
BaseUrl string "http://localhost:5000" Basis-URL der DocumentService-API
Timeout TimeSpan TimeSpan.FromMinutes(5) HTTP-Anfrage-Timeout
MaxRetries int 3 Maximale Wiederholungsversuche bei transienten Fehlern
ThrowOnError bool true Exception bei HTTP 4xx/5xx-Antworten

Verwendungsbeispiele

Alle Beispiele nutzen ausschließlich den statischen Client-Einstiegspunkt. Voraussetzung ist eine einmalige Konfiguration wie oben beschrieben.


Workflows – Kombinierte Operationen

Client.Workflows fasst häufig benötigte Abläufe zusammen, die mehrere Einzelendpunkte orchestrieren. 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 auf result.QrCode ist gleichwertig. Gibt TryExtract false zurück, ist der out-Parameter qrCode null – 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

⚠️ Hinweis zur Validierungstiefe: ValidatePdfAAsync führt eine heuristische PDF/A-Erkennung durch, keine vollständige ISO-19005-Konformitätsprüfung. Die Implementierung erkennt das PDF/A-Konformitätslevel anhand der XMP-Metadaten (pdfaid:part, pdfaid:conformance) und prüft auf Verschlüsselung. Folgende Anforderungen der Norm werden nicht geprüft:

  • Einbettung aller verwendeten Schriftarten
  • Gültigkeit und Vollständigkeit der Farbprofile (ICC)
  • Fehlen von JavaScript, eingebetteten Multimedia-Inhalten und externen Referenzen
  • Strukturelle Barrierefreiheit (Level A)

Das Ergebnis IsValid = true bedeutet daher: „Keine erkannten Verstöße", nicht „nachgewiesene ISO-19005-Konformität". Für eine zertifizierungsfähige Prüfung ist ein dedizierter Validator (z. B. veraPDF) erforderlich.

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-Erkennung (heuristisch, keine vollständige ISO-19005-Prüfung)
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.