# Configuration : utilisation programmatique

> Appeler Postext depuis le code : buildDocument, Web Workers, la visionneuse HTML, les fichiers PDF, le livre en 3D, l’EPUB et les paquets .postext

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

## En bref

Cette page s’adresse aux personnes qui écrivent du code. Elle montre comment composer un livre avec un seul appel de fonction et comment lire les avertissements qu’il signale. Elle explique comment faire le travail en arrière-plan, pour que la page reste rapide. Elle montre comment produire une vue web, un fichier PDF, un livre en 3D et un livre numérique EPUB. La dernière section présente le fichier qui contient un livre entier avec ses polices et ses images.

## Utilisation programmatique

> **Voie recommandée : utilisez le Web Worker.** Dans un navigateur, la très grande majorité des intégrations devraient piloter la chaîne de mise en page par `createLayoutWorker()` de `postext/worker`, **et non** en appelant `buildDocument` directement sur le thread principal. Le worker garde l’interface réactive pendant les constructions, met en cache les mesures de texte d’une reconstruction incrémentale à l’autre et met en place une annulation où la dernière demande l’emporte, de sorte qu’une nouvelle frappe interrompt toute construction périmée déjà en cours. Passez directement à [Mise en page dans un Web Worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker) pour la recette de référence. Tout le reste de cette section (`buildDocument` en direct, résolveurs, fonctions d’élagage, caches) reste utile, puisque le worker expose exactement les mêmes entrées et sorties, mais pour du code d’interface, l’enveloppe du worker est le bon point de départ. N’appelez `buildDocument` sur le thread principal que pour des exports ponctuels, le rendu côté serveur (Node) ou des tests.

### Construire un document

La fonction `buildDocument` exécute toute la chaîne de mise en page et renvoie un arbre de document virtuel (VDT) avec les coordonnées précises de chaque élément. C’est le point d’entrée de plus bas niveau ; le code d’interface doit lui préférer l’[enveloppe Web Worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker), qui appelle `buildDocument` dans un thread de worker dédié avec les mêmes arguments.

```ts
import { buildDocument } from 'postext';

const content = {
  markdown: '# Chapter One\n\nThe story begins here...',
};

const config = {
  page: { sizePreset: '17x24' },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
};

// Build the layout — produces a VDT with one entry per page in `vdt.pages`
const vdt = buildDocument(content, config);
console.log(`Document has ${vdt.pages.length} pages`);
```

### Avertissements dans le document

`buildDocument` ne s’arrête pas sur une référence erronée ou sur un style inconnu : il applique une solution de repli et consigne ce qu’il a fait dans `doc.contentWarnings`. Les encadrés que la mise en page a dû forcer se trouvent dans `doc.warnings`, qui garde la forme qu’il avait dans postext 1.4 : chaque entrée y est un `calloutOverflow` avec ses `pageIndex`, `columnIndex` et `overflowPx`. Chaque champ est absent lorsqu’il n’y a rien à signaler. Chaque entrée a un `kind`. Les types de contenu portent la plage source de l’élément de syntaxe concerné (`sourceStart` / `sourceEnd`, des positions dans le markdown que vous avez passé, frontmatter compris) et, lorsque cet élément est tombé sur une page, son `pageIndex`.

