# Utilisation programmatique : fichiers PDF

> postext-pdf depuis le code : installation, API, fournisseurs de polices dans le navigateur et dans Node, polices CJK, octets des ressources, texte vertical, workers et fichiers prêts pour l’impression

- Version HTML: https://postext.dev/fr/docs/configuration-pdf-output
- Dernière mise à jour: 2026-10-16
- Temps de lecture: 22 min
- Autres langues: [en](https://postext.dev/en/docs/configuration-pdf-output.md), [es](https://postext.dev/es/docs/configuration-pdf-output.md), [ca](https://postext.dev/ca/docs/configuration-pdf-output.md), [de](https://postext.dev/de/docs/configuration-pdf-output.md), [it](https://postext.dev/it/docs/configuration-pdf-output.md), [pt](https://postext.dev/pt/docs/configuration-pdf-output.md), [zh](https://postext.dev/zh/docs/configuration-pdf-output.md), [ja](https://postext.dev/ja/docs/configuration-pdf-output.md), [ar](https://postext.dev/ar/docs/configuration-pdf-output.md)

## En bref

Cette page s’adresse aux personnes qui écrivent du code et veulent un fichier PDF. Elle montre comment installer le paquet PDF et transformer en quelques lignes un livre mis en page en fichier. Le paquet a besoin des fichiers de police : la page explique comment les lui fournir, dans un navigateur ou sur un serveur. Elle traite des polices pour le chinois, le japonais et le coréen, des images, du texte écrit de haut en bas et des liens. Elle se termine par un exemple complet et par des fichiers prêts pour un imprimeur.

## Générer des PDF

La sortie PDF se trouve dans un paquet séparé, **`postext-pdf`**, afin que les intégrations purement web ne paient pas le coût de `pdf-lib` et de `@pdf-lib/fontkit`. Le moteur PDF ne remesure pas le texte : il consomme exactement le même `VDTDocument` que celui que vous passeriez à `renderToCanvas` ou à `renderToHtml`, et convertit ses coordonnées en pixels en points PDF. Les trois sorties s’accordent donc forcément sur les coupures de lignes, les hauteurs de colonnes et la position des ressources.

> **Dans le navigateur, construisez le VDT au moyen du [Web Worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker).** `renderToPdf` est rapide une fois le VDT disponible : la partie coûteuse est le pipeline de mise en page qui l’a produit. Exécuter ce pipeline dans le worker garde l’interface réactive et permet à un export PDF de réutiliser le cache de mesures que l’aperçu en direct a déjà rempli. Voir [Piloter l’export PDF depuis le worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#piloter-lexport-pdf-depuis-le-worker) pour la démarche recommandée. Les exemples sur le thread principal ci-dessous servent de référence pour *ce que signifient les arguments* : dans le code d’interface, construisez d’abord le VDT dans le worker et n’appelez directement que `renderToPdf`.

### Installation

```bash
npm install postext postext-pdf
```

`postext` est une dépendance pair (peer dependency) de `postext-pdf`. Chaque version de `postext-pdf` a besoin du `postext` avec lequel elle est sortie, ou d’une version ultérieure de la même version majeure (sa plage pair est `^` cette version, `^1.5.0` pour la 1.5.0), car elle importe des fonctions que `postext` a ajoutées dans cette version. Mettez les deux à jour ensemble et, sur un CDN, épinglez-les à la même version.

### API publique

Le paquet expose un seul point d’entrée et quelques types :

- **`renderToPdf(doc, options): Promise<Uint8Array>`** : prend un `VDTDocument` (ou les chapitres d’un livre sous forme de tableau de documents) et renvoie les octets bruts du PDF.
- **`PdfFontProvider`** : la signature de rappel `(family, weight, style, request?) => Promise<Uint8Array | Uint8Array[]>` que `renderToPdf` utilise pour demander les octets d’une police quand il doit incorporer une nouvelle combinaison famille/graisse/style. `request.codePoints` contient les caractères que les pages composent dans cette fonte ; la réponse est un fichier, ou plusieurs qui forment ensemble la fonte (voir [Polices chinoises, japonaises et coréennes](https://postext.dev/fr/docs/configuration-pdf-output.md#polices-chinoises-japonaises-et-coréennes)).
- **`declaredFontFile(bytes, { unicodeRange?, order? }): PdfFontFile`** : un fichier de fonte qui indique comment la page l’a déclaré, c’est-à-dire l’`unicode-range` de sa règle `@font-face` et sa place dans l’ordre des déclarations. Un `PdfFontFile` est un `Uint8Array` doté de ces deux propriétés facultatives (voir [Polices chinoises, japonaises et coréennes](https://postext.dev/fr/docs/configuration-pdf-output.md#polices-chinoises-japonaises-et-coréennes)). Depuis postext-pdf 1.30.
- **`RenderToPdfOptions`** : `{ fontProvider, resourceBytes?, outlines?, accessible?, colorSpace?, pageNegative?, characterGrid?, onProgress?, onWarning?, rasterizeSvg?, harfbuzzWasm?, print?, outputProfile?, profileBaseUrl?, date? }`. Omis, `outlines`, `accessible` et `colorSpace` reprennent le `pdfGeneration` du document (voir [Génération PDF (configuration)](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#génération-pdf-configuration)). `resourceBytes` est décrit dans [Octets des ressources et originaux d’impression](https://postext.dev/fr/docs/configuration-pdf-output.md#octets-des-ressources-et-originaux-dimpression) ; `onWarning` dans [Quelles fontes sont demandées au fournisseur](https://postext.dev/fr/docs/configuration-pdf-output.md#quelles-fontes-sont-demandées-au-fournisseur) et dans [Avertissements dans le document](https://postext.dev/fr/docs/configuration-programmatic-usage.md#avertissements-dans-le-document). `characterGrid: true` imprime la grille que `cjk.grid.show` dessine à l’écran et que le PDF omet sinon (voir [Grille de caractères](https://postext.dev/fr/docs/configuration-east-asian.md#grille-de-caractères)). `harfbuzzWasm` indique où charger le `harfbuzz.wasm` de HarfBuzz (une URL, relative à la page, ou les octets du fichier) pour un document contenant du texte de droite à gauche ou à liaisons ; omis, c’est la copie placée à côté du module de postext-pdf, puis la même version de harfbuzzjs sur jsDelivr et esm.sh. `print` reçoit les réglages de production pour l’impression (norme PDF/X, profil de sortie, noir, contrôle en amont) et reprend sinon le `print` du document ; avec une norme PDF/X, ou `colorSpace: 'cmyk'`, chaque couleur est séparée au moyen du profil ICC de sortie, dont `outputProfile` fournit les octets (sinon il est téléchargé depuis `profileBaseUrl`, par défaut la copie du dossier `icc/` de postext sur le CDN de npm). `date` (une `Date`, un temps en millisecondes ou une chaîne de date, comme le `mtime` de `createBundle`) est la date de création et de modification du fichier et, dans un fichier PDF/X, la date de son paquet XMP et la source de son identifiant de document : le même document rendu deux fois avec une même `date` donne les mêmes octets. Omise, le fichier porte l’heure de l’appel. Une date invalide lève une exception.
- **`PdfWarning`** : un problème non bloquant signalé par `onWarning` ; distinguez-le par `kind` : `'fontFallback'` (`PdfFontFallbackWarning`), une fonte composée dans une autre coupe de sa famille ; `'missingGlyph'` (`PdfMissingGlyphWarning`), des caractères pour lesquels aucun fichier d’une fonte n’a de glyphe ; `'variableFontDefaultInstance'` (`PdfVariableFontWarning`), une police variable demandée à une graisse autre que son instance par défaut ; `'cffEmbeddedWhole'` (`PdfCffEmbeddedWholeWarning`), une fonte CFF de plus de 2 Mo incorporée en entier ; `'complexShapingUnavailable'` (`PdfComplexShapingWarning`), du texte de droite à gauche ou à liaisons dessiné sans HarfBuzz, qui n’a pas pu être chargé (`reason` liste où il a été cherché), si bien que les signes diacritiques arabes sont mal placés ; `'outputProfileUnavailable'` (`PdfPrintWarning`), un rendu CMJN dont le profil de sortie n’a pas pu être chargé, converti avec la formule naïve ; `'pageNegativeIgnored'` (`PdfPrintWarning`), la page en négatif omise d’un fichier PDF/X-1a ; ou `'missingImage'`, une image sans octets dessinée sous forme d’emplacement vide (signalé uniquement à un `onWarning` que vous passez ; sans lui, les avertissements de polices vont à `console.warn`).
- **`decompressWoff2(bytes): Uint8Array`** : fonction utilitaire qui transforme un fichier WOFF2 en octets TTF, le format que `pdf-lib` sait incorporer directement.
- **`createPdfWorker(options?)`**, depuis `postext-pdf/worker` : le même rendu dans un Web Worker ; voir [Rendre le PDF dans un worker](https://postext.dev/fr/docs/configuration-pdf-output.md#rendre-le-pdf-dans-un-worker).

### Exemple minimal

```ts
import { buildDocument } from 'postext';
import { renderToPdf } from 'postext-pdf';

const vdt = buildDocument(
  { markdown: '# Chapter One\n\nThe story begins here…' },
  {
    page: { sizePreset: '17x24' },
    layout: { layoutType: 'double' },
    bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
  },
);

const pdfBytes = await renderToPdf(vdt, {
  fontProvider: async (family, weight, style) => {
    // Return TTF bytes for this family/weight/style.
    // See the "Font provider" section below for a real implementation.
    const res = await fetch(`/fonts/${family}-${weight}${style === 'italic' ? 'i' : ''}.ttf`);
    return new Uint8Array(await res.arrayBuffer());
  },
});

// `pdfBytes` is a Uint8Array — save, download, or stream it.
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
```

### Pourquoi un fournisseur de polices ?

`pdf-lib` incorpore de vrais fichiers de polices dans le PDF : les polices installées dans le navigateur ne sont pas disponibles au moment du rendu, et une police chargée uniquement pour la mesure à l’écran ne suffit pas, à elle seule, à produire un PDF autonome. `renderToPdf` parcourt les pages à la recherche de chaque fonte qu’elles peignent (une par combinaison `family|weight|style` ; voir [Quelles fontes sont demandées au fournisseur](https://postext.dev/fr/docs/configuration-pdf-output.md#quelles-fontes-sont-demandées-au-fournisseur)) et appelle votre fournisseur une fois par combinaison distincte. Le fournisseur renvoie un `Uint8Array` d’octets **TTF ou OTF**, ou une liste de ceux-ci pour une fonte servie en plusieurs fichiers (voir [Polices chinoises, japonaises et coréennes](https://postext.dev/fr/docs/configuration-pdf-output.md#polices-chinoises-japonaises-et-coréennes)) ; `pdf-lib` sous-ensemble les contours TrueType et incorpore les fichiers CFF (`.otf`) en entier. Les fontes auxquelles le fournisseur répond avec le même fichier, par exemple une coupe romaine qui remplace le gras dont une famille est dépourvue, partagent une seule police incorporée. Une fonte qu’aucune page n’utilise finalement pour dessiner, comme celle du texte d’une figure SVG quand la figure est dessinée sous forme d’image, est laissée hors du fichier.

**Utilisez des polices statiques par graisse, pas une seule police variable.** Google Fonts sert souvent un unique WOFF2 variable par famille, qui couvre tout l’axe des graisses. `pdf-lib` ne peut incorporer que l’instance par défaut d’un fichier variable, si bien qu’un paragraphe en gras serait rendu en graisse normale. Fontsource publie des fichiers WOFF2 statiques par graisse qui règlent proprement le problème : c’est la méthode qu’utilise le Sandbox. Un fichier variable demandé à une graisse autre que son instance par défaut est signalé par un avertissement `variableFontDefaultInstance`.

**Chaque mot du texte arrive là où la mise en page l’a placé.** Dans les paragraphes, les éléments de liste, les citations, les encadrés et les autres textes courants, chaque mot commence à la position mesurée par le VDT, si bien qu’un écart entre les largeurs du navigateur et celles de la fonte incorporée ne s’accumule jamais le long d’une ligne. Une ligne sans mise en forme en ligne est peinte comme un seul objet texte qui déplace la plume entre les mots ; les lignes justifiées, centrées ou comportant de la mise en forme sont peintes mot par mot. Un caractère pour lequel la fonte n’a pas de glyphe, que le navigateur a mesuré dans une autre police et que le PDF peint avec la boîte de glyphe manquant de la fonte, ne déplace aucun des mots qui le suivent, et il est signalé une fois par fonte par un avertissement `missingGlyph`. Une espace absente de la fonte, comme une espace fine insécable ou une espace tabulaire, prend la largeur que le navigateur lui a donnée, et les caractères invisibles comme le gluon de mots (word joiner) et l’espace sans chasse ne sont pas dessinés. Un trait d’union insécable (U+2011) absent de la fonte est dessiné avec le trait d’union de la fonte (U+2010), ou avec son trait d’union-signe moins quand elle n’a pas non plus de trait d’union, comme l’affiche le navigateur ; Open Sans et Outfit, entre autres, n’ont ni l’un ni l’autre. Aucun de ces deux cas ne compte comme un glyphe manquant. Il y a deux exceptions. Une ligne qui contient des lettres de droite à gauche est toujours peinte d’un seul tenant (voir [Langues et écritures](https://postext.dev/fr/docs/configuration-language.md#langues-et-écritures)). Le texte placé par une maquette (titres courants et pieds de page, ouvertures, titres d’encadrés et autres éléments de maquette) est composé avec les largeurs propres de la fonte incorporée, si bien qu’un glyphe absent de la fonte y décale toujours le reste de sa ligne.

### Quelles fontes sont demandées au fournisseur

`renderToPdf` parcourt les pages comme il les peint et ne demande au fournisseur que les fontes qu’elles dessinent :

- la fonte romaine d’un bloc qui compose au moins une ligne, et la fonte grasse, italique ou grasse italique de chaque suite de texte réellement composée ainsi ;
- les suites de pastilles, les puces et numéros de liste, et le texte des emplacements de maquette (titres courants, folios, bandeaux d’ouverture et de partie) ;
- le texte des légendes, des notes et des cellules de tableau de chaque ressource ;
- les fontes que nomme le `<text>` d’une figure SVG, quand la figure est incorporée. Une famille que le fournisseur ne peut pas fournir du tout cède la place à la famille suivante de la liste `font-family` du SVG.

Une famille de titres que personne ne met en italique n’est donc jamais demandée en italique, et une figure sans note n’a jamais besoin des fontes des notes.

Quand le fournisseur refuse une fonte, le rendu continue. Une autre fonte de la même famille est incorporée à sa place et un `PdfWarning` est signalé. La remplaçante est la première fonte qui se charge, en essayant les neuf graisses standard (100 à 900) dans l’ordre qu’utilise la correspondance des polices CSS, qui est aussi la fonte que le navigateur affiche dans l’aperçu :

1. d’abord le même style. Pour une graisse de 400 à 500, les graisses jusqu’à 500 passent en premier, puis les plus maigres de la plus proche vers le bas, puis les plus grasses à partir de 600. Pour une graisse inférieure à 400, les graisses plus maigres passent en premier, de la plus proche vers le bas, puis les plus grasses. Pour une graisse supérieure à 500, les graisses plus grasses passent en premier, puis les plus maigres ;
2. ensuite l’autre style, italique pour le romain et romain pour l’italique, à la graisse demandée puis aux autres graisses dans le même ordre.

Une famille sans italique compose donc ses suites italiques en romain, une famille qui ne propose que 400 et 700 prend un 600 pour un 700, et une famille qui n’a qu’une coupe compose tout dans celle-ci. Le fournisseur est sollicité une fonte à la fois, dans cet ordre, et jamais deux fois pour la même fonte, si bien qu’une fonte que rien n’utilise n’est jamais incorporée. Une famille que le fournisseur ne peut pas servir du tout est demandée pour chacune de ces 18 fontes avant que le rendu échoue.

```ts
const bytes = await renderToPdf(doc, {
  fontProvider,
  onWarning: (w) => {
    // { kind: 'fontFallback', family: 'Oswald', weight: 700, style: 'italic',
    //   fallback: { weight: 700, style: 'normal' }, reason: '…', message: '…' }
    console.info(w.message);
  },
});
```

Sans `onWarning`, le message va à `console.warn`. Le texte garde ses positions, qui viennent du VDT et ont été mesurées avec les fontes dont disposait le navigateur : une remplaçante aux largeurs différentes peut donc paraître serrée ou lâche. Fournissez la vraie fonte pour corriger cela. Un rendu n’échoue encore (`postext-pdf: failed to load font(s): …`) que lorsque le fournisseur ne peut fournir aucune fonte d’une famille à une graisse standard, en romain ou en italique.

### Fournisseur de polices dans le navigateur (Fontsource + WOFF2)

Le Sandbox fournit `createPdfFontProvider()` (`packages/postext-sandbox/src/viewport/pdfFontProvider.ts`), que vous pouvez copier dans n’importe quelle application web. L’essentiel :

```ts
import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';

const bytesCache = new Map<string, Promise<Uint8Array>>();

function fontsourceId(family: string): string {
  return family.toLowerCase().replace(/\s+/g, '-');
}

function fontsourceWoff2Url(
  family: string,
  weight: number,
  style: 'normal' | 'italic',
): string {
  const id = fontsourceId(family);
  return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}-${style}.woff2`;
}

export function createPdfFontProvider(): PdfFontProvider {
  return async (family, weight, style) => {
    const key = `${family}|${weight}|${style}`;
    const cached = bytesCache.get(key);
    if (cached) return cached;

    const promise = (async (): Promise<Uint8Array> => {
      const url = fontsourceWoff2Url(family, weight, style);
      const res = await fetch(url, { mode: 'cors' });
      if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${url}`);
      // pdf-lib needs TTF bytes, so decompress the WOFF2 wrapper client-side.
      return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
    })();

    bytesCache.set(key, promise);
    return promise;
  };
}
```

Une version de production devrait aussi :

- **Interroger les graisses disponibles** (via `https://api.fontsource.org/v1/fonts/{id}`) et ramener la graisse demandée à la plus proche que la famille propose réellement, afin qu’une demande de `weight: 600` sur une famille qui n’a que `{400, 700}` aboutisse quand même.
- **Se rabattre de l’italique sur le romain** quand une famille n’a pas de coupe italique pour la graisse demandée, au lieu de faire échouer tout le rendu.
- **Réutiliser le cache d’un rendu à l’autre** (garder `bytesCache` au niveau du module, pas par appel), de sorte que régénérer le PDF après un changement de configuration ne coûte pratiquement rien.

### Polices chinoises, japonaises et coréennes

Une fonte CJK ne se présente pas comme un seul petit fichier. Fontsource livre Noto Serif SC en une centaine de fichiers par graisse, chacun contenant une partie des caractères et déclaré dans la feuille de style de la famille avec son `unicode-range` (`@fontsource/noto-serif-sc/400.css`) ; le navigateur télécharge les fichiers que touche le texte d’une page. Le fichier `latin` que télécharge le fournisseur ci-dessus ne contient aucun caractère han, et les sous-ensembles nommés sont incomplets : le `chinese-simplified` de Noto Serif SC n’a pas 釵, et le `chinese-traditional` de Noto Serif TC n’a aucun des signes pleine chasse （），！？：；.

Un fournisseur peut donc répondre pour une fonte avec plusieurs fichiers. `renderToPdf` lui passe les caractères que les pages composent dans cette fonte (`request.codePoints`), rassemblés dans tous les chapitres avant que quoi que ce soit ne soit dessiné, et le fournisseur renvoie les fichiers qui les contiennent, dans l’ordre où le navigateur les consulte. Chaque fichier est incorporé en sous-ensemble distinct, et chaque caractère est dessiné à partir du premier fichier qui a un glyphe pour lui : un chapitre qui touche 60 tranches incorpore 60 petits sous-ensembles. Une fonte demandée à nouveau, pour le texte d’une figure SVG par exemple, n’est demandée que pour les caractères qui manquent à ses fichiers. Un fournisseur qui renvoie un seul `Uint8Array` fonctionne comme avant. Le fournisseur du Sandbox lit la feuille de style Fontsource de la graisse et du style, et télécharge les fichiers dont les plages contiennent le texte ; en voici le cœur :

```ts
import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';

type Slice = { url: string; ranges: Array<[number, number]> };

async function fontsourceSlices(family: string, weight: number, style: 'normal' | 'italic'): Promise<Slice[]> {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const cssUrl = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`;
  const css = await (await fetch(cssUrl)).text();
  return [...css.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => ({
    url: new URL(/url\(\.?\/?([^)]+\.woff2)\)/.exec(rule)![1], cssUrl).href,
    ranges: /unicode-range:\s*([^;]+);/.exec(rule)![1].split(',').map((part) => {
      const [lo, hi = lo] = part.trim().slice(2).split('-');
      return [parseInt(lo, 16), parseInt(hi, 16)] as [number, number];
    }),
  }));
}

export const sliceFontProvider: PdfFontProvider = async (family, weight, style, request) => {
  // Where ranges overlap, the browser tries the last rule first.
  const slices = (await fontsourceSlices(family, weight, style)).reverse();
  const picked = new Set<Slice>();
  for (const cp of request?.codePoints ?? []) {
    const slice = slices.find((s) => s.ranges.some(([lo, hi]) => cp >= lo && cp <= hi));
    if (slice) picked.add(slice);
  }
  if (picked.size === 0) picked.add(slices[0]!);
  return Promise.all(slices.filter((s) => picked.has(s)).map(async (s) =>
    decompressWoff2(new Uint8Array(await (await fetch(s.url)).arrayBuffer()))));
};
```

Une famille latine passe par le même code : un texte anglais reçoit son seul fichier `latin`, un texte tchèque reçoit `latin` et `latin-ext`.

Un navigateur ne s’arrête pas au premier fichier qui a un glyphe. Il compose un caractère dans le fichier déclaré en dernier parmi ceux dont l’`unicode-range` le contient, et les fichiers d’une famille se chevauchent : Fontsource répète les lettres ASCII de Noto Serif JP dans son fichier `latin` et dans ses tranches numérotées. Deux caractères composés dans deux fichiers ne sont pas crénés entre eux, si bien que le fichier détermine la largeur : Chrome 154 compose `Shift_JIS` sur 4,25 em avec les tranches déclarées après `latin`, et sur 4,32 em avec le seul fichier `latin`, qui crène `t_` et `_J`. Depuis postext-pdf 1.30, un fournisseur peut indiquer comment la page a déclaré chaque fichier : `declaredFontFile(bytes, { unicodeRange, order })` renvoie les mêmes octets accompagnés de l’`unicode-range` de la règle du fichier, tel que l’écrit le CSS, et de la place de cette règle parmi les règles de la fonte, n’importe quel nombre qui croît avec l’ordre de déclaration (le plus grand est déclaré en dernier).

```ts
import { declaredFontFile } from 'postext-pdf';

declaredFontFile(bytes, { unicodeRange: 'U+0000-00FF,U+0131,U+0152-0153', order: 0 });
```

Quand les fichiers d’une fonte portent leurs plages, dans un document façonné dans sa propre langue ([`bodyText.shapingLanguage: 'document'`](https://postext.dev/fr/docs/configuration-text.md#texte-courant), la valeur par défaut), un caractère est dessiné à partir du fichier déclaré en dernier dont la plage le contient et qui a un glyphe pour lui, comme un navigateur choisit parmi les règles `@font-face`, et une lettre et ses signes diacritiques combinants restent dans un même fichier. Un caractère qu’aucune plage ne contient est dessiné à partir du premier fichier qui a un glyphe. Les fichiers simples, et tous les fichiers d’un document en `'host'`, sont lus dans l’ordre donné, comme avant : un fournisseur qui décrit ses fichiers place donc toujours en tête celui déclaré en dernier. Une fonte décrite ainsi est sollicitée à nouveau pour chaque caractère qui ne lui avait pas encore été demandé, puisqu’un fichier déclaré plus tard peut le contenir. Le fournisseur du Sandbox décrit ses fichiers.

Les caractères pour lesquels aucun fichier de la fonte n’a de glyphe sont dessinés avec le glyphe `.notdef` de la police (une boîte vide dans la plupart des polices), et `renderToPdf` les signale une fois par fonte après avoir dessiné les pages :

```ts
// { kind: 'missingGlyph', family: 'Noto Serif TC', weight: 400, style: 'normal',
//   characters: ['，', '！', '？'], message: '…' }
```

Le Sandbox les liste, ainsi que les deux avertissements ci-dessous, dans son panneau Vérifications après chaque PDF qu’il génère. Quand le livre, ses réglages ou ses ressources changent, ils sont marqués comme provenant d’un PDF antérieur jusqu’à ce que le suivant les remplace ; ouvrir un autre livre les efface.

- **Le gras exige un fichier statique par graisse.** Fontsource sert chaque graisse de Noto Serif SC et TC sous forme de fichiers statiques distincts, si bien que le gras fonctionne dans le Sandbox. Les fichiers de Google Fonts (`NotoSerifSC[wght].ttf`, 25 Mo) sont des polices variables : pdf-lib incorpore leur instance par défaut, si bien qu’une fonte 700 s’imprime en 400, et `renderToPdf` le signale par `variableFontDefaultInstance`. Pour un paquet, découpez une instance statique par graisse avec fontTools (`fonttools varLib.instancer NotoSerifSC[wght].ttf wght=700`) et réduisez-la aux caractères du livre avec `pyftsubset`.
- **Utilisez les versions TrueType.** Les fichiers `.otf` de Source Han Serif et de Noto Serif CJK ont des contours CFF, que postext-pdf incorpore en entier, de 8 à 25 Mo par graisse ; une fonte CFF de plus de 2 Mo est signalée par `cffEmbeddedWhole`. Les versions TrueType (Google Fonts, Fontsource) sont réduites aux glyphes utilisés. Pour le japonais, Noto Serif JP et Noto Sans JP (Google Fonts, ou les tranches numérotées de Fontsource), Shippori Mincho, Zen Old Mincho et BIZ UDMincho existent en TrueType ; Source Han Serif JP et les fichiers `.otf` `JP` de Noto Serif CJK sont en CFF.
- **Formes japonaises d’une fonte pan-CJK.** Un même point de code han, de ponctuation ou de guillemet peut se dessiner d’une façon au Japon et d’une autre en Chine, et une fonte pan-CJK (Source Han, Noto CJK) contient les deux. Le PDF façonne un document japonais (`locale: 'ja'`), et un isolat en japonais (`:ltr[…]{lang=ja}`) dans n’importe quel document, avec le système de langue OpenType `JAN `, si bien que la fonctionnalité `locl` de la fonte imprime les formes japonaises que le canvas et le HTML impriment grâce à `lang`. Un isolat dans une autre langue à l’intérieur d’un livre japonais est façonné avec les formes de cette langue (les formes par défaut de la police pour le chinois en `'host'`) et balisé comme un `Span` avec son `/Lang`. Avec [`bodyText.shapingLanguage: 'document'`](https://postext.dev/fr/docs/configuration-text.md#texte-courant), la valeur par défaut depuis postext 1.30, chaque document et chaque isolat est façonné dans sa propre langue (`ZHS` pour le chinois simplifié), comme la mise en page l’a mesuré ; avec `'host'`, les documents chinois et les autres sont façonnés avec les formes par défaut de la police, comme avant. Une fonte conçue pour le japonais comme Noto Serif JP a des formes par défaut japonaises ; elle compose tout de même les “ ” d’un texte japonais dans leurs formes `JAN `.

Un caractère absent d’une famille n’est pas pris dans une autre : Noto Serif TC n’emprunte rien à Noto Serif SC. Réglez la couverture au moment de construire les fichiers de polices ; la vitrine 红楼梦 copie les glyphes qui manquent à ses sous-ensembles TC depuis la fonte SC.

### Fournisseur de polices côté serveur (Node, fichiers locaux)

Sous Node, vous pouvez sauter entièrement l’étape WOFF2 et lire des fichiers TTF/OTF sur le disque :

```ts
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { PdfFontProvider } from 'postext-pdf';

const FONT_DIR = '/path/to/fonts';

function filename(family: string, weight: number, style: 'normal' | 'italic'): string {
  const slug = family.replace(/\s+/g, '');
  const styleSuffix = style === 'italic' ? 'Italic' : '';
  const weightName =
    weight >= 700 ? 'Bold'
    : weight >= 600 ? 'SemiBold'
    : weight >= 500 ? 'Medium'
    : weight >= 300 ? 'Light'
    : 'Regular';
  return `${slug}-${weightName}${styleSuffix}.ttf`;
}

export const localFontProvider: PdfFontProvider = async (family, weight, style) => {
  const buf = await readFile(join(FONT_DIR, filename(family, weight, style)));
  return new Uint8Array(buf);
};
```

### Octets des ressources et originaux d’impression

`resourceBytes(fileId)` renvoie les octets bruts d’une image, et le moteur en détecte le format :

- PNG, JPEG, GIF et WebP sont incorporés comme images ;
- le balisage SVG est dessiné en tracés vectoriels, son `text` composé en texte réel dans les polices incorporées du document, ou bien rastérisé à 600 dpi dans le navigateur lorsqu’il emploie des fonctions hors du sous-ensemble vectoriel. Un `<style>` qui ne contient que des règles `@font-face` (des fontes qu’un auteur a incorporées) est ignoré et laisse la figure vectorielle (depuis postext-pdf 1.25) ; toute autre feuille de style en fait une image matricielle, produite avec les fontes que nomme son texte, insérées depuis `fontProvider` (sauf si `diagramStyle.inlineFonts` ou le `svg.inlineFonts` de la ressource vaut `false`) ;
- un PDF voit sa première page incorporée telle quelle, sous forme de XObject de formulaire.

Chaque image est stockée une seule fois dans le fichier, quel que soit le nombre de fois où elle est dessinée. Un SVG dessiné en tracés vectoriels devient un XObject de formulaire que chaque page peint : un cadre ou un logo dans la maquette de page d’un document de trente pages est écrit une fois, et non trente ; chaque page supplémentaire ajoute quelques centaines d’octets. Jusqu’à postext-pdf 1.4, chaque page portait sa propre copie des tracés.

Une figure SVG peut désigner un **original d’impression** dans `svg.pdfFileId` : un PDF d’une page de la même figure, en général l’original dont le SVG a été exporté. `renderToPdf` demande d’abord à `resourceBytes` l’identifiant de l’original. Il incorpore cette page à la place du SVG, avec ses polices, ses dégradés et ses espaces colorimétriques intacts, partout où le SVG est dessiné : comme figure, comme image d’une cellule de tableau (`TableCell.image`), comme image de maquette ou comme icône d’encadré (son `marker` aussi). Le VDT porte l’identifiant de l’original à chacun de ces usages (`svg.pdfFileId` sur la ressource de la figure, `pdfFileId` sur une image de cellule et sur un bloc d’image de maquette), si bien que, dans un livre, chaque chapitre reçoit l’original. Les moteurs canvas et HTML continuent de dessiner le SVG. Les octets du SVG lui-même sont utilisés à la place dans trois cas : l’original est absent, ce n’est pas un PDF, ou l’encre unique est activée (`diagramStyle.singleInk` ne recolore que le balisage SVG).

```ts
const resources: Resource[] = [{
  id: 'map', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
  svg: { fileId: 'map.svg', width: 800, height: 600, pdfFileId: 'map.pdf' },
}];
const files = new Map([['map.svg', svgBytes], ['map.pdf', masterPdfBytes]]);
const pdf = await renderToPdf(buildDocument({ markdown, resources }, config), {
  fontProvider,
  resourceBytes: (fileId) => files.get(fileId),
});
```

Un hôte peut aussi renvoyer les octets de l’original pour l’identifiant propre du SVG, comme le fait `bundleResourceBytes` ; les deux façons fonctionnent.

### Texte vertical dans le PDF

Une page verticale (`layout.writingMode: 'vertical-rl'`) est dessinée dans un repère tourné d’un quart de tour, comme le canvas la peint, et son texte est composé de haut en bas de la colonne :

- **Les caractères debout** sont affichés au moyen d’une seconde police Type0 tirée du même fichier incorporé : même CIDFont, mêmes largeurs et même table ToUnicode, avec `Encoding /Identity-V` (mode vertical). Une suite de caractères forme un seul objet texte dont les glyphes avancent d’eux-mêmes d’un cadratin vers le bas de la colonne (`DW2 [880 −1000]`), si bien que les lecteurs sélectionnent et extraient une colonne comme une seule ligne. Les glyphes sont façonnés avec les fonctionnalités OpenType `vert` et `fwid`, qui donnent leurs formes verticales aux parenthèses, aux guillemets, aux virgules d’énumération du continent, aux points de suspension et aux tirets ; un caractère qui se tient debout tel quel garde son glyphe horizontal. Rien de la police n’est incorporé deux fois.
- **Les mots latins et les nombres longs** sont couchés sur le côté avec la police horizontale ; **un nombre dans une cellule** se tient debout, comprimé en largeur jusqu’au cadratin s’il est plus large ; un signe pour lequel la police n’a pas de forme verticale est tourné ou déplacé, comme sur le canvas.
- **L’approche** entre les caractères est écrite sous forme de nombres `TJ`, qui, en mode vertical, déplacent la plume vers le bas de la colonne.
- **Chaque ligne verticale** est marquée d’un `/ActualText` contenant son texte, si bien que la copie et l’extraction de texte la lisent telle qu’elle est écrite. `pdftotext` et pdf.js lisent les colonnes de haut en bas et de droite à gauche ; pdf.js commence une nouvelle ligne à chaque nombre composé dans une cellule.
- **Les liens, signets et destinations** sont reportés sur la feuille : un lien sur une ligne verticale est un rectangle haut et étroit, et un signet ouvre la page en haut de la colonne de son titre.
- **Un PDF balisé** déclare le mode d’écriture sur son élément `Document` (l’attribut de mise en page `WritingMode /TbRl`, dont hérite chaque élément) ; la validation PDF/UA-1 (veraPDF) réussit sur un chapitre vertical.
- **Visionneuses :** Acrobat, Aperçu, Chrome (PDFium), pdf.js et Poppler affichent les polices verticales. Un livre relié à droite (`page.binding`) demande aussi aux visionneuses de disposer ses doubles pages de droite à gauche (`/Direction /R2L`, `/PageLayout /TwoPageRight`) ; Acrobat et Foxit le suivent, Chrome non.

Un chapitre de 43 pages composé en Noto Serif TC (les caractères du livre, en TrueType) pèse environ 820 Ko, dont la plus grande partie revient aux deux sous-ensembles de polices.

### Liens dans le PDF

Les mots d’un lien Markdown (voir [Format du document › Liens](https://postext.dev/fr/docs/document-format-inline.md#liens)) deviennent des annotations de lien URI, une par suite de mots liés sur une ligne. Chacune couvre la boîte de la ligne et n’a pas de bordure. Dans un rendu accessible, chaque suite est un élément `Link` dont le `/Contents` est son texte. Seules les cibles absolues `http:`, `https:`, `mailto:`, `tel:` et `ftp:` sont liées, car une URL relative n’a pas de base à l’intérieur d’un PDF. Les caractères hors de l’ASCII imprimable sont encodés en pourcentage. Les renvois `:ref` et les lignes de la table des matières gardent leurs liens à l’intérieur du document.

### Exemple complet dans le navigateur : construire, rendre, télécharger

Tout assembler : construire le VDT, le rendre en PDF et déclencher un téléchargement depuis le navigateur :

```tsx
import { buildDocument, createMeasurementCache } from 'postext';
import { renderToPdf } from 'postext-pdf';
import { createPdfFontProvider } from './pdfFontProvider';

const fontProvider = createPdfFontProvider();

export async function downloadPdf(markdown: string, config: PostextConfig) {
  const cache = createMeasurementCache();
  const vdt = buildDocument({ markdown }, config, cache);

  const bytes = await renderToPdf(vdt, { fontProvider });

  const blob = new Blob([bytes.slice().buffer], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'document.pdf';
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
```

**Important :** appelez `ensureConfigFontsLoaded(config)` (ou l’équivalent) *avant* `buildDocument` quand votre configuration fait référence à des polices web. La mise en page est mesurée avec les métriques dont dispose le navigateur à ce moment pour la famille : si la vraie police n’est pas encore arrivée, le VDT est mesuré avec une police de secours et le PDF ne correspondra pas à la sortie canvas ou HTML. Le Sandbox le fait explicitement avant chaque rendu (voir `packages/postext-sandbox/src/viewport/PdfViewport.tsx`).

### Exemple en direct : un PDF dans le navigateur

Le déroulé complet ci-dessus, exécuté dans le navigateur : le pen importe `postext` et `postext-pdf` depuis un CDN, charge les polices web, construit le document, incorpore les coupes Fontsource au moyen du fournisseur de polices et transmet les octets à un lien qui ouvre le fichier dans un nouvel onglet, ainsi qu’à un lien de téléchargement. Le PDF obtenu a les mêmes coupures de lignes que la sortie canvas et HTML, de vraies polices incorporées et des signets.

> **Exemple exécutable: Postext · générer un PDF dans le navigateur** — Mettre en page un document Markdown avec postext et le rendre en PDF avec postext-pdf. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/render-pdf))

### Rendre le PDF dans un worker

`postext-pdf/worker` sort `renderToPdf` du thread principal. Le worker écrit le texte, les figures vectorielles, l’arbre de structure et le fichier lui-même. Deux tâches ont besoin de la page, et le worker les demande donc au thread principal : télécharger les polices et rastériser un SVG au moyen d’un `<img>`. Pour un livre de plusieurs centaines de pages, le rendu prend des secondes qui figeraient sinon la page ; pour quelques pages, appeler directement `renderToPdf` est plus simple.

```ts
import { createPdfWorker } from 'postext-pdf/worker';

const pdfWorker = createPdfWorker();
const bytes = await pdfWorker.render(docs, {
  fontProvider,                                     // runs on this thread
  resourceBytes: new Map([['map.svg', svgBytes]]),  // a Map; its buffers move to the worker
  onProgress: ({ phase, pages, totalPages }) => showProgress(phase, pages, totalPages),
  onWarning: (w) => console.info(w.message),
});
pdfWorker.dispose();
```

- `render(docs, options)` prend un document, ou le tableau des documents de chapitres d’un livre. Il accepte les options de `renderToPdf`, avec deux différences. `resourceBytes` est une `Map<string, Uint8Array>` dont les tampons sont transférés : passez des copies des octets que vous conservez. `rasterizeSvg`, s’il est fourni, s’exécute sur le thread principal ; par défaut, ce sont l’`Image` et le canvas de la page qui s’en chargent.
- Un même objet ne rend qu’un document à la fois. `dispose()` termine le worker et rejette tout rendu encore en attente.
- `createPdfWorker({ worker })` accepte un `Worker` que vous créez vous-même, pour les outils de build qui contrôlent les URL des workers. Ce worker doit exécuter `postext-pdf/worker/entry`.

**Depuis un CDN.** Par défaut, le script du worker est chargé depuis l’URL propre du paquet (`new URL('./pdf.worker.js', import.meta.url)`). Une page d’une autre origine peut ne pas pouvoir le démarrer : importé depuis `esm.sh`, `createPdfWorker()` lève `Failed to construct 'Worker': Script at 'https://esm.sh/postext-pdf@…/pdf.worker.js' cannot be accessed from origin …`. Démarrez plutôt un worker de module de même origine qui importe le point d’entrée (si vous épinglez une version, épinglez la même dans les deux URL) :

```js
import { createPdfWorker } from 'https://esm.sh/postext-pdf/worker';

const entry = URL.createObjectURL(new Blob(
  ["import 'https://esm.sh/postext-pdf/worker/entry';"],
  { type: 'text/javascript' },
));
const pdfWorker = createPdfWorker({ worker: new Worker(entry, { type: 'module' }) });
```

Le worker de mise en page de `postext/worker` a besoin de la même enveloppe autour de `postext/worker/entry` (voir [Exécuter la mise en page dans un Web Worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker)).

### PDF prêts pour l’impression

Pour une chaîne de production imprimée, réglez ces options de configuration avant le rendu :

- **`page.cutLines.enabled: true`** : ajoute la zone de fond perdu et les traits de coupe autour du format rogné, et donne à chaque page une TrimBox et une BleedBox. Voir [Traits de coupe](https://postext.dev/fr/docs/configuration-page-layout.md#traits-de-coupe).
- **`print: { standard: 'pdfx4', outputProfile: 'fogra51' }`** (ou `'pdfx1a'`) : un fichier PDF/X avec l’intention de sortie (output intent), l’identification et les boîtes que vérifie un imprimeur ; chaque couleur et chaque image RVB séparées au moyen du profil ICC, le noir 100 % K en surimpression et les grands aplats noirs en noir enrichi. Voir [Production pour l’impression (configuration)](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#production-pour-limpression-configuration).
- **`colorSpace: 'cmyk'`** (ou `pdfGeneration: { forceColorSpace: true, colorSpace: 'cmyk' }`) : la même séparation sans l’identification PDF/X (les traits de coupe sont toujours en couleur de repérage). Les originaux d’impression PDF sont incorporés tels quels.
- **`page.dpi: 300`** : les px par pouce de la mise en page ; un bitmap sans résolution propre s’imprime à cette résolution à sa taille naturelle. Le [contrôle en amont](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#contrôle-en-amont) signale les images de moins de 300 ppp à leur taille imprimée.
- **`ColorValue.cmyk`** : une couleur définie en CMJN s’imprime avec ses valeurs exactes.
- **`{ pageNegative: true }`** dans `RenderToPdfOptions` : inverse la zone rognée au moyen d’un mode de fusion Différence (les traits de coupe ne sont pas inversés). Utile pour les contrôles en amont d’une typographie sombre sur fond clair.

### Implémentation de référence

Le composant `PdfViewport` du Sandbox (`packages/postext-sandbox/src/viewport/PdfViewport.tsx`) relie les pièces ci-dessus dans un aperçu en direct doté de boutons de régénération, de téléchargement et d’impression, et constitue un bon point de départ pour toute intégration PDF dans le navigateur. Il construit le VDT au moyen du worker de mise en page partagé (voir [Exécuter la mise en page dans un Web Worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker)), si bien qu’un clic sur *Recalculer* ne fige pas l’interface pendant l’exécution du pipeline : le thread principal ne s’occupe que de `renderToPdf` (déjà rapide une fois le VDT disponible).
