JAK.DocGen.Client 0.3.0

dotnet add package JAK.DocGen.Client --version 0.3.0
                    
NuGet\Install-Package JAK.DocGen.Client -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JAK.DocGen.Client" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JAK.DocGen.Client" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="JAK.DocGen.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JAK.DocGen.Client --version 0.3.0
                    
#r "nuget: JAK.DocGen.Client, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JAK.DocGen.Client@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JAK.DocGen.Client&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=JAK.DocGen.Client&version=0.3.0
                    
Install as a Cake Tool

JAK.DocGen.Client

Client .NET officiel du service JAK.DocGen. Il permet de construire des documents PDF à partir de blocs sémantiques, de résoudre un thème depuis une charte graphique, de générer un PDF et d'obtenir l'aperçu PNG d'une page.

La bibliothèque ne calcule aucun montant et ne contient aucune mise en page. L'application appelante fournit les données métier dans leur ordre d'affichage ; le service applique le thème, les règles de coupure, les en-têtes, les pieds de page et la pagination.

Sommaire

Installation et compatibilité

Le package cible netstandard2.0 et net8.0. Il est utilisable notamment depuis .NET 8+ et .NET Framework 4.6.2+.

Depuis NuGet :

dotnet add package JAK.DocGen.Client --version 0.3.0

Depuis ce dépôt, pendant le développement :

<ItemGroup>
  <ProjectReference Include="../JAK.DocGen/clients/dotnet/JAK.DocGen.Client/JAK.DocGen.Client.csproj" />
</ItemGroup>

Tous les types publics se trouvent dans le namespace suivant :

using Jak.DocGen;

Configuration

ASP.NET Core et Generic Host

Enregistrez le client une fois dans Program.cs :

using Microsoft.Extensions.DependencyInjection;

builder.Services.AddDocGenClient(options =>
{
    options.BaseAddress = new Uri(builder.Configuration["DocGen:BaseAddress"]!);
    options.ApiKey = builder.Configuration["DocGen:ApiKey"]!;
});

Exemple de configuration non sensible :

{
  "DocGen": {
    "BaseAddress": "http://docgen:8090"
  }
}

Conservez la clé dans un secret, une variable d'environnement ou un coffre de secrets. Ne la committez pas dans appsettings.json.

AddDocGenClient enregistre IDocGenClient et DocGenClient en singleton. Le HttpClient sous-jacent reste géré par IHttpClientFactory. La méthode retourne un IHttpClientBuilder, ce qui permet de fixer le délai HTTP sans créer un second client :

builder.Services
    .AddDocGenClient(options =>
    {
        options.BaseAddress = new Uri(builder.Configuration["DocGen:BaseAddress"]!);
        options.ApiKey = builder.Configuration["DocGen:ApiKey"]!;
    })
    .ConfigureHttpClient(http => http.Timeout = TimeSpan.FromSeconds(45));

Injectez ensuite l'interface :

public sealed class InvoicePdfService(IDocGenClient docGen)
{
    private readonly IDocGenClient _docGen = docGen;
}

Une adresse ou une clé absente provoque une ArgumentException lors de la première résolution du client par le conteneur. L'erreur nomme l'option manquante.

Construction directe

Pour une application console ou une application sans conteneur DI :

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(45) };

var client = new DocGenClient(http, new DocGenClientOptions
{
    BaseAddress = new Uri("http://docgen:8090"),
    ApiKey = Environment.GetEnvironmentVariable("DOCGEN_API_KEY")
        ?? throw new InvalidOperationException("DOCGEN_API_KEY manque."),
});

Dans ce mode, la durée de vie du HttpClient appartient à l'appelant. DocGenClient ne le dispose jamais.

Démarrage rapide

Le flux normal comporte deux étapes : résoudre le thème lors de la sauvegarde d'une charte, puis réutiliser le thème résolu pour chaque rendu.

using Jak.DocGen;

// À la création ou à la modification de la charte.
ThemeResolution resolution = await client.ResolveThemeAsync(theme => theme
    .WithBrand("#1b4965")
    .WithBodyFont("Liberation Sans", size: "9.5pt")
    .WithDensity(ThemeDensity.Comfortable));

ResolvedTheme theme = resolution.Theme;