| Type | Levé quand | Ce que fait la sortie |
| --- | --- | --- |
| `calloutOverflow` | Un encadré `:::callout` ne tient dans aucune colonne et aucune coupure ne peut le scinder. Un encadré `span: 'side'` plus haut qu’une colonne latérale vide l’est aussi (depuis postext 1.25). | Il est placé malgré tout et déborde de sa colonne de `overflowPx` (sur `pageIndex` / `columnIndex`). C’est le seul type listé dans `doc.warnings` ; les types suivants se trouvent dans `doc.contentWarnings`. |
| `invalidFrontmatter` | Le frontmatter n’est pas du YAML valide (un guillemet non fermé, du texte après une valeur entre guillemets). `message` donne la raison fournie par l’analyseur, avec sa ligne et sa colonne. | Le document est composé sans ses métadonnées ; le corps qui suit le `---` fermant est mis en page normalement. |
| `unknownResourceId` | Une insertion `::resource` (`usage: 'embed'`), un `:ref` en ligne (`'ref'`) ou l’image d’une cellule de tableau (`'cellImage'`) nomme un identifiant qu’aucune ressource ne porte. | L’insertion est omise, le renvoi imprime `?` (ou son libellé `text=`) sans numéro ni lien, la cellule reste en texte seul. `inResource` nomme la ressource dont la légende, la note ou la cellule contient la référence. |
| `unknownDirective` | Une ligne `:::name` dont le nom n’est ni une directive ni un conteneur. | La ligne est composée comme du texte. |
| `malformedEmbed` | Une ligne `::name` qui n’est pas une insertion bien formée et isolée : `::resource` avec un identifiant sans guillemets ou entre apostrophes simples, ou avec un autre attribut, ou une ligne collée sous un paragraphe sans ligne vide. | La ligne est composée comme du texte. |
| `fullwidthMarkup` | Une ligne contient un balisage saisi avec une méthode de saisie chinoise ou japonaise : une clôture `：：：`, un titre `＃`, un appel de note `［＾…］`, des attributs `｛…｝` après une clôture ou un titre, ou du gras `＊＊…＊＊`. `typed` est le balisage tel qu’il a été saisi, `ascii` la forme à saisir. Un par ligne. | La ligne est composée comme du texte ; rien n’est converti. |
| `attributeKeyInvalid` | Une clé d’attribut contient des lettres hors ASCII (`作者=曹雪芹`) ; pointe sur la clé. | L’attribut est ignoré. |
| `unknownParagraphStyle` | `:::paragraphs{style}` ne nomme aucun style de paragraphe. | Les paragraphes sont composés comme du texte courant. |
| `unknownCalloutType` | `:::callout{type}` ne nomme aucun des `calloutStyles` ; levé seulement une fois que certains sont configurés. | L’encadré prend le premier style d’encadré. |
| `columnsFlowUnknown` | `:::columns{flow}` ne vaut ni `snake` ni `parallel` ; `value` est la valeur indiquée. | Le groupe prend la valeur par défaut : `parallel` avec `breaks`, `snake` sans. |
| `unknownChipStyle` | `:chip[…]{style}` ne nomme aucun style de pastille. | La pastille prend le premier style de pastille. |
| `undefinedFootnote` | Un appel de note `[^id]` qu’aucun paragraphe `[^id]:` ne définit (`id` est celui de la note). | Le numéro s’imprime ; la note est vide. |
| `unusedFootnote` | Une définition de note `[^id]:` qu’aucun appel ne cite. | La note n’est pas composée. |
| `indexMarkInvalid` | Une marque d’index sans terme : `:index{}`, ou des attributs sans `term` sur une marque sans texte entre crochets. | La marque n’indexe rien. |
| `indexSeeUnknown` | Une cible `see` ou `seealso` (`target`) qui n’est pas une entrée de son index (`index`, `''` pour l’index principal). Pointe sur la ligne `:::index`. | Le renvoi s’imprime malgré tout. |
| `indexRangeUnclosed` | Une marque `range="start"` sans `range="end"` correspondant, ou l’inverse (`missing` indique quelle extrémité manque ; `term` nomme l’entrée). Pointe sur la ligne `:::index`. | La plage n’imprime que sa page. |
| `unknownHeadingStyle` | Le `{style="…"}` d’un titre ne nomme aucun style de titre (`level` est celui du titre). | Le titre et sa section gardent les réglages propres à leur niveau. |
| `unknownTableStyle` | Le `table.styleId` d’une ressource tableau ne nomme aucune entrée de `tableStyles`. | Le tableau est composé dans `tableStyle`. |
| `raggedTableGrid` | La grille d’un tableau n’est pas rectangulaire une fois ses fusions comptées (voir [Construire des modèles de tableau](https://postext.dev/fr/docs/configuration-resources.md#construire-des-modèles-de-tableau)). | Des cellules glissent sur une fusion ou laissent un trou. `reason` (`'spanOverlap'` / `'missingCells'`), `row` et `col` situent le premier problème ; `count` indique combien il y en a. |
| `lineNumberOverlap` | Avec `lineNumbers.position: 'side'`, un numéro de ligne chevauche un encadré latéral, une légende latérale ou une figure de la colonne latérale. Pointe sur la ligne numérotée ; `number` est le numéro tel qu’il est imprimé. | Le numéro est peint malgré tout, et ni l’un ni l’autre ne bouge. |
| `dropCap` | Un paragraphe ouvert par une [lettrine](https://postext.dev/fr/docs/configuration-text.md#lettrines) qui ne peut pas la recevoir telle qu’elle est configurée. `reason` : `'shortParagraph'` (moins de lignes que la lettrine n’en occupe ; `handling` indique ce qu’a fait `shortParagraph`, `lines` les lignes que couvre une lettrine réduite), `'split'` (il se coupe avant la dernière ligne de la lettrine, seul dans une colonne trop courte), `'joiningScript'` (sa première lettre se lie à la suivante), `'verticalText'` ou `'noLetter'` (il s’ouvre sur une référence, une formule ou un appel de note). `text` est sa première ligne. | La place est gardée, la lettrine est réduite ou omise, comme l’indique l’avertissement. |
| `codeOverflow` | Un [listing de code](https://postext.dev/fr/docs/configuration-styles.md#listings-de-code) a des lignes plus larges que sa boîte. `mode` indique ce qu’a fait `codeStyle.overflow` (`'wrap'`, `'shrink'`, `'clip'`), `lines` combien de lignes source étaient trop larges, `scale` le corps auquel un listing réduit a été composé (une fraction de `fontSize`), `lang` le langage du bloc de code. Pointe sur le listing. | Les lignes sont renvoyées à la ligne, composées plus petit ou coupées, comme l’indique `mode`. |
| `floatShrunk` | Une image flottante a été composée plus petite que sa taille pour tenir dans la place de son emplacement (`placement.shrink`). `resourceId` la nomme, `scale` est la part de sa largeur qu’elle garde, et `overflowPx`, s’il est présent, la distance dont elle dépasse encore le pied du bloc de texte à son échelle minimale (`placement.minScale`), sur une nouvelle page où elle n’avait nulle part ailleurs où aller. Pointe sur le paragraphe qui la cite en premier. | L’image est imprimée à cette échelle ; au-delà du bloc de texte seulement lorsque `overflowPx` l’indique. |
| `textWrap` | Une ressource ou un encadré réglé pour que le texte l’habille (`placement.wrap`, le `wrap` d’un encadré) et qui ne le fait pas comme demandé. `reason` : `'tooNarrow'` (le texte à côté serait plus étroit que `layout.wrap.minTextWidth`), `'fewLines'` (il fait moins de `layout.wrap.minLinesBeside` lignes), `'moved'` (un élément en ligne trop haut pour la place restante dans sa colonne est passé à la suivante, avec son ancre) ou `'verticalText'`. `resourceId` nomme une ressource, `box` le style d’un encadré. Pointe sur l’insertion ou sur l’encadré. | L’élément occupe toute sa bande, ou se place dans la colonne suivante, selon la raison. |
| `columnsTooNarrow` | Les sous-colonnes d’un groupe `:::columns` sont plus étroites que six cadratins de leur texte : `columns` sous-colonnes, de `widthPx` chacune. Pointe sur la clôture du groupe. | Le groupe est composé comme demandé, avec peu de mots par ligne. |
| `afterText` | Un encadré `span: 'side'`, ou une figure ou un tableau de la colonne latérale (`resourceId`), placé sur une page qui ne contient pas de texte : le texte de son chapitre, ou du document, s’est terminé alors qu’il attendait encore de la place dans la colonne latérale. Un par encadré ou par flottant ; pointe sur l’encadré (sur le bloc qui cite le flottant), avec sa page. Depuis postext 1.25. | Il se place dans la colonne latérale d’une page ouverte après le texte, les encadrés dans l’ordre de leurs clôtures. |
| `unplaced` | Un encadré ou une ressource flottante (`resourceId`) qui attend encore un emplacement à la fin de la mise en page : les pages ouvertes pour l’accueillir n’ont pas pu le faire (un encadré latéral alors que ces pages n’ont pas de colonne latérale, par exemple). Pointe sur l’encadré ou sur le bloc qui cite la ressource ; pas de page. Depuis postext 1.25. | Il ne figure sur aucune page. Jusqu’à postext 1.24, il disparaissait sans avertissement. |
| `shortListSplit` | Une liste de `items` éléments, courte au sens de `bodyText.shortListMaxItems`, coupée entre deux colonnes ou deux pages. Avec `bodyText.shortListSplit: 'warn'`, toute liste de ce genre est signalée ; avec `'avoid'`, seulement celle qu’aucune colonne ne pouvait contenir entière. Un par liste ; pointe sur la liste, avec la page de son premier élément. Depuis postext 1.28. | La liste est coupée là où la colonne se termine. |
| `openerFloat` | Un flottant placé sur une page d’ouverture avec `layout.openerFloats: 'warn'` : une figure, un tableau ou une bande (`resourceId`), ou un encadré flottant. Un par flottant ; pointe sur le bloc qui le cite (sur l’encadré), avec sa page. Depuis postext 1.28. | Le flottant reste sur la page d’ouverture ; `'avoid'` le déplace vers le premier emplacement qui la suit. |
| `fontFallback` | Une police dans laquelle le texte a été composé (`family`, `weight`, `style`) et que le jeu de polices ne pouvait pas fournir au moment de la construction : `reason: 'missing'`, aucune police de la famille n’était chargée ni installée, ou celle qui répond pour cette graisse et cette inclinaison n’était pas encore chargée ; `'synthesized'`, la famille n’a pas de police de cette graisse ou de cette inclinaison et le navigateur la tire d’une autre, telle quelle ou dessinée plus grasse ou penchée (un 600 tiré du 700, un 700 italique demandé à une famille qui n’a qu’un 400 romain). Vérifié là où il existe un jeu de polices (`document.fonts`, le `self.fonts` d’un worker, ou `BuildDocumentOptions.fontSet`), derrière `debug.warnings.missingFont`. Ni page ni plage source. | Le texte est mesuré et dessiné avec la police de repli, ou avec une autre police de la famille, telle quelle ou rendue plus grasse ou penchée ; ses coupures de ligne changent quand la police arrive. Voir [Charger les polices avant la mise en page](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-les-polices-avant-la-mise-en-page). |

Les avertissements qui concernent une ressource (son style de tableau, sa grille, une référence dans sa légende, sa note ou ses cellules) pointent sur la première insertion ou la première référence de la ressource dans le texte, et chacun n’est listé qu’une fois par ressource. Seules les ressources que le document utilise sont vérifiées : un chapitre d’un livre signale les tableaux qu’il cite, pas tous les tableaux du livre.

```ts
import { buildDocument, formatWarning } from 'postext';

const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.\n\n:::sidebar\nNotes.' }, config);
for (const w of [...(doc.warnings ?? []), ...(doc.contentWarnings ?? []), ...(doc.configWarnings ?? [])]) console.warn(formatWarning(w));
// Unknown resource id "fig-map" in :ref — it prints "?" (or its text= label), with no number or link (page 1, offset 4)
// Unknown directive ":::sidebar" — the line is set as text (page 1, offset 25)

// Narrow on `kind` to read the fields of a kind.
const missing = (doc.contentWarnings ?? []).flatMap((w) => (w.kind === 'unknownResourceId' ? [w.resourceId] : []));
```

`formatWarning(w)` renvoie une description d’une ligne en anglais. Un hôte qui localise ses messages s’appuie plutôt sur `kind`, en gardant une branche par défaut, car des versions mineures peuvent ajouter des types. `collectContentWarnings(markdown, config, resources)` renvoie les avertissements de contenu sans rien mettre en page (la liste qu’ajoute la construction, sans `pageIndex`), pour un éditeur qui vérifie le texte pendant la saisie. `collectHeadingDesignCuts(doc)` cherche dans une mise en page terminée les designs de titre dont le texte dépasse le pied de leur page ou de leur colonne (`kind: 'headingDesignCut'` ; voir [Hauteur réservée](https://postext.dev/fr/docs/configuration-text.md#hauteur-réservée)), ce que la mise en page elle-même ne signale pas, et `formatWarning` décrit aussi ses résultats. Le panneau **Vérifications** du Sandbox les liste tous.

Les moteurs de rendu signalent ce qu’ils ne peuvent pas peindre comme demandé par une option `onWarning` : `renderPageToCanvas`, `renderPage` et `renderToCanvas` (`RenderPageOptions`), `renderToHtml` et `renderToHtmlIndexed` (`RenderHtmlOptions`), et `renderToPdf` (`RenderToPdfOptions`, aussi par le worker PDF). Le principal type d’avertissement de rendu est `missingImage` : une image (une figure, l’image d’une cellule de tableau, une icône d’encadré, une image de design) qui n’a rien à dessiner est peinte comme un espace réservé neutre et signalée, une fois par `fileId` et par appel de rendu, avec son `pageIndex`, le `resourceId` quand le code de peinture le connaît (figures et images de cellule) et, dans le PDF, le `documentIndex` d’un rendu de plusieurs documents. « Rien à dessiner » signifie : aucun `registerResourceImage` pour le `fileId` dans le canvas, aucune URL renvoyée par `resourceImageUrl` en HTML, et aucun octet fourni par `resourceBytes` (ou des octets qui ne se décodent pas) dans le PDF. Une ressource bitmap ou SVG qui ne nomme aucun `fileId` n’a rien à demander : elle est dessinée comme un espace réservé, sans signalement. Deux autres types viennent des hôtes qui incorporent des polices dans des images SVG (`registerSvgImage`, `registerBundleImages`, `bundleImageUrl`, `renderToHtml` avec `inlineSvgFonts`, postext-epub ; voir [Polices dans le texte SVG](https://postext.dev/fr/docs/configuration-resources.md#polices-du-texte-svg)), avec le `fileId` et le `resourceId` de l’image : `svgFontUnavailable` (`family`, `weight`, `style`), une famille que nomme son texte sans police à incorporer, si bien que l’image compose ce texte dans une police de repli ; et `svgFontsTooLarge` (`bytes`, `maxBytes`), des polices au-delà du plafond de taille, dont aucune n’est incorporée. Les avertissements de rendu ne sont pas stockés dans le VDT : ce qu’un hôte peut fournir change après la mise en page.

```ts
import { buildDocument, renderPage, type RenderWarning, type Resource } from 'postext';

const map: Resource = {
  id: 'fig-map', typeId: 'figure', kind: 'bitmap', caption: 'The route.', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'map-file', format: 'png', width: 1200, height: 800 },
};
const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.', resources: [map] }, config);

const warnings: RenderWarning[] = [];
const canvas = renderPage(doc.pages[0], doc, { onWarning: (w) => warnings.push(w) });
// Until 'map-file' is registered with registerResourceImage:
// [{ kind: 'missingImage', fileId: 'map-file', resourceId: 'fig-map', pageIndex: 0 }]
```

### Rendre une page en bitmap

Chaque page peut être rastérisée indépendamment. Utilisez `renderPage(page, doc)` pour obtenir un `HTMLCanvasElement` pour un numéro de page donné : le canvas est un bitmap aux dimensions exactes de la page en pixels (à la résolution configurée), que vous pouvez afficher, exporter ou passer à n’importe quelle chaîne de traitement d’images :

```ts
import { buildDocument, renderPage } from 'postext';

const vdt = buildDocument(content, config);

// Render page 3 (zero-indexed) to a bitmap canvas
const pageNumber = 2;
const page = vdt.pages[pageNumber];
if (!page) throw new Error(`Page ${pageNumber} does not exist`);

const canvas = renderPage(page, vdt);
// canvas.width / canvas.height are the page bitmap size in pixels

// Show it in the DOM
document.body.appendChild(canvas);

// …or export it as a PNG data URL
const pngDataUrl = canvas.toDataURL('image/png');

// …or get a Blob for download / upload
canvas.toBlob((blob) => {
  if (blob) saveAs(blob, `page-${pageNumber + 1}.png`);
}, 'image/png');

// …or grab raw RGBA pixels
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
```

Si vous préférez dessiner dans un canvas que vous possédez déjà (par exemple un canvas attaché au DOM avec une mise en page précise), utilisez `renderPageToCanvas(page, doc, canvas)` : il redimensionne le canvas que vous lui passez et y peint, au lieu d’en créer un nouveau.

Pour rendre toutes les pages, parcourez `vdt.pages` :

```ts
const bitmaps = vdt.pages.map((page) => renderPage(page, vdt));
```

#### Exemple en direct : une page en image

Tout ce qui précède, exécuté dans le navigateur. Le pen importe la dernière version de `postext` depuis un CDN, attend les polices web, met en page un court document sur deux colonnes, peint sa première page dans un canvas et propose ce bitmap en PNG. Appuyez sur *Run on CodePen* pour charger l’éditeur et modifier le markdown ou la configuration ; la page est repeinte à chaque modification.

> **Exemple exécutable: Postext · render a page to an image** — Lay out a markdown document with postext and rasterise its first page to a canvas / PNG. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/render-page))

### React

`postext/react` exporte `createLayout(content, config?)` : un composant qui met le document en page une seule fois, au montage, et affiche chaque page sous forme de `<canvas>` dans une `<div>`.

```tsx
import { createLayout } from 'postext/react';

const Article = createLayout(
  { markdown: '# Hello\n\nThe first paragraph of the article.' },
  { page: { sizePreset: '17x24' } },
);

export function ArticlePage() {
  return <Article className="pages" style={{ maxWidth: 480 }} />;
}
```

- **Sur le thread principal, une seule fois.** Les pages sont peintes à la résolution du document et mises à l’échelle à la largeur du conteneur. `content` et `config` sont fixés au moment où vous appelez `createLayout` ; créez un autre composant pour afficher autre chose. Pour un aperçu en direct, construisez dans le [Web Worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker) et peignez avec `renderPageToCanvas`, comme dans l’exemple React de cette section.
- **Les polices et les images d’abord.** Chargez les polices web du document avant le montage du composant, et enregistrez ses images avec `registerResourceImage`. Lorsque le markdown contient un `$`, le composant démarre lui-même le [moteur mathématique](https://postext.dev/fr/docs/configuration-text.md#démarrer-le-moteur-mathématique).
- **React reste hors du point d’entrée principal.** `postext` n’importe jamais React ; seul `postext/react` le fait. `createLayout` est toujours exporté depuis `postext` pour que le code existant continue de fonctionner, mais il est déprécié : il charge `postext/react` quand vous l’appelez, et le composant se suspend jusqu’à son arrivée (React le rend de nouveau de lui-même). Importez-le depuis `postext/react`.
- **Le composant déprécié se suspend.** Tant que `postext/react` n’est pas arrivé, le `createLayout` de `postext` a besoin d’une racine concurrente (`createRoot`) ou d’une frontière `<Suspense>` au-dessus de lui. Dans une racine `ReactDOM.render` historique, ou dans `renderToString`, sans frontière, React signale une erreur à la place. `react` reste une dépendance pair obligatoire, afin que les bundlers puissent résoudre cet import différé.

### Résoudre les valeurs par défaut

Les fonctions de résolution complètent les objets de configuration partiels avec les valeurs par défaut. C’est utile lorsque vous avez besoin d’une configuration complète pour l’inspecter ou la comparer :

```ts
import { resolvePageConfig, resolveBodyTextConfig } from 'postext';

const fullPage = resolvePageConfig({ sizePreset: '21x28' });
// => { sizePreset: '21x28', width: { value: 21, unit: 'cm' }, height: { value: 28, unit: 'cm' },
//      margins: { top: { value: 2, unit: 'cm' }, ... }, dpi: 300, cutLines: { enabled: false, ... }, ... }

const fullBody = resolveBodyTextConfig({ fontFamily: 'Inter' });
// => { fontFamily: 'Inter', fontSize: { value: 8, unit: 'pt' }, lineHeight: { value: 1.5, unit: 'em' }, ... }
```

Résolveurs disponibles, un par section de premier niveau : `resolvePageConfig`, `resolveLayoutConfig`, `resolveBodyTextConfig`, `resolveHeadingsConfig`, `resolveHeadingStylesConfig`, `resolveTocConfig`, `resolvePartsConfig`, `resolveUnorderedListsConfig`, `resolveOrderedListsConfig`, `resolveMathConfig`, `resolveTableStyleConfig`, `resolveCaptionStyleConfig`, `resolveDiagramStyleConfig`, `resolveParagraphStylesConfig`, `resolveCalloutStylesConfig`, `resolveHeaderFooterConfig`, `resolveDebugConfig`, `resolveHtmlViewerConfig`, `resolvePdfGenerationConfig`, plus `resolveDesignSlot` pour un seul emplacement de design. Les palettes de couleurs s’appliquent séparément par `applyPaletteToConfig(config)`, `applyPaletteToResolvedConfig(resolved, palette)` et `resolveColorValue(value, palette, fallback)` : voir [Palette de couleurs](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#palette-de-couleurs).

Les résolveurs dont les valeurs par défaut découlent d’une autre section prennent cette section, déjà résolue, comme argument supplémentaire. `resolveUnorderedListsConfig` et `resolveOrderedListsConfig` prennent le texte courant résolu, car les valeurs par défaut des listes pour `fontFamily` et `color` en découlent ; `resolveCalloutStylesConfig` prend le texte courant, les titres et les listes à puces résolus (voir l’exemple dans [Styles d’encadré](https://postext.dev/fr/docs/configuration-styles.md#styles-dencadré)), et `resolveHeadingStylesConfig` la page, le texte courant et les deux sections de listes résolus. Consultez les déclarations de types du paquet npm pour la signature exacte de chacun :

```ts
import { resolveBodyTextConfig, resolveUnorderedListsConfig } from 'postext';

const body = resolveBodyTextConfig({ fontFamily: 'Inter' });
const lists = resolveUnorderedListsConfig({ bulletChar: '—' }, body);
// => lists.fontFamily === 'Inter' (inherited)
```

Les jeux de valeurs par défaut statiques (les valeurs utilisées quand aucune cascade n’intervient) sont aussi exportés : `DEFAULT_PAGE_CONFIG`, `DEFAULT_CUT_LINES`, `DEFAULT_PAGE_NUMBERING`, `PAGE_SIZE_PRESETS`, `DEFAULT_LAYOUT_CONFIG`, `DEFAULT_COLUMN_RULE`, `DEFAULT_COLUMN_BALANCING`, `DEFAULT_BODY_TEXT_CONFIG`, `DEFAULT_HYPHENATION_CONFIG`, `DEFAULT_HEADINGS_CONFIG`, `DEFAULT_UNORDERED_LISTS_STATIC`, `DEFAULT_ORDERED_LISTS_STATIC`, `DEFAULT_PARAGRAPH_STYLES`, `DEFAULT_CALLOUT_STYLES`, `DEFAULT_CALLOUT_STYLE_STATIC`, `DEFAULT_PARTS_CONFIG`, `DEFAULT_HEADING_STYLES`, `DEFAULT_TOC_CONFIG`, `DEFAULT_MATH_CONFIG`, `DEFAULT_DIAGRAM_STYLE_CONFIG`, `DEFAULT_DEBUG_CONFIG`, `DEFAULT_HTML_VIEWER_CONFIG`, `DEFAULT_PDF_GENERATION_CONFIG`, `DEFAULT_COLOR_PALETTE`, `DEFAULT_MAIN_COLOR`, `DEFAULT_MAIN_COLOR_ID`, `DEFAULT_MAIN_COLOR_NAME`, `DEFAULT_MAIN_COLOR_HEX`, plus les valeurs par défaut des éléments d’en-tête et de pied de page (`DEFAULT_HEADER_FOOTER_SLOT`, `DEFAULT_HEADER_SLOT`, `DEFAULT_FOOTER_SLOT`, `DEFAULT_TEXT_ELEMENT`, `DEFAULT_RULE_ELEMENT`, `DEFAULT_BOX_ELEMENT`) et `defaultResourceTypes(locale)`, qui dépend de la langue (voir [Types de ressources](https://postext.dev/fr/docs/configuration-resources.md#types-de-ressources)).

### Élaguer les valeurs par défaut

Lorsque vous enregistrez une configuration (par exemple dans localStorage ou dans un fichier), utilisez `stripConfigDefaults` pour retirer les valeurs égales aux valeurs par défaut. Les configurations stockées restent ainsi minimales, seules les modifications voulues étant enregistrées :

```ts
import { stripConfigDefaults } from 'postext';

const minimal = stripConfigDefaults(fullConfig);
// Only properties that differ from defaults remain
```

Des fonctions d’élagage individuelles existent aussi, une par résolveur : `stripPageDefaults`, `stripLayoutDefaults`, `stripBodyTextDefaults`, `stripHeadingsDefaults`, `stripHeadingStylesDefaults`, `stripTocDefaults`, `stripPartsDefaults`, `stripUnorderedListsDefaults`, `stripOrderedListsDefaults`, `stripMathDefaults`, `stripTableStyleDefaults`, `stripCaptionStyleDefaults`, `stripDiagramStyleDefaults`, `stripParagraphStylesDefaults`, `stripCalloutStylesDefaults`, `stripHeaderFooterDefaults`, `stripDesignSlotDefaults`, `stripDebugDefaults`, `stripHtmlViewerDefaults`, `stripPdfGenerationDefaults`.

Certaines valeurs par défaut dépendent du reste de la configuration : l’équilibrage des colonnes est désactivé sur une grille de caractères et en texte vertical, `bodyText.avoidWidows` est désactivé sur une grille de caractères (et `bodyText.avoidOrphans` aussi dans la région `japan`), et les notes de bas de page, les légendes et l’index suivent la langue du document. `stripConfigDefaults` compare chaque valeur à la valeur par défaut de la configuration qu’on lui donne : `headings.balancing.enabled: true` est donc conservé là où l’équilibrage est désactivé par défaut, et `false` y est supprimé. `bodyText.avoidWidows: true` est conservé de la même façon sur une grille de caractères. Une fonction d’élagage appelée seule reçoit ce contexte en arguments : `stripHeadingsDefaults(headings, balancingOnByDefault(config))`, `stripIndexDefaults(index, locale)`, `stripCaptionStyleDefaults(captionStyle, locale)`, `stripFootnotesDefaults(footnotes, locale, writingMode)`, `stripBodyTextDefaults(bodyText, locale, avoidWidowsByDefault(config), avoidOrphansByDefault(config))`.

Une valeur qui signifie « rien » est conservée partout où « rien » n’est pas la valeur par défaut. Un style de titre reprend l’en-tête et le pied de page du document lorsqu’il n’en définit pas : un style qui les vide (`footer: { elements: [] }` sur une couverture) garde donc l’emplacement vide, et ses `margins`, `layout` et `bodyStyle` sont conservés même vides. Un niveau de titre ramené à sa valeur par défaut alors que les valeurs communes à tous les titres diffèrent garde sa valeur. `calloutStyles: []` et `chipStyles: []` restent des listes vides : omis, le style intégré reviendrait. Dans tous les cas, `resolveAllConfig(stripConfigDefaults(config))` donne la même résolution que `resolveAllConfig(config)`.

### Analyse

Le moteur expose son analyseur de markdown et son lecteur de frontmatter. Utilisez-les pour inspecter un document avant de le construire, ou pour alimenter d’autres outils avec la même structure de blocs que celle que voit Postext :

```ts
import { parseMarkdown, extractFrontmatter } from 'postext';

const source = '---\ntitle: Chapter One\n---\n\n# Opening\n\nThe story begins here.';

const { metadata, content } = extractFrontmatter(source);
// metadata.title === 'Chapter One'

const blocks = parseMarkdown(content);
// => [ { type: 'heading', level: 1, text: 'Opening', … },
//      { type: 'paragraph', text: 'The story begins here.', … } ]
```

Voir la page [Format du document](https://postext.dev/fr/docs/document-format.md) pour la liste complète des éléments de syntaxe markdown que Postext reconnaît.

### Charger les polices avant la mise en page

La mise en page mesure le texte avec les polices dont dispose le jeu de polices au moment où elle s’exécute. `prepareFonts` charge, avant la première construction, toutes les polices que demandent une configuration et son texte : le texte courant, les titres, les listes, les titres et le corps des encadrés, les tableaux, les textes de design, les titres courants, la table des matières, le code et le lettrage de bande dessinée, dans chaque graisse et chaque inclinaison que définit la configuration (les quatre de la famille du texte courant, puisque `**` et `*` y composent le gras et l’italique). Chaque police est chargée pour les caractères que le document compose, si bien qu’une famille servie en tranches `unicode-range` (latin étendu, grec, arabe, les tranches CJK de Google Fonts et de Fontsource) apporte les fichiers dont le texte a besoin.

```ts
import { prepareFonts, buildDocument, buildDocumentWithFonts } from 'postext';

// The host's files: the PDF font provider's contract, so one function serves both.
async function resolve(family: string, weight: number, style: 'normal' | 'italic') {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}-${style}.woff2`;
}

const report = await prepareFonts(content, config, { resolve });
// report.loaded, report.missing, report.synthesized: { family, weight, style }[]
const doc = buildDocument(content, config);

// Or in one call: prepare, build, load any face the pages used that was not there, build again.
const same = await buildDocumentWithFonts(content, config, { resolve });
```

- **Les polices que la page déclare** (une règle `@font-face`, une `FontFace` déjà ajoutée) sont chargées par le jeu de polices (`document.fonts.load`, ou `self.fonts` dans un worker). **Celles qu’elle ne déclare pas** sont demandées à `resolve(family, weight, style, { text, codePoints })`, qui répond par un fichier (des octets, ou une URL), par plusieurs fichiers (les tranches d’une police), par `{ source, unicodeRange, weight, style }` pour une tranche ou une plage variable, ou par `null`. Le moteur les ajoute comme `FontFace` une fois que toutes sont chargées, dans l’ordre de la configuration et de chaque réponse (latin, puis latin-ext, puis greek quand le résolveur répond ainsi), si bien que le jeu de polices est le même quel que soit l’ordre d’arrivée des fichiers. Il les enregistre aussi dans le registre de polices que lisent les [images SVG](https://postext.dev/fr/docs/configuration-resources.md#polices-du-texte-svg) et les workers de mise en page. Un résolveur peut déclarer la police lui-même (en ajoutant une feuille de style) et répondre `null`.
- **Le rapport** liste les polices couvertes par une police chargée (ou par une famille installée), celles qui restent `missing`, et celles que le navigateur devrait `synthesize` à partir d’une autre graisse ou inclinaison. `timeoutMs` (10 000 par défaut) borne l’attente ; une police encore en cours de chargement compte alors comme manquante. Là où il n’y a pas de jeu de polices (Node), `prepareFonts` ne fait rien et signale toutes les polices comme chargées.
- **`buildDocumentWithFonts(content, config, options)`** prépare, construit avec `buildDocumentAsync`, lit les polices dans lesquelles les pages composent réellement du texte, charge celles que le jeu de polices n’a pas pu fournir (une graisse que seules les pages révèlent est demandée à `resolve` même si une autre graisse de la famille pourrait la remplacer), puis reconstruit (deux constructions supplémentaires au plus). `withLoadedFonts(build, options)` fait de même autour de n’importe quelle fonction de construction, pour un livre construit chapitre par chapitre ou pour un paquet (`buildBundle`) qui renvoie plusieurs documents. `options.onFonts` reçoit le rapport final.
- **Après la construction**, chaque police dans laquelle le texte a été composé et que le jeu de polices n’a pas pu fournir figure dans `doc.contentWarnings` sous la forme `fontFallback` (voir [Avertissements dans le document](https://postext.dev/fr/docs/configuration-programmatic-usage.md#avertissements-dans-le-document)).

Les polices qui arrivent plus tard sont prises en compte d’elles-mêmes. Le moteur garde, pour chaque jeu de polices, la liste des polices de chaque famille qui étaient chargées lors de sa dernière vérification ; une construction vérifie de nouveau à son début (seulement si le jeu a grandi ou diminué, est en cours de chargement ou vient de finir de charger une police) et abandonne ce qui a été mesuré dans les familles dont les polices ont changé. `watchFonts(fontSet)` fait de même au fil du chargement des polices, au plus une fois par image d’animation quel que soit le nombre de tranches qu’apporte une rafale, et `onFontsChanged(listener)` indique à l’hôte quelles familles ont changé, pour qu’il puisse refaire la mise en page :

```ts
import { watchFonts, onFontsChanged } from 'postext';

const stop = watchFonts();                         // document.fonts by default
const off = onFontsChanged((families) => relayout());
```

`prepareFonts` lance la surveillance sur le jeu dans lequel il charge les polices (`watch: false` la laisse désactivée).

### Cache de mesures

La mesure du texte est l’étape coûteuse de la mise en page. Deux sortes de cache la gardent bon marché :

- **Un cache de blocs qui vous appartient.** `createMeasurementCache()` renvoie un `MeasurementCache` qui mémorise chaque paragraphe mesuré, indexé par son texte, ses polices, sa largeur, ses options de coupure des lignes et le dictionnaire de césure actif. Passez-le comme troisième argument de `buildDocument` (ou de `buildDocumentAsync`) pour réutiliser les mesures entre les passes de placement d’une construction et d’une construction à l’autre : un éditeur qui met le document en page à chaque frappe ne mesure alors que les paragraphes qui ont changé. Sans lui, chaque passe mesure de nouveau chaque bloc. Un paragraphe lu dans le cache est identique à un paragraphe mesuré à neuf : une construction avec cache compose donc chaque ligne comme une construction sans cache ; dans postext 1.4.1, un paragraphe en cache perdait la marque d’une dernière ligne trop courte, et le resserrement des fins de paragraphe trop courtes ainsi que l’équilibrage des colonnes pouvaient alors le couper autrement. Le cache porte la génération de mesure sous laquelle il a été rempli : lorsque des polices d’une famille arrivent ou disparaissent, sa consultation suivante abandonne les blocs composés dans cette famille, si bien qu’un cache conservé pendant des chargements de polices ne sert jamais de lignes mesurées avec une police de repli.
- **Les caches globaux de largeurs.** Les largeurs de mots sont mises en cache par chaîne de police dans un état de module que partagent toutes les constructions de la page, et pretext tient son propre cache. Le moteur abandonne les largeurs d’une famille lorsque ses polices changent (au début d’une construction, depuis `watchFonts`, depuis `prepareFonts` et `loadBundleFonts`) ; le cache de pretext n’a pas d’index par famille et il est vidé en entier.

```ts
import { buildDocument, createMeasurementCache, evictFontFamilies, clearMeasurementCache } from 'postext';

const cache = createMeasurementCache();
let doc = buildDocument(content, config, cache);

// A face of "EB Garamond" was added to document.fonts: the next build sees it,
// with the same cache, and measures that family again.
doc = buildDocument(content, config, cache);

// A host that changes faces the engine cannot see (a font set of its own) says so:
evictFontFamilies(['EB Garamond']);   // that family's widths and cached blocks
clearMeasurementCache();              // every family
```

Pour les applications qui mesurent le texte morceau par morceau, `cachedMeasureBlock(text, font, maxWidthPx, lineHeightPx, options, cache)` et `cachedMeasureRichBlock(spans, normalFont, boldFont, italicFont, boldItalicFont, maxWidthPx, lineHeightPx, options, cache)` prennent les arguments de `measureBlock` et de `measureRichBlock`, plus le cache, en dernier.

### Le module de coupure des lignes (WebAssembly)

Les paragraphes sont coupés en lignes par Knuth–Plass. Depuis postext 1.33, le moteur l’exécute dans un module de coupure des lignes écrit en Rust et compilé en WebAssembly (30 Ko, inclus dans le paquet npm). Il choisit les mêmes coupures que le module TypeScript, avec la même arithmétique dans le même ordre, et un livre entier se met en page environ 15 % plus vite. Rien n’est à configurer :

- Dans un worker, dans Node et dans Bun, le module est compilé au premier paragraphe.
- Sur le thread principal d’une page, où un navigateur ne compile un module de cette taille que de façon asynchrone, il se compile en arrière-plan et le module TypeScript répond en attendant. `await initLineBreaker()` avant la première mise en page le met au travail dès le premier paragraphe ; la promesse se résout à `false` là où WebAssembly n’est pas disponible.
- Là où WebAssembly manque, ou là où une Content Security Policy le refuse (`script-src` sans `'wasm-unsafe-eval'`), le module TypeScript répond pour chaque paragraphe.

Les deux choisissent les mêmes coupures : le module qui a répondu ne change donc jamais une mise en page. `useWasmLineBreaker(false)` garde le module TypeScript, pour comparer les deux.

```ts
import { initLineBreaker, buildDocument } from 'postext';

await initLineBreaker(); // optional: the first layout already uses it
const doc = buildDocument({ markdown }, config);
```

### État global partagé dans une page

Une partie de l’état de Postext vit dans des variables de niveau module. Tout ce qui importe `postext` dans le même domaine d’exécution JavaScript (realm) le partage : une page et ses scripts partagent une copie, tandis que chaque iframe et chaque worker a la sienne. Avec un seul document par page, on ne s’en aperçoit jamais. Avec plusieurs documents dans une page (deux aperçus en direct, une galerie d’exemples), on s’en aperçoit :

- **Images de ressources.** `registerResourceImage(fileId, image)` remplit un registre unique indexé par `fileId`, que lisent `renderPage` et `renderPageToCanvas`. Deux documents qui enregistrent tous deux `figure.svg` partagent cette entrée : le dernier enregistrement l’emporte, pour les deux. Donnez aux identifiants de fichier un préfixe propre à chaque document, et appelez `unregisterResourceImage(fileId)` ou `clearResourceImages()` quand un document disparaît. Les rastérisations que le moteur de rendu canvas met en cache sont indexées de la même façon et abandonnées avec l’image.
- **Mesures de texte.** Les largeurs mesurées sont mises en cache par chaîne de police et par texte pour tout le domaine d’exécution. Lorsque des polices d’une famille arrivent ou disparaissent, le moteur abandonne les largeurs de cette famille pour tous les documents (voir [Charger les polices avant la mise en page](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-les-polices-avant-la-mise-en-page)) ; `clearMeasurementCache()` les abandonne toutes.
- **Configurations résolues.** Chaque objet de configuration est résolu une fois et le résultat est mis en cache pour cet objet. Chaque construction compare d’abord l’objet au texte qu’il avait lors de sa résolution (un `JSON.stringify`, environ 0,1 ms pour une configuration de livre de 55 Ko et 1,5 ms pour une configuration de 240 Ko), si bien qu’une configuration modifiée sur place, à n’importe quelle profondeur (`config.bodyText.fontSize = …`, une couleur de palette), est résolue de nouveau. `invalidateConfig(config)` abandonne la résolution à la main. `stableStringify` et `hashString` donnent une clé de contenu indépendante de l’ordre des clés, pour les hôtes qui mettent en cache des mises en page par configuration.
- **Langue de césure.** Chaque construction règle la langue de césure globale du processus sur le `bodyText.hyphenation.locale` de son document. Les fonctions exportées `hyphenateText(text)` et `layoutDesignSlot` utilisent la langue de la dernière construction, sauf si vous en passez une : appelez `hyphenateText(text, 'es')`.
- **Moteur mathématique.** Il y a un seul moteur MathJax et un seul cache de formules rendues pour le domaine d’exécution ; `initMathEngine()` le démarre pour tout le monde.
- **Module de coupure des lignes.** Il y a un seul module WebAssembly de coupure des lignes pour le domaine d’exécution, compilé une fois ; `useWasmLineBreaker(false)` le désactive pour tout le monde.

L’isolation la plus simple consiste à donner un domaine d’exécution à chaque document : une iframe pour chaque exemple en direct (un embed CodePen en est une), ou un [worker de mise en page](https://postext.dev/fr/docs/configuration-programmatic-usage.md#mise-en-page-dans-un-web-worker) par document pour les mesures et la césure (les images restent enregistrées sur la page).

## Mise en page dans un Web Worker

**C’est la façon recommandée d’utiliser Postext dans le navigateur.** Si vous construisez quoi que ce soit d’interactif (un aperçu en direct, un éditeur, une visionneuse qui suit le redimensionnement, un bac à sable dans le style du Sandbox), pilotez la chaîne par `createLayoutWorker()` de `postext/worker`. N’appelez pas `buildDocument` directement sur le thread principal pour du code d’interface.

Appeler `buildDocument` sur le thread principal exécute toute la chaîne (analyse, mesure, placement et chaque nouvelle exécution de la passe de placement dont la mise en page a besoin) sur le thread qui l’a appelée. Pour un export ponctuel, cela convient. Pour une interface interactive, c’est le mauvais thread : une mise en page de 150 ms bloque les événements d’entrée, les frappes s’accumulent et le défilement saccade. Le worker déplace chacune de ces millisecondes vers un thread d’arrière-plan.

Postext fournit un point d’entrée Web Worker dédié, `postext/worker`, qui sort la chaîne du thread principal. C’est la voie que nous attendons de la majorité des intégrations : les vues Canvas, HTML et PDF du Sandbox partagent toutes le même handle `createLayoutWorker()` par un unique hook `useLayoutWorker` (`packages/postext-sandbox/src/worker/useLayoutWorker.ts`) et le pilotent avec une annulation où la dernière demande l’emporte : une nouvelle frappe interrompt la construction en cours avant même qu’elle se termine.

D’un coup d’œil, l’intégration de référence se présente ainsi :

1. **Créez** un worker une fois par vue avec `createLayoutWorker()`.
2. **Enregistrez les polices** une fois par famille en postant des `ArrayBuffer` transférables par `registerFonts(payloads)`.
3. **Construisez** avec `build(content, config, { signal })`, en passant un nouvel `AbortSignal` à chaque appel pour pouvoir annuler les constructions périmées.
4. **Remplacez** toute construction précédente en interrompant son signal *avant* de lancer la suivante : c’est le modèle « la dernière l’emporte ».
5. **Libérez** le worker lorsque le composant qui le possède est démonté.

Le même `VDTDocument` que renvoie `build(...)` alimente tous les moteurs de rendu en aval : `renderPage`/`renderPageToCanvas` pour le canvas, `renderToHtmlIndexed` pour le HTML et `renderToPdf` (de `postext-pdf`) pour le PDF. Vous construisez une fois dans le worker et vous rastérisez sur le thread principal autant de fois que l’interface en a besoin.

### Ce que le worker vous apporte

- **Le thread principal reste libre.** L’analyse, la mesure et chaque passe de placement s’exécutent toutes dans le worker. Le thread principal n’est sollicité que lorsque le `VDTDocument` terminé lui est renvoyé.
- **Annulation où la dernière demande l’emporte.** `build(content, config, { signal })` transmet un `AbortSignal` au worker. Une interruption avant la fin lève une `AbortError` côté principal ; dans le worker, la chaîne lève une `BuildCancelledError` au point de contrôle d’annulation suivant (il y en a un par bloc) et s’arrête immédiatement.
- **Cache de mesures par worker.** Le worker garde un seul `MeasurementCache` pendant toute sa durée de vie. Les constructions suivantes qui partagent police, texte et largeur réutilisent les mesures de lignes en cache : taper un seul caractère dans un long document ne remesure que les blocs dont l’entrée a réellement changé.
- **Des métriques identiques à celles du thread principal.** Les polices sont envoyées dans le worker sous forme d’`ArrayBuffer` transférables et enregistrées par `new FontFace(...)` dans le propre `FontFaceSet` du worker. Le worker mesure avec les mêmes métriques de police canvas que celles qu’utiliserait le thread principal : les coupures de ligne et les hauteurs de colonne sont donc identiques à l’octet près.
- **Le cache de rastérisation des formules survit aux constructions du worker.** Le moteur de rendu mathématique fournit un cache de rastérisation indexé par contenu à côté de celui indexé par identité : sans lui, le clonage structuré d’un `MathRender` à travers la frontière du worker manquerait le cache par identité à chaque reconstruction.
- **Les mêmes lignes que sur la page.** Un canvas créé dans un worker n’a pas de page dont tirer une langue, et une police répond avec d’autres glyphes selon la langue. Le moteur fixe la langue du document (`locale`) sur chaque contexte avec lequel il mesure, dans le worker comme sur la page, si bien que `build()` renvoie les lignes que `buildDocument` donne sur une page, dans n’importe quelle langue ([`bodyText.shapingLanguage: 'document'`](https://postext.dev/fr/docs/configuration-text.md#texte-courant), la valeur par défaut depuis postext 1.30). Avec `'host'`, que garde une configuration enregistrée avant 1.30, un worker mesure sans langue et une page avec la sienne, comme jusqu’à postext 1.28.

### API publique

Le client du worker se trouve dans le sous-chemin `postext/worker` et tient en quelques noms :

- **`createLayoutWorker(opts?): LayoutWorkerHandle`** : lance un worker dédié (ou enveloppe celui que vous passez par `opts.worker`, ou démarre le point d’entrée du worker situé à `opts.url`) et renvoie un handle typé. Voir [Charger le worker depuis un CDN](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-le-worker-depuis-un-cdn).
- **`LayoutWorkerHandle.registerFonts(faces: FontPayload[]): Promise<void>`** : envoie les octets des polices dans le worker. Les buffers sont transférés : gardez une copie neuve sur le thread principal si vous devez les renvoyer plus tard.
- **`LayoutWorkerHandle.build(content, config?, { signal? }): Promise<VDTDocument>`** : exécute la chaîne. Interrompre le signal annule la construction en cours.
- **`LayoutWorkerHandle.dispose(): void`** : termine le worker et rejette toute construction en attente avec `AbortError`.
- **`FontPayload`** : `{ family, weight, style, unicodeRange?, buffer: ArrayBuffer }`. `weight` est une chaîne de graisse CSS (`'700'`, `'bold'`) ; `registerFonts` l’accepte aussi sous forme de nombre (`700`). Le `buffer` est transféré au worker lorsque vous appelez `registerFonts`.
- **`BuildCancelledError`** (réexportée depuis `postext`) : ce que `buildDocument` lève en interne lorsque `options.shouldCancel` renvoie `true`. Vous ne la voyez généralement pas sur le thread principal : le protocole du worker la convertit en `AbortError` avant qu’elle n’atteigne votre code.

Le paquet npm publie aussi un chemin `postext/worker/entry` qui pointe vers le script compilé du worker. `createLayoutWorker()` résout cette URL automatiquement ; vous n’avez besoin de la référencer explicitement que si votre bundler exige un appel `new Worker(new URL(...), { type: 'module' })` construit à la main, ou si vous servez le point d’entrée vous-même (`opts.url`).

### Intégration minimale

```ts
import { createLayoutWorker } from 'postext/worker';
import type { FontPayload, LayoutWorkerHandle } from 'postext/worker';
import type { PostextConfig, VDTDocument } from 'postext';

// 1. Create the worker once and keep the handle for the lifetime of your viewport.
const layout: LayoutWorkerHandle = createLayoutWorker();

// 2. Register fonts once per family (transferable ArrayBuffers).
//    getConfigFontFamilies(config) is a helper that lists the families your config will render.
const payloads: FontPayload[] = await collectFontPayloadsForFamilies([
  'EB Garamond',
  'Open Sans',
]);
await layout.registerFonts(payloads);

// 3. Drive builds with last-wins cancellation: abort the previous signal
//    before starting a new build. A stale build is thrown away inside the worker.
let pending: AbortController | null = null;

async function rebuild(
  markdown: string,
  config: PostextConfig,
): Promise<VDTDocument | null> {
  pending?.abort();
  pending = new AbortController();
  try {
    return await layout.build({ markdown }, config, { signal: pending.signal });
  } catch (err) {
    if ((err as { name?: string } | null)?.name === 'AbortError') return null;
    throw err;
  }
}

// 4. Dispose when the component that owns the worker unmounts.
//    Pending builds reject with AbortError.
layout.dispose();
```

Enveloppé dans un composant React, cela donne :

```tsx
import { useEffect, useRef } from 'react';
import { createLayoutWorker } from 'postext/worker';
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderPageToCanvas } from 'postext';
import type { PostextConfig } from 'postext';

export function CanvasPreview({
  markdown,
  config,
}: {
  markdown: string;
  config: PostextConfig;
}) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const workerRef = useRef<LayoutWorkerHandle | null>(null);
  const pendingRef = useRef<AbortController | null>(null);

  // Mount: spin up the worker and ship the fonts once.
  useEffect(() => {
    const handle = createLayoutWorker();
    workerRef.current = handle;
    (async () => {
      const payloads = await collectFontPayloadsForFamilies(
        getConfigFontFamilies(config),
      );
      await handle.registerFonts(payloads);
    })();
    return () => {
      pendingRef.current?.abort();
      handle.dispose();
    };
  }, []); // fonts registered once; re-register only when the family set changes

  // Every keystroke or config change: supersede the in-flight build and kick a new one.
  useEffect(() => {
    const handle = workerRef.current;
    if (!handle) return;
    pendingRef.current?.abort();
    const ac = new AbortController();
    pendingRef.current = ac;
    (async () => {
      try {
        const vdt = await handle.build({ markdown }, config, { signal: ac.signal });
        const canvas = canvasRef.current;
        if (!canvas || !vdt.pages[0]) return;
        renderPageToCanvas(vdt.pages[0], vdt, canvas); // rasterise on the main thread
      } catch (err) {
        if ((err as { name?: string } | null)?.name !== 'AbortError') throw err;
      }
    })();
  }, [markdown, config]);

  return <canvas ref={canvasRef} />;
}
```

Le schéma est toujours le même : **créer une fois, enregistrer les polices une fois, construire avec un AbortSignal autant de fois que nécessaire, libérer au démontage.**

### Charger le worker depuis un CDN

Un script de worker doit provenir de l’origine de la page : une copie de `postext/worker` servie par un CDN ne peut donc pas démarrer le fichier `layout.worker.js` placé à côté d’elle. `createLayoutWorker()` s’en charge :

- **esm.sh, sans option.** Lorsque `postext/worker` a lui-même été chargé depuis esm.sh (l’URL de son module ressemble à `https://esm.sh/postext@1.5.0/es2022/worker.mjs`), le client démarre le `https://esm.sh/postext@1.5.0/worker/entry` correspondant par un module blob d’une ligne, de même origine, qui l’importe. Il en va de même pour les imports qui ajoutent `?deps=`, `?external=` ou `?alias=`, et pour la forme `https://esm.sh/*postext@1.5.0/worker`. Le worker reçoit toujours le build simple de cette version, car un worker n’a pas d’import map pour résoudre des dépendances externes.
- **Tout autre serveur, avec `url`.** Les autres CDN, comme jsDelivr (`/+esm`) ou unpkg, ne sont pas détectés. `createLayoutWorker({ url })` démarre le module d’entrée du worker situé à `url` : directement pour une URL de même origine, par la même enveloppe blob pour une URL d’une autre origine. Ce serveur doit autoriser les requêtes cross-origin (CORS).
- **Les bundlers ne changent rien.** Avec Vite, webpack ou Next.js, continuez d’appeler `createLayoutWorker()` sans option : ils émettent le worker comme un chunk de votre application.

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

const layout = createLayoutWorker();
const face = async (weight, style) => ({
  family: 'EB Garamond',
  weight,
  style,
  buffer: await (await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/eb-garamond@5/files/eb-garamond-latin-${weight}-${style}.woff2`)).arrayBuffer(),
});
await layout.registerFonts(await Promise.all([face(400, 'normal'), face(700, 'normal'), face(400, 'italic')]));
const doc = await layout.build({ markdown }, { bodyText: { fontFamily: 'EB Garamond' } });
```

**Un worker ne voit pas les polices de la page.** Il a son propre jeu de polices, qui ne contient que les polices envoyées par `registerFonts` et les polices installées sur le système. Lorsqu’une construction compose du texte dans une famille que le worker ne trouve pas, ce texte est mesuré avec une police de repli, et ses coupures de ligne ne correspondront pas à celles de la page. Le client affiche alors un avertissement dans la console par famille (`"EB Garamond" is not available inside the layout worker…`) et liste les familles dans `BuildStats.missingFonts`, que reçoit le callback `onStats` de `build`. Le document lui-même porte les mêmes polices sous forme d’avertissements de contenu `fontFallback`.

`handle.prepareFonts(content, config, options)` exécute [`prepareFonts`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-les-polices-avant-la-mise-en-page) sur la page, puis envoie au worker les fichiers de chaque police trouvée que contient le registre de polices (les fichiers du résolveur, les polices d’un paquet, les règles `@font-face` lisibles de la page), en se limitant aux tranches qui contiennent les caractères du document. Lorsque des polices arrivent dans le worker, il abandonne uniquement les mesures de ces familles, ainsi que son cache de documents terminés.

Des polices peuvent aussi atteindre le worker après une construction : une graisse dont les pages se révèlent avoir besoin, une autre tranche `unicode-range` d’une police, un fichier téléversé de nouveau sous le même nom de famille. La construction suivante mesure avec elles. Le canvas d’un worker ne le ferait pas de lui-même : dans Chrome, il résout une police la première fois qu’une notation abrégée de police est définie et ne consulte plus le jeu de polices pour cette notation, si bien que le texte garderait les largeurs de la police qui a répondu en premier. Le worker nomme donc une famille une fois de plus dans les notations abrégées qu’il définit, dès qu’une police de cette famille est arrivée en retard (`Geist, Geist`) : une autre liste de familles pour le canvas, la même police et les mêmes largeurs. Les documents qu’il renvoie gardent les chaînes de police telles que le moteur les écrit, et un worker qui a reçu ses polices avant sa première construction définit chaque notation abrégée telle qu’elle est écrite.

### Collecte des polices (Fontsource / Google Fonts)

`registerFonts` prend des octets de police bruts. Le thread principal est le bon endroit pour les récupérer, car un cache central permet à plusieurs instances de worker de partager les mêmes octets.

Le `collectFontPayloadsForFamilies` du Sandbox (`packages/postext-sandbox/src/controls/fontLoader.ts`) sert d’implémentation de référence. Il :

1. Interroge `https://api.fontsource.org/v1/fonts/{family-id}` pour connaître les graisses et les styles de la famille.
2. Lit les sous-ensembles de la famille et leur `unicode-range` dans son paquet sur jsDelivr (`@fontsource/{family-id}@5/400.css`), une fois par famille. Les fichiers sont ensuite demandés à la version exacte depuis laquelle cette feuille de style a été servie (le `x-jsd-version` de jsDelivr), si bien qu’une famille n’arrive jamais de deux versions de son paquet.
3. Télécharge chaque fichier du romain, du gras et de leurs italiques pour chaque famille (`files/{family-id}-{subset}-{weight}-{style}.woff2`) ; `collectFontPayloadsForConfig(config, markdown)` prend plutôt les graisses et les styles que demande une configuration. Ce sont des fichiers statiques, ceux que le PDF incorpore, si bien que le worker mesure avec ce que dessine le PDF. Un fichier variable donne d’autres chasses entre deux de ses masters, et une ligne à une fraction de pixel de sa justification se coupe ailleurs.
4. Renvoie un `FontPayload[]` dont le `buffer` est un `ArrayBuffer` neuf à chaque appel. C’est important, car `registerFonts` transfère le buffer et laisse la copie de l’expéditeur détachée.

Les fichiers d’une police s’ajoutent dans l’ordre où un pen les déclare, `latin` en premier et les fichiers propres à l’écriture en dernier (`arabic`, les tranches numérotées d’une famille chinoise ou japonaise), car le fichier ajouté en dernier répond pour un caractère que deux fichiers contiennent. Une famille pour laquelle Fontsource n’a pas de fichiers est lue dans la feuille de style CSS2 de Google, qui ne renvoie du WOFF2 qu’aux chaînes User-Agent de type navigateur.

Associez-le à `getConfigFontFamilies(config)` pour obtenir la liste des familles qu’une `PostextConfig` donnée rendra réellement (texte courant, titres, puces de liste, numéros de liste ordonnée).

### Annulation coopérative dans le moteur

Si vous pilotez `buildDocument` vous-même (par exemple dans un worker personnalisé), la chaîne expose un hook `shouldCancel` que vous pouvez utiliser directement :

```ts
import { buildDocument, BuildCancelledError } from 'postext';

let superseded = false;
try {
  const vdt = buildDocument(content, config, cache, {
    shouldCancel: () => superseded,
  });
} catch (err) {
  if (err instanceof BuildCancelledError) return; // a newer build took over
  throw err;
}
```

`shouldCancel` est appelé une fois par bloc de premier niveau pendant le placement. Le hook est volontairement coopératif : il ne peut pas arrêter l’appel de mise en page de pretext au milieu d’une ligne, mais il garde une granularité d’annulation assez fine (quelques millisecondes) pour qu’un utilisateur qui tape vite n’attende jamais une construction périmée.

### Piloter l’export PDF depuis le worker

Le moteur de rendu PDF prend un `VDTDocument` prêt et le transforme en octets PDF. Il ne relance **pas** la mise en page. Le flux PDF de référence dans le navigateur s’associe donc proprement au worker : construisez le VDT dans le worker (hors du thread principal, annulable, avec réutilisation du cache), puis appelez `renderToPdf` sur le thread principal avec ce même VDT.

```ts
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderToPdf } from 'postext-pdf';
import type { PostextConfig } from 'postext';
import { createPdfFontProvider } from './pdfFontProvider';

const fontProvider = createPdfFontProvider();

export async function exportPdf(
  layout: LayoutWorkerHandle,
  markdown: string,
  config: PostextConfig,
): Promise<Uint8Array> {
  // 1. Build the VDT in the worker — UI stays responsive during the layout passes.
  const vdt = await layout.build({ markdown }, config);

  // 2. Rasterise to PDF on the main thread. renderToPdf is fast once the VDT exists
  //    because it is walking precomputed coordinates, not remeasuring text.
  return renderToPdf(vdt, {
    fontProvider,
    // pdfGeneration config on `vdt.config` is honoured automatically.
  });
}
```

Si vous gérez déjà un handle de worker pour l’aperçu en direct, réutilisez-le pour l’export au lieu de lancer un second worker : grâce au cache de mesures du worker, un export PDF qui suit un aperçu à l’écran ne coûte presque rien.

Pour un long livre, l’écriture du PDF lui-même prend aussi quelques secondes ; `postext-pdf/worker` exécute cette étape dans un worker à part (voir [Rendre le PDF dans un worker](https://postext.dev/fr/docs/configuration-programmatic-usage.md#rendre-le-pdf-dans-un-worker)).

### Quand utiliser le worker, et quand s’en passer

Utilisez le worker pour :

- **Les aperçus en direct, les éditeurs et les bacs à sable.** Tout ce qui reconstruit le document en réponse à une saisie de l’utilisateur.
- **Les visionneuses HTML qui suivent le redimensionnement** et relancent la mise en page à chaque tick de `ResizeObserver`.
- **L’export PDF dans le navigateur** déclenché depuis une interface qui a déjà un aperçu en direct : réutilisez le handle de worker existant pour que l’export profite du cache de mesures.
- **Plusieurs onglets de sortie** qui ont tous besoin du même VDT (les vues Canvas / HTML / PDF du Sandbox partagent un handle de worker par montage de vue).

Passez-vous du worker pour :

- **La génération côté serveur** : Node n’a pas de `FontFaceSet` de navigateur, et vous contrôlez de toute façon le thread.
- **Les exports ponctuels isolés** (une CLI, un script d’export sans interface, une Cloud Function) où aucune interface interactive ne risque d’être bloquée. Appeler `buildDocument` directement est plus simple et évite le coût du transfert initial des polices.

## Intégrer la visionneuse HTML

La visionneuse HTML est le moteur de rendu de Postext conçu d’abord pour l’écran. Au lieu de rastériser les pages en image bitmap, elle produit des nœuds DOM positionnés en absolu, dont la géométrie vient du même pipeline que la sortie imprimée. C’est donc le bon choix quand vous voulez, dans un navigateur, une typographie lisible, sélectionnable et qui suit le redimensionnement (une application de lecture, un aperçu intégré à un produit, une documentation embarquée) sans charger de visionneuse PDF.

Les pièces essentielles de l’API publique :

- **`buildDocument(content, config, cache?)`** : exécute tout le pipeline de mise en page et renvoie un `VDTDocument`.
- **`renderToHtmlIndexed(doc, options)`** : transforme le VDT en une seule chaîne HTML accompagnée d’un découpage par page et par bloc. Ce découpage permet de patcher le DOM à peu de frais quand seuls quelques blocs ont changé d’un rendu à l’autre.
- **`resolveHtmlViewerConfig(partial)`** : complète la configuration avec les valeurs par défaut de la visionneuse HTML (`maxCharsPerLine`, `columnGap`, `optimalLineBreaking`).
- **`buildFontString` + `measureGlyphWidth` + `dimensionToPx`** : primitives de mesure qui servent à déduire une largeur de colonne réelle, en pixels, d’un nombre de caractères visé.
- **`createMeasurementCache` / `clearMeasurementCache`** : caches interchangeables qui permettent de réutiliser les mesures d’une mise en page à l’autre.
- **`prepareFonts` / `buildDocumentWithFonts` / `watchFonts` / `onFontsChanged`** : chargent les fontes du document avant la mise en page et relancent la mise en page quand des fontes arrivent (voir [Charger les polices avant la mise en page](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-les-polices-avant-la-mise-en-page)).

### Exemple en direct : une chaîne HTML

L’aller-retour complet en JavaScript simple, avant l’intégration React plus bas : construisez le document, passez le `VDTDocument` à `renderToHtml` et déposez la chaîne dans un conteneur. `mode: 'single'` empile les pages verticalement ; `background` leur donne une couleur, car les pages sont transparentes par défaut. Le pen affiche aussi le balisage généré, ce qui vous laisse voir les lignes positionnées en absolu que produit le moteur de rendu : le navigateur les peint mais ne les recompose jamais.

> **Exemple exécutable: Postext · rendre un document en HTML** — Mettre en page un document Markdown avec postext et le rendre en chaîne HTML. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/render-html))

### Intégration minimale

L’extrait ci-dessous est l’intégration utile la plus courte : construire le document à la taille actuelle de la fenêtre, le rendre dans un conteneur et recommencer à chaque redimensionnement.

```tsx
import { useEffect, useRef } from 'react';
import {
  buildDocument,
  renderToHtmlIndexed,
  resolveHtmlViewerConfig,
  buildFontString,
  measureGlyphWidth,
  dimensionToPx,
  createMeasurementCache,
  watchFonts,
  onFontsChanged,
} from 'postext';
import type { PostextConfig, MeasurementCache } from 'postext';

// Screen-friendly DPI: at 144 DPI, an 8pt body size resolves to 16 px.
const HTML_DPI = 144;
const PADDING_PX = 24;

// Prose sample used to measure the target column width. Proportional fonts
// make "N × average width" unreliable, so we measure a representative string.
const SAMPLE =
  'The quick brown fox jumps over the lazy dog. Sphinx of black quartz, judge my vow.';

function sampleForChars(n: number): string {
  let s = SAMPLE;
  while (s.length < n) s += ' ' + SAMPLE;
  return s.slice(0, n);
}

export function PostextHtmlViewer({
  markdown,
  config,
  mode = 'multi',
}: {
  markdown: string;
  config: PostextConfig;
  mode?: 'single' | 'multi';
}) {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const cacheRef = useRef<MeasurementCache>(createMeasurementCache());

  useEffect(() => {
    const host = hostRef.current;
    if (!host) return;

    const relayout = () => {
      const rect = host.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) return;

      const viewer = resolveHtmlViewerConfig(config.htmlViewer);
      const fontFamily = config.bodyText?.fontFamily ?? 'EB Garamond';
      const fontWeight = config.bodyText?.fontWeight ?? 400;
      const fontSize = config.bodyText?.fontSize ?? { value: 8, unit: 'pt' as const };
      const fontSizePx = dimensionToPx(fontSize, HTML_DPI);

      // Measure the *actual* column width for N characters of body prose.
      const targetColumnPx = measureGlyphWidth(
        sampleForChars(viewer.maxCharsPerLine),
        buildFontString(fontFamily, fontSizePx, String(fontWeight), 'normal'),
      );

      const inner = Math.max(rect.width - PADDING_PX * 2, 100);
      let columnWidthPx: number;
      if (mode === 'single') {
        columnWidthPx = Math.min(targetColumnPx, inner);
      } else {
        // Fit as many columns as we can at the target width.
        const count = Math.max(
          1,
          Math.floor((inner + viewer.columnGap) / (targetColumnPx + viewer.columnGap)),
        );
        columnWidthPx = (inner - viewer.columnGap * (count - 1)) / count;
      }
      columnWidthPx = Math.max(Math.floor(columnWidthPx), 80);

      // Single mode uses one very tall page; multi mode uses the viewport
      // height so each VDT "page" becomes one column.
      const pageHeightPx =
        mode === 'single' ? Math.max(rect.height * 20, 200_000) : Math.max(rect.height - PADDING_PX * 2, 400);

      const override: PostextConfig = {
        ...config,
        page: {
          ...config.page,
          dpi: HTML_DPI,
          width: { value: columnWidthPx, unit: 'px' },
          height: { value: pageHeightPx, unit: 'px' },
          margins: {
            top: { value: 0, unit: 'px' },
            bottom: { value: 0, unit: 'px' },
            left: { value: 0, unit: 'px' },
            right: { value: 0, unit: 'px' },
          },
        },
        layout: { ...config.layout, layoutType: 'single' },
        bodyText: {
          ...config.bodyText,
          optimalLineBreaking: viewer.optimalLineBreaking,
        },
      };

      const doc = buildDocument({ markdown }, override, cacheRef.current);
      const { html } = renderToHtmlIndexed(doc, {
        mode,
        columnGap: viewer.columnGap,
        padding: PADDING_PX,
        background: 'transparent',
      });

      host.innerHTML = html;
    };

    relayout();

    const ro = new ResizeObserver(() => relayout());
    ro.observe(host);

    // Re-measure when web fonts land so glyph widths aren't taken from fallbacks.
    const stopWatching = watchFonts();
    const off = onFontsChanged(() => relayout());

    return () => {
      ro.disconnect();
      off();
      stopWatching();
    };
  }, [markdown, config, mode]);

  return <div ref={hostRef} style={{ width: '100%', height: '100%', overflow: 'auto' }} />;
}
```

Quelques remarques sur ce que fait cet exemple :

- **Mesurer la colonne plutôt que l’estimer.** Comme `maxCharsPerLine` est une *cible* exprimée en caractères, la largeur réelle en pixels dépend de la police du texte courant. `measureGlyphWidth` donne une vraie mesure dans la police choisie, ce qui garde la longueur de ligne constante quand on change de police.
- **Réécrire la page.** La visionneuse HTML traite chaque « page » du VDT comme une colonne à l’écran. L’exemple remplace `page.width` par la largeur de colonne mesurée, met les marges à zéro (la marge intérieure se trouve hors de la page, dans le div `.pt-doc` qui l’enveloppe) et utilise `HTML_DPI = 144` pour qu’un texte courant de `8pt` donne `16px`.
- **Tenir compte du chargement des polices.** `watchFonts` écoute `document.fonts` et, une fois par image, oublie ce qui a été mesuré dans les familles dont des fontes sont arrivées ; `onFontsChanged` relance ensuite la mise en page de la colonne. Sans cette relance, le premier rendu utilise les métriques d’une police de secours et saute quand la vraie police arrive.
- **Réutiliser le cache de mesures.** Créer le cache une seule fois par composant permet aux redimensionnements et aux changements d’échelle de police de reprendre les mesures du rendu précédent au lieu de remesurer chaque paragraphe.

### Aller plus loin

L’exemple ci-dessus est volontairement dépouillé. Les intégrations en production ajoutent en général :

- **L’isolation par Shadow DOM** : rendre dans `host.attachShadow({ mode: 'open' })` pour qu’aucune règle CSS de la page extérieure ne déborde sur la visionneuse.
- **Le patch incrémental** : `renderToHtmlIndexed` renvoie `pages[i].blocks`, chacun avec un `id` stable et le HTML extérieur du bloc. Quand seuls quelques blocs diffèrent entre deux rendus, vous pouvez remplacer ces enveloppes de blocs sur place au lieu de reconstruire `innerHTML`.
- **Les calques** : superposer un SVG positionné en absolu sur chaque `.pt-page` pour les curseurs, les sélections ou les grilles de ligne de base.
- **Les liens** : les mots d’un lien Markdown sont enveloppés dans `<a href="…" rel="noopener noreferrer">`, qui prend la couleur du texte et n’est pas souligné ; voir [Format du document › Liens](https://postext.dev/fr/docs/document-format.md#liens). Dans une visionneuse de type éditeur, interceptez les clics sur les `a[href]` qui ne commencent pas par `#` et ouvrez-les dans un nouvel onglet (les ancres de `:ref` renvoient à l’intérieur du document).
- **Les images à encre unique** : avec `diagramStyle.singleInk` activé, les `<img>` SVG reçoivent un filtre CSS, sauf si vous passez `singleInk: false` pour des URL déjà recolorées ; voir [Encre unique sur le canvas et en HTML](https://postext.dev/fr/docs/configuration-resources.md#encre-unique-sur-le-canvas-et-en-html).

Le composant `HtmlPreview` du Sandbox (`packages/postext-sandbox/src/viewport/HtmlPreview/index.tsx`) met tout cela en œuvre sur la même API que ci-dessus et peut servir de référence. Il fait aussi passer chaque construction par un 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 que les modifications en direct et les redimensionnements ne bloquent jamais le thread principal : remplacez l’appel direct `buildDocument(...)` de l’extrait ci-dessus par `layoutWorker.build(...)` quand vous êtes prêt à sortir la mise en page du thread principal.

### En quoi la sortie HTML diffère du canvas et du PDF

`renderToHtml` place chaque ligne, chaque figure et chaque élément de maquette exactement là où les placent le canvas et le PDF, mais peint moins de choses autour :

| Fonction | Canvas (`renderPage`) | HTML (`renderToHtml`) | PDF (`renderToPdf`) |
| --- | --- | --- | --- |
| Fond de page | Blanc, avec `page.backgroundColor` sur le format rogné et le fond perdu. | **Transparent**, sauf si vous passez `background` ou définissez `page.backgroundColor` (qui remplit alors toute la boîte de la page, zone de repères comprise). | Blanc, avec `page.backgroundColor` sur le format rogné et le fond perdu. |
| Grille de ligne de base (`page.baselineGrid`) | Dessinée | Non dessinée | Dessinée |
| Filet entre colonnes (`layout.columnRule`) | Dessiné | Non dessiné | Dessiné |
| Traits de coupe (`page.cutLines`) | Dessinés | Non dessinés ; la boîte de la page inclut toujours la zone de repères autour du format rogné. | Dessinés |
| Page en négatif | Option `pageNegative` | Non disponible | Option `pageNegative` |
| Texte | Pixels | Texte sélectionnable dans des éléments positionnés en absolu, composé dans les familles de polices CSS : la page doit charger les mêmes fontes. | Polices incorporées fournies par votre `fontProvider` ; texte sélectionnable, consultable par recherche et balisé. |
| Texte vertical (`layout.writingMode: 'vertical-rl'`) | Caractères peints une cellule à la fois, remis debout ; formes verticales obtenues par une fonte jumelle (`loadVerticalAlternates`). | Le flux tient dans une boîte tournée d’un quart de tour ; chaque ligne est remise debout et composée avec `writing-mode: vertical-rl`, si bien que le navigateur prend les formes verticales et redresse les caractères ; les nombres courts sont en `text-combine-upright: all` ; un tiret, des points de suspension, un point médian ou un tiret ondulé occupent une boîte de leur cellule (le navigateur les ferait avancer de leur largeur horizontale), un tiret étant étiré pour la remplir selon `flow.dashAdvances`. | Caractères debout grâce à un jumeau `Identity-V` de chaque police ; voir [Texte vertical dans le PDF](https://postext.dev/fr/docs/configuration-programmatic-usage.md#texte-vertical-dans-le-pdf). |
| Images | `registerResourceImage` | L’option `resourceImageUrl(fileId)` ; sans elle, une boîte grise en guise d’image. | L’option `resourceBytes(fileId)`. |
| Formules | Tracés vectoriels | `<svg>` en ligne | Tracés vectoriels |
| Liens | Aucun | Les renvois `:ref` pointent vers leur ressource ; les lignes de la table des matières, non. | Les renvois `:ref` et les lignes de la table des matières, plus les signets (outline). |

La page transparente compte sur un site sombre : un aperçu sans `background` affiche du texte noir sur le fond sombre du site. Passez `renderToHtml(doc, { background: '#ffffff' })`, ou donnez au document une `page.backgroundColor`.

**Les styles de texte de la page hôte restent dehors.** Chaque ligne est composée aux largeurs que le moteur a mesurées : un `letter-spacing`, un `word-spacing`, un `text-transform` ou un `font-variant` que la sortie hériterait de la page environnante élargirait les suites de glyphes et ferait se chevaucher les lignes. La racine `.pt-doc` réinitialise donc les propriétés de texte héritées (espacement des lettres et des mots, casse, retrait, espaces blancs, style, variante, graisse et chasse de la police, fonctionnalités OpenType et crénage, hauteur de ligne, alignement, ombre et emphase du texte, césure, direction, mode d’écriture, contour et remplissage du texte, agrandissement automatique du texte sur mobile) avant ses propres déclarations de mise en page, si bien que la sortie a le même aspect dans une racine fantôme (shadow root) ou sous un élément stylé. Cette liste est exportée sous le nom `HTML_TEXT_RESET`, une chaîne de déclarations CSS : un hôte qui monte le `innerHtml` des pages (issu de `renderToHtmlIndexed`) dans ses propres conteneurs l’applique à leur racine. Jusqu’à postext 1.4, la racine ne réinitialisait rien ; le contournement consistait à l’envelopper dans un élément avec `all: initial`.

## 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-programmatic-usage.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-programmatic-usage.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-programmatic-usage.md#octets-des-ressources-et-originaux-dimpression) ; `onWarning` dans [Quelles fontes sont demandées au fournisseur](https://postext.dev/fr/docs/configuration-programmatic-usage.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-programmatic-usage.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-programmatic-usage.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-programmatic-usage.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-text.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.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).

## Un livre en 3D (`postext-folio`)

`postext-folio` présente à l’écran un document mis en page comme un livre imprimé ouvert sur un bureau : des doubles pages selon la règle du recto, et des feuillets que le lecteur tourne avec les boutons ‹ ›, les flèches du clavier, un balayage, un clic sur une page, ou en saisissant une page par son bord pour la faire passer de l’autre côté. Chaque feuillet se courbe dans three.js selon son papier et projette une ombre réelle sur les pages qu’il recouvre. Le canvas WebGL dessine le livre de la même façon, immobile ou en train de tourner ses pages, si bien qu’une page ne change jamais d’aspect en se posant. C’est la visionneuse des recettes du livre de recettes et de l’onglet **Folio** du Sandbox.

```bash
npm install postext postext-folio three
```

```ts
import { buildDocument } from 'postext';
import { createFolioFromDocument } from 'postext-folio';

const doc = buildDocument({ markdown }, config);
const book = createFolioFromDocument(document.getElementById('book')!, doc, {
  onChange: ({ pages }) => console.log('showing pages', pages),
});

// After an edit: the same viewer, on the same page.
book.setDocument(buildDocument({ markdown: edited }, config));
```

- **Les pages sont peintes au fur et à mesure.** `createFolioFromDocument` peint chaque page avec `renderPageToCanvas` exactement aux pixels physiques d’un emplacement de page (WebGL l’affiche alors texel pour pixel, aussi net que l’aperçu canvas), et seulement les doubles pages autour de celle qui est ouverte (`window`, trois de chaque côté par défaut). Les pages qui sortent de cette fenêtre sont libérées, si bien qu’un livre de mille pages ne coûte que la mémoire de quelques-unes. Un saut vers une page lointaine peint d’abord cette double page. Jusqu’à dix pages d’écart, les feuillets tournent un par un ; au-delà, le bloc de pages intermédiaire se soulève d’un seul tenant, aussi épais que ces pages (leurs épaisseurs additionnées), et se pose de l’autre côté. `setDocument` conserve la peinture de chaque page qui reste identique dans la nouvelle mise en page (`{ repaint: true }` les repeint toutes, après l’arrivée d’une image).
- **Le document définit le livre.** Sa première page s’ouvre seule à droite quand c’est un recto (`pageIndexOffset` pair), un livre relié à droite (`page.binding: 'right'`, ou un document vertical) est disposé en miroir et tourne vers la gauche, les pages blanches prennent la couleur de fond de la page, et la largeur rognée de la page (`pageWidthMm`) met à l’échelle l’épaisseur du papier et les plats. Un chapitre mis en page avec une `continuation` compte les autres pages du livre (`pageIndexOffset` avant lui, `bookPageCount` après lui) dans l’épaisseur des deux blocs de pages, sans les dessiner (`extraPages`).
- **Le document définit l’aspect.** Le papier, la reliure, le bureau et l’éclairage sont les [réglages `folio`](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#visionneuse-folio-configuration) du document (`doc.config.folio`). Une page composée à l’intérieur d’une suite `:::paper` porte son propre papier (`VDTPage.paper`), et son feuillet est dessiné avec la couleur, la surface, l’épaisseur et la rigidité de ce papier. Avec `binding.cover: 'pages'`, la première page est le plat avant et la dernière, quand c’est un verso, le plat arrière (`covers`). Un format de journal (`'broadsheet'`, `'berliner'`, `'tabloid'`, `'compact'`) dont les réglages ne nomment ni papier ni reliure se présente comme du papier journal plié, y compris quand l’hôte passe son propre `folio`.
- **Le conteneur définit la taille.** Le livre le remplit, avec les boutons et le nombre de pages dans les marges : donnez donc une hauteur au conteneur ; un redimensionnement repeint les pages à la nouvelle taille. Sous 560 px de large, il affiche une page à la fois (`mode: 'auto'` ; `'single'` et `'double'` imposent l’un ou l’autre) : le dos longe le bord intérieur de la page et le feuillet tourne autour de lui ; faire glisser vers le dos tourne la page en avant, un balayage dans l’autre sens revient en arrière, un tapotement tourne la page.
- **Ce que fait le pointeur.** `interaction` (puis `setInteraction`) définit ce que font sur le livre le bouton gauche, un doigt ou un stylet : `'hand'` (la valeur par défaut) saisit et tourne les pages, `'orbit'` fait pivoter la vue comme un glissement du bouton droit (pour les pavés tactiles et les tablettes), `'select'` laisse le pointeur à l’hôte, pour sélectionner du texte par exemple. `pageAt(event)` donne la page sous un pointeur et l’endroit visé (`{ page, x, y }`, en fractions de la page depuis son coin supérieur gauche), sur le livre tel qu’on le voit, incliné ou pivoté ; `pointOnScreen(point)` fait le chemin inverse, pour dessiner un curseur ou une sélection sur la page. `refreshPage(src)` réaffiche un canvas de page que l’hôte a redessiné sur place. Le Sandbox les utilise tous pour sélectionner du texte et suivre le curseur de l’éditeur sur les pages en 3D.
- **Une loupe pour les petits caractères.** `interaction: 'magnify'` tient une loupe ronde cerclée de noir au-dessus du livre, là où se trouve le pointeur (un doigt la tient au-dessus de lui tant qu’il touche l’écran). Elle montre le livre tel que le voit l’œil du lecteur, éclairé et courbé comme il est posé, grossi au maximum au centre et s’incurvant vers le bord. La molette, `+` et `−` changent le grossissement (`setMagnification(zoom)`, de 1,5 à 10 ; la valeur par défaut montre la page à environ 5,5 px CSS par millimètre) et Échap range la loupe ; `magnifier: { zoom, diameter }` règle les deux dès le départ. `createFolioFromDocument` repeint les pages sous la loupe, assez nettes pour son centre, si bien que le texte courant d’un journal se lit ; `createFolio` prend ces peintures dans `detail: { paint(index, deviceWidth), release() }`. Le Sandbox la place sur le bouton **Loupe** (M). La loupe sélectionne aussi du texte : au-dessus des pages, le curseur est un curseur de texte, `pageAt` renvoie le point situé sous le centre de la loupe (au-dessus du doigt sur un écran tactile) et, dans le Sandbox, un clic à cet endroit place le curseur et un glissement sélectionne.
- **Le lecteur peut en faire le tour.** Un glissement du bouton droit fait tourner la vue autour du livre (jusqu’à 70° de la verticale), y compris pendant que des feuillets tournent ; `resetView()` la ramène en douceur aux valeurs `tilt` et `yaw` des réglages, et `getView()` donne la vue telle qu’on la voit à cet instant (`{ tilt, yaw }`, en degrés) pour l’enregistrer dans ces réglages. Le Sandbox les place sur deux boutons, **Réinitialiser la vue** et **Enregistrer comme vue par défaut**.
- **Les vidéos se lisent sur les pages.** Un clic sur l’affiche d’une vidéo la lit sur la page, dans tous les modes d’`interaction`, et elle continue pendant que son feuillet tourne ; un autre clic la met en pause, et elle s’arrête quand le livre s’immobilise sur une double page qui ne la montre pas. Une vidéo avec `player.autoplay` démarre d’elle-même la première fois que sa double page s’affiche ; une vidéo qui se lit aussi en même temps que les autres (`player.exclusive: false`) démarre, sans le son, à chaque fois et s’arrête quand on tourne la double page, plusieurs à la fois. Options `videos`, `videoUrl` et `onVideo`, et `stopVideo()` sur la visionneuse ; voir [Format du document › Vidéos sur les pages de Folio](https://postext.dev/fr/docs/document-format.md#les-vidéos-sur-les-pages-de-folio).
- **D’abord les polices et les images.** Comme pour `renderPage`, les fontes qu’utilise le document doivent être chargées dans `document.fonts` et ses images de ressources enregistrées avec `registerResourceImage` avant que les pages ne soient peintes.
- **Accessible.** La visionneuse est un groupe focalisable qui répond à ←/→ (inversées pour un livre relié à droite), Page précédente/suivante, Début et Fin ; ses boutons et son nombre de pages sont étiquetés (`labels` les traduit), et chaque canvas de page porte un texte `alt` (`alt: (index) => …`).
- **Sans WebGL2**, ou quand le lecteur demande des animations réduites, les doubles pages changent simplement. Les livres WebGL sont lourds pour un téléphone (une texture par face de page) : le Sandbox ne propose son onglet Folio que là où WebGL2 est disponible et où le petit côté de l’écran mesure au moins 600 px. `canFlip()` indique si les feuillets tourneront en 3D à cet endroit : WebGL2, et pas d’animations réduites.

### Apparence

L’option `appearance`, puis `setAppearance`, l’emportent sur ce que dit le document. Tout ce qui est omis garde la valeur du document :

```ts
const book = createFolioFromDocument(container, doc, {
  appearance: {
    folio: {
      tilt: 22,
      paper: { type: 'bookWove', texture: 'laid' },
      binding: { type: 'hardcover', coverColor: { hex: '#5a1f1f', model: 'hex' } },
      surface: { type: 'walnut' },
      lighting: { environment: 'lamp' },
    },
    // Photographed desks: a folder laid out as postext.dev's /folio/textures/
    // (manifest.json and one folder per desk). Without it, procedural maps.
    textureBaseUrl: '/folio/textures',
    // The picture for `folio.binding.spineImage`: the resource's URL,
    // or a drawn canvas or image.
    spineImage: spineUrl,
  },
});

// A settings panel: the book redrawn in place, nothing painted again.
book.setAppearance({ folio: { ...folio, lighting: { environment: 'daylight' } } });
book.resetView();
```

| Champ | Effet |
| --- | --- |
| `folio` | Les [réglages `folio`](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#visionneuse-folio-configuration) : inclinaison, papier, reliure, surface, éclairage. Remplace ceux du document lorsqu’il est fourni. |
| `pageWidthMm` | La largeur rognée d’une page en mm, par rapport à laquelle l’épaisseur du papier et les plats sont mis à l’échelle. Tirée du document : la page rognée à sa résolution en dpi. 150 par défaut pour `createFolio`. |
| `extraPages` | `{ before, after }` : des pages du livre en dehors de celles fournies, comptées dans l’épaisseur des blocs de pages et jamais dessinées. |
| `covers` | `{ front, back }` : la première page fournie est la couverture, la dernière la quatrième de couverture (quand elle tombe sur un verso). Elles tournent comme des plats et aucune couverture cartonnée n’est dessinée. Tirée du document : `binding.cover: 'pages'` sur un livre qui commence à sa première page et finit à sa dernière. |
| `spineImage` | L’image imprimée sur le dos, sous forme d’URL, de canvas ou d’image. `createFolioFromDocument` ne cherche pas les ressources : passez l’image de la ressource que nomme `folio.binding.spineImage`. Ignorée pour une piqûre à cheval. |
| `textureBaseUrl` | L’adresse où sont servies les textures photographiées du bureau. Tant qu’elles ne sont pas chargées, ou sans elle, le bureau est dessiné avec des textures procédurales. |

`createFolio(container, { pages })` est la même visionneuse sur n’importe quelles pages : des URL d’images, des éléments `<img>` ou `<canvas>`, et `""` pour une page blanche ; une page peut être `{ src, alt, paper }`, `paper` étant un papier à la manière de `:::paper` pour ce feuillet. `PageFlipper` est le moteur three.js seul, pour un hôte qui construit son propre DOM de double page ; `FlatPageFlipper` est le tourne-page à plat antérieur au livre en 3D, conservé pour la table lumineuse du livre de recettes. La liste complète des options se trouve dans le [README du paquet](https://www.npmjs.com/package/postext-folio).

### Exemple en direct : un livre en 3D

Le pen importe `postext` et `postext-folio` depuis un CDN, met en page un court document et l’ouvre comme un livre. Saisissez la page de droite par son bord et faites-la passer de l’autre côté.

> **Exemple exécutable: Postext · un document en livre 3D** — Mettre en page un document Markdown avec postext et en tourner les pages en 3D avec postext-folio. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/render-folio))

### Exemple en direct : des images de pages

`createFolio` avec des pages dessinées sur des canvas, une dernière page blanche et la couleur du papier.

> **Exemple exécutable: Postext · un livre d’images en 3D** — Tourner n’importe quelles pages en 3D avec postext-folio : URL d’images, éléments img ou canvas. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/folio-images))

## Livres EPUB (`postext-epub`)

`postext-epub` écrit un livre mis en page sous forme de fichier EPUB 3.3, dans le navigateur ou sous Node, sans serveur. Il lit les mêmes documents de chapitres que `renderToPdf` prend pour un livre, si bien que les numéros de page, les notes, les citations, les renvois, la table des matières et l’index arrivent résolus, et il renvoie le fichier sous forme d’octets. C’est lui qui écrit les fichiers de l’onglet **EPUB 3** du Sandbox.

```bash
npm install postext postext-epub
```

`postext` est une dépendance pair (peer dependency), comme pour `postext-pdf` : mettez les deux à jour ensemble et, sur un CDN, épinglez-les à la même version.

### Mise en page fixe et recomposable

EPUB 3 définit deux rendus, fixés par la propriété `rendition:layout` du paquet ; `layout` choisit l’un des deux :

|  | `layout: 'fixed'` | `layout: 'reflowable'` |
| --- | --- | --- |
| Nom EPUB | `pre-paginated` (mise en page fixe, FXL) | `reflowable`, la valeur par défaut d’EPUB |
| Documents de contenu | Un document XHTML par page imprimée, au format rogné de la page en px CSS | Un document XHTML par chapitre (une partie ouvre le sien) |
| Ce qui est conservé | La page : colonnes, flottants, titres courants, ouvertures, coupures et positions des lignes, dans les polices incorporées. Le texte reste du vrai texte : sélectionnable, consultable par recherche, lisible à voix haute | Le texte et sa structure : titres, paragraphes reconstruits à partir des lignes, listes, encadrés sous forme d’apartés, figures et tableaux après le texte qui les cite, notes, liens, repères de pages imprimées. Une feuille de style dérivée de la configuration |
| Ce qui est abandonné | Le choix du caractère, du corps et des marges par le lecteur ; sur un petit écran, la page est réduite | Les colonnes, les titres courants, la maquette des pages et les coupures de lignes exactes |
| Doubles pages et sens de lecture | `page-spread-left` / `page-spread-right` selon la parité et la reliure ; un livre relié à droite se lit de droite à gauche | Sens de lecture tiré de la reliure ; le chinois vertical garde `vertical-rl`, l’arabe est en `dir="rtl"` |
| Adapté à | Des pages composées avec soin : livres illustrés, manuels, catalogues, magazines ; grands écrans | Le texte suivi : romans, essais, rapports ; téléphones et liseuses à encre électronique |

Les deux rendus portent la même navigation : une table des matières tirée des titres et des pages de partie, une liste des pages avec les numéros de pages imprimées, des repères (couverture, table des matières imprimée, début du corps du texte) et un NCX pour les anciens systèmes de lecture.

### Écrire un livre

```ts
import { openBundle, buildBundle } from 'postext';
import { renderToEpub } from 'postext-epub';

const bundle = await openBundle(fileBytes);
const docs = buildBundle(bundle); // one VDTDocument per chapter, in book order

const bytes = await renderToEpub(docs, {
  layout: 'reflowable',
  metadata: { title: 'Lantern', creators: ['Ada Lovelace'], language: 'en' },
  fonts: bundle.fonts.map((f) => ({ family: f.family, weight: f.weight, style: f.style, bytes: new Uint8Array(f.bytes), format: f.format })),
  resourceBytes: (fileId) => {
    const data = bundle.files.get(fileId);
    return data ? { bytes: data, mediaType: '' } : undefined;
  },
  onWarning: (w) => console.warn(w),
});
```

Un document seul est un livre d’un chapitre : `renderToEpub([doc], options)`.

- **`renderToEpub(docs, options): Promise<Uint8Array>`** écrit le fichier. `options` vaut `{ layout, metadata, fonts?, svgFonts?, resourceBytes?, cover?, onProgress?, onWarning?, signal? }`.
- **`metadata`** : `title` et `language` (une étiquette BCP 47) sont obligatoires ; `subtitle`, `creators`, `identifier`, `date`, `publisher`, `rights`, `description` et `modified` sont facultatifs. Un ISBN seul devient `urn:isbn:…`. Sans `identifier`, le livre reçoit un `urn:uuid:` dérivé de son titre, de ses auteurs et de sa langue, si bien qu’une nouvelle version du même livre garde sa place dans la bibliothèque d’un lecteur. Passez aussi `modified` pour obtenir une sortie identique à l’octet près.
- **`fonts`** : les fontes à incorporer, `{ family, weight, style, bytes, format, unicodeRange? }`, `format` valant `woff2`, `woff`, `ttf` ou `otf`. Chaque fonte devient un fichier et une règle `@font-face` ; plusieurs fichiers avec leur `unicodeRange` forment une seule fonte (les tranches de Google Fonts). Une famille, une graisse ou un style que les pages utilisent sans fonte incorporée est signalé par `missingFont`, et les systèmes de lecture y substituent les leurs. N’incorporez que des polices dont la licence le permet : une fonte marquée `redistributable: false` n’est jamais écrite dans le fichier (le livre peut être composé avec elle, mais son fichier reste dehors, y compris des images SVG).
- **`resourceBytes(fileId)`** : les images que placent les pages, de façon synchrone ou asynchrone, sous la forme `{ bytes, mediaType }` ; un `mediaType` vide est déduit des octets. Fournissez les bitmaps tels qu’ils sont stockés et les SVG sous forme de source, pas l’original d’impression PDF (`svg.pdfFileId`). Chaque image est stockée une seule fois. Un livre à encre unique (`diagramStyle.singleInk`) voit ses SVG recolorés dans le fichier. Un SVG reçoit, incorporées en lui, les fontes que nomme son texte, puisqu’un système de lecture l’affiche comme une image qui ne voit pas les polices du livre (voir [Polices dans le texte SVG](https://postext.dev/fr/docs/configuration-resources.md#polices-du-texte-svg)) : depuis `fonts` (les tranches qui contiennent ses caractères), puis depuis **`svgFonts.provider`** pour une famille que le texte du livre n’utilise pas. Les familles des fontes marquées `redistributable: false`, et celles que nomme `svgFonts.withhold(family)`, restent dehors et sont signalées une fois chacune par `fontWithheld` ; une famille sans fonte est signalée par `svgFontUnavailable`, des fontes au-delà de `svgFonts.maxBytes` (2 Mio) par `svgFontsTooLarge`. `svgFonts.inline: false`, `diagramStyle.inlineFonts: false` et le `svg.inlineFonts: false` d’une ressource gardent les octets tels quels. Une image sans octets est signalée par `missingImage` et laissée comme un cadre vide.
- **`cover`** : `{ bytes, mediaType, alt? }`, une image JPEG, PNG, WebP ou SVG. Le livre s’ouvre alors sur un document de couverture qui la contient, et elle devient le `cover-image` du paquet (la vignette dans une bibliothèque). Sans elle, la mise en page fixe désigne sa première page comme couverture, et le livre recomposable n’a pas d’image de couverture.
- **`onProgress({ phase, done, total })`** : `resources` (polices et images), `documents` (pages pour une mise en page fixe, chapitres pour une recomposable), puis `package`. **`signal`** interrompt le travail entre deux étapes.
- **`readEpub(bytes)`** relit un fichier pour une visionneuse, sans `DOMParser` : mise en page, métadonnées, sens de lecture, chaque fichier par chemin, le manifeste, la séquence de lecture (spine), la table des matières, la liste des pages, la zone d’affichage de la mise en page fixe et la couverture. Le lecteur du Sandbox repose sur elle.

Les deux rendus portent les métadonnées EPUB Accessibility 1.1 (modes d’accès, fonctions comme la table des matières et les numéros de pages imprimées, risques et un résumé) et ne revendiquent par défaut aucune conformité WCAG. La liste complète des options et les limites se trouvent dans le [README du paquet](https://www.npmjs.com/package/postext-epub).

### Vérifier un fichier avec EPUBCheck

[W3C EPUBCheck](https://www.w3.org/publishing/epubcheck/) est le validateur de référence pour EPUB ; les librairies numériques vérifient avec lui les fichiers qu’elles reçoivent. Une fois installé (`brew install epubcheck`, ou la version Java), `epubcheck book.epub` liste les erreurs, les avertissements et les remarques d’usage ; les remarques d’usage que laisse la sortie de Postext (`CSS-028`, `OBS-001`, `HTM_062`) sont informatives. Dans le dépôt de Postext, `pnpm --filter postext-epub epubcheck` vérifie les livres d’exemple de la suite de tests, `node packages/postext-epub/scripts/epubcheck.mjs book.postext --layout both` met en page un fichier `.postext` ou un dossier de préréglage et vérifie les deux rendus, et `pnpm --filter postext-epub validate` exécute toute la matrice de livres (le guide, les préréglages vitrines, les livres chinois, arabes et ceux du livre de recettes), qui passent tous sans erreur ni avertissement.

## Paquets (fichiers `.postext`)

Un **fichier `.postext`** est un livre entier en un seul fichier : une archive zip qui contient un manifeste `preset.json`, un fichier Markdown par chapitre, les contenus des ressources (bitmaps, SVG, originaux d’impression PDF) et les fichiers des polices que nomme la configuration. Le [Sandbox](https://postext.dev/fr/docs/sandbox.md#export-et-import) l’exporte et l’importe, et le [skill d’agent](https://postext.dev/fr/docs/skill.md) le livre. Le paquet `postext` sait aussi le créer et l’ouvrir, si bien qu’un livre peut circuler entre ces outils et votre propre programme sans rien perdre.

```
my-book.postext
├── preset.json            manifest: name, locale, chapters, config, resources, fonts
├── chapters/01-dusk.md
├── chapters/02-night.md
├── resources/lantern.svg
└── fonts/ebgaramond-400-normal.woff2
```

Le manifeste est décrit champ par champ dans l’annexe [Format des paquets de préréglages](https://postext.dev/fr/docs/sandbox.md#format-des-paquets-de-préréglage) du Sandbox. Un fichier peut aussi contenir `layouts.json`, les nombres de pages du Sandbox, afin que le livre s’y ouvre déjà paginé, ou un par édition d’un livre multilingue (`layouts.zh-Hant.json`, lu en premier). `openBundle` les ignore.

L’API est exportée par `postext` lui-même et par le sous-chemin `postext/bundle`, qui ajoute les fonctions de bas niveau. Importez depuis `postext` quand vous faites aussi le rendu. Les adaptateurs de paquets et les moteurs de rendu partagent ainsi une seule instance de module, ce qui compte sur un CDN comme esm.sh, où chaque point d’entrée est un build distinct.

### Ouvrir un paquet

`openBundle` prend les octets du fichier (un `Uint8Array`, un `ArrayBuffer`, ou un `Blob` / `File` issu d’un `<input type="file">`) et renvoie tout ce dont le moteur et ses sorties ont besoin :

```ts
import { openBundle } from 'postext';

const bundle = await openBundle(await file.arrayBuffer(), { locale: 'es' });

bundle.chapters;   // [{ title, file, markdown }, …] in book order
bundle.config;     // PostextConfig, ready for buildDocument
bundle.resources;  // Resource[]
bundle.files;      // Map<path, Uint8Array>: every file of the bundle
```

| Champ | Contenu |
| --- | --- |
| `manifest` | Le `preset.json` validé. |
| `id`, `name`, `description` | Tirés du manifeste. |
| `locale`, `locales` | La langue dans laquelle le contenu a été lu, et toutes les langues que contient un paquet bilingue. `options.locale` en choisit une : d’abord l’étiquette exacte, puis la langue de base, puis la langue propre du paquet. |
| `chapters` | `{ title, file, markdown }` par chapitre. Un chapitre sans titre dans le manifeste prend le texte de son premier titre `#`. |
| `config` | La palette de couleurs par défaut et les types de ressources dans la langue du paquet, puis la `config` du manifeste, puis les surcharges de la langue. La langue du paquet est le `locale` ci-dessus : un paquet dans une langue reçoit donc ses propres libellés quoi que demande `options.locale`. Un manifeste qui ne nomme aucune langue prend celle que fixe sa `config` (`locale`, puis la langue de césure), sinon `options.locale`. `customFonts` liste les familles de polices du paquet. C’est la même configuration que celle avec laquelle le Sandbox ouvre le paquet. |
| `resources` | Les ressources, avec les légendes de la langue choisie. Une taille absente du manifeste est lue dans le fichier. |
| `fonts` | Une entrée par fonte : `{ family, weight, style, format, file, bytes }`. |
| `files` | Chaque fichier de l’archive, indexé par son chemin. |
| `thumbnail`, `canvasScope` | Le chemin de l’image de couverture, et la façon dont le paquet demande à être affiché : la `view` du manifeste, surchargée par le `localized[…].view` de la langue servie. |
| `start` | Où commence le livre, pour un paquet qui contient une partie d’un ouvrage plus long : le `start` du manifeste, ou le `localized[…].start` de la langue servie. Absent pour un livre qui commence à son début. |
| `warnings` | Problèmes non bloquants : un fichier de police non pris en charge, un original d’impression manquant. |

Le **`fileId` d’un contenu est son chemin dans le paquet**. `resource.svg.fileId`, `resource.bitmap.fileId` et le `fileId` de chaque variante de `customFonts` peuvent être cherchés directement dans `bundle.files`. `openBundle` lève une exception quand les octets ne sont pas un zip, quand il n’y a pas de `preset.json` valide (à la racine ou dans un unique dossier de premier niveau), ou quand un fichier que nomme le manifeste est absent.

#### Paquets écrits par postext 1.4 ou antérieur

Chaque manifeste qu’écrivent `createBundle` et le Sandbox porte `configVersion: 14` : les règles de configuration pour lesquelles sa `config` a été écrite. Un manifeste qui ne le porte pas a été écrit par postext 1.4 ou antérieur, qui réglait dix-huit choses autrement :

- **Les sauts aux titres** (règles 3) : jusqu’à la 1.4, un objet `headings` sans saut pour le H1 n’en avait aucun (voir [Surcharges par niveau](https://postext.dev/fr/docs/configuration-text.md#réglages-par-niveau)).
- **Le corps des formules** (règles 4) : jusqu’à la 1.4, les formules sortaient 1,131 fois plus grandes que ne l’indique `fontSizeScale` (voir [Taille des formules](https://postext.dev/fr/docs/configuration-text.md#mathématiques)).
- **L’espace sous une ressource en ligne** (règles 5) : jusqu’à la 1.4, le texte qui suit une figure ou un tableau en `placement.position: 'here'` reprenait à la ligne de grille suivante, sans espace de flottant en dessous (voir `layout.inlineResourceGap` dans [Mise en page](https://postext.dev/fr/docs/configuration-page-layout.md#mise-en-page)).
- **L’espace autour d’une ressource en ligne dans un encadré** (règles 6) : jusqu’à la 1.4, une telle ressource était collée au texte de l’encadré qui l’entoure (voir `layout.inlineResourceGapInBoxes` dans [Mise en page](https://postext.dev/fr/docs/configuration-page-layout.md#mise-en-page)).
- **Les marques en ligne dans les titres** (règles 6) : jusqu’à la 1.4, un titre imprimait les mots de ses marques `*italic*`, `**bold**` et autres dans son propre style simple (voir `headings.inlineMarks` dans [Titres](https://postext.dev/fr/docs/configuration-text.md#titres)).
- **La taille d’une lettrine** (règles 6) : jusqu’à la 1.4, la `dropCap` d’un texte de maquette sans `fontSize` était aussi haute que toutes les boîtes de lignes qu’elle couvre, son sommet au-dessus de la première ligne (voir `dropCap` dans [Éléments de texte](https://postext.dev/fr/docs/configuration-page-layout.md#éléments-de-texte)).
- **La place sous une ligne terminée par un deux-points** (règles 6) : jusqu’à la 1.4, `keepColonWithList` jugeait suffisante pour la liste une ligne de place sous la ligne finissant par un deux-points, et un premier élément de deux lignes, que les règles des orphelines et des veuves gardent entier, passait à la colonne suivante sans elle (voir `bodyText.colonListRoom`).
- **Les lignes que laisse la coupure d’un encadré** (règles 6) : jusqu’à la 1.4, un encadré coupé à l’intérieur d’un paragraphe ou d’un élément de liste pouvait en laisser une seule ligne d’un côté, pourvu que chaque côté de l’encadré contienne en tout ses `splitMinLines` lignes (voir `layout.boxChildSplitMinLines` dans [Mise en page](https://postext.dev/fr/docs/configuration-page-layout.md#mise-en-page)).
- **Les coupures de ligne à un tiret** (règles 7) : jusqu’à la 1.4, Knuth-Plass ne terminait jamais une ligne après un tiret cadratin ou demi-cadratin collé entre deux mots (`say—that’s`), et l’algorithme ligne à ligne du texte mis en forme seulement entre deux lettres (voir `bodyText.breakAfterDashes` dans [Texte courant](https://postext.dev/fr/docs/configuration-text.md#texte-courant)).
- **Le texte en drapeau** (règles 7) : jusqu’à la 1.4, le texte courant en drapeau était composé ligne à ligne, chaque ligne remplie avant la suivante, quoi qu’indique `optimalLineBreaking` (voir `bodyText.optimalRagged` dans [Texte courant](https://postext.dev/fr/docs/configuration-text.md#texte-courant)).
- **La coupure sous un titre** (règles 8) : jusqu’à la 1.4, le paragraphe sous un titre en pied de colonne gardait autant de lignes qu’il en tenait là, si peu qu’il en passe à la colonne suivante (voir `headings.keepWithNextSplit` dans [Titres](https://postext.dev/fr/docs/configuration-text.md#titres)).
- **L’espace sous un conteneur `:::paragraphs`** (règles 8) : jusqu’à la 1.4, l’espace du style était placé sous le dernier paragraphe avant l’alignement sur la grille, l’espace au-dessus du bloc suivant (le `marginTop` d’un titre) s’y ajoutait, et l’espacement entre paragraphes du texte était omis (voir `bodyText.paragraphContainerSpacing` dans [Texte courant](https://postext.dev/fr/docs/configuration-text.md#texte-courant)).
- **Les coupures de ligne au trait d’union d’un mot composé** (règles 8) : jusqu’à la 1.4, Knuth-Plass ne terminait jamais une ligne justifiée après un trait d’union entre deux lettres (`well-known`) dans un paragraphe sans mise en forme en ligne, alors qu’il le faisait dans un paragraphe mis en forme (voir `bodyText.breakAfterHyphens` dans [Texte courant](https://postext.dev/fr/docs/configuration-text.md#texte-courant)).
- **Les poèmes sans séparateur** (règles 9) : jusqu’à la 1.22, un poème `:::verse` dont les vers ne portaient pas de `||` était composé en hémistiches isolés, chaque vers centré (voir `bodyText.verse.layout` dans [Vers](https://postext.dev/fr/docs/configuration-text.md#vers)).
- **Un retrait de première ligne à côté d’un retrait négatif** (règles 9) : jusqu’à la 1.22, le `hangingIndent` d’un style de paragraphe remplaçait son `firstLineIndent`, et la première ligne commençait à `indent` (voir [Styles de paragraphe](https://postext.dev/fr/docs/configuration-styles.md#styles-de-paragraphe)).
- **Une barre oblique inverse en fin de ligne** (règles 9) : jusqu’à la 1.22, une barre oblique inverse qui terminait une ligne d’un paragraphe, d’une citation ou d’un élément de liste, ainsi que `\\` devant une espace, s’imprimaient, et les lignes étaient jointes par une espace (voir `bodyText.hardLineBreaks` dans [Texte courant](https://postext.dev/fr/docs/configuration-text.md#texte-courant)).
- **Les blocs de code délimités** (règles 9) : jusqu’à la 1.22, une clôture ```` ``` ```` ou `~~~` et les lignes qu’elle contient étaient lues comme du Markdown : lignes jointes en paragraphes, une ligne `#` devenue titre, clôtures imprimées (voir `codeStyle.blocks` dans [Listings de code](https://postext.dev/fr/docs/configuration-styles.md#listings-de-code)).
- **Les vers reportés** (règles 10) : en 1.23, un vers d’un poème composé ligne à ligne et plus large que la justification était reporté à son espacement naturel entre les mots, si peu qu’il déborde (voir `bodyText.verse.tighten` dans [Vers](https://postext.dev/fr/docs/configuration-text.md#vers)).

`openBundle` et `readBundle` lisent la `config` d’un tel manifeste, et la configuration `localized` de chaque langue, au moyen de `migrateConfig`, qui écrit en clair les sauts que la 1.4 mettait en page et multiplie l’échelle des formules par 1,131 (ses marges d’affichage en `em` étant divisées par ce facteur). Un manifeste marqué de `3` à `7`, écrit par une préversion de la 1.5, ne reçoit que les épinglages des règles postérieures à sa marque. Avec `3`, ce sont le corps des formules, l’espace en ligne, les cinq épinglages des règles 6, les deux des règles 7 et les trois des règles 8 ; avec `4`, l’espace en ligne et les épinglages des règles 6, 7 et 8 ; avec `5`, les épinglages des règles 6, 7 et 8 ; avec `6`, les épinglages des règles 7 et 8 ; avec `7`, les seuls épinglages des règles 8. La coupure sous un titre (`pinLegacyHeadingSplit`) est écrite sous la forme `headings.keepWithNextSplit: 'fill'` sur les `headings` que les couches laissent en vigueur, quand un chapitre lu contient un titre et que la configuration ne nomme aucune valeur propre, garde `headings.keepWithNext` activé et ne désactive pas `bodyText.avoidOrphans`. Les coupures aux mots composés (`pinLegacyHyphenBreaks`) sont écrites sous la forme `bodyText.breakAfterHyphens: false` sur le `bodyText` en vigueur, quand un chapitre lu compose un trait d’union entre deux lettres et que la configuration ne le fixe pas déjà et ne désactive pas `optimalLineBreaking`. L’espace sous les conteneurs (`pinLegacyParagraphContainerSpacing`) est écrit sous la forme `bodyText.paragraphContainerSpacing: 'add'` sur le `bodyText` en vigueur, quand la configuration déclare un style de paragraphe (dans `paragraphStyles` ou dans les surcharges de la visionneuse HTML), qu’un chapitre lu ouvre un conteneur `:::paragraphs` sur une ligne à part et que la configuration ne le fixe pas déjà. Les coupures aux tirets (`pinLegacyDashBreaks`) sont écrites sous la forme `bodyText.breakAfterDashes: false` sur le `bodyText` en vigueur, quand un chapitre lu compose un tiret cadratin ou demi-cadratin collé entre deux mots (une lettre, un chiffre ou une ponctuation fermante avant lui, une lettre, un chiffre ou une parenthèse ou un guillemet ouvrant après lui ; un guillemet avant lui compte quand une lettre, un chiffre, une ponctuation fermante ou une espace insécable précède le guillemet, comme dans `"no"—and`, et non dans `said "—Hola` ; une marque en ligne qui touche le tiret ou le guillemet, comme les `**` de `**riddles.**—I`, compte d’un côté comme de l’autre) et que la configuration ne le fixe pas déjà. La composition en drapeau (`pinLegacyRaggedBreaking`) est écrite sous la forme `bodyText.optimalRagged: false` sur le `bodyText` en vigueur, quand la configuration compose en drapeau une partie du texte courant (un `textAlign` autre que `'justify'` sur le texte courant, un style de paragraphe, le corps d’un encadré, le corps d’une partie ou le corps d’un style de section (`headingStyles[].bodyStyle`), ou dans les surcharges de la visionneuse HTML), ne le fixe pas déjà et ne désactive pas `optimalLineBreaking`. L’espace dans les encadrés (`pinLegacyBoxResourceGap`) est écrit sous la forme `layout.inlineResourceGapInBoxes: false` sur le `layout` en vigueur, quand une ressource est incorporée sur une ligne à part à l’intérieur d’un `:::callout` des chapitres lus et que la configuration ne le fixe pas déjà. La coupure des encadrés (`pinLegacyBoxChildCut`) est écrite sous la forme `layout.boxChildSplitMinLines: 1` sur le `layout` en vigueur, quand un chapitre lu ouvre un `:::callout` sur une ligne à part et que la configuration ne le fixe pas déjà. Les marques des titres (`pinLegacyHeadingMarks`) sont écrites sous la forme `headings.inlineMarks: false` sur les `headings` que les couches laissent en vigueur, quand un titre des chapitres lus porte une marque (`*`, `_`, `^`, `~`, `:smallcaps[` ou un lien dans son intitulé) et que la configuration ne nomme aucune valeur propre. Les lettrines (`pinLegacyDropCapSize`) voient leur taille de la 1.4 écrite en clair sous la forme `dropCap.fontSize`, où qu’elles se trouvent : dans l’unité de l’interlignage de l’élément quand c’est une longueur, sinon dans l’unité de son corps. La place sous une ligne terminée par un deux-points (`pinLegacyColonListRoom`) est écrite sous la forme `bodyText.colonListRoom: 'line'` sur le `bodyText` en vigueur, quand une liste des chapitres lus suit une ligne qui finit par un deux-points (des lignes vides entre les deux étant admises) et que la configuration ne nomme pas de place et ne désactive pas `keepColonWithList`. L’espace en ligne (`pinLegacyInlineGap`) est écrit sous la forme `layout.inlineResourceGap: 'above'` sur le `layout` que les couches laissent en vigueur, quand une ligne des chapitres lus incorpore une ressource (`::resource{id="…"}` seul sur sa ligne, tel que le lit l’analyseur : une mention dans le texte courant ou dans un code en ligne ne compte pas) et que la configuration ne nomme aucun espace propre. Le corps des formules est épinglé sur le `math` que les couches laissent en vigueur (le `math` propre d’une langue remplace celui qui est partagé), et seulement quand les chapitres lus contiennent un `$` : un paquet sans formules garde sa `config` telle qu’elle a été écrite. Quand ni le manifeste ni la langue ne nomment de `math`, celui qui est en vigueur est le `baseConfig` de `readBundle` (celui du lecteur), et il est épinglé lui aussi, puisque la 1.4 composait les formules du paquet à ce corps : un `fontSizeScale: 1.5` de base se lit comme 1,5 × 1,1312. Les sauts aux titres de la base sont pris tels quels. Un ancien paquet garde donc ce que ces règles mettaient en page, et `bundle.config` montre les sauts, le corps des formules, les espaces, les marques des titres, les tailles des lettrines, la place sous une ligne terminée par un deux-points, la coupure des encadrés, les coupures aux tirets, les coupures aux mots composés, la composition en drapeau, la coupure sous un titre et l’espace sous les conteneurs avec lesquels il est mis en page. Les corrections de mise en page de la 1.5 n’ont pas d’épinglage et s’appliquent à lui comme à n’importe quel livre, si bien qu’une page qu’elles touchent peut encore bouger (voir [Taille des formules](https://postext.dev/fr/docs/configuration-text.md#mathématiques) pour la liste). Un `preset.json` écrit à la main pour les règles actuelles fixe `"configVersion": 14` ; marquer ainsi le manifeste d’un ancien paquet est aussi la façon, en une ligne, de le lire avec les règles actuelles (un paquet sans version perd alors aussi son épinglage des sauts aux titres). Un manifeste marqué `8`, écrit par postext 1.5 à 1.22, reçoit les seuls quatre épinglages des règles 9, comme tous les manifestes plus anciens : la disposition des vers (`pinLegacyVerseLayout`) est écrite sous la forme `bodyText.verse.layout: 'bayt'` sur le `bodyText` en vigueur, quand un chapitre lu compose un poème `:::verse` dont la clôture ne nomme aucune disposition et dont les vers ne portent aucun séparateur d’hémistiches, et que la configuration ne le fixe pas déjà ; les retraits appariés (`pinLegacyPairedIndents`) suppriment le `firstLineIndent` de chaque style de paragraphe (dans `paragraphStyles` ou dans les surcharges de la visionneuse HTML) qui fixe aussi un `hangingIndent` non nul ; les sauts de ligne forcés (`pinLegacyHardBreaks`) sont écrits sous la forme `bodyText.hardLineBreaks: false` sur le `bodyText` en vigueur, quand un chapitre lu termine par une barre oblique inverse une ligne d’un paragraphe, d’une citation ou d’un élément de liste dont le bloc continue en dessous, ou compose `\\` devant une espace suivie d’autre texte (hors code et formules en ligne, formules centrées, titres et poèmes `:::verse`), et que la configuration ne le fixe pas déjà ; enfin, les blocs de code délimités (`pinLegacyCodeBlocks`) sont écrits sous la forme `codeStyle.blocks: false`, quand un chapitre lu ouvre une clôture ```` ``` ```` ou `~~~` (trois caractères ou plus, avec au plus trois espaces de retrait) et que la configuration ne le fixe pas déjà. Un manifeste marqué `9`, écrit par postext 1.23, reçoit le seul épinglage des règles 10, comme tous les manifestes plus anciens : les vers reportés (`pinLegacyVerseTightening`) sont écrits sous la forme `bodyText.verse.tighten: false` sur le `bodyText` en vigueur, quand un chapitre lu compose un poème ligne à ligne (une clôture `:::verse` qui nomme `layout=lines`, ou qui ne nomme aucune disposition sur des vers sans séparateur d’hémistiches alors que la configuration ne compose pas ces poèmes en bayts) et que la configuration ne le fixe pas déjà. Un manifeste marqué `10`, écrit par postext 1.24, reçoit le seul épinglage des règles 11, comme tous les manifestes plus anciens : l’équilibrage d’une grille de caractères (`pinLegacyGridBalancing`) est écrit sous la forme `headings.balancing.enabled: true` sur les `headings` en vigueur, quand la configuration fusionnée active `cjk.grid.enabled` en texte horizontal et ne fixe pas elle-même `enabled`, puisque la 1.24 équilibrait une telle page par défaut. Les règles 11 évitent aussi aux titres d’œuvres les coupures d’un seul caractère, composent les nombres cerclés comme des caractères chinois et composent le texte de maquette CJK selon les règles du texte courant (#637) : un manifeste marqué `10` ou antérieur reçoit `cjk.titleMinChars: 1` (`pinLegacyTitleBreaks`) quand un chapitre lu compose un titre d’œuvre (《 〈 ou `:book[`), `cjk.circledNumbers: 'western'` (`pinLegacyCircledNumbers`) quand un chapitre compose un nombre cerclé (U+2460–U+24FF, U+2776–U+2793), et `cjk.composeDesignText: false` (`pinLegacyDesignText`) quand un chapitre lu ou la configuration elle-même contient du texte CJK, chacun sur le `cjk` en vigueur et seulement quand la configuration ne le fixe pas déjà. Elles coupent aussi les tableaux en ligne et composent `:::columns` dans le texte courant (#634) : un manifeste marqué `10` ou antérieur reçoit `tableStyle.splitInline: false` (`pinLegacyInlineTableSplit`) sur le `tableStyle` en vigueur quand un chapitre lu incorpore une ressource, et `layout.flowColumns: false` (`pinLegacyFlowColumns`) sur le `layout` en vigueur quand un chapitre lu ouvre une clôture `:::columns` sur une ligne à part, chacun seulement quand la configuration ne le fixe pas déjà. Elles offrent aussi à un flottant le haut de la colonne propre d’une ouverture sur toute la largeur de la page (#639) : un manifeste marqué `10` ou antérieur reçoit `layout.floatsUnderOpener: false` (`pinLegacyOpenerHeadFloats`) sur le `layout` en vigueur quand la configuration fusionnée fixe un niveau ou un style de titre avec `span: 'page'` et qu’un chapitre lu contient un titre, seulement quand la configuration ne le fixe pas déjà. Les règles 12 permettent à une page sur grille de caractères de tenir ses lignes (#717) : un manifeste marqué `11` ou antérieur dont la configuration fusionnée active `cjk.grid.enabled` reçoit `bodyText.avoidWidows: true`, et dans la région `japan` `bodyText.avoidOrphans: true` (`pinLegacyGridBreakRules`), sur le `bodyText` en vigueur, chacun seulement quand la configuration ne le fixe pas déjà. Elles gardent aussi deux caractères d’une note sur deux rangs de part et d’autre d’un saut de ligne (#720) : un tel manifeste reçoit `cjk.warichu.minChars: 1` (`pinLegacyNoteBreaks`) sur le `cjk` en vigueur quand un chapitre lu compose une note `:warichu[` ou que la configuration fusionnée fixe `citations.notes: 'warichu'`, seulement quand la configuration ne le fixe pas déjà. Et dans la région Japon, un mot occidental prend l’espace han–latin à côté de ses parenthèses et de ses signes (#721) : un manifeste marqué `11` ou antérieur dont la configuration fusionnée se résout en région Japon reçoit `cjk.latinSpacingEdge: 'letters'` (`pinLegacyLatinSpacingEdge`) sur le `cjk` en vigueur quand un chapitre lu contient des kana ou des kanji, seulement quand la configuration ne le fixe pas déjà. Enfin, une ressource placée dans le texte prend `placement.captionSide` comme le fait un flottant de colonne (#729) : un manifeste marqué `11` ou antérieur dont la configuration fusionnée fixe `layout.layoutType: 'oneAndHalf'` reçoit `layout.inlineCaptionSide: false` (`pinLegacyInlineCaptionSide`) quand un chapitre lu incorpore une ressource, seulement quand la configuration ne le fixe pas déjà.

Les règles 12 changent aussi la façon d’équilibrer une colonne courte, et un manifeste marqué `11` ou antérieur garde ce avec quoi il a été mis en page. L’ordre des leviers (#707) : il reçoit `headings.balancing.maxLinesBeforeLoose` fixé à son propre `maxLinesPerHeading`, ou à `4` s’il n’en fixe aucun (`pinLegacyLooseOrder`), sur les `headings` en vigueur, quand un chapitre lu contient un titre et que la configuration fusionnée équilibre ses colonnes, permet à un titre d’occuper plus de deux lignes et ne fixe pas déjà la clé. La ligne sous une figure qui s’étend sur plusieurs colonnes (#708) : il reçoit `headings.balancing.stretchAfterWideFloats: 'always'` (`pinLegacyWideFloatStretch`) sur les `headings` en vigueur, quand un chapitre lu incorpore ou cite une ressource ou fait flotter un encadré, et que la configuration fusionnée équilibre ses colonnes, ne désactive pas `stretchAfterFloats` et ne fixe pas déjà la clé. Les coupures à l’intérieur des groupes d’un index (#712) : il reçoit `index.groups.splitMinEntries: 1` (`pinLegacyIndexGroupSplit`) sur l’`index` en vigueur, quand un chapitre lu compose une clôture `:::index` sur une ligne à part et que la configuration ne fixe pas déjà la clé. L’unité de la coupe de niveau (#711) : il reçoit `headings.balancing.trailingStep: 'grid'` (`pinLegacyTrailingStep`) sur les `headings` en vigueur, quand un chapitre lu peut composer du texte hors de l’interlignage du texte courant (une clôture `:::`, un bloc de code délimité, une citation, une liste, une formule centrée ou une citation bibliographique), et que la configuration fusionnée équilibre ses colonnes, garde `trailing` ou `beforeSpan` activé et ne fixe pas déjà la clé.

Les règles 12 coupent le libellé d’un renvoi à ses espaces quand ce libellé est fait de mots (#702). Un manifeste marqué `11` ou antérieur reçoit `crossRefs.breakLabels: false` (`pinLegacyRefLabelBreaks`) sur les `crossRefs` en vigueur, quand un chapitre lu contient un renvoi (`:ref{…}`, `\ref{…}`, `\eqref{…}` ou un `@id` pandoc-crossref) et que la configuration ne nomme pas ce réglage : chaque libellé reste le mot unique sous lequel il a été mis en page.

Les règles 12 façonnent un mot sans lettre (`46.`, `1367)`) dans l’écriture du texte qui l’entoure (#725). Un manifeste marqué `11` ou antérieur reçoit `bodyText.scriptContext: false` (`pinLegacyScriptContext`) sur le `bodyText` en vigueur, quel que soit le contenu de ses chapitres, quand la configuration ne nomme pas ce réglage : un tel mot est façonné seul, comme il a été mis en page. La règle agit dans une fonte qui dessine sa ponctuation selon l’écriture, et on ne sait pas quelles fontes un livre charge au moment où son manifeste est lu.

Les règles 13 font de la langue dans laquelle le texte est façonné un réglage (#764), et les règles 14 lui donnent pour valeur par défaut la langue propre du document (#770). Un manifeste marqué `13` ou antérieur reçoit `bodyText.shapingLanguage: 'host'` (`pinLegacyShapingLanguage`) sur le `bodyText` en vigueur, quel que soit le contenu de ses chapitres, quand la configuration ne nomme pas ce réglage ; un manifeste qui nomme une valeur la garde. Son texte est mesuré dans la langue de la page qui héberge la mise en page, et sans langue dans un worker, comme il a été mis en page. Un manifeste marqué `13` a été écrit par postext 1.29 : les versions 1.29.1 et 1.29.2 mettaient en page dans la langue de l’hôte un livre qui ne nomme aucune valeur, comme le fait l’épinglage ; la 1.29.0, dont la valeur par défaut était `'document'`, le mettait en page dans celle du document avec un canvas et un PDF qui ne suivaient pas la mesure, et il se lit lui aussi avec `'host'`. Un manifeste marqué `14` qui ne nomme aucune valeur est mis en page dans la langue du document.

Les règles 13 dessinent aussi la case d’un élément de tâche (`- [ ]`, `- [x]`) : un carré que dessine le moteur, coché quand l’élément est fait, qui occupe une place à lui avant le texte de l’élément (#765). Un manifeste marqué `12` ou antérieur reçoit `unorderedLists.taskBox: 'character'` (`pinLegacyTaskBox`) sur les `unorderedLists` en vigueur, quand un chapitre contient un élément de tâche et que la configuration ne nomme pas ce réglage. Sa case reste le caractère `☐` ou `☑` composé dans la fonte de la liste, et le texte de l’élément commence après la largeur de ce caractère, comme il a été mis en page. Cette largeur venait de la police, quelle qu’elle soit, qui contenait le caractère, une police du système dans la plupart des livres, et on ne la connaît pas au moment où le manifeste est lu.

```ts
import { CONFIG_VERSION, migrateConfig } from 'postext/bundle';

migrateConfig({ headings: { fontFamily: 'Georgia' } }, undefined, { content: 'A book with no maths.' });
// => { headings: { fontFamily: 'Georgia', levels: [{ level: 1, breakBefore: { enabled: false } }] } }
migrateConfig({ math: { fontSizeScale: 1.2 } }, 3);
// => { math: { fontSizeScale: 1.35746…, marginTop: { value: 0.7072, unit: 'em' }, marginBottom: { value: 0.7072, unit: 'em' } },
//      layout: { inlineResourceGap: 'above', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 },
//      headings: { inlineMarks: false, keepWithNextSplit: 'fill' }, bodyText: { colonListRoom: 'line', breakAfterDashes: false, breakAfterHyphens: false, verse: { layout: 'bayt', tighten: false }, hardLineBreaks: false }, codeStyle: { blocks: false } }
migrateConfig({ layout: { layoutType: 'single' } }, 4, { content: 'Text.\n\n::resource{id="fig"}' });
// => { layout: { layoutType: 'single', inlineResourceGap: 'above' } }
migrateConfig({ layout: { layoutType: 'single' } }, 5, { content: ':::callout\nText.\n\n::resource{id="fig"}\n:::' });
// => { layout: { layoutType: 'single', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 } }
migrateConfig({ bodyText: { textAlign: 'left' } }, 6, { content: 'I say—that is all.' });
// => { bodyText: { textAlign: 'left', breakAfterDashes: false, optimalRagged: false } }
migrateConfig({ paragraphStyles: [{ id: 'verse' }] }, 7, { content: ':::paragraphs{style="verse"}\nA line.\n:::' });
// => { paragraphStyles: [{ id: 'verse' }], bodyText: { paragraphContainerSpacing: 'add' } }
migrateConfig({ bodyText: { fontFamily: 'Georgia' } }, 7, { content: 'A well-known tale.' });
// => { bodyText: { fontFamily: 'Georgia', breakAfterHyphens: false } }
migrateConfig(config, CONFIG_VERSION); // today's rules: `config` itself
```

`content` est le Markdown que la configuration met en page (une chaîne ou une liste de chapitres). Sans lui, le corps des formules est épinglé dès que les formules sont activées, l’espace sous les conteneurs dès que la configuration déclare un style de paragraphe, et les deux espaces, les marques des titres, la place sous une ligne terminée par un deux-points, la coupure des encadrés, les coupures aux tirets, la coupure sous un titre et les coupures aux mots composés toujours, puisque le moteur ne peut pas savoir si le livre contient une formule, un conteneur `:::paragraphs`, une figure en ligne, un titre comportant une marque, une liste introduite par un deux-points, un encadré, un tiret collé, un titre ou un mot composé. La composition en drapeau est épinglée par la seule configuration, avec ou sans contenu. Migrez une configuration stockée une seule fois et stockez-la de nouveau sous `CONFIG_VERSION` : l’épinglage des formules multiplie l’échelle, si bien qu’une configuration migrée deux fois grandirait deux fois. La disposition des vers et les blocs de code délimités sont épinglés sans lui eux aussi, puisque le livre peut contenir un poème `:::verse` sans séparateur ou une clôture, et les retraits appariés sont épinglés par la seule configuration. Il en va de même des vers reportés de la 1.23, puisque le livre peut composer un poème ligne à ligne.

### Mettre en page et rendre un paquet

Quatre fonctions relient un paquet ouvert au moteur et aux sorties :

- **`loadBundleFonts(bundle)`** enregistre les fontes du paquet dans `document.fonts`, et leurs octets dans le registre de polices du moteur pour les images SVG (`registerFontBytes`). Attendez sa fin avant la mise en page, car celle-ci mesure le texte avec les polices dont dispose le navigateur. Les familles que le paquet nomme sans les contenir (Google Fonts) restent à charger par vos soins, comme pour tout autre document.
- **`registerBundleImages(bundle)`** décode les images pour la sortie canvas (`renderPage`, `renderToCanvas`), affiches des vidéos comprises. **`bundleImageUrl(bundle)`** est le résolveur `resourceImageUrl` de `renderToHtml`, et **`bundleVideoUrl(bundle)`** son résolveur `resourceVideoUrl` pour les fichiers vidéo que contient un paquet. Les deux recolorent les figures SVG quand `diagramStyle.singleInk` est activé, une seule fois : ils recolorent le balisage et marquent les images pour qu’aucune sortie ne les teinte de nouveau (voir [Encre unique sur le canvas et en HTML](https://postext.dev/fr/docs/configuration-resources.md#encre-unique-sur-le-canvas-et-en-html)). Les deux incorporent aussi dans chaque SVG les fontes que nomme son texte, d’abord à partir des polices propres du paquet, puis des fontes enregistrées auprès du moteur (voir [Polices dans le texte SVG](https://postext.dev/fr/docs/configuration-resources.md#polices-du-texte-svg)) ; `registerBundleImages(bundle, { onWarning })` et `bundleImageUrl(bundle, { onWarning })` signalent une famille sans fonte.
- **`buildBundle(bundle)`** met les chapitres en page dans l’ordre et renvoie un `VDTDocument` par chapitre. Chaque chapitre continue le précédent : compteurs de titres et de ressources, partie ouverte, parité et numérotation des pages. Un chapitre qui imprime la table des matières (`:::toc`) ou l’index (`:::index`) reçoit le plan du livre entier. Il accepte les mêmes options que `buildDocument`, plus `config` pour remplacer la configuration du paquet, `cache` pour partager un cache de mesures et `metadata` (voir plus bas).
- **`bundleResourceBytes(bundle)`** et **`bundleFontProvider(bundle, { decodeWoff2, fallback })`** sont les options `resourceBytes` et `fontProvider` du `renderToPdf` de `postext-pdf`. Le fournisseur de polices choisit dans le paquet la graisse la plus proche du style demandé. Pour une fonte `.woff2`, il a besoin de `decompressWoff2`, et pour une famille que le paquet ne contient pas, il appelle `fallback` avec les arguments du moteur de rendu, `request` compris, et transmet ce qu’il renvoie. Une fonction de secours qui ne télécharge que le fichier `latin` d’une famille imprime sous forme de boîtes vides une famille chinoise que le paquet n’incorpore pas ; une fonction de secours qui répond avec des tranches, comme `sliceFontProvider` dans [Polices chinoises, japonaises et coréennes](https://postext.dev/fr/docs/configuration-programmatic-usage.md#polices-chinoises-japonaises-et-coréennes), l’imprime en entier.

```ts
import { openBundle, loadBundleFonts, registerBundleImages, buildBundle, renderPage,
  bundleResourceBytes, bundleFontProvider } from 'postext';
import { renderToPdf, decompressWoff2 } from 'postext-pdf';

const bundle = await openBundle(bytes);
await loadBundleFonts(bundle);
await registerBundleImages(bundle);

const docs = buildBundle(bundle);                        // one VDTDocument per chapter
const firstPage = renderPage(docs[0].pages[0], docs[0]); // a <canvas>

const pdf = await renderToPdf(docs, {                    // the whole book
  fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
  resourceBytes: bundleResourceBytes(bundle),
});
```

Pour mettre en page un seul chapitre vous-même, passez `bundle.chapters[i].markdown`, `bundle.resources` et `bundle.config` à `buildDocument`, comme pour n’importe quel document.

**Les métadonnées du livre.** Comme dans le Sandbox, l’en-tête de métadonnées (front matter) du premier chapitre est celui du livre : `buildBundle` transmet son `title`, son `author` et le reste à chaque chapitre, si bien que les titres courants `{title}` et `{author}` tiennent sur toutes les pages et que le `doc.metadata` de chaque chapitre les porte. Un bloc de métadonnées en tête d’un chapitre ultérieur est ignoré : il est repéré par ses lignes `---` et effacé sans être analysé, si bien qu’un YAML que l’analyseur rejetterait ne fait aucun mal. `options.metadata` fournit les valeurs que l’en-tête de métadonnées ne fixe pas (l’en-tête l’emporte). Le nombre de pages du livre parvient aussi à chaque chapitre : `{bookTotalPages}` l’imprime, alors que `{totalPages}` compte les pages du chapitre (voir [Nombre de pages du livre](https://postext.dev/fr/docs/configuration-page-layout.md#nombre-de-pages-du-livre)).

```ts
const docs = buildBundle(bundle, { metadata: { author: 'A. Author' } });
docs[3].metadata.title;   // the first chapter's `title:`
```

**Où commence le livre.** Un paquet peut contenir une partie d’une publication plus longue : les pages 58 à 61 d’un numéro, le chapitre 4 d’un manuel. Son `start` dit ce qui le précède, dans les termes de la `continuation` de `buildDocument` : les pages qui précèdent la première (`pageIndexOffset`, qui décide du côté où tombe la page 1, et avec lui des marges en miroir et des titres courants pairs et impairs), la numérotation des pages en vigueur (`pageNumbering`), les compteurs de titres (`headings`), la partie ouverte (`part`) et les compteurs de ressources, d’énoncés, de notes de bas de page et de lignes. `createBundle` l’écrit comme `start` de `preset.json`, `openBundle` le renvoie sous forme de `bundle.start`, et `buildBundle` met en page le premier chapitre avec lui, comme le ferait `buildDocument({ markdown, continuation: start }, config)`, puis enchaîne les chapitres suivants à partir de là ; `{bookTotalPages}` compte aussi les pages qui précèdent le livre. Un manifeste sans `start` se lit comme avant, et un lecteur qui ne connaît pas ce champ l’ignore. `bookPageCount` n’en fait pas partie : c’est le lecteur qui compte les pages. Dans un paquet en plusieurs langues, `localized[…].start` donne à une édition son propre début.

```ts
const { bytes } = await createBundle({
  name: 'Field notes, chapter 4',
  markdown,
  config,
  // Page 58, an even page, in chapter 4.
  start: { pageIndexOffset: 57, pageNumbering: { startAt: 58 }, headings: { h1: 3 } },
});
const bundle = await openBundle(bytes);
bundle.start;                 // { pageIndexOffset: 57, pageNumbering: { startAt: 58 }, headings: { h1: 3, h2: 0, … } }
const [doc] = buildBundle(bundle);
doc.pages[0].pageLabel;       // '58'
```

### Exemple en direct : ouvrir un paquet

Le pen charge depuis le dépôt un livre d’exemple de deux chapitres (`lantern.postext`, avec son propre caractère, une figure SVG et un tableau). Il enregistre les polices et les images du paquet, met le livre en page avec `buildBundle` et peint chaque page. *Make the PDF* rend les mêmes documents avec `postext-pdf`, en incorporant les polices du paquet. Choisissez un fichier `.postext` à vous, exporté du Sandbox par exemple, pour le voir de la même façon.

> **Exemple exécutable: Postext · ouvrir un paquet .postext** — Ouvrir un fichier .postext avec postext, mettre le livre en page et le rendre en canvas et en PDF. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/open-bundle))

### Créer un paquet

`createBundle` écrit un fichier `.postext` à partir d’un document : ses chapitres, sa configuration, ses ressources et les contenus auxquels elles font référence.

```ts
import { createBundle } from 'postext';

const { bytes, manifest, warnings } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters: [
    { markdown: '# Dusk\n\nIt is drawn in :ref{id="lantern"}.' },
    { title: 'Night', markdown: '# Night\n\n…' },
  ],
  config,
  resources: [{
    id: 'lantern', typeId: 'figure', kind: 'svg', caption: 'The lantern.',
    svg: { fileId: 'lantern.svg', width: 240, height: 150 },
    createdAt: 0, updatedAt: 0,
  }],
  files: { 'lantern.svg': svgMarkup, 'garamond-regular': fontBytes },
});
```

| Entrée | Signification |
| --- | --- |
| `name`, `id`, `description`, `locale` | Les métadonnées du manifeste. `id` vaut par défaut une version normalisée (slug) de `name`. |
| `chapters` ou `markdown` | Le livre, un `{ title?, markdown }` par chapitre, ou un document unique. |
| `config` | La `PostextConfig`. Les valeurs égales aux valeurs par défaut sont omises du manifeste. |
| `resources` | Les ressources. Une image désigne son contenu par `bitmap.fileId` / `svg.fileId` (et `svg.pdfFileId` pour un original d’impression). |
| `files` | Les contenus par `fileId` (un objet ou une `Map`) : les images auxquelles font référence les ressources et les fichiers de polices auxquels font référence les variantes de `config.customFonts`. Les valeurs peuvent être un `Uint8Array`, un `ArrayBuffer`, un `Blob` ou une chaîne (balisage SVG). |
| `thumbnail` | `{ data, mime }` : une image de couverture (PNG, JPEG, WebP, GIF ou SVG). |
| `canvasScope` | `'book'` demande aux visionneuses de mettre en page le livre entier sur un seul canvas. |
| `start` | Où commence le livre, pour un paquet qui contient une partie d’un ouvrage plus long : la `continuation` avec laquelle un document unique serait construit. Écrit comme `start` du manifeste. |
| `mtime` | La date de modification écrite sur chaque fichier de l’archive (une `Date`, un horodatage ou une chaîne de date). Omise, c’est l’heure de l’appel, si bien que deux appels avec la même entrée donnent des octets différents. Passez une date fixe et la même entrée donne les mêmes octets, que vous pouvez hacher ou comparer. Un zip conserve une date et une heure sans fuseau horaire, par pas de deux secondes, de 1980 à 2099, et la date est écrite dans l’heure locale de la machine. Pour des octets identiques sur toutes les machines, construisez la date à partir de champs locaux, comme `new Date(1980, 0, 1)` : un horodatage ou une chaîne finissant par `Z` désigne un instant, qui tombe sur une heure locale différente dans chaque fuseau horaire (`'1980-01-01T00:00:00Z'` est encore en 1979 à l’ouest d’UTC). Une date hors de ces années, en heure locale, lève une exception. |
| `localized` | D’autres langues du même livre, par étiquette de langue : `{ es: { chapters?, config?, resources? } }`. Les entrées ci-dessus sont alors le contenu de `locale`, qui devient obligatoire. Voir [Paquets bilingues](https://postext.dev/fr/docs/configuration-programmatic-usage.md#paquets-bilingues). |

Il renvoie les `bytes` de l’archive, le `manifest` écrit sous forme de `preset.json`, chaque fichier dans `files` (chemin → octets) et une liste de `warnings`. Les fichiers sont nommés d’après l’identifiant de leur ressource (`resources/lantern.svg`) ou le nom de leur fichier de police (`fonts/…`), et les chapitres d’après leur ordre et leur titre (`chapters/01-dusk.md`). Les polices sont déclarées dans les `fonts` du manifeste, jamais dans `config.customFonts`. Certains éléments sont laissés de côté, chacun avec un avertissement :
- une ressource ou une fonte dont le contenu n’est pas dans `files`
- une fonte `.woff` (la sortie PDF ne sait pas l’incorporer)
- une famille marquée `redistributable: false`

Dans le navigateur, passez `bytes` à un lien de téléchargement : `URL.createObjectURL(new Blob([bytes], { type: 'application/zip' }))`. Sous Node, écrivez-les avec `fs.writeFile`. `createBundle` et `openBundle` n’ont pas besoin de DOM. Les dist utilisent des chemins de modules sans extension : sous Node seul, sans bundler, il leur faut un hook de résolution. Le fichier `docs/examples/open-bundle/build-sample.mjs` du dépôt en montre un en quelques lignes.

### Paquets bilingues

Un fichier `.postext` peut contenir un livre en plusieurs langues, et `openBundle(bytes, { locale })` le lit dans n’importe laquelle d’entre elles. `createBundle` en écrit un à partir de `localized` : une entrée par langue supplémentaire, chacune avec ce qui diffère du contenu principal (l’entrée `locale`) :

```ts
const { bytes, manifest } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters: [{ markdown: '# Dusk\n\n…' }, { markdown: '# Night\n\n…' }],
  config,
  resources: [lanternFigure, hoursTable],
  files: { 'lantern.svg': svgEn, 'lantern-es.svg': svgEs },
  localized: {
    es: {
      chapters: [{ markdown: '# Anochecer\n\n…' }, { markdown: '# Noche\n\n…' }],
      config: { headings: { levels: [{ level: 1, numberingTemplate: 'Capítulo {1}' }] } },
      resources: [
        { id: 'lantern', caption: 'El farol.', svg: { fileId: 'lantern-es.svg', width: 240, height: 150 } },
        { id: 'hours', caption: 'Horas de luz.' },
      ],
    },
  },
});

const es = await openBundle(bytes, { locale: 'es' });   // Spanish chapters, config and captions
```

- **`chapters`** : le livre dans cette langue. Les fichiers des chapitres vont dans un dossier par langue (`chapters/en/01-dusk.md`, `chapters/es/01-anochecer.md`) et les `chapters` du manifeste deviennent une table langue → chapitres. Une langue sans `chapters` lit les chapitres principaux ; quand aucune langue n’a ses propres chapitres, ceux-ci restent une liste unique.
- **`config`** : la configuration pour cette langue. Chaque clé de premier niveau remplace en bloc la clé partagée quand le paquet est lu dans cette langue : `headings`, ci-dessus, remplace tout l’objet `headings`. Les clés omises, ou égales aux clés partagées, sont partagées et ne sont pas écrites : passer la configuration complète de la langue fonctionne donc aussi bien que passer les quelques clés qui changent. Une clé fixée à ses valeurs par défaut alors que la clé partagée ne l’est pas (`layout: {}`) est écrite telle quelle, et réinitialise donc la valeur partagée. Les polices sont partagées : les familles du `customFonts` d’une langue rejoignent les `fonts` du paquet.
- **`resources`** : le texte des ressources partagées, apparié par `id` : `caption`, `note`, `altText` et le `table` d’un tableau. Une image qui contient du texte peut avoir sa propre illustration : `bitmap.fileId` ou `svg.fileId` (et `svg.pdfFileId`) désignent un autre contenu dans `files`, écrit sous `resources/es/lantern.svg`. Les autres champs, comme le type ou le placement, sont partagés. Un identifiant qui ne figure pas dans `resources` est laissé de côté avec un avertissement, et une image de langue absente laisse la langue sur l’image partagée, avec un avertissement également.

Le manifeste liste toutes les langues dans `locales` (`['en', 'es']`), garde la langue principale comme `locale` et range les autres sous `localized`. `openBundle` sans langue lit la langue principale.

**Quelle langue reçoit un lecteur.** `openBundle(bytes, { locale })` sert la langue exacte, sinon sa langue de base (`es-MX` lit `es`), sinon la langue principale, et `bundle.locale` indique celle qu’il a servie. Les chapitres et le texte viennent toujours de la même langue. La langue principale garde le texte partagé même quand `localized` contient une variante régionale de celle-ci : un paquet `pt-PT` avec une entrée `pt-BR` lit les légendes brésiliennes pour `pt-BR` seulement, et les légendes partagées pour `pt-PT` et `pt`.

### Exemple en direct : créer un paquet

Le pen construit un livre de deux chapitres avec une figure SVG et liste les fichiers que `createBundle` a écrits, ainsi que le manifeste. Il propose l’archive en téléchargement, puis la rouvre avec `openBundle` et peint sa première page : l’aller-retour complet en quelques lignes. Importez le fichier téléchargé dans le Sandbox pour continuer à y travailler.

> **Exemple exécutable: Postext · créer un paquet .postext** — Écrire un fichier .postext avec createBundle de postext, le télécharger et le rouvrir. ([code](https://github.com/drnachio/postext/tree/main/docs/examples/create-bundle))

### Travailler avec des paquets

Comme le Sandbox, le skill d’agent et le paquet `postext` lisent et écrivent tous le même fichier, un fichier `.postext` est un moyen commode de faire passer un livre d’un outil à l’autre :

- **Partir d’un paquet.** Portez une publication existante avec le [skill d’agent](https://postext.dev/fr/docs/skill.md), ou concevez un livre dans le [Sandbox](https://postext.dev/fr/sandbox.md) et exportez-le (**Télécharger (.postext)** dans le menu ⋯ de sa ligne, dans le panneau Livres). Chargez le fichier depuis votre programme avec `openBundle` pour le rendre en canvas, en HTML ou en PDF. Gardez le fichier comme source du livre : modifiez les chapitres, la configuration ou les ressources dans le code et réécrivez-le avec `createBundle`, ou rechargez-le simplement chaque fois qu’il change.
- **Déboguer et peaufiner dans le Sandbox.** Quand quelque chose dans la sortie de votre programme demande du travail (une figure qui tombe sur la mauvaise page, un style de titre, l’équilibrage des colonnes), exportez avec `createBundle` ce que votre programme met en page. Importez ce fichier dans le Sandbox (*Livres → Nouveau → Ouvrir un fichier .postext…*), corrigez le texte, la maquette ou les figures avec l’aperçu en direct, le panneau Vérifications et la vue PDF, puis exportez-le à nouveau. Votre programme charge alors le fichier corrigé avec `openBundle`. Vous pouvez aussi reporter dans votre code ce qui a changé : la `config` du manifeste ne contient que les valeurs qui diffèrent des valeurs par défaut, et se lit donc comme un court diff.

### API de bas niveau

`postext/bundle` exporte aussi les briques qui se trouvent derrière `openBundle` et `createBundle`, pour les hôtes qui stockent ou servent les paquets à leur façon (un répertoire décompressé servi en HTTP, des enregistrements dans une base de données) :

- **`openBundleZip(bytes)` / `zipBundle(files, { mtime })`** : la couche d’archive. L’ouverture tolère un dossier de premier niveau et ignore les entrées `__MACOSX` et les fichiers cachés. Les chemins qui sortent du paquet sont refusés. `mtime` date les fichiers comme le fait l’entrée de `createBundle`.
- **`readBundle(manifest, readFile, options)`** lit un manifeste, avec un rappel `readFile(path)`, et en tire chapitres, configuration, ressources, images et polices. `options` fixe la langue, la façon de nommer les identifiants de fichiers (`ids`), la configuration de base (`baseConfig`, sous celle du manifeste ; par défaut la palette et les types de ressources de `bundleBaseConfig` dans la langue du paquet, que renvoie `resolveBundleConfigLocale(manifest, locale)`, et un hôte qui passe son propre `baseConfig` devrait le localiser dans cette langue ; sous un manifeste antérieur à `configVersion: 4`, son `math` est épinglé avec celui du paquet, sous un manifeste antérieur à 5 l’espace en ligne de son `layout`, sous un manifeste antérieur à 6 l’espace dans les encadrés de son `layout`, la place sous une ligne terminée par un deux-points de son `bodyText`, les marques en ligne de ses `headings` et les tailles de ses lettrines, sous un manifeste antérieur à 7 les coupures aux tirets et la composition en drapeau de son `bodyText`, et sous un manifeste antérieur à 8 la coupure sous un titre de ses `headings` et les coupures aux mots composés et l’espace sous les conteneurs `:::paragraphs` de son `bodyText` ; voir [Paquets écrits par postext 1.4 ou antérieur](https://postext.dev/fr/docs/configuration-programmatic-usage.md#paquets-écrits-par-postext-14-ou-antérieur)) et la façon de mesurer les tailles intrinsèques. `readResolution` lit la résolution de chaque bitmap dans son fichier et la place dans `bitmap.fileResolution` ; il vaut true par défaut quand le paquet fixe `layout.bitmapResolution: 'file'`.
- **`planBundle(meta, content)` / `resolveBundleFiles(plan, sources)`** : le côté écriture, divisé en un plan pur (noms de fichiers et manifeste) et la résolution des octets au moyen des rappels `readBlob` / `readFont`.
- **`isBundleManifest(value)`**, les sélecteurs de langue (`pickChapterSpecs`, `pickLocaleOverrides`, `pickBundleView`, `resolveBundleLocale`, `resolveBundleConfigLocale`), `svgSize` / `bitmapSize` / `bitmapInfo` (les pixels d’un bitmap et la résolution qu’indique son fichier), et les types du format (`BundleManifest`, `BundleResourceSpec`, `BundleFontFamilySpec`, …).
- **`CONFIG_VERSION`, `migrateConfig(config, configVersion, { content })`, `pinLegacyHeadingBreaks(config)`, `pinLegacyMathSize(config)`, `pinLegacyInlineGap(config)`, `pinLegacyBoxResourceGap(config)`, `pinLegacyHeadingMarks(config)`, `pinLegacyDropCapSize(config)`, `pinLegacyColonListRoom(config)`, `pinLegacyBoxChildCut(config)`, `pinLegacyDashBreaks(config)`, `pinLegacyRaggedBreaking(config)`, `pinLegacyHeadingSplit(config)`, `pinLegacyParagraphContainerSpacing(config)`, `pinLegacyHyphenBreaks(config)` et `LEGACY_MATH_SIZE`** (0,5 ÷ 0,442) : une configuration stockée exprimée dans les termes actuels (voir [Paquets écrits par postext 1.4 ou antérieur](https://postext.dev/fr/docs/configuration-programmatic-usage.md#paquets-écrits-par-postext-14-ou-antérieur)). `readBundle` l’applique ; un hôte qui stocke les configurations à sa façon peut le faire aussi, une fois par copie stockée.

Le Sandbox repose sur ces fonctions. Il y ajoute ses propres identifiants de stockage et les enregistrements de pages `layouts.json`.
