# Konfiguration: Ressourcen und Tabellen

> Ressourcentypen und ihre Nummerierung, Tabellen- und Legendenstile, einfarbige Diagramme und gedruckte Videos

- HTML-Fassung: https://postext.dev/de/docs/configuration-resources
- Zuletzt aktualisiert: 2026-10-10
- Lesezeit: 5 min
- Andere Sprachen: [en](https://postext.dev/en/docs/configuration-resources.md), [es](https://postext.dev/es/docs/configuration-resources.md), [ca](https://postext.dev/ca/docs/configuration-resources.md), [fr](https://postext.dev/fr/docs/configuration-resources.md), [it](https://postext.dev/it/docs/configuration-resources.md), [pt](https://postext.dev/pt/docs/configuration-resources.md), [zh](https://postext.dev/zh/docs/configuration-resources.md), [ja](https://postext.dev/ja/docs/configuration-resources.md), [ar](https://postext.dev/ar/docs/configuration-resources.md)

## Kurz gesagt

Diese Seite behandelt die Einstellungen für Abbildungen, Tabellen, Diagramme und Videos. Postext nennt sie Ressourcen und nummeriert jede Art für sich. Sie legen fest, wie Tabellen aussehen: ihre Linien, ihre Flächen, ihre Schrift und wie sie sich über Seiten verteilen. Sie legen fest, wie die Legende einer Abbildung oder Tabelle gesetzt wird. Außerdem können Sie Diagramme in einer einzigen Druckfarbe drucken und wählen, wie ein Video auf Papier erscheint.

## Ressourcentypen

Ein **Ressourcentyp** ist eine frei definierbare Kategorie – *Abbildung*, *Tabelle*, *Diagramm*, *Listing* … –, die bestimmt, wie die Ressourcen dieser Art nummeriert, mit Legenden versehen und referenziert werden. Die Liste steht in `config.resourceTypes`; die Sandbox bearbeitet sie unter **Gestaltung → Abbildungen & Tabellen → Nummerierung und Platzierung**.

Ist `config.resourceTypes` nicht gesetzt, bringt Postext drei eingebaute Standardtypen mit: **Abbildung**, **Tabelle** und **Video**, jeder für sich nach `{h1}.{n}` nummeriert (mit Neubeginn bei jeder Überschrift der Ebene 1) und mit Dezimalzählern. Eine Liste ohne den Typ `video` – ein Buch, das gespeichert wurde, bevor es Videos gab – nummeriert Video-Ressourcen vom Typ `video` trotzdem: Für sie wird der eingebaute Videotyp ergänzt (`effectiveResourceTypes(config, resources)`), und `defaultVideoResourceType(locale)` liefert ihn einzeln. Benannt werden sie in der Sprache des Dokuments: `config.locale`, sonst `bodyText.hyphenation.locale`, sonst Englisch (siehe [Dokumentsprache](https://postext.dev/de/docs/configuration-text.md#dokumentsprache)).

Die eingebauten Standardtypen richten sich nach der Sprache. Die exportierte Funktion `defaultResourceTypes(locale = 'en')` übersetzt die Typnamen, Kurzbezeichnungen und Legendenpräfixe in die Sprache des Dokuments – Englisch ergibt *Figure*/*Fig.* und *Table*/*Tab.*, Spanisch *Figura*/*Fig.* und *Tabla*/*Tabla*; Französisch, Deutsch, Italienisch, Portugiesisch, Katalanisch und Niederländisch haben ihre eigenen (die Tabelle unter [Dokumentsprache](https://postext.dev/de/docs/configuration-text.md#dokumentsprache) führt sie auf). Regionale Tags wie `es-ES` werden über die Sprache aufgelöst, und jede Sprache ohne Übersetzungen fällt auf Englisch zurück. Das Nummerierungsverhalten (`numberingTemplate: '{h1}.{n}'`, `resetOn: 'h1'`, Dezimalzähler) ist sprachunabhängig. Jeder Aufruf liefert neue Objekte, Sie können das Ergebnis also frei verändern:

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

const types = defaultResourceTypes('es');
// => [{ id: 'figure', name: 'Figura', shortLabel: 'Fig.', captionPrefix: 'Figura',
//       numberingTemplate: '{h1}.{n}', resetOn: 'h1', counterFormat: 'decimal', … },
//     { id: 'table',  name: 'Tabla',  shortLabel: 'Tabla', captionPrefix: 'Tabla', … },
//     { id: 'video',  name: 'Vídeo',  shortLabel: 'Vídeo', captionPrefix: 'Vídeo', … }]
```

```ts
type ResourceCounterFormat =
  | 'decimal'
  | 'roman-lower'
  | 'roman-upper'
  | 'alpha-lower'
  | 'alpha-upper';

type ResourceCounterReset = 'never' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';

interface ResourcePlacement {
  position?: 'auto' | 'top' | 'bottom' | 'here'; // which free slot a float may take; 'here' = inline embed at the ::resource directive
  span?: 'column' | 'page' | 'side';             // one column, the full content width, or the float-only side column
  rotate?: 'ccw' | 'cw';                         // a quarter turn: a landscape table on a page of its own
  width?: number;                                // fraction (0 < width < 1) of the column or page width; default: the whole width
  align?: 'left' | 'center' | 'right';           // where a float narrower than its column sits; default 'left'
  captionSide?: boolean;                         // caption beside the figure, in the side column of a oneAndHalf layout (column floats and resources set in the text)
  columns?: number;                              // a 'column' float across this many adjacent columns (since 1.18)
  wrap?: 'none' | 'left' | 'right' | 'start' | 'end'; // text beside the resource, at that side of its column (since 1.24)
  wrapGap?: Dimension;                           // space between the wrapped resource and the text (since 1.24)
}

interface ResourceType {
  id: string;                          // stable id, referenced by Resource.typeId
  name: string;                        // singular display name, e.g. "Figure"
  namePlural?: string;                 // optional plural, e.g. "Figures"
  shortLabel: string;                  // compact label for inline refs, e.g. "Fig."
  numberingTemplate: string;           // "{h1}.{n}" or "{n}"
  resetOn: ResourceCounterReset;       // when the {n} counter resets
  counterFormat: ResourceCounterFormat;// how {n} is formatted
  captionPrefix: string;               // prepended to the caption, e.g. "Figure"
  defaultPlacement?: ResourcePlacement;// fallback placement for this type's resources
}
```

`ResourcePlacement` hat dieselbe Form, die eine Ressource in ihrem eigenen `placement` setzt. `position` wählt die Art des freien Platzes, den ein Gleitobjekt einnehmen darf – `auto` (der Standard) nimmt den ersten nach dem ersten Verweis, `top` / `bottom` beschränken es auf diese Art von Band, `here` bettet die Ressource in den Text ein. `span` legt die Ausdehnung des Gleitobjekts fest: eine Spalte, die volle Inhaltsbreite oder die nur für Gleitobjekte bestimmte Seitenspalte eines Layouts mit anderthalb Spalten. `rotate` dreht die Ressource um eine Vierteldrehung und macht sie zu einem Gleitobjekt über die ganze Seitenbreite auf einer eigenen Seite. `width` verschmälert ein Gleitobjekt auf einen Bruchteil seiner Spalte (oder der Seite bei einem Gleitobjekt über die ganze Seitenbreite) – etwa eine kleine Tabelle in einer breiten Spalte. `align` gibt an, wo ein solches schmaleres Gleitobjekt sitzt – standardmäßig links, mittig oder rechts – und wo ein Bild, das schmaler als sein Platz ist, darin sitzt: eine Bitmap, die kleiner als die Spalte ist, oder ein Bild, das `layout.fitFiguresToPage` verkleinert hat. Legende und Anmerkung behalten die Satzbreite des Platzes. (Bis postext 1.4 wurde ein solches Bild immer linksbündig gesetzt.) `captionSide` setzt die Legende neben die Abbildung in die nur für Gleitobjekte bestimmte Seitenspalte eines Layouts mit anderthalb Spalten (`layout.sideColumnRole: 'floats'`), auf Höhe der Oberkante der Abbildung (ihrer Unterkante bei einem Gleitobjekt unten); es gilt für Spalten-Gleitobjekte und seit postext 1.28 auch für eine im Text gesetzte Ressource (`position: 'here'`, eine Spalte breit, ohne Textumfluss), deren Legende und Anmerkung in der Seitenspalte auf Höhe der Oberkante ihres Körpers stehen, wo diese Spalte daneben frei ist, und unter dem Körper, wo sie es nicht ist (`layout.inlineCaptionSide`, standardmäßig eingeschaltet; `false` beschränkt seitliche Legenden auf Spalten-Gleitobjekte); eine Seite ohne eine solche Spalte setzt die Legende unter die Abbildung. Setzen weder die Ressource noch ihr Typ eine Platzierung, gilt der eingebaute Standard `auto` / `column`. `shrink` (`'never'`, `'page'`, `'slot'`) und `minScale` (0,7, wenn nicht gesetzt) verkleinern ein gleitendes Bild auf den Raum seines Platzes, statt es weiterzuschieben, und `captionMeasure: 'body'` setzt Legende und Anmerkung eines Bildes, das schmaler als sein Platz ist, auf die Breite des Bildes (siehe [Dokumentformat › Platzierung](https://postext.dev/de/docs/document-format.md#platzierung)); `layout.floatShrink` liefert den Dokumentstandard für die ersten beiden. `wrap` setzt eine eingebettete Ressource oder ein einspaltiges Gleitobjekt an eine Seite seiner Spalte, und der Text läuft daneben, mit dem Abstand `wrapGap`; `layout.wrap` enthält die Standardwerte (siehe [Dokumentformat › Textumfluss](https://postext.dev/de/docs/document-format.md#textumfluss)). `citingPage` lässt ein Gleitobjekt mit `top` oder `auto` die Seite oder Spalte anführen, auf der seine verweisende Zeile landet, statt den ersten freien Platz danach zu nehmen; `layout.floatsAtCitingPage` liefert den Dokumentstandard und `layout.maxTopFraction` den Anteil der Spalte, den es einnehmen darf (siehe [Dokumentformat › Platzierung](https://postext.dev/de/docs/document-format.md#platzierung)).

`columns` (seit postext 1.18) legt ein Gleitobjekt mit `span: 'column'` auf einer mehrspaltigen Seite über so viele benachbarte Spalten: ein Bild über zwei der fünf Spalten einer Zeitung. Seine Satzbreite sind diese Spalten samt den Spaltenabständen dazwischen. Es nimmt den Kopf einer Folge leerer Spalten ein, die auf gleicher Höhe beginnen, oder den Fuß der verweisenden Spalte und der leeren Spalten danach; so viele Spalten, wie die Seite hat, oder mehr machen es zu einem Gleitobjekt über die ganze Seitenbreite. Es wird ignoriert bei den Ausdehnungen `'page'` und `'side'`, bei einer gedrehten Ressource (`rotate`) und bei einer eingebetteten Ressource (`here`), und `captionSide` gilt nur für ein Gleitobjekt von einer Spalte Breite.

| Eigenschaft | Typ | Beschreibung |
| --- | --- | --- |
| `id` | `string` | Stabiler Bezeichner, auf den das `typeId` jeder Ressource verweist. Wird einmal beim Anlegen des Typs gesetzt; wer einen Typ löscht, auf den noch Ressourcen verweisen, löst die Warnung **verwaister Typ** aus. |
| `name` | `string` | Anzeigename im Singular. Wird vom Verweis mit `style="full"` verwendet (z. B. `Figure 1.7`). |
| `namePlural` | `string` (optional) | Anzeigename im Plural, für Beschriftungen der Oberfläche und Ressourcenlisten. |
| `shortLabel` | `string` | Kompakte Abkürzung für den Standardstil des Verweises im Text (z. B. `Fig. 1.7`). |
| `numberingTemplate` | `string` | Vorlage für die berechnete Nummer. Siehe **Platzhalter der Vorlage** weiter unten. Übliche Formen sind `{h1}.{n}` (kapitelbezogen, z. B. `2.3`) und `{n}` (eine einzige fortlaufende Zählung). |
| `resetOn` | `ResourceCounterReset` | `'never'` ergibt eine fortlaufende Zählung über das ganze Dokument; `'h1'`..`'h6'` setzen den Zähler `{n}` jedes Mal zurück, wenn eine Überschrift dieser Ebene (oder einer übergeordneten) auftritt. Wählen Sie die Überschriftenebene, die in der Vorlage vorkommt – z. B. `{h1}.{n}` mit `resetOn: 'h1'`. |
| `counterFormat` | `ResourceCounterFormat` | Wie der Zähler `{n}` dargestellt wird: dezimal (`1, 2, 3`), römisch klein/groß (`i, ii` / `I, II`) oder alphabetisch klein/groß (`a, b` / `A, B`). Die Schreibweisen für Seiten und Listen werden ebenfalls angenommen (`'lower-roman'`, `'arabic'` …; siehe [Schreibweisen der Nummernformate](https://postext.dev/de/docs/configuration-page-layout.md#schreibweisen-der-nummernformate)); ein unbekannter Wert zählt dezimal und wird gemeldet. Überschriften-Platzhalter (`{h1}` …) werden immer dezimal dargestellt. |
| `captionPrefix` | `string` | Text, der der Bildunterschrift bzw. Tabellenüberschrift vorangestellt wird. Die berechnete Nummer folgt dem Präfix – eine Legende erscheint als `{captionPrefix} {number}. {caption text}`, z. B. **Figure 1.7. The original plan.** Ein Typ mit leerem `numberingTemplate` hat keine Nummer, und seine Legende lautet `{captionPrefix}. {caption text}`. Leerzeichen am Ende des Präfixes entfallen, und ein Präfix, das bereits auf `.`, `:`, `!`, `?` oder `…` endet (oder auf deren Form in voller Breite), erhält keinen zweiten Punkt: **Pl. Lines at 0°**. |
| `defaultPlacement` | `ResourcePlacement` (optional) | Platzierung für Ressourcen dieses Typs, die kein eigenes `placement` setzen: `position`, `span`, `rotate`, `width`, `align`, `captionSide` und `columns`, jedes unabhängig aufgelöst. Setzen weder die Ressource noch der Typ ein Feld, gilt der eingebaute Standard: `auto` / `column`, aufrecht, volle Breite, linksbündig, Legende unter der Abbildung. Siehe [Nummerierung und Verweise](https://postext.dev/de/docs/configuration-resources.md#nummerierung-und-verweise) weiter unten für die Auflösungskette und [Dokumentformat › Ressourcen](https://postext.dev/de/docs/document-format.md#ressourcen) für die Wirkung jedes Werts, auch bei gedrehten Ressourcen. |
| `captionStyle` | `CaptionStyleConfig` (optional) | Teilweise Überschreibung des [Legendenstils](https://postext.dev/de/docs/configuration-resources.md#legendenstil) für Ressourcen dieses Typs. Nur die gesetzten Schlüssel ersetzen den globalen `captionStyle`; alles andere wird geerbt (eine überschriebene `color` bestimmt auch die Farben von Bezeichnung und Anmerkung, sofern diese nicht ausdrücklich gesetzt sind). Typischer Einsatz: Tabellen mit Überschrift *oben* auf einem farbigen Balken, während Abbildungen ihre Legende unten behalten. Palettenverweise werden wie jede andere Farbe aufgelöst. |

### Platzhalter der Vorlage

`numberingTemplate` wird mit derselben Engine dargestellt wie die Nummerierung der Überschriften (siehe [Überschriften](https://postext.dev/de/docs/configuration-text.md#überschriften)). Sie kennt zwei Arten von Platzhaltern:

- `{n}` – der Zähler des Typs, formatiert nach `counterFormat`. Dieser Wert steigt mit jeder Ressource und wird gemäß `resetOn` zurückgesetzt.
- `{h1}` … `{h6}` – die an der Stelle des ersten Verweises geltenden Überschriftennummern, dezimal, sofern der Platzhalter keinen Stil nennt. `{h1}` ist die aktuelle Nummer der Überschrift der Ebene 1, `{h2}` die der Ebene 2 und so weiter.
- `{h1:A}`, `{h2:i}`, `{n:i}` – ein Platzhalter, gefolgt von einem Doppelpunkt und einem Stil, wird in diesem Stil geschrieben, und zwar in jedem, den eine Überschriftenvorlage annimmt: `1`, `01`, `A`, `a`, `I`, `i`, ein Name eines Nummerierungsformats wie `upper-roman`, ein Ein-Zeichen-Platzhalter wie `一` oder `①` oder ein ausgeschriebener Stil wie `Words`. Bei `{n}` hat der Stil für diesen Platzhalter Vorrang vor `counterFormat`. Ein unbekannter Stil zählt dezimal. Seit postext 1.28.

Jeder andere Text wird wörtlich übernommen. Ein Backslash maskiert ein wörtliches `{`, `}` oder `\`. Hat ein Überschriften-Platzhalter keinen gültigen Wert (z. B. `{h1}` vor der ersten Überschrift der Ebene 1), entfällt er samt dem angrenzenden Trennzeichen – `{h1}.{n}` wird so sauber zum bloßen Zähler.

Eine leere Vorlage (`''`) druckt keine Nummer, der Typ zählt seine Ressourcen aber dennoch: Eine Legende lautet *Do. Lines at 0°*, und ein `:ref` druckt nur die Bezeichnung (*Do*). Bis postext 1.4 lautete eine solche Legende *Do .Lines at 0°*, und der Verweis endete mit einem geschützten Leerzeichen.

| Vorlage | Mit `h1 = 2`, Zähler = 3 | Hinweise |
| --- | --- | --- |
| `{n}` | `3` | Eine einzige fortlaufende Zählung. Mit `resetOn: 'never'` kombinieren. |
| `{h1}.{n}` | `2.3` | Kapitelbezogen. Mit `resetOn: 'h1'` kombinieren. |
| `{h1}.{h2}.{n}` | `2.0.3` | Abschnittsbezogen. Mit `resetOn: 'h2'` kombinieren. |
| `{h1:A}.{n}` | `B.3` | Mit Buchstaben, für die Anhänge. Mit `resetOn: 'h1'` kombinieren. |

**Anhänge.** Ein Buch, dessen Anhänge über einen Überschriftenstil mit Buchstaben gezählt werden (`numberingTemplate: 'Appendix {1:A}'`, mit `startAt=1` beim ersten), gibt ihren Abbildungen und Tabellen einen eigenen Typ, `{ ...table, id: 'table-a', numberingTemplate: '{h1:A}.{n}', resetOn: 'h1' }`. Ein Typ dient allen Anhängen: Er druckt *Table A.1*, *A.2* und *B.1*, in der Legende wie in `:ref`. Ein schlichtes `{h1}` druckt dort die Nummer der Überschrift dezimal (*Table 1.1*).

### Nummerierung und Verweise

Die Nummer, die ein Ressourcentyp erzeugt, ist das, was `:ref` druckt und dem das Legendenpräfix vorangeht. `:ref{id}` ist die Hauptform: Der erste Verweis in Lesereihenfolge **bindet** die Ressource **ein**, die dann auf den ersten freien Platz nach diesem Verweis gleitet – an den Fuß der verweisenden Spalte, an Kopf oder Fuß der nächsten leeren Spalte oder in ein Band der nächsten Seite (gemäß ihrer aufgelösten Platzierung – `position: 'auto' | 'top' | 'bottom' | 'here'` und `span: 'column' | 'page' | 'side'`, dazu `rotate`, `width`, `align` und `captionSide`, aufgelöst pro Ressource, dann über `defaultPlacement` des Typs, dann über den eingebauten Standard `auto` / `column`; `'top'` / `'bottom'` beschränken die Suche auf diese Art von Platz). Die Blockeinbettung `::resource{id}` ist optional und nur für `placement.position: 'here'` nötig – eine nicht gleitende Einbettung an genau einer Stelle des Textflusses. Die vollständige Grammatik auf der Dokumentseite – beide Formen sowie die Optionen `style` und `text` von `:ref` – ist unter [Dokumentformat › Ressourcen](https://postext.dev/de/docs/document-format.md#ressourcen) beschrieben, einschließlich der Frage, wie die Reihenfolge der ersten Verweise die Zählung bestimmt.

Das entspricht der Nummerierung der Überschriften: So wie eine Überschriftenebene ein `numberingTemplate` trägt, trägt auch ein Ressourcentyp eines – der Ressourcenzähler (`{n}`) steigt aber mit jedem ersten Verweis statt mit jeder Überschrift, und `resetOn` bindet ihn wieder an die Gliederung der Überschriften.

### Was nummeriert wird

Eine Ressource wird nummeriert, wenn der Text auf sie verweist – mit `:ref` oder mit einer Einbettung `::resource` –, in der Reihenfolge dieser ersten Verweise und unabhängig von ihrer Platzierung: gleitend, eingebettet (`here`), in der Seitenspalte oder gedreht. Eine Ressource, die nur eine Gestaltung zeichnet – ein Element `image` eines Kapitelauftakts, eines Kolumnentitels oder einer Teiltitelseite –, oder auf die nichts verweist, erhält keine Nummer und lässt den Zähler ihres Typs unverändert. In einem Fotoessay, dessen randabfallende Tafeln Auftaktbilder sind und dessen einzige kleinere Tafel ein zitiertes Gleitobjekt ist, ist dieses Gleitobjekt also Tafel **I**, wie viele Tafeln die Auftakte auch davor gezeigt haben; nummerieren Sie die Auftakttafeln in ihrer Gestaltung (mit einem Attribut wie `{attr.plate}`) und behalten Sie den Zähler den Tafeln vor, die der Text zitiert.

In einem Buch, das Kapitel für Kapitel umbrochen wird (die Sandbox, `buildBundle` oder `buildDocument` mit den Zählern, die `continuationAfter()` weiterreicht), zählt der erste Verweis im ganzen Buch: Eine Ressource behält die Nummer, die sie in dem Kapitel erhalten hat, das zuerst auf sie verweist, und nur dieses Kapitel platziert sie. Das `:ref` eines späteren Kapitels druckt diese Nummer und platziert nichts, und eine Einbettung `::resource` einer gleitenden Ressource ist dort nur ein weiterer Verweis (eine eingebettete Ressource mit `here` wird weiterhin dort gesetzt, wo sie steht). Ein solcher Verweis verlinkt auf die Abbildung, wo sich die Abbildung in derselben Ausgabe befindet: Ein PDF des ganzen Buchs verlinkt ihn auf die Seite des früheren Kapitels. Ein einzeln gerendertes Kapitel, in HTML oder PDF, setzt ihn als schlichten Text in der Linkfarbe, da seine Abbildung nicht in diesem Dokument steht. Ein Host, der das HTML der Kapitel auf einer Seite zusammenführt, übergibt `renderToHtml` die von den Kapiteln verankerten Ressourcen als `refTargets`, und ein solcher Verweis verlinkt wieder auf die Abbildung des früheren Kapitels:

```ts
import { anchoredResourceIds, buildBundle, renderToHtml } from 'postext';

const docs = buildBundle(bundle);
const refTargets = new Set(docs.flatMap((d) => [...anchoredResourceIds(d)]));
const html = docs.map((d) => renderToHtml(d, { refTargets })).join('');
```

**Geändert in postext 1.5:** Bis 1.4 ließ jedes Kapitel, das auf eine Abbildung verwies, sie erneut gleiten, und das HTML jedes `:ref` war ein Link, ob seine Abbildung auf der Seite stand oder nicht.

`{h1}` ist die fortlaufende Zählung der Überschriften der Ebene 1: Jede H1 erhöht sie, sofern ihr Überschriftenstil nicht `numbered: false` setzt – ein leeres `numberingTemplate` blendet die Nummer der Überschrift aus, hält die Zählung aber nicht an. Ein Artikel, dessen einzige H1 sein Titel ist, nummeriert seine Abbildungen mit den eingebauten Typen `{h1}.{n}` daher `1.1`, `1.2` … Zwei Wege, um Abbildung 1, 2 … zu drucken:

- ein Typ, nummeriert mit `{n}` und `resetOn: 'never'` (mit `resetOn: 'h1'` begänne die Zählung bei jeder H1 neu);
- ein [Überschriftenstil](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile) mit `numbered: false` für den Titel und jede andere H1, die nicht zählen soll: Eine solche Überschrift erhöht `{h1}` nicht, sondern lässt es, wie es war – leer vor der ersten gezählten H1, wo `{h1}.{n}` zum bloßen Zähler wird –, und löst nie `resetOn: 'h1'` aus, die Zählung läuft also über sie hinweg weiter. Nach `# Introduction` und deren Abbildung 1.1 ist die erste Abbildung unter einem nicht nummerierten `# Appendix` die 1.2, nicht die 2.1.

```ts
// Figure 1, 2, 3… in a single-article document
resourceTypes: defaultResourceTypes('en').map((t) => ({ ...t, numberingTemplate: '{n}', resetOn: 'never' })),
```

## Tabellenstil

Die Eigenschaft `tableStyle` steuert Typografie und Schmuck der Tabellenressourcen – aller Tabellen, sofern eine nicht einen [benannten Tabellenstil](https://postext.dev/de/docs/configuration-resources.md#benannte-tabellenstile) wählt. Kopf- und Inhaltszellen werden unabhängig gestaltet. Schriftfamilie, Schriftgröße und Farben erben, wenn sie nicht gesetzt sind, die Werte des aufgelösten Fließtexts; ein Dokument ohne `tableStyle` setzt Tabellen also in der Typografie des Fließtexts.

```ts
const config: PostextConfig = {
  tableStyle: {
    headerBold: true,
    headerBackground: { hex: '#f0f0f0', model: 'hex' },
    borders: true,
    borderWidth: { value: 0.75, unit: 'pt' },
  },
};
```

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `bodyFontFamily` | `string` | Schrift des Fließtexts | Schriftfamilie der Inhaltszellen. |
| `bodyFontSize` | `Dimension` | Schriftgröße des Fließtexts | Schriftgröße der Inhaltszellen. |
| `bodyColor` | `ColorValue` | Farbe des Fließtexts | Textfarbe der Inhaltszellen. |
| `headerFontFamily` | `string` | Schrift des Fließtexts | Schriftfamilie der Kopfzellen. |
| `headerFontSize` | `Dimension` | Schriftgröße des Fließtexts | Schriftgröße der Kopfzellen. |
| `headerColor` | `ColorValue` | Farbe des Fließtexts | Textfarbe der Kopfzellen. |
| `headerBold` | `boolean` | `true` | Kopfzellen fett setzen. |
| `headerItalic` | `boolean` | `false` | Kopfzellen kursiv setzen. |
| `headerLetterSpacing` | `Dimension` | `0pt` | Laufweite nach jedem Zeichen einer Kopfzelle, Leerzeichen eingeschlossen, wie CSS `letter-spacing`. Positive Werte sperren die Buchstaben (ein Kopf in Versalien verlangt meist `0.05em` bis `0.1em`), negative Werte ziehen sie zusammen. Ein `em` entspricht der Schriftgröße des Kopfs. Die Kopfzeilen werden damit gemessen, sie umbrechen, zentrieren und richten sich also mit der Sperrung aus, und Canvas, HTML und PDF zeichnen sie gleich. Es gilt für jede Kopfzelle: die Kopfzeilen und jede mit `isHeader` markierte Zelle. |
| `headerTextTransform` | `'none' \| 'uppercase'` | `'none'` | Die Kopfzellen in Versalien setzen. Der Text behält seine Länge, sodass die Sandbox jeden Buchstaben weiterhin der Quelle zuordnet: Ein Buchstabe, dessen Großbuchstabe länger ist (`ß`), bleibt, wie er ist. Ressourcenverweise behalten ihre Bezeichnung. |
| `headerBackgroundEnabled` | `boolean` | `true` | Eine Fläche hinter die Kopfzeile legen. |
| `headerBackground` | `ColorValue` | `#f0f0f0` | Flächenfarbe der Kopfzeile. |
| `bodyBackgroundEnabled` | `boolean` | `false` | Eine Fläche hinter die Inhaltszeilen legen. |
| `bodyBackground` | `ColorValue` | `#ffffff` | Flächenfarbe der Inhaltszeilen (nur gezeichnet, wenn eingeschaltet). |
| `bodyAlternateBackgroundEnabled` | `boolean` | `false` | Zebrastreifen: jede zweite Inhaltszeile mit `bodyAlternateBackground` füllen. Siehe [Zebrastreifen](https://postext.dev/de/docs/configuration-resources.md#zebrastreifen). |
| `bodyAlternateBackground` | `ColorValue` | `#f2f2f2` | Fläche der abwechselnden Inhaltszeilen (nur gezeichnet, wenn eingeschaltet). |
| `borders` | `boolean` | `true` | Zellrahmen zeichnen. |
| `borderColor` | `ColorValue` | Farbe des Fließtexts | Farbe der Rahmenlinien. |
| `borderWidth` | `Dimension` | `0.75pt` | Stärke der Rahmenlinien (≈ 1 px bei 96 DPI; skaliert mit der DPI der Seite). Wird von `'booktabs'` nicht verwendet, das eigene Stärken hat. |
| `cellPadding` | `Dimension` | `0.375em` | Innenabstand jeder Zelle. |
| `rules` | `'grid' \| 'horizontal' \| 'outer' \| 'none' \| 'booktabs'` | `'grid'` | Welche Linien gezeichnet werden, wenn `borders` eingeschaltet ist: das volle Zellraster, nur waagerechte Linien (Ober- und Unterkante jeder Zeile, keine senkrechten), nur der äußere Rahmen, keine oder die drei Linien einer Zeitschriftentabelle (siehe [Booktabs-Linien](https://postext.dev/de/docs/configuration-resources.md#booktabs-linien)). |
| `borderRadius` | `Dimension` | `0` | Eckenradius des äußeren Tabellenrahmens. Der Rahmen wird gerundet gezeichnet (mit den Linien `grid` oder `outer`), die Zellflächen und der Kopfhintergrund werden darauf beschnitten – auch mit `rules: 'none'` oder ausgeschalteten Rahmen –, und waagerechte Linien werden an seiner Außenkontur gekürzt; die inneren Linien bleiben gerade. Eine über Seiten geteilte Tabelle rundet die oberen Ecken ihres ersten Teils und die unteren ihres letzten. Begrenzt auf die halbe Breite und Höhe der Tabelle. Linien mit `'booktabs'` bleiben gerade (die Flächen werden trotzdem beschnitten). |
| `heavyRuleWidth` | `Dimension` | `0.08em` | Booktabs: die Linien über der Tabelle und unter ihrer letzten Zeile. Ein `em` entspricht der Schriftgröße der Inhaltszellen. |
| `lightRuleWidth` | `Dimension` | `0.05em` | Booktabs: die Linie unter den Kopfzeilen und die Gruppenlinien. |
| `spanRuleWidth` | `Dimension` | `0.03em` | Booktabs: die Linien unter Kopfzellen, die mehrere Spalten überspannen. |
| `spanRules` | `'trimmed' \| 'full' \| 'none'` | `'trimmed'` | Booktabs: die Linien unter Kopfzellen, die mehrere Spalten überspannen, über der letzten Kopfzeile: an beiden Enden um `spanRuleTrim` gekürzt, über die ganze Zelle oder keine. |
| `spanRuleTrim` | `Dimension` | `0.5em` | Booktabs: um wie viel eine gekürzte Spannlinie an jedem Ende verkürzt wird. |
| `groupRules` | `boolean` | `false` | Booktabs: eine dünne Linie über jeder Inhaltszeile, die eine Gruppe anführt. |
| `continuedFootRule` | `'bottom' \| 'light' \| 'none'` | `'light'` | Booktabs: was einen Teil einer geteilten Tabelle abschließt, die auf der Folgeseite weitergeht. |
| `overflow` | `'split' \| 'clip' \| 'hide'` | `'split'` | Was mit einer Tabelle geschieht, die höher als die Seite ist: sie auf den folgenden Seiten fortsetzen, nur die passenden Zeilen behalten oder sie weglassen. Mit `splitInline` auch mit einer im Text gesetzten Tabelle, die nicht in den Rest ihrer Spalte passt. Siehe unten. |
| `splitInline` | `boolean` | `true` | `overflow` auch auf mit `here` platzierte Tabellen anwenden: Eine eingebettete Tabelle, die nicht in den restlichen Raum ihrer Spalte passt, wird zwischen Zeilen geteilt und am Kopf der nächsten Spalte fortgesetzt. `false` schiebt eine solche Tabelle ganz in die nächste Spalte, wie bis postext 1.24; Konfigurationen früherer Versionen, deren Kapitel eine Ressource einbetten, werden mit `false` gelesen. Siehe [Tabellen, die höher als die Seite sind](https://postext.dev/de/docs/configuration-resources.md#tabellen-die-höher-als-die-seite-sind). Seit postext 1.25. |
| `continuedSuffix` | `string` | `'(cont.)'` | Wird kursiv, nach einem Leerzeichen, an die Legende jedes fortgesetzten Teils einer geteilten Tabelle angehängt; ein Suffix, das mit einem chinesischen Zeichen oder einem Zeichen voller Breite beginnt (`'（续）'`), wird ohne Abstand an die Legende gesetzt. |
| `continuesMarkerEnabled` | `boolean` | `true` | Unter jeden Teil, der auf der nächsten Seite weitergeht, einen Hinweis setzen. |
| `continuesMarker` | `string` | `'Continued'` / `'Continúa'` | Text dieses Hinweises, rechtsbündig unter den Teil in der Schrift der Anmerkung gesetzt (siehe [Legendenstil](https://postext.dev/de/docs/configuration-resources.md#legendenstil)). Der Standard folgt der Sprache des Dokuments (siehe [Dokumentsprache](https://postext.dev/de/docs/configuration-text.md#dokumentsprache) für die acht Sprachen). |

Rahmenstärken bleiben gebrochen: Eine Linie von `0.5pt` wird im PDF und auf dem Bildschirm als Haarlinie gezeichnet, statt auf ein volles Pixel aufgerundet zu werden (die Untergrenze liegt bei 0,25 px).

### Zebrastreifen

Lange Datentabellen lassen sich quer leichter verfolgen, wenn jede zweite Zeile getönt ist. `bodyAlternateBackgroundEnabled` schaltet die Streifen ein, `bodyAlternateBackground` setzt ihre Farbe:

```ts
const config: PostextConfig = {
  tableStyle: {
    bodyBackgroundEnabled: true,
    bodyBackground: { hex: '#ffffff', model: 'hex' },
    bodyAlternateBackgroundEnabled: true,
    bodyAlternateBackground: { hex: '#eef3fa', model: 'hex' },
  },
};
```

Gezählt wird ab der ersten Zeile nach den Kopfzeilen (`TableModel.headerRowCount` oder die führenden Zeilen aus Kopfzellen): Diese Zeile behält `bodyBackground` – oder keine Fläche, solange `bodyBackgroundEnabled` ausgeschaltet ist –, die nächste erhält die abwechselnde Fläche und so weiter. Die Zählung folgt dem Tabellenmodell, nicht der Seite; eine über Seiten geteilte Tabelle behält also auf jeder Seite den Streifen jeder Zeile, und eine über Zeilen verbundene Zelle nimmt den Streifen ihrer ersten Zeile an. Kopfzellen behalten die Kopffläche, ein eigenes `background` einer Zelle hat Vorrang vor beiden, und eine an die Palette gebundene Farbe folgt der Palette. Ein [benannter Tabellenstil](https://postext.dev/de/docs/configuration-resources.md#benannte-tabellenstile) setzt die beiden Felder wie jedes andere, sodass ein Stil gestreift sein kann, während die übrigen Tabellen des Dokuments es nicht sind; in der Sandbox sind das der Schalter *Zebrastreifen* und seine Farbe unter **Inhaltszellen**.

Im VDT tragen die Zellen der abwechselnden Zeilen `alternate: true`, und das Tabellenlayout trägt `bodyAlternateBackground`. `tableCellFill(table, cell)` liefert die Fläche, mit der eine Zelle gezeichnet wird – ihre eigene, die des Kopfs, die abwechselnde oder die der Inhaltszeilen –, und genau diese zeichnen Canvas, HTML und PDF. Benachbarte Flächen stoßen ohne Fuge aneinander: Ein Browser mit gebrochenem Pixelverhältnis oder ein PDF-Betrachter glättet jede Fläche für sich und ließe die Seite durch eine Haarlinie zwischen zwei Zellen durchscheinen; daher zeichnen die HTML- und PDF-Ausgaben `tableCellFillRects(table)` – die Fläche jeder Zelle mit einem Streifen über jede Kante, die sie mit einer später gezeichneten Zelle teilt, die ihn dann überdeckt –, und der Canvas rastet seine Flächen auf Gerätepixel ein.

### Booktabs-Linien

Tabellen in Zeitschriften und Lehrbüchern werden meist mit drei Linien und ohne senkrechte Linien gesetzt: einer kräftigen Linie über der Tabelle, einer dünnen unter dem Kopf und einer kräftigen unter der letzten Zeile, dazu kurze Linien unter den Köpfen, die mehrere Spalten zusammenfassen (das LaTeX-Paket `booktabs`: `\toprule`, `\midrule`, `\cmidrule`, `\bottomrule`). `rules: 'booktabs'` zeichnet dieses Muster:

```ts
const config: PostextConfig = {
  tableStyle: {
    rules: 'booktabs',
    borderColor: { hex: '#000000', model: 'hex' },
    headerBackgroundEnabled: false,
  },
};
```

- Die Linie über der Tabelle und die unter ihrer letzten Zeile sind `heavyRuleWidth` stark (`0.08em`), die Linie unter den Kopfzeilen `lightRuleWidth` (`0.05em`). Eine Tabelle ohne Kopfzeilen hat keine Kopflinie.
- Eine Kopfzelle, die über der letzten Kopfzeile mehrere Spalten überspannt, erhält darunter eine Linie der Stärke `spanRuleWidth` (`0.03em`). Mit `spanRules: 'trimmed'` (dem Standard) wird die Linie an beiden Enden um `spanRuleTrim` (`0.5em`) gekürzt, sodass sich die Linien unter zwei benachbarten Köpfen nicht berühren; `'full'` führt sie über die ganze Zelle, `'none'` lässt sie weg.
- `groupRules: true` fügt über jeder Inhaltszeile, die eine Gruppe anführt (eine einzelne Zelle über die ganze Tabelle oder eine Zeile aus Kopfzellen), eine dünne Linie hinzu, es sei denn, die Zeile eröffnet die Tabelle oder eine Seite, wo bereits die Kopflinie steht.
- Die Stärken werden gegen die Schriftgröße der Inhaltszellen (`bodyFontSize`) aufgelöst, eine größere Kopfschrift macht die Kopflinie also nicht dicker. Eine Stärke von `0` lässt die betreffende Linie entfallen.
- Die Linien nehmen `borderColor` (eine an die Palette gebundene Farbe folgt der Palette und `:::part palette`), und `borders: false` schaltet sie aus. `borderWidth` gilt nicht, ebenso wenig `borderRadius`: Die Linien bleiben gerade, während die Zellflächen weiterhin auf den gerundeten Rahmen beschnitten werden. Kopfflächen, Zebrastreifen und ein eigenes `background` einer Zelle wirken wie bei den anderen Mustern, unter den Linien.
- Eine über Seiten geteilte Tabelle wiederholt ihre Kopfzeilen in jedem Teil, jeder Teil beginnt also mit der kräftigen Linie und der Kopflinie. Die kräftige Linie unter der letzten Zeile schließt nur den letzten Teil ab; ein Teil, der auf der Folgeseite weitergeht, endet mit `continuedFootRule`: einer dünnen Linie (`'light'`, der Standard), der kräftigen Linie (`'bottom'`) oder keiner (`'none'`).

Das Layout berechnet die Linien einmal. Die VDT-Tabelle trägt sie als `strokes` (`{ x1, y1, x2, y2, widthPx }`, relativ zur linken oberen Ecke des Tabellenkörpers), und Canvas, HTML-Betrachter, PDF und EPUB mit festem Layout zeichnen genau diese; in einem getaggten PDF sind sie Layout-Artefakte. Das umfließende EPUB schreibt sie als CSS-Rahmen der Tabelle und ihres Kopfs, die gekürzten Linien als Hintergrundlinien. In der Sandbox zeigt **Booktabs** in der Auswahl *Linien* diese Felder und blendet *Rahmenstärke* und *Eckenradius* aus.

### Benannte Tabellenstile

Selten setzt ein Dokument alle Tabellen gleich: eine Checkliste in einem marineblauen Raster mit gerundetem Rahmen, eine Optionszeile nur mit äußerem Rahmen, eine Datentabelle mit schlichten waagerechten Linien. `tableStyles` deklariert benannte Varianten, und eine Tabellenressource wählt eine davon mit `table.styleId`. Jedes Feld, das ein Stil nicht setzt, wird zuerst aus `tableStyle` und danach aus dem Fließtext gelesen; ein Stil nennt also nur, was seine Tabellen unterscheidet. Eine Tabelle ohne `styleId` oder mit einer ID, die kein Stil deklariert, behält `tableStyle` – ein Dokument ohne `tableStyles` erscheint genau wie zuvor.

```ts
const config: PostextConfig = {
  tableStyle: {
    borderColor: { hex: '#163a76', model: 'hex' },
    borderWidth: { value: 1.3, unit: 'pt' },
    borderRadius: { value: 10, unit: 'pt' },
  },
  tableStyles: [
    {
      id: 'option',
      name: 'Option row',
      rules: 'outer',
      borderColor: { hex: '#7a9cc6', model: 'hex' },
      borderWidth: { value: 1, unit: 'pt' },
      borderRadius: { value: 8, unit: 'pt' },
      headerBackgroundEnabled: false,
    },
  ],
};

// In the resources: this table is set in the "option" style.
const resource: Resource = {
  id: 'choices', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
  table: { model: { rows: [/* … */] }, styleId: 'option' },
};
```

Jeder Eintrag nimmt jedes Feld von `tableStyle` sowie `id` (worauf `table.styleId` verweist) und einen optionalen `name` für den Editor (standardmäßig die ID). Alles, was ein Stil setzen kann, gilt pro Tabelle – Typografie, Flächen, Rahmen, Linien, Eckenradius, Innenabstand und das Überlaufverhalten samt seinen Fortsetzungstexten. `resolveTableStylesConfig(styles, tableStyle, resolvedBodyText, locale?)` liefert die aufgelöste Liste, `pickTableStyle(resolved, styleId)` den Stil, in dem eine Tabelle gesetzt wird, und `stripTableStylesDefaults` entfernt die nicht gesetzten Felder (es behält ein Feld, das seinem eingebauten Standard gleicht, denn dieses überschreibt weiterhin einen abweichenden Wert von `tableStyle`). In einem umfließenden EPUB ist ein benannter Stil eine Klasse der Tabelle (`pt-table-<id>`), gestaltet vom Stylesheet des Buchs.

### Tabellen, die höher als die Seite sind

Eine gleitende Tabelle, die nicht auf die frische Seite passt, die ihr angeboten wird, wird weder gestaucht noch läuft sie über: Mit `overflow: 'split'` (dem Standard) teilt die Engine sie zwischen Zeilen an der letzten Kante, die auf die Seite passt, und setzt sie auf so vielen folgenden Seiten fort wie nötig. Jeder fortgesetzte Teil wiederholt die Kopfzeilen der Tabelle (`TableModel.headerRowCount` oder, wenn nicht gesetzt, die führenden Zeilen aus Kopfzellen) und trägt erneut die Legende, mit `continuedSuffix` nach der Beschreibung – „Table 6-4. Title *(cont.)*“. Jeder Teil, der weitergeht, erhält `continuesMarker` darunter, rechtsbündig, in der Schrift der Anmerkung; die Anmerkung der Tabelle wird für den letzten Teil zurückgehalten. Ein Schnitt verläuft nie durch eine verbundene Zelle (ein Rowspan wandert ganz in den nächsten Teil), und eine Zeile, die die Zeilen darunter anführt – eine einzelne Zelle über die ganze Tabelle –, wird in den nächsten Teil mitgenommen, statt allein am Fuß einer Seite zu stehen. Eine Booktabs-Tabelle schließt jeden weitergehenden Teil mit `continuedFootRule` ab (siehe [Booktabs-Linien](https://postext.dev/de/docs/configuration-resources.md#booktabs-linien)).

**Wo der erste Teil endet.** Eine Tabelle, der nach ihrem Verweis der Kopf einer leeren Spalte angeboten wird, nimmt die Zeilen, die dort passen, und geht im nächsten Platz weiter. Sie füllt die Spalte bis zum Fuß, wenn sie die Spalte für sich hat: Ein Teil, der weniger als drei Zeilen Fließtext darunter ließe, nimmt auch diese, statt einen Textrest zu hinterlassen. Enthält die Spalte bereits ein anderes Gleitobjekt-Band – etwa eine Abbildung über die ganze Seitenbreite am Kopf der Seite –, endet der Teil stattdessen mindestens drei Zeilen Fließtext vor dem Fuß; das ist der Raum für Text, den jedes Gleitobjekt lässt, wenn es eine Spalte mit einem anderen teilt, sodass die Spalte mit etwas Text unter der Tabelle endet statt nur mit Gleitobjekten. Damit eine lange Tabelle bis zum Fuß ihrer Spalte läuft, verweisen Sie auf sie dort, wo die Seite, auf der sie beginnt, kein anderes Gleitobjekt trägt (etwa nach der Seite einer Abbildung über die ganze Seitenbreite), oder passen Sie ihre Zeilen der Spalte an.

`'clip'` behält die führenden Zeilen, die auf die Seite passen, und verwirft den Rest stillschweigend (die Anmerkung schließt den Teil dennoch ab); `'hide'` lässt die Tabelle ganz weg. Beide gelten nur, wenn die Tabelle höher als eine Seite ist: Eine Tabelle, die passt, wird in jedem Modus ganz platziert.

**Im Text gesetzte Tabellen.** Eine mit `here` platzierte Tabelle (eingebettet mit `::resource`) folgt seit postext 1.25 denselben Regeln (`splitInline`, standardmäßig eingeschaltet). Passt sie nicht in den restlichen Raum ihrer Spalte, wird sie zwischen Zeilen geteilt: Der erste Teil behält den Gleitobjektabstand darüber und mindestens die Kopfzeilen und zwei Inhaltszeilen (sind es weniger, beginnt die ganze Tabelle wie bisher in der nächsten Spalte), jeder spätere Teil eröffnet die nächste Spalte ohne Abstand darüber, gesetzt auf die Breite dieser Spalte (ein Layout mit anderthalb Spalten setzt einen Teil in der schmalen Spalte auf deren Breite), und der Text nach der Zeile `::resource` folgt dem letzten Teil. Wiederholter Kopf, Legende mit Suffix, Hinweis, Anmerkung am letzten Teil, der Rest von drei Zeilen und die Schnitte, die verbundene Zellen und Gruppenköpfe respektieren, sind die einer gleitenden Tabelle. Eine Tabelle mit weniger als fünf Inhaltszeilen wird nie geteilt. `'clip'` behält die führenden Zeilen einer eingebetteten Tabelle, die höher als eine Spalte ist, gesetzt am Kopf einer Spalte; `'hide'` lässt eine solche Tabelle weg; eine kürzere wandert in beiden Modi ganz in die nächste Spalte. Eine Seite, die eine eingebettete Tabelle eröffnet, hält nur den Raum ihres ersten Teils frei von den Gleitobjekten, die sie aufnimmt, sodass eine wartende Abbildung über die ganze Seitenbreite diese Seite anführt und die Tabelle darunter weitergeht. Eingebettete Tabellen auf einer vertikalen Seite und Tabellen in einem Kasten werden nicht geteilt. `splitInline: false` schiebt eine eingebettete Tabelle ganz in die nächste Spalte, wie bis postext 1.24.

Die Fortsetzungstexte richten sich standardmäßig nach der Sprache des Dokuments (`locale`, sonst die Sprache der Silbentrennung): Englisch `(cont.)` / `Continued`, Spanisch `(cont.)` / `Continúa`, entsprechend auf Französisch, Deutsch, Italienisch, Portugiesisch, Katalanisch und Niederländisch (aufgeführt unter [Dokumentsprache](https://postext.dev/de/docs/configuration-text.md#dokumentsprache)).

Zellinhalt ist Inline-Markdown, und ein Zeilenumbruch in einer Zelle – ein Zeilenvorschub oder `\\` wie in Legenden und Anmerkungen – beginnt einen neuen Absatz. Ein Absatz, der mit einem Aufzählungszeichen oder Strich (`•`, `-`, `*`, `–`) oder einer Zahl (`1.`, `1)`) gefolgt von einem Leerzeichen beginnt, wird als Listeneintrag gesetzt: Das Zeichen wird so gezeichnet, wie es geschrieben ist, der Text hängt um den Wert `unorderedLists.gap` des Dokuments davon ab, umbrochene Zeilen fluchten mit dem Text, und zwei führende Leerzeichen verschachteln eine Ebene. Eine Zelle, geschrieben als `• Ofrece elección\n• Acomoda a personas diestras y zurdas`, ergibt also eine Liste mit zwei Einträgen. Eine Zeile aus nichts als gewöhnlichen Leerzeichen fügt nichts hinzu; eine Zeile mit einem geschützten Leerzeichen (U+00A0) ist eine Zeile der Zelle, wie in CommonMark, sodass `1\n` gefolgt von einem geschützten Leerzeichen die Zeile zwei Zeilen hoch macht. Ein geschütztes Leerzeichen am Ende des Zelltexts behält seine Breite: `760` mit einem geschützten Leerzeichen, rechtsbündig über `(231)`, endet ein Leerzeichen vor der Kante, was die 0 nahe an die 1 rückt. Genau fluchten die Ziffern nur, wo das Leerzeichen so breit ist wie die Klammer, und in den meisten Schriften ist es schmaler. (Bis postext 1.4 entfielen beide.)

Spaltenbreiten gehören zum Tabellenmodell, nicht zum Stil: `TableModel.columnWidths` ist ein optionales Array relativer Gewichte, eines pro Spalte, beim Umbruch normalisiert – `[2, 1, 1]` gibt der ersten Spalte die halbe Breite. Ein fehlendes Array, eine falsche Länge oder ein nicht positives Gewicht fallen auf eine gleichmäßige Aufteilung zurück. Der Tabelleneditor hält das Array stimmig, wenn Spalten hinzugefügt oder entfernt werden.

### Tabellenmodelle aufbauen

Ein `TableModel` ist ein zeilenweises Raster, und jede Zelle wird nach ihrem Platz darin gesetzt: `rows[r][c]` steht in Spalte `c`. Eine verbundene Zelle behält die von ihr überdeckten Zellen daher im Raster, jede mit `hiddenBy` auf ihre Hauptzelle markiert – anders als eine HTML-Tabelle, die sie weglässt. Die aus `postext` exportierten Modellhelfer erhalten diese Form; es sind reine Funktionen, die ein neues Modell liefern: `mergeCells(model, { start, end })` und `unmergeCell(model, at)`, `addRow`, `addColumn`, `removeRow`, `removeColumn`, `setCellContent`, `setCellImage`, `setCellBackground` und `setAlignment`. Die vier Helfer für Zeilen und Spalten halten Verbindungen ganz: Eine innerhalb eines verbundenen Blocks hinzugefügte Zeile oder Spalte verbreitert ihn, eine davor hinzugefügte verschiebt ihn, eine daraus entfernte verkleinert ihn – ein Block, der seine erste Zeile oder Spalte verliert, behält seinen Inhalt in seiner neuen linken oberen Zelle –, und jedes `hiddenBy` zeigt weiterhin auf seine Hauptzelle.

`parseTSV(text, options?)` baut ein Modell aus tabulatorgetrenntem Text – einem aus einer Tabellenkalkulation eingefügten Bereich: Zeilen werden an Zeilenumbrüchen getrennt, Zellen an Tabulatoren, und kurze Zeilen werden aufgefüllt, damit das Raster rechteckig ist. `headerRows` macht die führenden Zeilen zu Kopfzeilen: Ihre Zellen erhalten `isHeader` und das Modell `headerRowCount`, sodass eine über Seiten geteilte Tabelle sie wiederholt.

```ts
import { parseTSV, mergeCells } from 'postext';

let model = parseTSV('Part\tQty\tNote\nBolt\t4\tM6\nNut\t8\t', { headerRows: 1 });
// model.headerRowCount === 1; model.rows[0][0] is { content: 'Part', isHeader: true }
model = mergeCells(model, { start: { row: 2, col: 1 }, end: { row: 2, col: 2 } });
// rows[2][1] gets colSpan: 2; rows[2][2] stays in the grid with hiddenBy: { row: 2, col: 1 }
```

`tableGridIssues(model)` prüft das Raster. Für ein einwandfreies Modell liefert es eine leere Liste, andernfalls jede Stelle, an der das Raster bricht, in Zeilenreihenfolge: `spanOverlap`, eine sichtbare Zelle unter dem `colSpan` / `rowSpan` einer anderen Zelle (`coveredBy` nennt diese Zelle) – was eine nach HTML-Art weggelassene überdeckte Zelle bewirkt, da jede Zelle danach auf die Verbindung rutscht –, und `missingCells`, eine Zeile, die vor der letzten Spalte endet, ohne dass eine Verbindung den Rest überdeckt, was ein Loch hinterlässt.

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

tableGridIssues({
  rows: [
    [{ content: 'A', colSpan: 2 }, { content: 'C' }],
    [{ content: '1' }, { content: '2' }, { content: '3' }],
  ],
});
// => [{ kind: 'spanOverlap', row: 0, col: 1, coveredBy: { row: 0, col: 0 } },
//     { kind: 'missingCells', row: 0, col: 2 }]
```

Eine vom Dokument verwendete Tabelle, deren Raster solche Probleme hat, wird in `doc.contentWarnings` als `raggedTableGrid` gemeldet (siehe [Warnungen im Dokument](https://postext.dev/de/docs/configuration-programmatic-usage.md#warnungen-im-dokument)).

## Legendenstil

Die Eigenschaft `captionStyle` steuert die Legenden der Ressourcen (die Zeile `Figure 1 — …` unter – oder über – Bildern, SVGs und Tabellen). Nummerierte Bezeichnung und Beschreibung teilen Schriftart und Schriftgröße – eine Vorgabe der Engine –, die Bezeichnung kann aber eigene Schriftstärke, Kursivierung und Farbe haben. Schriftfamilie, Schriftgröße und Farbe erben, wenn nicht gesetzt, die Werte des Fließtexts. Die Legende kann **über** der Ressource stehen (bei Tabellen üblich) und auf einem farbigen **Balken** über die Breite des Blocks gesetzt werden; eine optionale kleinere **Anmerkung** (Quellenangabe, Bildnachweis – `Resource.note`) wird über das Unterobjekt `note` gestaltet. Ein Ressourcentyp kann jedes dieser Felder für seine eigenen Ressourcen über `ResourceType.captionStyle` überschreiben (siehe [Ressourcentypen](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)).

```ts
const config: PostextConfig = {
  captionStyle: {
    align: 'center',
    labelBold: true,
    labelColor: { hex: '#295AA3', model: 'hex' },
    descriptionItalic: true,
    position: 'above',
    backgroundEnabled: true,
    padding: { value: 0.35, unit: 'em' },
    note: { italic: true, align: 'left' },
  },
};
```

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `fontFamily` | `string` | Schrift des Fließtexts | Schriftfamilie der Legende (Bezeichnung und Beschreibung). |
| `fontSize` | `Dimension` | Schriftgröße des Fließtexts | Schriftgröße der Legende (Bezeichnung und Beschreibung). |
| `color` | `ColorValue` | Farbe des Fließtexts | Textfarbe der Beschreibung. |
| `align` | `'left' \| 'center' \| 'right' \| 'justify' \| 'start' \| 'end'` | `'left'` | Waagerechte Ausrichtung der Legendenzeilen. `'justify'` treibt jede Zeile außer der letzten auf die volle Breite aus. Auf einem Legendenbalken richten sich die Zeilen innerhalb seines Innenabstands aus; eine seitliche Legende richtet sich innerhalb ihrer eigenen Breite aus. |
| `gap` | `Dimension` | `0.75em` | Senkrechter Abstand zwischen der Ressource und ihrer Legende. |
| `labelBold` | `boolean` | `true` | Die nummerierte Bezeichnung (z. B. `Figure 1`) fett setzen. |
| `labelItalic` | `boolean` | `false` | Die nummerierte Bezeichnung kursiv setzen. |
| `labelColor` | `ColorValue` | `color` der Legende | Farbe der nummerierten Bezeichnung. |
| `descriptionItalic` | `boolean` | `false` | Den Beschreibungstext kursiv setzen. |
| `position` | `'above' \| 'below'` | `'below'` | Wo die Legende steht. Mit `'above'` kommt die Legende (samt Balken) zuerst, und der Körper der Ressource rückt um die Höhe der Legende plus `gap` nach unten; die Anmerkung steht dann unter dem Körper. |
| `backgroundEnabled` | `boolean` | `false` | Einen Balken hinter die Legende legen. Der Balken reicht über die volle Breite des Blocks und umschließt die Legendenzeilen plus `padding` auf jeder Seite. |
| `background` | `ColorValue` | Hauptfarbe der Palette | Flächenfarbe des Balkens (nur gezeichnet, wenn eingeschaltet). |
| `padding` | `Dimension` | `0.35em` | Innenabstand zwischen der Kante des Balkens und dem Legendentext. Wird ignoriert, wenn der Balken ausgeschaltet ist. |
| `note` | `object` | — | Gestaltung der Anmerkung der Ressource – siehe die Untertabelle unten. |
| `labelNumberGap` | `string` | geschütztes Leerzeichen; `''` in einem japanischen Dokument | Was zwischen Bezeichnung und Nummer steht, in der Legende und in einem `:ref` im Text: *Figure 1.7*, *Fig. 1.7*. Chinesisch und Japanisch setzen beide ohne Abstand: `''` ergibt 图1-1 und ist der Standard in einem japanischen Dokument (図1-1). |
| `labelSeparator` | `string` | `'. '`; `'　'` in einem japanischen Dokument | Was der Nummer folgt, vor der Beschreibung: *Figure 1.7. A caption*. Chinesische Legenden nehmen ein ideografisches Leerzeichen, `'　'` (图1-1　标题), japanische standardmäßig ebenso (図1-1　東京の地図, JLReq §4.3). Eine Bezeichnung ohne Nummer behält ihre eigene Regel: ein Punkt, es sei denn, das Präfix endet bereits mit einem. |

Das Unterobjekt `note` gestaltet `Resource.note`, einen kurzen Text (Quelle, Bildnachweis, eine Bemerkung), der in kleinerer Schrift unter die Ressource gesetzt wird. Er nimmt dieselben Inline-Auszeichnungen und `:ref`-Verweise an wie die Legende und erbt deren Schrift. Er steht unter der Legende, wenn die Legende unten steht, und unter dem Körper der Ressource, wenn sie oben steht; seine Höhe zählt zum Block, eine Ressource mit Anmerkung gleitet also als eine Einheit.

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `note.fontSize` | `Dimension` | 0,85 × Schriftgröße der Legende | Schriftgröße der Anmerkung. |
| `note.color` | `ColorValue` | `color` der Legende | Textfarbe der Anmerkung. |
| `note.italic` | `boolean` | `false` | Die Anmerkung kursiv setzen. |
| `note.gap` | `Dimension` | `0.35em` | Abstand zwischen der Anmerkung und dem, was ihr vorangeht (Legende oder Körper). |
| `note.align` | `'left' \| 'center' \| 'right' \| 'justify' \| 'start' \| 'end'` | `'left'` | Waagerechte Ausrichtung der Anmerkungszeilen, wie `align` bei der Legende. |

Überschreibungen pro Typ werden mit `mergeCaptionStyle(resolvedCaptionStyle, override, palette?)` zusammengeführt, das für Hosts exportiert wird, die dieselbe Auflösung außerhalb der Pipeline brauchen.

## Diagrammstil

Die Eigenschaft `diagramStyle` steuert, wie eingebettete SVG-Diagramme (Ressourcen mit `kind: 'svg'`) eingefärbt werden und in welchen Schriften ihr Text gesetzt wird. Der **Einfarbmodus** ist ein Umfärbedurchgang, der jede Farbe eines Diagramms auf einen Tonwert einer einzigen Druckfarbe abbildet, sodass Abbildungen getreu wiedergegeben werden, wenn das Dokument mit einer einzigen Schmuckfarbe gedruckt wird. **Eingebettete Schriften** bringen in jedes SVG die Schnitte mit, die sein Text nennt, sodass seine Beschriftungen auf dem Canvas, in HTML und in EPUB in den Schriften des Dokuments gesetzt werden (siehe [Schriften in SVG-Text](https://postext.dev/de/docs/configuration-resources.md#schriften-in-svg-text)).

```ts
const config: PostextConfig = {
  diagramStyle: {
    singleInk: true,
    inkColor: { hex: '#295AA3', model: 'hex' },
  },
};
```

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `singleInk` | `boolean` | `false` | Jedes eingebettete SVG-Diagramm auf Tonwerte einer einzigen Druckfarbe umfärben. |
| `inkColor` | `ColorValue` | Hauptfarbe (`#295AA3`) | Die Druckfarbe. Standard ist die Hauptfarbe der Palette des Dokuments (über `paletteId: 'main-color'` an die Palette gebunden); wer das Farbfeld der Palette austauscht, tönt also die Diagramme zusammen mit Überschriften und fettem Text neu. |
| `inlineFonts` | `boolean` | `true` | In jedes SVG die Schnitte, die sein Text nennt (`font-family`), als `@font-face`-Data-URIs einbetten, bevor es als Bild gezeigt wird: auf dem Canvas, in HTML, in EPUB und im Raster-Ersatz des PDF. Wird nie in die gespeicherte Datei geschrieben. Eine Ressource verzichtet darauf mit `svg.inlineFonts: false` (seit postext 1.25). |

### Wie der Einfarbmodus arbeitet

Ist `singleInk` eingeschaltet, wird jede Farbe im SVG-Markup in einen Tonwert von `inkColor` umgeschrieben, dessen Stärke **1 − relative Leuchtdichte** beträgt (Koeffizienten nach Rec. 709, angewandt auf die gammakodierten Kanäle – eine wahrnehmungsnahe Näherung, die für die Tonwertabbildung völlig genügt). Die Abbildung erhält den wahrgenommenen Helligkeitswert: Weiß wird zu Papierweiß, Schwarz zur vollen Druckfarbe, und helle Flächen bleiben hell, unabhängig von ihrem ursprünglichen Farbton. Ein blassgelber Hintergrund wird zu einem blassen Tonwert der Druckfarbe; eine dunkle Kontur nähert sich der vollen Druckfarbe.

Das Umfärben übernimmt die exportierte Funktion `applySingleInkToSvg(svgText, inkHex)`, die ohne DOM auf dem SVG-Markup als Text arbeitet:

- Hex-Literale `#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa`, die Funktionen `rgb()` / `rgba()` und `hsl()` / `hsla()` werden umgeschrieben, wo immer sie vorkommen – Präsentationsattribute, Inline-`style`, Verläufe, `<defs>`. Die Funktionen dürfen ganzzahlige, dezimale oder prozentuale Kanäle und die Komma- oder Leerzeichensyntax verwenden, sodass auch `rgb(11.37%, 20%, 50.59%)` (wie Cairo es schreibt) und `rgb(51 102 153 / 50%)` umgefärbt werden. Eine getönte Funktion wird als `rgb(…)` oder `rgba(…)` zurückgeschrieben.
- Die Schlüsselwörter `white` und `black` werden nur dort ersetzt, wo sie als Farbwerte stehen (`fill`, `stroke`, `stop-color`, `flood-color`, `color` – als Attribute oder Eigenschaften im Inline-Stil), nie in Textinhalten oder Beschriftungen.
- `none`, `transparent` und `currentColor` bleiben unberührt, ebenso die übrigen benannten Farben (`red`, `steelblue` …) und das Standardschwarz einer Form oder eines Texts ohne Füllung. Geben Sie solchen Elementen eine ausdrückliche Farbe, damit sie umgefärbt werden.
- Alphakanäle bleiben erhalten (die Halbbytes von `#rgba` / `#rrggbbaa` und die Alphakomponenten von `rgba(…)` werden unverändert mitgeführt; ein prozentuales Alpha wird als Zahl geschrieben).
- Lässt sich `inkHex` nicht auswerten, wird die Eingabe unverändert zurückgegeben.
- Das Ergebnis trägt `data-postext-single-ink="#…"` (die Druckfarbe) an seinem Wurzelelement `<svg>`, und Markup, das diese Markierung bereits trägt, wird so zurückgegeben, wie es ist, gleich welche Druckfarbe es nennt. Die Abbildung ist nicht idempotent – ein zweiter Durchgang hellt jede Farbe auf, Schwarz kommt bei etwa zwei Dritteln der Druckfarbe heraus –, daher wird ein Bild einmal umgefärbt, gleich ob Ihr Code oder eine der Ausgaben es zuerst erreicht. (Die Markierung ist neu in postext 1.5; von 1.4 umgefärbtes Markup hat keine.)

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

const recoloured = applySingleInkToSvg(svgText, '#295AA3');
applySingleInkToSvg(recoloured, '#295AA3') === recoloured; // true: never twice
```

Der Einfarbmodus gilt in allen drei Ausgaben: Die PDF-Ausgabe färbt die SVG-Bytes um, die `resourceBytes` ihr übergibt, bevor sie sie als Vektoren zeichnet, und die Canvas- und HTML-Ausgaben tönen die SVG-Bilder, die sie zeichnen, wenn Sie es verlangen (siehe [Einfarbmodus auf dem Canvas und in HTML](https://postext.dev/de/docs/configuration-resources.md#einfarbmodus-auf-dem-canvas-und-in-html)); das exportierte PDF entspricht also der Vorschau auf dem Bildschirm.

Auflöser und Bereiniger entsprechen denen der anderen Abschnitte, neben den Typen `DiagramStyleConfig` / `ResolvedDiagramStyleConfig`:

```ts
import {
  DEFAULT_DIAGRAM_STYLE_CONFIG,
  resolveDiagramStyleConfig,
  stripDiagramStyleDefaults,
  applySingleInkToSvg,
} from 'postext';
import type { DiagramStyleConfig, ResolvedDiagramStyleConfig } from 'postext';

const resolved = resolveDiagramStyleConfig(config.diagramStyle);
// => { singleInk: false, inkColor: { hex: '#295AA3', model: 'hex', paletteId: 'main-color' } }

const minimal  = stripDiagramStyleDefaults(config.diagramStyle);
// => undefined when everything matches the defaults
```

### Einfarbmodus auf dem Canvas und in HTML

Die Canvas- und HTML-Ausgaben erhalten Bilder bereits dekodiert (`registerResourceImage`) oder als URLs (`resourceImageUrl`), nicht als SVG-Markup. Auf Anforderung wenden sie dieselbe Abbildung auf das an, was sie zeichnen:

- **Canvas** (`renderPage`, `renderPageToCanvas`, `renderToCanvas`). Jedes SVG-Bild, für das die Tönung gilt – eine Abbildung, das Bild einer Tabellenzelle, ein Gestaltungsbild, ein Kastensymbol oder -marker –, wird in seiner platzierten Größe gerastert und seine Pixel werden auf die Druckfarbe getönt, gleich ob es als `<img>` oder als `ImageBitmap` registriert wurde. Die getönte Bitmap wird wie jedes Vektorraster zwischengespeichert. Ein Bitmap-Bild wird nie getönt.
- **HTML** (`renderToHtml`, `renderToHtmlIndexed`). Jedes SVG-`<img>` erhält `filter: url(#pt-ink-…)`, das auf eine `feColorMatrix` verweist, die seine Seite mitführt: ein `<svg>` ohne Größe, das den `<filter>` enthält, als Erstes auf der Seite steht und in der indizierten Ausgabe Teil des `decorationHtml` der Seite ist. Jede Seite trägt ihn, solange der Einfarbmodus gilt, ob sie ein Bild enthält oder nicht, sodass ein Host, der Blöcke einzeln austauscht, nie ein Bild einbringt, dessen Filter fehlt.

**Nie zweimal getönt.** Bis postext 1.4 zeichneten die Canvas- und HTML-Ausgaben Bilder so, wie sie übergeben wurden; Hosts färbten das Markup daher selbst mit `applySingleInkToSvg` um, bevor sie es übergaben. Die Paket-Adapter und die Sandbox tun das noch immer, weil der Markup-Durchgang genau die Farben des PDF ergibt (siehe den letzten Absatz unten). Ein Bild wird daher einmal getönt, nach drei Regeln, die in allen drei Ausgaben gelten:

- **Markiertes Markup bleibt unberührt.** Die PDF-Ausgabe färbt `resourceBytes` mit `applySingleInkToSvg` um, bereits umgefärbte SVG-Bytes werden also so gezeichnet, wie sie sind. Auf dem Canvas und in HTML wird ein Bild, das aus einer SVG-Data-URI geladen wurde, deren Markup die Markierung trägt, ebenfalls nie getönt.
- **Aus, sofern nicht verlangt, in postext 1.x.** Der Canvas tönt ein SVG-Bild, das ohne eigenes Flag registriert wurde, nur, wenn der Render-Aufruf `singleInk: true` übergibt (`RenderPageOptions`), und eines, das mit `registerResourceImage(id, img, { singleInk: true })` registriert wurde, bei jedem Render-Aufruf. Die HTML-Ausgabe tönt, wenn `renderToHtml` `singleInk: true` erhält oder wenn sein Auflöser `resourceImageUrl` `singleInk: true` trägt. Ein für 1.4 geschriebener Host, der das Markup umfärbt und das dekodierte Bild ohne Flag registriert, behält seine Ausgabe. Das nächste Hauptrelease wird standardmäßig tönen.
- **`singleInk: false` wird nie getönt.** Hinter einer Blob- oder Netzwerk-URL lässt sich das Markup nicht zurücklesen; ein Bild, das Sie selbst umgefärbt haben und auf diesem Weg dekodieren, wird daher mit `singleInk: false` registriert, wie es `registerBundleImages` und die Sandbox tun. `bundleImageUrl(bundle)` liefert einen Auflöser, der `singleInk: false` trägt, und `bundleResourceBytes` übergibt dem PDF die eigenen Bytes des Pakets, die die PDF-Ausgabe einmal umfärbt.

Beide Ausgaben kennen die Art jedes Bildes aus dem VDT: Eine Abbildung und ein Zellbild tragen die Art ihrer Ressource, und ein Block mit Gestaltungsbild trägt `imageKind` (`'svg'` oder `'bitmap'`), beim Umbruch von seiner Ressource übernommen. Bei einem VDT, der erstellt wurde, bevor es `imageKind` gab, behandelt der Canvas ein als Vektorquelle registriertes Gestaltungsbild als SVG, und die HTML-Ausgabe eines, dessen URL eine SVG-Data-URI ist oder auf `.svg` endet.

Färben Sie entweder das Markup selbst um oder lassen Sie die Ausgaben das unveränderte Bild tönen, nicht beides:

```ts
import { applySingleInkToSvg, registerResourceImage, renderPage, renderToHtml } from 'postext';

// Raw SVG: tinted while diagramStyle.singleInk is on…
registerResourceImage('diagram.svg', rawImg, { singleInk: true });
// …or register it plainly and ask on each render.
registerResourceImage('diagram.svg', rawImg);
const canvas = renderPage(doc.pages[0], doc, { singleInk: true });

// Recoloured before decoding (as postext 1.4 hosts do): painted as given.
const inked = applySingleInkToSvg(svgText, ink);
registerResourceImage('diagram.svg', await decode(inked), { singleInk: false });

// The HTML backend with URLs to the raw markup.
const html = renderToHtml(doc, { resourceImageUrl: urlFor, singleInk: true });
```

`renderToHtml` übernimmt seinen Standardwert für `singleInk` aus dem eigenen Flag des Auflösers, `bundleImageUrl(bundle)` braucht also keine Option.

Für jede Farbe, die der Markup-Durchgang umschreibt (Hex-, `rgb()`- und `hsl()`-Werte, `white` und `black`; siehe [Wie der Einfarbmodus arbeitet](https://postext.dev/de/docs/configuration-resources.md#wie-der-einfarbmodus-arbeitet)), ergibt die Pixelabbildung dasselbe Ergebnis, geglättete Kanten und Verläufe eingeschlossen. Die beiden unterscheiden sich dort, wo der Markup-Durchgang eine Farbe unberührt lässt: Benannte Farben außer `white` und `black`, `currentColor`, Formen und Text ohne Füllung (im Standardschwarz gezeichnet) und im SVG eingebettete Bitmaps werden auf dem Bildschirm getönt, behalten im PDF aber ihre Farbe. Geben Sie jedem Element eines Diagramms eine ausdrückliche Farbe als Hex-, `rgb()`- oder `hsl()`-Wert, um eine identische Ausgabe zu erhalten. Kann der Canvas die Pixel nicht zurücklesen (ein `<img>` von einem anderen Ursprung, ohne CORS geladen), wird das Bild ungetönt gezeichnet.

### Schriften in SVG-Text

Ein SVG-Bild wird über ein Bild gezeigt: ein `<img>` auf dem Canvas, in HTML und in EPUB. Ein Bilddokument sieht die Webschriften der Seite nicht, sodass `<text font-family="IBM Plex Sans">` auf eine Systemschrift zurückfiele. Seit postext 1.25 bettet die Engine die vom Text genannten Schnitte in das Markup ein, bevor das Bild dekodiert oder als URL herausgegeben wird: eine `@font-face`-Regel pro Schriftdatei, ihre Bytes als Data-URI, in einem `<style>` direkt nach dem Wurzel-Tag `<svg>`. Das PDF braucht nichts davon: Es setzt SVG-Text als echten Text in seinen eingebetteten Schriften (siehe [Ressourcenbytes und Druckoriginale](https://postext.dev/de/docs/configuration-programmatic-usage.md#ressourcenbytes-und-druckoriginale)).

Was der Text verlangt, wird aus `font-family`, `font-weight`, `font-style` und `font` gelesen, als Attribute, in `style`-Attributen und von umschließenden Gruppen geerbt; eine `<style>`-Regel, die eine Familie nennt, zählt ebenfalls. Generische Familien (`serif`, `sans-serif` …) und Text in `<title>` oder `<desc>` bleiben außen vor, und eine Familie, die das SVG selbst mit `@font-face` deklariert, bleibt unberührt. Ein Textlauf geht seine Liste `font-family` bis zur ersten Familie durch, die einen Schnitt hat. Zuerst wird für den Einfarbmodus umgefärbt, danach kommen die Schriften.

Die Schnitte stammen von einem **Provider** mit dem Vertrag von `PdfFontProvider` aus postext-pdf, sodass ein Provider beiden dient: Er wird mit Familie, Schriftstärke und Stil sowie den Zeichen aufgerufen, die das SVG in diesem Schnitt setzt, und antwortet mit einer oder mehreren Dateien. Eine Familie, die in Unicode-Range-Teilen ausgeliefert wird (Fontsource, Google Fonts), wird mit den Teilen beantwortet, die diese Zeichen brauchen, sodass ein SVG mit lateinischen Beschriftungen nur die Datei `latin` mitführt. Der Standard-Provider liest das Schriftregister der Engine: `loadBundleFonts` registriert dort die Schnitte eines Pakets, und ein Host registriert seine eigenen mit `registerFontBytes(family, weight, style, bytes, { unicodeRange })` oder mit `registerFontUrl(…)` für eine Datei, die beim ersten Bedarf eines SVG geladen wird. Eine Familie, die niemand registriert hat, wird in den `@font-face`-Regeln der lesbaren Stylesheets der Seite gesucht. Ein `FontFace`, das aus Bytes zu `document.fonts` hinzugefügt wurde, behält keine Bytes, die Engine kann es also nicht zurücklesen: Registrieren Sie auch diese Schnitte.

```ts
import { registerFontBytes, registerSvgImage, renderPage } from 'postext';

registerFontBytes('IBM Plex Sans', 700, 'normal', plexBoldWoff2);
await registerSvgImage('chart.svg', svgText);   // recoloured, fonts inlined, decoded, registered
const canvas = renderPage(doc.pages[0], doc);
```

Wo es geschieht:

- **Canvas.** `registerSvgImage(fileId, svgText, options)` färbt um (`inkHex`), bettet ein (`fonts`, ein Provider; `inlineFonts: false` überspringt das), dekodiert und registriert das Bild als Vektorquelle und liefert, was aus jedem Schnitt geworden ist. `registerBundleImages(bundle)` tut dasselbe für die SVGs eines Pakets, zuerst aus den eigenen Schnitten des Pakets. `prepareSvgMarkup(svgText, options)` liefert das vorbereitete Markup für einen Host, der selbst dekodiert.
- **HTML.** `bundleImageUrl(bundle)` liefert SVG-Markup mit eingebetteten Schnitten des Pakets. `renderToHtml(doc, { inlineSvgFonts: true })` bettet in die SVG-`data:`-URIs ein, die `resourceImageUrl` liefert, und zwar aus den Schnitten, die das Register im Speicher hält (oder `inlineSvgFonts: { fonts, maxBytes, withhold }`). Eine Objekt-URL lässt sich nicht synchron lesen; ein Host, der Blob-URLs ausliefert, bettet daher ein, bevor er sie erzeugt.
- **EPUB.** `postext-epub` bettet aus den `fonts` des Buchs ein, dann aus `svgFonts.provider`, bevor es ein SVG schreibt (siehe [EPUB-Bücher](https://postext.dev/de/docs/configuration-programmatic-usage.md#epub-bücher-postext-epub)).
- **PDF.** SVG-Text wird als echter Text in den eingebetteten Schriften gesetzt. Ein `<style>`, das nur `@font-face`-Regeln enthält (von einem Autor eingebettete Schnitte), lässt die Abbildung nicht mehr auf ein Raster zurückfallen. Fällt eine Abbildung doch zurück (ein Filter, ein Verlauf), wird ihr Raster mit den aus dem `fontProvider` des PDF eingebetteten Schnitten erzeugt.

Auch die Funktionen der unteren Ebene werden exportiert: `svgFontRequests(svgText)` listet Familien, Schriftstärke, Stil und Zeichen jedes Textlaufs; `inlineSvgFonts(svgText, provider, options)` und `inlineSvgFontsSync(svgText, syncProvider, options)` liefern das Markup; `inlineSvgFontsDetailed` fügt einen Bericht zu jedem Schnitt hinzu (`inlined`, `declared`, `unavailable`, `withheld`, `tooLarge`).

| Option | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `maxBytes` | `number` | 2 MiB | Höchstmenge an Schrift-Bytes, die in ein SVG eingebettet werden (vor Base64, das ein Drittel hinzufügt). Schnitte, die zusammen darüber liegen, werden gar nicht eingebettet, und `svgFontsTooLarge` wird gemeldet. |
| `formats` | `('woff2' \| 'woff' \| 'ttf' \| 'otf')[]` | alle vier | Die einzubettenden Dateiformate; Dateien anderer Formate werden übersprungen. |
| `withhold` | `(family) => boolean` | keine | Familien, die aus einer Datei herausgehalten werden, die die App verlässt (nicht weitergebbar). Ihr Verweis bleibt, und der Leser fällt auf eine Ersatzschrift zurück; `onWithheld(family)` wird über jede informiert. |
| `onWarning` | `(warning) => void` | keine | Wird über eine Familie ohne Schnitt (`svgFontUnavailable`) und über die Größengrenze (`svgFontsTooLarge`) informiert. |

**Verzichten.** `diagramStyle.inlineFonts: false` lässt jedes SVG so, wie es gespeichert ist; `svg.inlineFonts: false` an einer Ressource lässt diese Byte für Byte unverändert, für ein SVG, das seine eigenen Schnitte mitbringt oder sich nicht ändern darf. Ein Paket schreibt den Verzicht der Ressource als `"inlineFonts": false` in seine `preset.json`.

**Lizenzen.** Das Einbetten legt Schriftdateien in Bilder, die die App verlassen können (ein HTML-Export, ein EPUB). Es geschieht, wenn ein Bild gezeigt oder exportiert wird, nie in den gespeicherten Ressourcen-Bytes, und `withhold` hält die Familien heraus, deren Lizenz die Weitergabe nicht erlaubt: Der EPUB-Writer hält die mit `redistributable: false` markierten Schnitte zurück, die Sandbox die so markierten eigenen Familien.

## Videostil

Die Eigenschaft `videoStyle` legt fest, wie [Video-Ressourcen](https://postext.dev/de/docs/document-format.md#videos) gedruckt werden – das Abspielzeichen und der QR-Code über dem Posterbild und ob das Posterbild auf das Video verlinkt – und was ihre Player im HTML-Betrachter und in EPUB anbieten.

```ts
const config: PostextConfig = {
  videoStyle: {
    playMark: { shape: 'rounded', position: 'top-left', size: { value: 10, unit: 'mm' } },
    qr: { position: 'bottom-right', size: { value: 20, unit: 'mm' }, errorCorrection: 'Q' },
    player: { download: false, privacy: true },
  },
};
```

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `playMark` | `VideoPlayMarkConfig` | siehe unten | Das auf das Posterbild gedruckte Zeichen, das anzeigt, dass es abgespielt werden kann. |
| `qr` | `VideoQrConfig` | siehe unten | Der auf das Posterbild gedruckte QR-Code: Er öffnet die YouTube- oder Vimeo-Seite des Videos oder die Produktionsadresse einer Datei. |
| `linkPoster` | `boolean` | `true` | Das Posterbild zu einem Link auf das Video machen: eine Link-Annotation darüber im PDF und ein `<a>` darum in HTML und EPUB, wo immer das Posterbild gezeigt wird. |
| `html` | `'player'` · `'poster'` | `'player'` | Was die HTML-Ausgabe für ein Video setzt: seinen Player oder das gedruckte Posterbild mit seinen Überlagerungen. |
| `player` | `VideoPlayerOptions` | siehe unten | Die Player-Optionen aller Videos; das eigene `video.player` eines Videos wird darübergelegt. |

### Abspielzeichen

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Das Zeichen drucken. |
| `shape` | `'circle'` · `'rounded'` · `'triangle'` | `'circle'` | Eine Scheibe mit Dreieck, ein abgerundetes Rechteck mit Dreieck (1,45-mal so breit wie hoch) oder das Dreieck allein, in der Hintergrundfarbe umrandet. |
| `position` | `VideoOverlayPosition` | `'center'` | `'center'`, eine Ecke (`'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`) oder die Mitte einer Seite (`'top'`, `'bottom'`, `'left'`, `'right'`). Die Positionen sind physisch: Die obere rechte Ecke ist auch in einem Buch mit Leserichtung von rechts nach links die obere rechte Ecke. |
| `size` | `Dimension` | `12mm` | Höhe des Zeichens; nie mehr als 40 % der kürzeren Seite des Posterbilds. |
| `inset` | `Dimension` | `4mm` | Abstand von den Kanten des Posterbilds an einer Ecke oder Seite. |
| `color` | `ColorValue` | Weiß | Das Dreieck. |
| `background` | `ColorValue` | Hauptfarbe | Die Scheibe oder das Rechteck dahinter; die Umrandung des Dreiecks, wenn es allein steht. Standardmäßig an die Palette gebunden. |
| `backgroundOpacity` | `number` | `0.9` | Deckkraft des Hintergrunds, 0–1. |

### QR-Code

| Eigenschaft | Typ | Standard | Beschreibung |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Den Code drucken. Eine Datei ohne Produktionsadresse erhält keinen. |
| `position` | `VideoOverlayPosition` | `'bottom-right'` | Wie beim Abspielzeichen. Geben Sie beiden unterschiedliche Positionen. |
| `size` | `Dimension` | `18mm` | Seitenlänge des Codes samt Ruhezone; nie mehr als 45 % der kürzeren Seite des Posterbilds. Handykameras lesen Module ab einem Drittel Millimeter: Eine Adresse von 30 Zeichen ergibt einen Code mit 29 Modulen, 18 mm mit einer Ruhezone von 2 ergeben also Module von 0,55 mm. |
| `inset` | `Dimension` | `3mm` | Abstand von den Kanten des Posterbilds. |
| `errorCorrection` | `'L'` · `'M'` · `'Q'` · `'H'` | `'M'` | Wie viel des Codes beschädigt oder verdeckt sein darf, ohne dass er unlesbar wird: etwa 7 %, 15 %, 25 % oder 30 %. Wird von selbst angehoben, solange der Code dieselbe Zahl an Modulen behält. |
| `quietZone` | `number` | `2` | Helle Module um den Code, auf seiner Platte (0–8). Die Platte hebt sich vom Posterbild ab, die vier Module, die der Standard auf offenem Papier verlangt, sind also nicht nötig. |
| `color` | `ColorValue` | Schwarz | Die dunklen Module. Halten Sie sie dunkel auf heller Platte: Die meisten Lesegeräte lesen keine invertierten Codes. |
| `background` | `ColorValue` | Weiß | Die Platte. |
| `radius` | `Dimension` | `1mm` | Eckenradius der Platte. |

Der Code wird von der Engine selbst kodiert (`encodeQr(text, level)`: Byte-Modus, UTF-8, Versionen 1 bis 40, die Maske mit der geringsten Strafpunktzahl) und als Vektoren gezeichnet: Der Canvas füllt einen Pfad aus Modulfolgen, das PDF ein `drawSvgPath` und HTML ein `<path>` mit `shape-rendering="crispEdges"`, sodass er in jeder Druckgröße scharf bleibt.

### Player-Optionen

`VideoPlayerOptions`, in `videoStyle.player` und im `video.player` jedes Videos. Der HTML5-Player einer Datei beachtet sie alle; die Player von YouTube und Vimeo beachten, was ihre Einbettungsparameter zulassen.

| Eigenschaft | Standard | Beachtet von | Beschreibung |
| --- | --- | --- | --- |
| `controls` | `true` | YouTube, Vimeo, Dateien | Die Bedienelemente des Players zeigen. |
| `download` | `true` | Dateien | Die Download-Schaltfläche des Browsers anbieten (`controlslist="nodownload"`, wenn ausgeschaltet). Das blendet die Schaltfläche aus, schützt die Datei aber nicht. YouTube und Vimeo bieten nie einen Download an. |
| `fullscreen` | `true` | YouTube, Vimeo, Dateien | Vollbild anbieten (`fs=0`, `allowfullscreen` des iframe, `nofullscreen`). |
| `playbackRate` | `true` | Vimeo, Dateien | Das Geschwindigkeitsmenü anbieten (`speed=0`, `noplaybackrate`). |
| `pictureInPicture` | `true` | Vimeo, Dateien | Bild-in-Bild anbieten (`pip=0`, `disablepictureinpicture`). |
| `remotePlayback` | `true` | Dateien | Die Übertragung auf einen anderen Bildschirm anbieten (`disableremoteplayback`). |
| `autoplay` | `false` | YouTube, Vimeo, Dateien | Von selbst abspielen, immer stumm, wie Browser es verlangen. |
| `muted` | `false` | YouTube, Vimeo, Dateien | Ohne Ton starten. |
| `loop` | `false` | YouTube, Vimeo, Dateien | Am Ende erneut von vorn abspielen. |
| `exclusive` | `true` | Folio, HTML-Betrachter, EPUB (wo Skripte laufen); Dateien | Der Start dieses Videos hält die anderen sichtbaren an, sodass immer nur eines läuft. `false` lässt es neben den anderen laufen: die stummen Endlosclips einer Seite, mehrere gleichzeitig. Seit postext 1.18. |
| `preload` | `'metadata'` | Dateien | Wie viel der Browser vor dem Abspielen lädt: `'none'`, `'metadata'` oder `'auto'`. |
| `privacy` | `true` | YouTube, Vimeo | Einbettungen mit erweitertem Datenschutz: YouTube über `youtube-nocookie.com`, Vimeo mit `dnt=1`. |

Ein EPUB behält nur die Attribute, die sein Schema kennt: Eine Datei spielt mit `controls`, `autoplay`, `muted`, `loop`, `playsinline` und `preload`, dazu der Markierung `data-pt-alongside`, und das Lesesystem entscheidet über den Rest.

Ein Video, das nicht exklusiv ist, trägt in der HTML-Ausgabe `data-pt-alongside`. `coordinateVideoPlayback(root)` hält die Player unter `root` (dem Element, das die Ausgabe von `renderToHtml` enthält) an die Regel: Der Start eines exklusiven Videos hält jedes andere laufende an, und der Start eines, das daneben läuft, hält nur die exklusiven an. Die Funktion liefert eine Funktion zurück, die das Lauschen beendet. `playsAlongside(el)` und `videosToPause(started, videos, alongside)` stellen dieselbe Regel einem Host mit eigenen Playern bereit. In [Folio](https://postext.dev/de/docs/document-format.md#videos-auf-den-seiten-von-folio) startet ein Video, das von selbst und neben den anderen läuft (`autoplay` mit `exclusive: false`), stumm, sobald seine Seite ins Bild kommt, und hält an, wenn die Seite weggeblättert wird, mehrere gleichzeitig, und `loop` spielt es erneut von vorn ab. Eine EPUB-Seite oder ein EPUB-Kapitel mit zu koordinierenden Videos (zwei oder mehr, eines davon exklusiv) verlinkt dieselbe Regel als kleines Skript, `scripts/videos.js` (der Export `VIDEO_PLAYBACK_SCRIPT`), und das Paket deklariert dieses Dokument als `scripted`. Ein Lesesystem, das Skripte ausführt, hält die Videos dieses Dokuments an die Regel, nicht aber die einer gegenüberliegenden Seite, die ein anderes Dokument ist; eines, das keine Skripte ausführt, spielt jedes Video nach dessen eigenen Bedingungen ab, wie es die Player von YouTube und Vimeo immer tun.

Auflöser und Bereiniger entsprechen denen der anderen Abschnitte, neben den Typen `VideoStyleConfig` / `ResolvedVideoStyleConfig`:

```ts
import {
  DEFAULT_VIDEO_STYLE_CONFIG,
  DEFAULT_VIDEO_PLAYER_OPTIONS,
  resolveVideoStyleConfig,
  resolveVideoPlayerOptions,
  stripVideoStyleDefaults,
} from 'postext';

const resolved = resolveVideoStyleConfig(config.videoStyle);
const player = resolveVideoPlayerOptions(resource.video?.player, resolved.player);
const minimal = stripVideoStyleDefaults(config.videoStyle); // undefined when all defaults
```