// À chaque génération de document.
DocumentRequest request = DocumentBuilder.ForLocale(DocGenLocale.FrBE)
    .WithTheme(theme)
    .AddTitle(title => title
        .Text("Offre 2026-001")
        .Reference("2026-001")
        .Date(new DateOnly(2026, 8, 20)))
    .AddParties(parties => parties
        .Issuer(issuer => issuer
            .Name("Société Exemple SRL")
            .Address(["Rue de l'Exemple 1"], "1000", "Bruxelles")
            .VatNumber("BE 0123.456.789"))
        .Recipient(recipient => recipient
            .Name("Destinataire Exemple SA")
            .Address(["Avenue de la Démonstration 2"], "5000", "Namur")))
    .AddLineItems(items => items
        .Currency("EUR")
        .Columns(
            LineItemColumn.Description,
            LineItemColumn.Quantity,
            LineItemColumn.UnitPrice,
            LineItemColumn.Total)
        .Section("Prestations", section => section
            .Line(
                "Analyse",
                total: 1250m,
                quantity: 1m,
                unit: "forfait",
                unitPrice: 1250m)
            .Subtotal(1250m))
        .Totals(
            exclVat: 1250m,
            inclVat: 1512.50m,
            (Rate: 21m, Base: 1250m, Amount: 262.50m)))
    .AddSignature(signature => signature
        .Place("Bruxelles")
        .Date(new DateOnly(2026, 8, 20))
        .Signatory("Alex Exemple", role: "Administrateur"))
    .Build();

RenderedDocument result = await client.RenderAsync(request);
await File.WriteAllBytesAsync("offre.pdf", result.Pdf);

DateOnly est disponible sur la target net8.0. Sur netstandard2.0, utilisez les surcharges DateTime. Les deux sont sérialisées au format stable yyyy-MM-dd, indépendamment de la culture et du fuseau horaire du processus.

Cycle de vie d'un thème

Résoudre les primitives

Le builder de thème accepte uniquement les choix de charte que le service sait dériver :

ThemeResolution resolution = await client.ResolveThemeAsync(theme => theme
    .WithBrand("#1b4965", neutral: "#5b5b5b", alert: "#c92a2a")
    .WithBodyFont("Liberation Sans", size: "9.5pt")
    .WithHeadingFont("Liberation Sans", weight: 700)
    .WithDensity(ThemeDensity.Compact)
    .WithPage(PageSize.A4, PageOrientation.Portrait)
    .WithLogo(
        DataUri.Svg(svgLogo),
        maxWidth: "14mm",
        position: LogoPosition.HeaderLeft));

Référence de ThemeBuilder :

Méthode Effet exact
Create() crée un builder vide ; le service utilisera ses valeurs neutres pour tout ce qui reste absent
ForBrand(color) crée un builder et définit immédiatement la couleur principale de la charte
WithBrand(brand, neutral, alert) définit la couleur principale obligatoire et, si fournies, une couleur neutre et une couleur d'alerte
WithBodyFont(family, size) choisit la police du texte courant et éventuellement sa taille en pt
WithHeadingFont(family, weight) choisit la police des titres et éventuellement sa graisse de 100 à 900
WithDensity(density) choisit l'espacement général Comfortable ou Compact
WithPage(size, orientation) définit le format et l'orientation utilisés par défaut par les documents de cette charte
WithLogo(dataUri, maxWidth, position) ajoute le logo embarqué dans le thème, sa largeur maximale d'impression et sa position dans l'en-tête
Build() produit les ThemePrimitives à transmettre à ResolveThemeAsync ; cette méthode ne résout pas elle-même le thème

Les unités de taille acceptées par le service sont mm et pt. Une police doit exister dans le catalogue du service et être autorisée pour l'embarquement PDF. Les URLs d'image sont interdites : logo et signature voyagent en Data URI.

Pour le cas courant, une couleur suffit :

ThemeResolution resolution = await client.ResolveThemeAsync(
    ThemePrimitives.ForBrand("#1b4965"));

ThemeResolution.Warnings contient les avertissements non bloquants, par exemple un contraste faible :

foreach (ThemeIssue warning in resolution.Warnings)
{
    logger.LogWarning(
        "Thème DocGen : {Code} sur {Path} — {Message}",
        warning.Code,
        warning.Path,
        warning.Message);
}

Persister et recharger le thème résolu

Le service est sans état. L'application appelante persiste le thème complet retourné par ResolveThemeAsync, puis le recharge sans le remodeler :

string jsonToStore = resolution.Theme.ToJson();

// Plus tard, depuis la valeur stockée par l'application appelante.
ResolvedTheme storedTheme = ResolvedTheme.FromJson(jsonFromStorage);

Ne rappelez pas ResolveThemeAsync à chaque document. Rappelez-le uniquement quand la charte change ou doit être migrée vers une nouvelle version de schéma.

Construire un document

Un document est une liste ordonnée de blocs : l'ordre des appels Add... est l'ordre de rendu. Le builder exige un thème et au moins un bloc.

Référence de DocumentBuilder :

Méthode Obligatoire Effet exact
ForLocale(locale) oui démarre un document en fr-BE ou nl-BE ; la locale pilote les libellés par défaut et le format des nombres et dates
WithTheme(theme) oui associe au document un thème complet préalablement résolu par le service
WithWatermark(watermark) non ajoute ou retire le filigrane prédéfini Draft, Duplicate ou None
WithPageSetup(size, orientation) non surcharge, pour ce document seulement, le format et l'orientation définis dans le thème
OverrideLabels(configure) non remplace certains libellés mécaniques du service sans modifier les données ni la mise en page
AddTitle(...) non ajoute un bandeau de titre répété en haut de chaque page ; un second appel ouvre une nouvelle section, sur page neuve, avec son propre bandeau
AddParties(...) non ajoute les fiches de l'émetteur et du destinataire
AddKeyValues(...) non ajoute une liste de propriétés sous forme de couples libellé/valeur
AddRichText(...) non ajoute une section de texte composée d'un titre de section optionnel et de paragraphes
AddLineItems(...) non ajoute un tableau de lignes chiffrées, ses sections et ses totaux déjà calculés
AddPaymentSchedule(...) non ajoute un tableau d'échéances de paiement
AddSignature(...) non ajoute une zone de signature pour un ou deux signataires
AddLegalTerms(...) non ajoute des conditions générales complètes sous forme de sections de texte
AddLegalTermsLink(...) non ajoute une référence imprimée et cliquable vers des conditions générales en ligne
AddSpacer(size) non ajoute un espacement vertical prédéfini entre deux blocs
AddPageBreak() non force le bloc suivant à commencer sur une nouvelle page
Build() oui, en dernier vérifie la présence du thème et d'au moins un bloc, puis produit le DocumentRequest
DocumentBuilder builder = DocumentBuilder.ForLocale(DocGenLocale.NlBE)
    .WithTheme(theme)
    .WithPageSetup(PageSize.A4, PageOrientation.Landscape)
    .WithWatermark(Watermark.Draft);

DocumentRequest request = builder
    .AddRichText(text => text.Paragraph("Inhoud van het document."))
    .Build();

WithPageSetup surcharge le format du thème pour ce document uniquement. Les filigranes disponibles sont None, Draft et Duplicate.

Le builder valide immédiatement les champs structurels évidents : titre vide, thème absent, bloc sans élément obligatoire, etc. La validation exhaustive du contrat reste effectuée par le service.

Catalogue des blocs

La version 0.3.0 expose les blocs suivants :

Bloc ajouté Ce qui apparaît dans le document Minimum requis
AddTitle bandeau de titre répété sur les pages de sa section ; chaque titre ouvre une section Text
AddParties deux fiches d'identité : émetteur et destinataire Issuer, Recipient et leur Name
AddKeyValues liste de propriétés comme « Projet : Rénovation » au moins un Item
AddRichText section de texte courant ; Heading ajoute seulement un titre au-dessus des paragraphes au moins un Paragraph
AddLineItems tableau de prestations ou produits avec montants et totaux Currency, Columns, une Section et Totals
AddPaymentSchedule tableau des acomptes, échéances ou tranches à payer Currency et un Installment
AddSignature zone réservée aux noms, rôles et signatures un Signatory
AddLegalTerms conditions générales imprimées intégralement une Section contenant un paragraphe
AddLegalTermsLink titre éventuel, texte d'introduction et URL cliquable url
AddSpacer espace vertical prédéfini une taille
AddPageBreak fin de page immédiate avant le bloc suivant rien

Depuis 0.3.0, le client expose également AddAttendance, AddPhotoGrid et AddFormattedText. Ils nécessitent un moteur incluant ces blocs (évolution du service). Les blocs existants et le contrat DocumentRequest.Blocks restent compatibles.

Présences, photos et texte formaté

DocumentRequest request = DocumentBuilder.ForLocale(DocGenLocale.FrBE)
    .WithTheme(theme)
    .AddAttendance(table => table.Heading("Participants")
        .Row("Bureau Exemple", personPresent: true, companyPresent: false,
            person: "Alex", role: "Conseil"))
    .AddFormattedText(text => text.Heading("Compte rendu")
        .Section(TextHeadingLevel.Section, new TextRun("Observations"))
        .Paragraph(new TextRun("Texte en gras", Bold: true), new TextRun(" et "),
            new TextRun("un lien", Href: "https://example.com"))
        .List(false, [new TextRun("Premier point")], [new TextRun("Second point")]))
    .AddPhotoGrid(grid => grid.Heading("Illustrations").Columns(PhotoGridColumns.Two)
        .Photo(DataUri.Jpeg(imageBytes), "Vue générale"))
    .Build();

TextRun porte Bold, Italic, Underline, Strike et Href (HTTP, HTTPS ou mailto). Les espaces entre segments restent significatifs. TextHeadingLevel propose Section (2) et Subsection (3) ; PhotoGridColumns propose Two, Three et Four. Les photos utilisent des data URI PNG/JPEG, jamais des URL externes. Aucune limite de grille ne tronque la liste des photos.

Pour composer le texte depuis un parseur, FormattedTextBuilder.Nodes(...) accepte des FormattedParagraph, FormattedHeading et FormattedList. Ces types dérivent de FormattedTextNode et leur sérialisation conserve uniquement les clés prévues par le service. Les listes sont simples, sans imbrication. Le SDK ne parse pas de Markdown et n’accepte pas de HTML/CSS. Les builders refusent les collections vides et les options hors énumération ; la validation complète, notamment des images et liens, appartient au service.

Titre répété

Chaque titleBlock ouvre une section et devient le bandeau répété en haut de chaque page de cette section (service 0.3.0 et plus ; avant, seul le premier titre devenait le bandeau et les suivants étaient imprimés dans le flux) :

Méthode de TitleBuilder Obligatoire Ce qu'elle affiche
Text(title) oui titre principal du document dans le bandeau
Subtitle(subtitle) non ligne secondaire sous le titre principal
Reference(reference) non référence métier du document dans la zone de métadonnées
Date(date) non date du document dans la zone de métadonnées
ValidUntil(date) non date limite de validité dans la zone de métadonnées
Variant(variant) non Standard masque les métadonnées et conserve le titre seul ; WithReference affiche les métadonnées fournies
.AddTitle(title => title
    .Text("Facture 2026-042")
    .Subtitle("Travaux de rénovation")
    .Reference("2026-042")
    .Date(new DateOnly(2026, 8, 20))
    .ValidUntil(new DateOnly(2026, 9, 19))
    .Variant(TitleVariant.WithReference))

TitleVariant.Standard n'affiche que le titre. Utilisez WithReference pour les métadonnées.

Un second AddTitle ouvre une nouvelle section : elle commence sur une page neuve et porte son propre bandeau sur toutes ses pages, le bandeau précédent ne s'y répète plus. C'est ainsi qu'un devis enchaîne l'offre chiffrée et la convention qui la suit :

DocumentRequest request = DocumentBuilder.ForLocale(DocGenLocale.FrBE)
    .WithTheme(theme)
    .AddTitle(title => title.Text("Devis CSS no. 26-001").Reference("26-001"))
    .AddParties(parties => /* ... */)
    .AddLineItems(items => /* ... */)
    .AddTitle(title => title.Text("Convention CSS").Variant(TitleVariant.Standard))
    .AddLegalTerms(terms => /* ... */)
    .AddSignature(signature => /* ... */)
    .Build();

Aucun AddPageBreak n'est nécessaire avant le second titre ; en poser un ne crée pas de page blanche. Les blocs placés avant le premier AddTitle restent sous son bandeau, dans la première section.

Émetteur et destinataire

PartiesBuilder crée les deux fiches ; chaque fiche est configurée avec un PartyBuilder :

Méthode Obligatoire Ce qu'elle affiche
Issuer(...) oui fiche de l'organisation qui émet le document
Recipient(...) oui fiche de la personne ou organisation destinataire
Variant(variant) non fiches côte à côte avec SideBySide, ou l'une sous l'autre avec Stacked
Name(name) oui par fiche nom principal de la partie
Address(lines, postalCode, city, country) non adresse sur une ou plusieurs lignes, suivie des informations postales fournies
VatNumber(value) non numéro de TVA
CompanyNumber(value) non numéro d'entreprise
Email(value) non adresse e-mail
Phone(value) non numéro de téléphone
Iban(value) non numéro de compte IBAN
Bic(value) non code BIC
LegalFooter(value) non mention légale de l'émetteur ; prévue pour la fiche Issuer
.AddParties(parties => parties
    .Variant(PartiesVariant.SideBySide)
    .Issuer(party => party
        .Name("Société Exemple SRL")
        .Address(["Rue de l'Exemple 1"], "1000", "Bruxelles", "Belgique")
        .VatNumber("BE 0123.456.789")
        .CompanyNumber("0123.456.789")
        .Email("info@example.test")
        .Phone("+32 2 000 00 00")
        .Iban("BE00 0000 0000 0000")
        .Bic("EXAMPLEBIC")
        .LegalFooter("Société Exemple SRL — RPM Bruxelles"))
    .Recipient(party => party
        .Name("Destinataire Exemple SA")
        .Address(["Avenue de la Démonstration 2"], "5000", "Namur")))

LegalFooter est prévu pour l'émetteur. Les variantes sont SideBySide et Stacked.

Paires clé/valeur

Ce bloc sert aux informations qui ne sont ni une adresse, ni un paragraphe : référence de commande, nom de projet, chantier, mission ou échéance contractuelle.

Méthode de KeyValuesBuilder Obligatoire Effet exact
Heading(heading) non ajoute un titre de section au-dessus de la liste
Item(label, value) oui, au moins une fois ajoute une propriété : le label décrit le champ et value est sa valeur affichée
Variant(variant) non organise les propriétés en pile, sur deux colonnes ou sur trois colonnes
.AddKeyValues(values => values
    .Heading("Informations")
    .Variant(KeyValuesVariant.TwoColumn)
    .Item("Projet", "Rénovation du bâtiment A")
    .Item("Votre référence", "PO-2026-123"))

Les variantes sont Stacked, TwoColumn et ThreeColumn.

Texte structuré

AddRichText sert à afficher du texte courant : introduction, note, explication ou commentaire. Ce n'est pas un bloc destiné uniquement à placer un titre.

Méthode de RichTextBuilder Obligatoire Effet exact
Heading(heading) non ajoute un titre de section au-dessus du texte ; il ne remplace pas le titre principal du document
Paragraph(text) oui, au moins une fois ajoute un paragraphe de texte courant
Paragraphs(paragraphs) non ajoute plusieurs paragraphes dans leur ordre d'énumération ; équivaut à appeler Paragraph plusieurs fois
.AddRichText(text => text
    .Heading("Introduction")
    .Paragraph("Premier paragraphe.")
    .Paragraphs(new[]
    {
        "Deuxième paragraphe.",
        "Troisième paragraphe.",
    }))

Le contenu est du texte, jamais du HTML ou du Markdown. Le service l'échappe avant rendu.

Lignes chiffrées et totaux

Méthode de LineItemsBuilder Obligatoire Effet exact
Currency(isoCode) oui définit la devise utilisée pour tous les montants du bloc
Columns(columns) oui choisit les colonnes visibles et leur ordre de gauche à droite
Section(title, configure) oui, au moins une fois crée un groupe de lignes ; title peut être nul pour un groupe sans en-tête
Totals(exclVat, inclVat, vat) oui fournit les totaux hors TVA, TVA par taux et TVA comprise ; aucune valeur n'est recalculée
Variant(variant) non choisit le rendu Compact ou Detailed
ShowVatBreakdown(value) non affiche ou masque le détail des montants de TVA par taux
ShowDiscounts(value) non affiche ou masque les informations de remise
CarryOverSubtotals(value) non active ou désactive les lignes de report lors d'une coupure du tableau entre deux pages

Chaque Section reçoit un LineSectionBuilder :

Méthode Obligatoire Effet exact
Line(description, total, quantity, unit, unitPrice, discountPercentage, detail) oui, au moins une fois ajoute une ligne chiffrée avec un descriptif et un total obligatoires ; les autres valeurs alimentent leurs colonnes si elles sont visibles
Subtotal(subtotal) non affiche le sous-total pré-calculé à la fin de la section

Paramètres de Line :

Paramètre Signification
description libellé principal de la prestation ou du produit
total montant final pré-calculé de cette ligne
quantity quantité numérique
unit unité textuelle, par exemple heure, jour ou forfait
unitPrice prix unitaire pré-calculé
discountPercentage pourcentage de remise déjà appliqué au total
detail précision secondaire affichée avec la description
.AddLineItems(items => items
    .Currency("EUR")
    .Columns(
        LineItemColumn.Description,
        LineItemColumn.Quantity,
        LineItemColumn.Unit,
        LineItemColumn.UnitPrice,
        LineItemColumn.Discount,
        LineItemColumn.Total)
    .Variant(LineItemsVariant.Detailed)
    .ShowDiscounts(true)
    .ShowVatBreakdown(true)
    .CarryOverSubtotals(true)
    .Section("Étude", section => section
        .Line(
            description: "Analyse préliminaire",
            detail: "Réunion et rapport de synthèse",
            quantity: 2m,
            unit: "jour",
            unitPrice: 750m,
            discountPercentage: 10m,
            total: 1350m)
        .Subtotal(1350m))
    .Section("Exécution", section => section
        .Line("Accompagnement", total: 2000m)
        .Subtotal(2000m))
    .Totals(
        exclVat: 3350m,
        inclVat: 4053.50m,
        (Rate: 21m, Base: 3350m, Amount: 703.50m)))

Points importants :

  • Columns décide quelles colonnes apparaissent et dans quel ordre.
  • Tous les montants, sous-totaux, remises, bases TVA et totaux sont pré-calculés par l'application appelante. Le client et le service ne font aucune addition.
  • Currency attend un code ISO 4217 tel que EUR.
  • CarryOverSubtotals(true) active les lignes « à reporter » et « report » aux coupures de page. Cette option demande une passe de mesure supplémentaire.
  • Les variantes sont Compact et Detailed.

Échéancier de paiement

Méthode de PaymentScheduleBuilder Obligatoire Effet exact
Heading(heading) non ajoute un titre de section au-dessus de l'échéancier
Currency(isoCode) oui définit la devise de toutes les échéances
Installment(label, amount, dueDate, note) oui, au moins une fois ajoute une tranche avec son libellé et son montant ; la date d'échéance et la note sont facultatives
.AddPaymentSchedule(schedule => schedule
    .Heading("Échéancier")
    .Currency("EUR")
    .Installment("Acompte", 1210m, new DateOnly(2026, 9, 1))
    .Installment("Solde", 2843.50m, new DateOnly(2026, 10, 1), "Après réception"))

Signature

Méthode de SignatureBuilder Obligatoire Effet exact
Place(place) non affiche le lieu de signature
Date(date) non affiche la date de signature
Signatory(name, role, caption, image) oui, au moins une fois ajoute un signataire ; son nom est obligatoire, son rôle, sa légende et son image sont facultatifs
Variant(variant) non choisit une zone simple avec Single ou deux zones avec Dual
byte[] signatureBytes = await File.ReadAllBytesAsync("signature.png");

// Dans la composition du document :
.AddSignature(signature => signature
    .Place("Bruxelles")
    .Date(new DateOnly(2026, 8, 20))
    .Variant(SignatureVariant.Dual)
    .Signatory(
        "Alex Exemple",
        role: "Administrateur",
        caption: "Pour accord",
        image: DataUri.Png(signatureBytes))
    .Signatory("Sam Exemple", role: "Client"))

DataUri.Png, DataUri.Jpeg et DataUri.Svg encodent les images en ligne. N'envoyez pas d'URL : le contexte de rendu n'a volontairement aucun accès réseau.

Conditions générales

Pour des conditions imprimées intégralement :

Méthode de LegalTermsBuilder Obligatoire Effet exact
Title(title) non ajoute un titre général au-dessus de toutes les conditions
Section(heading, paragraphs) oui, au moins une fois ajoute une section ; son titre est facultatif mais elle doit contenir au moins un paragraphe

Pour la variante en ligne, AddLegalTermsLink(url, linkText, title) affiche un titre général facultatif, un texte d'introduction facultatif, puis l'URL obligatoire imprimée en entier et cliquable.

Texte complet structuré :

.AddLegalTerms(terms => terms
    .Title("Conditions générales")
    .Section("1. Objet", "Premier paragraphe.", "Deuxième paragraphe.")
    .Section("2. Paiement", "Les factures sont payables à l'échéance indiquée."))

Ou lien vers une version en ligne :

.AddLegalTermsLink(
    "https://example.test/conditions-generales",
    linkText: "Version complète :",
    title: "Conditions générales")

La numérotation des sections appartient au contenu. Le lien est imprimé en entier et reste cliquable dans le PDF.

Espacement et saut de page

Méthode Effet exact
AddSpacer(SpacerSize.Small) ajoute le plus petit espacement vertical prédéfini
AddSpacer(SpacerSize.Medium) ajoute un espacement vertical intermédiaire prédéfini
AddSpacer(SpacerSize.Large) ajoute le plus grand espacement vertical prédéfini
AddPageBreak() termine la page courante ; le bloc ajouté ensuite commence sur une nouvelle page
.AddSpacer(SpacerSize.Medium)
.AddPageBreak()

Privilégiez la pagination automatique. AddPageBreak sert uniquement lorsqu'un nouveau chapitre doit réellement commencer sur une nouvelle page ; les règles de coupure internes aux blocs ne sont pas configurables.

Libellés et locales

Les locales disponibles sont FrBE et NlBE. Elles déterminent les libellés par défaut ainsi que le format des nombres et des dates.

OverrideLabels permet de remplacer uniquement les libellés mécaniques nécessaires au document :

.OverrideLabels(labels =>
{
    labels.Parties = new PartiesLabels
    {
        Issuer = "Prestataire",
        Recipient = "Client",
    };
    labels.TitleBlock = new TitleBlockLabels
    {
        Reference = "Numéro de facture",
        Date = "Date d'émission",
    };
    labels.LineItems = new LineItemsLabels
    {
        Columns = new LineItemColumnLabels
        {
            Description = "Prestation",
        },
        TotalExclVat = "Total HTVA",
        TotalInclVat = "Total TVAC",
        VatAtRate = "TVA {rate} %",
    };
    labels.Footer = new FooterLabels
    {
        PageOfTotal = "Page {page} sur {total}",
    };
    labels.Watermark = new WatermarkLabels
    {
        Draft = "BROUILLON",
        Duplicate = "DUPLICATA",
    };
});

Les placeholders {rate}, {page} et {total} doivent être conservés dans leurs gabarits. Une clé inconnue est rejetée par le service avec une erreur 422. Les textes métier, comme le titre « Facture » ou « Offre », restent du contenu fourni dans les blocs.

Générer un PDF ou un aperçu

PDF

RenderedDocument document = await client.RenderAsync(request, cancellationToken);

Le résultat contient :

Propriété Description
Pdf contenu binaire du PDF
PageCount nombre de pages annoncé par le service
ThemeHash empreinte du thème utilisé
ContentHash empreinte de la requête complète, utilisable comme clé de cache
ThemeWarnings avertissements de thème renvoyés pendant le rendu

Le service ne conserve pas le PDF. L'application appelante est responsable de sa transmission, de son archivage et de son éventuel cache.

Exemple de réponse ASP.NET Core :

RenderedDocument document = await docGen.RenderAsync(request, cancellationToken);
return Results.File(document.Pdf, "application/pdf", "facture.pdf");

Aperçu PNG

byte[] png = await client.PreviewAsync(
    request,
    page: 1,
    scale: 1.5,
    cancellationToken);

page est indexé à partir de 1. L'aperçu découpe le flux continu en bandes : sa pagination peut différer du PDF, notamment avec les lignes insécables et les bandeaux répétés. N'utilisez pas RenderedDocument.PageCount comme borne des pages PNG ; le PDF reste le document de référence.

Erreurs et annulation

Validation locale

Le builder lève :

  • ArgumentException pour une valeur obligatoire vide ou une collection vide ;
  • ArgumentNullException pour une dépendance ou une action de configuration nulle ;
  • InvalidOperationException lorsqu'un bloc incomplet est finalisé ou que le document n'a ni thème ni bloc.

Construisez la requête avant d'entrer dans la partie qui envoie une réponse HTTP afin de traiter séparément les erreurs de programmation et les erreurs du service.

Erreur du service

Toute réponse HTTP non réussie devient une DocGenApiException :

try
{
    return await client.RenderAsync(request, cancellationToken);
}
catch (DocGenApiException error) when (error.StatusCode == 422)
{
    foreach (DocGenFieldError field in error.Errors)
    {
        logger.LogWarning(
            "Document invalide : {Path} [{Code}] {Message}",
            field.Path,
            field.Code,
            field.Message);
    }

    throw;
}

L'exception expose StatusCode, ProblemType, Title, Detail et Errors. Chaque erreur de champ contient Path, Code et Message. Les statuts courants sont 401 (clé invalide), 422 (contrat invalide), 429 (capacité saturée), 500 (échec interne) et 504 (délai de rendu dépassé).

Un CancellationToken est accepté par tous les appels asynchrones. Une annulation ou un délai du HttpClient lève les exceptions HTTP/.NET habituelles, pas une DocGenApiException.

Ne journalisez jamais la requête ni les données du document : elles peuvent contenir des données personnelles. Journalisez un hash, une taille et les chemins d'erreur.

Tests

Le point d'injection est IDocGenClient. Une classe métier peut donc être testée avec le mécanisme de doublure déjà utilisé par votre projet, sans démarrer Chromium ni produire un vrai PDF.

Les tests de cette bibliothèque se lancent depuis la racine du dépôt :

dotnet test clients/dotnet/JAK.DocGen.sln

Les tests d'intégration réels utilisent les variables suivantes :

export DOCGEN_BASE_URL=http://localhost:8090
export DOCGEN_API_KEY=dev-secret
dotnet test clients/dotnet/JAK.DocGen.sln

Sans ces deux variables, les scénarios qui nécessitent le service retournent immédiatement ; les tests unitaires du builder, de la sérialisation et de l'injection restent exécutés.

Règles d'intégration

  • Résolvez et persistez le thème quand la charte change, pas à chaque rendu.
  • Calculez les montants dans le domaine métier avant de construire lineItems.
  • Utilisez uniquement des unités d'impression (mm, pt) et des options enumérées.
  • Envoyez les images en Data URI ; aucune ressource distante n'est chargée au rendu.
  • N'envoyez jamais de HTML, CSS, marge ou style libre.
  • Archivez le PDF côté appelant si le document doit être conservé.
  • Propagez un CancellationToken et configurez un délai HTTP supérieur au délai de rendu du service.
  • Traitez les 422 comme des erreurs de contrat et affichez les chemins fautifs pendant le développement.

La spécification complète du service et les règles de pagination se trouvent dans docs/SPEC.md.

Taux de TVA par ligne (0.2.0)

Le service doit inclure le contrat 0.2.0 pour accepter vatRate. Le client n'effectue aucun calcul de remise, de TVA ou de total : l'application transmet ses montants métier.

builder.AddLineItems(items => items
    .Currency("EUR")
    .Columns(LineItemColumn.Description, LineItemColumn.Discount, LineItemColumn.VatRate, LineItemColumn.Total)
    .Section(null, section => section.Line("Prestation", total: 90m,
        quantity: 1m, unitPrice: 100m, discountPercentage: 10m, vatRate: 21m))
    .Totals(exclVat: 90m, inclVat: 108.9m, (21m, 90m, 18.9m)));

L'appelant omet la colonne Discount si aucune ligne n'a de remise. Le taux 0 est affiché ; un taux absent reste absent. Les libellés LineItems.Columns.VatRate et LineItems.VatAtRate sont surchargeables ; ce dernier accepte {rate} et {base} (montant avec devise).

Publication du client

Publication : pousser le tag dotnet-v0.3.0 sur le commit validé. La pipeline CI teste le client contre le vrai moteur, vérifie la correspondance tag/version puis publie sur NuGet via OIDC. Les artefacts du même run contiennent le contrat OpenAPI et les schémas de thème.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 54 10/1/2026
0.2.0 552 9/10/2026
0.1.1 248 8/18/2026
0.1.0 103 8/18/2026