# Uso programático: arquivos PDF

> O postext-pdf a partir do código: instalação, a API, provedores de fontes no navegador e no Node, fontes CJK, bytes de recursos, texto vertical, workers e arquivos prontos para impressão

- Versão HTML: https://postext.dev/pt/docs/configuration-pdf-output
- Última atualização: 2026-10-16
- Tempo de leitura: 22 min
- Outros idiomas: [en](https://postext.dev/en/docs/configuration-pdf-output.md), [es](https://postext.dev/es/docs/configuration-pdf-output.md), [ca](https://postext.dev/ca/docs/configuration-pdf-output.md), [de](https://postext.dev/de/docs/configuration-pdf-output.md), [fr](https://postext.dev/fr/docs/configuration-pdf-output.md), [it](https://postext.dev/it/docs/configuration-pdf-output.md), [zh](https://postext.dev/zh/docs/configuration-pdf-output.md), [ja](https://postext.dev/ja/docs/configuration-pdf-output.md), [ar](https://postext.dev/ar/docs/configuration-pdf-output.md)

## Em poucas palavras

Esta página é para quem escreve código e quer um arquivo PDF. Mostra como instalar o pacote de PDF e transformar um livro diagramado em arquivo com poucas linhas. O pacote precisa dos arquivos das fontes, então a página explica como entregá-los a ele, num navegador ou num servidor. Trata das fontes para chinês, japonês e coreano, das imagens, do texto escrito de cima para baixo e dos links. Termina com um exemplo completo e com arquivos prontos para a gráfica.

## Geração de PDF

A saída em PDF fica em um pacote separado, **`postext-pdf`**, para que integrações apenas web não paguem o custo de `pdf-lib` e `@pdf-lib/fontkit`. O renderizador de PDF não mede o texto de novo: consome exatamente o mesmo `VDTDocument` que você passaria a `renderToCanvas` ou `renderToHtml` e converte as coordenadas em pixels para pontos de PDF. Por isso as três saídas sempre coincidem nas quebras de linha, nas alturas das colunas e na posição dos recursos.

> **No navegador, construa o VDT pelo [Web Worker](https://postext.dev/pt/docs/configuration-programmatic-usage.md#executar-o-layout-em-um-web-worker).** `renderToPdf` em si é rápido depois que o VDT existe; a parte cara é o pipeline de layout que o produziu. Executar esse pipeline no worker mantém a interface responsiva e permite que uma exportação de PDF reaproveite o mesmo cache de medição que a visualização ao vivo já aqueceu. Veja [Conduzir a exportação de PDF a partir do worker](https://postext.dev/pt/docs/configuration-programmatic-usage.md#conduzir-a-exportação-de-pdf-a-partir-do-worker) para o fluxo recomendado. Os exemplos na thread principal abaixo são a referência do *que os argumentos significam*; em código de interface, construa primeiro o VDT no worker e chame diretamente apenas `renderToPdf`.

### Instalação

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

`postext` é uma peer dependency de `postext-pdf`. Cada versão de `postext-pdf` precisa do `postext` com que foi lançada, ou de um posterior da mesma versão major (o seu intervalo de peer é `^` aquela versão, `^1.5.0` para a 1.5.0), porque importa funções que o `postext` acrescentou naquela versão. Atualize os dois juntos e, em uma CDN, fixe os dois na mesma versão.

### API pública

O pacote expõe um único ponto de entrada e alguns tipos:

- **`renderToPdf(doc, options): Promise<Uint8Array>`**: recebe um `VDTDocument` (ou os capítulos de um livro como uma lista deles) e devolve os bytes brutos do PDF.
- **`PdfFontProvider`**: a assinatura de callback `(family, weight, style, request?) => Promise<Uint8Array | Uint8Array[]>` que `renderToPdf` usa para pedir os bytes de uma fonte quando precisa incorporar uma nova combinação de família/peso/estilo. `request.codePoints` contém os caracteres que as páginas compõem naquela variante; a resposta é um arquivo, ou vários que juntos formam a variante (veja [Fontes chinesas, japonesas e coreanas](https://postext.dev/pt/docs/configuration-pdf-output.md#fontes-chinesas-japonesas-e-coreanas)).
- **`declaredFontFile(bytes, { unicodeRange?, order? }): PdfFontFile`**: um arquivo de uma variante que diz como a página o declarou: o `unicode-range` da regra `@font-face` dele e o lugar dela na ordem de declaração. Um `PdfFontFile` é um `Uint8Array` com essas duas propriedades opcionais (veja [Fontes chinesas, japonesas e coreanas](https://postext.dev/pt/docs/configuration-pdf-output.md#fontes-chinesas-japonesas-e-coreanas)). Desde o postext-pdf 1.30.
- **`RenderToPdfOptions`**: `{ fontProvider, resourceBytes?, outlines?, accessible?, colorSpace?, pageNegative?, characterGrid?, onProgress?, onWarning?, rasterizeSvg?, harfbuzzWasm?, print?, outputProfile?, profileBaseUrl?, date? }`. `outlines`, `accessible` e `colorSpace` assumem o `pdfGeneration` do documento quando omitidos (veja [Geração de PDF (configuração)](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#geração-de-pdf-configuração)). `resourceBytes` é descrito em [Bytes de recursos e matrizes de impressão](https://postext.dev/pt/docs/configuration-pdf-output.md#bytes-de-recursos-e-matrizes-de-impressão); `onWarning`, em [Quais variantes são pedidas ao provedor](https://postext.dev/pt/docs/configuration-pdf-output.md#quais-variantes-são-pedidas-ao-provedor) e em [Avisos no documento](https://postext.dev/pt/docs/configuration-programmatic-usage.md#avisos-no-documento). `characterGrid: true` imprime a grade que `cjk.grid.show` desenha na tela e que, de outro modo, o PDF deixa de fora (veja [Grade de caracteres](https://postext.dev/pt/docs/configuration-east-asian.md#grade-de-caracteres)). `harfbuzzWasm` diz de onde carregar o `harfbuzz.wasm` do HarfBuzz (uma URL, relativa à página, ou os bytes do arquivo) para um documento com texto da direita para a esquerda ou de letras ligadas; se omitido, usa a cópia ao lado do módulo do postext-pdf e, depois, a mesma versão do harfbuzzjs no jsDelivr e no esm.sh. `print` recebe as configurações de produção gráfica (padrão PDF/X, perfil de saída, preto, verificação de pré-impressão) e, se omitido, assume o `print` do documento; com um padrão PDF/X, ou com `colorSpace: 'cmyk'`, cada cor passa pela separação de cores com o perfil ICC de saída, cujos bytes `outputProfile` fornece (senão, ele é baixado de `profileBaseUrl`, por padrão a cópia da pasta `icc/` do postext na CDN do npm). `date` (um `Date`, um tempo em milissegundos ou uma data escrita, como o `mtime` de `createBundle`) é a data de criação e de modificação do arquivo e, em um PDF/X, as datas do pacote XMP e a origem do identificador do documento: o mesmo documento, gerado duas vezes com um mesmo `date`, dá os mesmos bytes. Se omitido, o arquivo leva a hora da chamada. Uma data inválida lança um erro.
- **`PdfWarning`**: um problema não fatal informado por `onWarning`; distinga pelo `kind`: `'fontFallback'` (`PdfFontFallbackWarning`), uma variante composta em outro corte da sua família; `'missingGlyph'` (`PdfMissingGlyphWarning`), caracteres para os quais nenhum arquivo de uma variante tem glifo; `'variableFontDefaultInstance'` (`PdfVariableFontWarning`), uma fonte variável pedida em um peso diferente da sua instância padrão; `'cffEmbeddedWhole'` (`PdfCffEmbeddedWholeWarning`), uma fonte CFF de mais de 2 MB incorporada inteira; `'complexShapingUnavailable'` (`PdfComplexShapingWarning`), texto da direita para a esquerda ou de letras ligadas desenhado sem o HarfBuzz, que não pôde ser carregado (`reason` lista onde ele foi procurado), de modo que os sinais do árabe ficam mal posicionados; `'outputProfileUnavailable'` (`PdfPrintWarning`), uma renderização em CMYK cujo perfil de saída não pôde ser carregado, convertida com a fórmula simples; `'pageNegativeIgnored'` (`PdfPrintWarning`), a página em negativo deixada de fora de um arquivo PDF/X-1a; ou `'missingImage'`, uma imagem sem bytes desenhada como espaço reservado (informado apenas a um `onWarning` que você passe; sem ele, os avisos de fontes vão para `console.warn`).
- **`decompressWoff2(bytes): Uint8Array`**: função auxiliar que converte um arquivo WOFF2 em bytes TTF, o formato que `pdf-lib` consegue incorporar diretamente.
- **`createPdfWorker(options?)`**, de `postext-pdf/worker`: a mesma renderização em um Web Worker; veja [Renderizar o PDF em um worker](https://postext.dev/pt/docs/configuration-pdf-output.md#renderizar-o-pdf-em-um-worker).

### Exemplo mínimo

```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 substitui o padrão de 8 pt
  },
);

const pdfBytes = await renderToPdf(vdt, {
  fontProvider: async (family, weight, style) => {
    // Devolve os bytes TTF desta família/peso/estilo.
    // Veja a seção "Provedor de fontes" abaixo para uma implementação real.
    const res = await fetch(`/fonts/${family}-${weight}${style === 'italic' ? 'i' : ''}.ttf`);
    return new Uint8Array(await res.arrayBuffer());
  },
});

// `pdfBytes` é um Uint8Array: salve, baixe ou transmita por streaming.
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
```

### Por que um provedor de fontes?

`pdf-lib` incorpora arquivos de fonte reais ao PDF: as fontes instaladas no navegador não estão disponíveis na hora da renderização, e uma fonte que você carregou só para medir na tela não basta, sozinha, para produzir um PDF autossuficiente. `renderToPdf` percorre as páginas em busca de cada variante que elas pintam (uma por combinação `family|weight|style`; veja [Quais variantes são pedidas ao provedor](https://postext.dev/pt/docs/configuration-pdf-output.md#quais-variantes-são-pedidas-ao-provedor)) e chama o seu provedor uma vez por combinação única. O provedor devolve um `Uint8Array` com bytes **TTF ou OTF**, ou uma lista deles para uma variante servida em vários arquivos (veja [Fontes chinesas, japonesas e coreanas](https://postext.dev/pt/docs/configuration-pdf-output.md#fontes-chinesas-japonesas-e-coreanas)); `pdf-lib` gera subconjuntos dos contornos TrueType e incorpora inteiros os arquivos CFF (`.otf`). Variantes às quais o provedor responde com o mesmo arquivo, como um corte regular que substitui o negrito que falta a uma família, compartilham uma única fonte incorporada. Uma variante com que nenhuma página acaba desenhando, como a do texto de uma figura SVG quando a figura é desenhada como imagem, fica fora do arquivo.

**Use fontes estáticas por peso, não uma única fonte variável.** O Google Fonts muitas vezes serve um único WOFF2 variável por família, cobrindo todo o eixo de pesos. `pdf-lib` só consegue incorporar a instância padrão de um arquivo variável, então um parágrafo em negrito sairia com peso regular. O Fontsource publica arquivos WOFF2 estáticos por peso, que resolvem isso de forma limpa; é o padrão que o Sandbox usa. Um arquivo variável pedido em um peso diferente da sua instância padrão é informado com um aviso `variableFontDefaultInstance`.

**Cada palavra do texto cai onde o layout a colocou.** Em parágrafos, itens de lista, citações, boxes e outros textos corridos, cada palavra começa na posição que o VDT mediu, de modo que uma diferença entre as larguras do navegador e as da fonte incorporada nunca se acumula ao longo de uma linha. Uma linha sem formatação inline é pintada como um único objeto de texto que move a pena entre as palavras; linhas justificadas, centralizadas e com formatação são pintadas palavra por palavra. Um caractere para o qual a fonte não tem glifo, que o navegador mediu em outra fonte e que o PDF pinta como a caixa de glifo ausente da fonte, não desloca nenhuma das palavras seguintes e é informado uma vez por variante como aviso `missingGlyph`. Um espaço que a fonte não tem, como o espaço estreito sem quebra ou o espaço de algarismo, assume a largura que o navegador lhe deu, e caracteres invisíveis como o word joiner e o espaço de largura zero não são desenhados. Um hífen sem quebra (U+2011) que a fonte não tem é desenhado com o hífen da fonte (U+2010), ou com o hífen-menos quando ela também não tem hífen, como o navegador o mostra; Open Sans e Outfit, entre outras, não têm nenhum dos dois. Nenhum desses casos conta como glifo ausente. Há duas exceções. Uma linha com letras da direita para a esquerda continua sendo pintada como um único trecho (veja [Idiomas e escritas](https://postext.dev/pt/docs/configuration-language.md#idiomas-e-escritas)). O texto posicionado por um design (cabeços e rodapés, aberturas, títulos de boxes e outros elementos de design) é composto com as larguras da própria fonte incorporada, de modo que ali um glifo que falta à fonte ainda desloca o resto da linha.

### Quais variantes são pedidas ao provedor

`renderToPdf` percorre as páginas do mesmo jeito que as pinta e pede ao provedor apenas as variantes que elas desenham:

- a variante regular de um bloco que compõe qualquer linha, e a negrito, itálica ou negrito-itálica de cada trecho efetivamente composto assim;
- trechos de chips, marcadores de lista e o texto dos espaços de design (cabeços, fólios, faixas de abertura e de parte);
- o texto de legenda, de nota e de células de tabela de cada recurso;
- as variantes que o `<text>` de uma figura SVG nomeia, quando a figura é incorporada. Uma família que o provedor não consegue fornecer de jeito nenhum passa para a família seguinte da lista `font-family` do SVG.

Assim, uma família de títulos que ninguém inclina nunca tem o seu itálico pedido, e uma figura sem nota nunca precisa das variantes de nota.

Quando o provedor recusa uma variante, a renderização continua. Outra variante da mesma família é incorporada no lugar e um `PdfWarning` é informado. A substituta é a primeira variante que carregar, testando os nove pesos padrão (100 a 900) na ordem que a correspondência de fontes do CSS usa, que também é a variante que o navegador mostra na visualização:

1. primeiro o mesmo estilo. Para um peso de 400 a 500, vêm primeiro os pesos até 500, depois os mais leves, do mais próximo para baixo, depois os mais pesados, de 600 para cima. Para um peso abaixo de 400, vêm primeiro os pesos mais leves, do mais próximo para baixo, depois os mais pesados. Para um peso acima de 500, vêm primeiro os pesos mais pesados, depois os mais leves;
2. depois o outro estilo, itálico para o redondo e redondo para o itálico, no peso pedido e nos outros pesos, na mesma ordem.

Por isso, uma família sem itálicos compõe os seus trechos em itálico em redondo, uma família que só traz 400 e 700 toma um 600 como 700, e uma família com um único corte compõe tudo nele. O provedor recebe um pedido de variante por vez, nessa ordem, e nunca duas vezes para a mesma variante, então uma variante que nada usa nunca é incorporada. Uma família que o provedor não consegue servir de jeito nenhum recebe pedidos para cada uma dessas 18 variantes antes de a renderização falhar.

```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);
  },
});
```

Sem `onWarning`, a mensagem vai para `console.warn`. O texto mantém as suas posições, que vêm do VDT e foram medidas com as variantes que o navegador tinha, então uma substituta com larguras diferentes pode parecer apertada ou frouxa. Forneça a variante verdadeira para corrigir isso. Uma renderização só falha (`postext-pdf: failed to load font(s): …`) quando o provedor não consegue fornecer nenhuma variante de uma família em um peso padrão, redonda ou itálica.

### Provedor de fontes no navegador (Fontsource + WOFF2)

O Sandbox traz `createPdfFontProvider()` (`packages/postext-sandbox/src/viewport/pdfFontProvider.ts`), que você pode copiar para qualquer app de navegador. O essencial:

```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}`);
      // O pdf-lib precisa de bytes TTF, então descompactamos o invólucro WOFF2 no cliente.
      return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
    })();

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

Uma versão de produção também deveria:

- **Consultar os pesos disponíveis** (via `https://api.fontsource.org/v1/fonts/{id}`) e ajustar o peso pedido ao mais próximo que a família realmente oferece, para que um pedido de `weight: 600` em uma família que só tem `{400, 700}` ainda funcione.
- **Recorrer do itálico ao normal** quando uma família não tem corte itálico para o peso pedido, em vez de fazer falhar a renderização inteira.
- **Reaproveitar o cache entre renderizações** (mantenha `bytesCache` no escopo do módulo, não por chamada), para que gerar de novo o PDF depois de uma mudança de configuração saia praticamente de graça.

### Fontes chinesas, japonesas e coreanas

Uma fonte CJK não vem em um único arquivo pequeno. O Fontsource distribui a Noto Serif SC em cerca de cem arquivos por peso, cada um com uma parte dos caracteres e declarado na folha de estilo da família com o seu `unicode-range` (`@fontsource/noto-serif-sc/400.css`); o navegador baixa os arquivos que o texto de uma página toca. O arquivo `latin` que o provedor acima busca não tem nenhum caractere Han, e os subconjuntos nomeados estão incompletos: o `chinese-simplified` da Noto Serif SC não tem 釵, e o `chinese-traditional` da Noto Serif TC não tem nenhuma das marcas de largura total （），！？：；.

Por isso, um provedor pode responder a uma variante com vários arquivos. `renderToPdf` passa a ele os caracteres que as páginas compõem naquela variante (`request.codePoints`), reunidos de todos os capítulos antes de qualquer coisa ser desenhada, e o provedor devolve os arquivos que os contêm, na ordem em que o navegador os consulta. Cada arquivo é incorporado como um subconjunto próprio, e cada caractere é desenhado a partir do primeiro arquivo que tiver um glifo para ele: um capítulo que toca 60 fatias incorpora 60 subconjuntos pequenos. Uma variante pedida de novo, para o texto de uma figura SVG, por exemplo, só é pedida para os caracteres que os seus arquivos não têm. Um provedor que devolve um único `Uint8Array` funciona como antes. O provedor do Sandbox lê a folha de estilo do Fontsource do peso e do estilo e busca os arquivos cujos intervalos contêm o texto; o núcleo dele:

```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) => {
  // Onde os intervalos se sobrepõem, o navegador tenta primeiro a última regra.
  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()))));
};
```

Uma família latina passa pelo mesmo código: um texto em inglês recebe só o seu arquivo `latin`, um texto em tcheco recebe `latin` e `latin-ext`.

Um navegador não fica com o primeiro arquivo que tem o glifo. Ele compõe cada caractere com o arquivo declarado por último entre aqueles cujo `unicode-range` o contém, e os arquivos de uma família se sobrepõem: o Fontsource repete as letras ASCII da Noto Serif JP no arquivo `latin` e nas fatias numeradas. Entre dois caracteres compostos com dois arquivos não há kerning, então o arquivo decide a largura: o Chrome 154 compõe `Shift_JIS` com 4,25 em de largura quando as fatias são declaradas depois de `latin`, e o arquivo `latin` sozinho, que aplica kerning a `t_` e a `_J`, o compõe com 4,32 em. Desde o postext-pdf 1.30, um provedor pode dizer como a página declarou cada arquivo: `declaredFontFile(bytes, { unicodeRange, order })` devolve os mesmos bytes com o `unicode-range` da regra do arquivo, como o CSS o escreve, e o lugar dessa regra entre as da variante, um número qualquer que cresce com a ordem de declaração (o maior é o declarado por último).

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

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

Quando os arquivos de uma variante trazem os seus intervalos, num documento composto no seu próprio idioma ([`bodyText.shapingLanguage: 'document'`](https://postext.dev/pt/docs/configuration-text.md#texto-do-corpo), o padrão), cada caractere é desenhado a partir do arquivo declarado por último cujo intervalo o contém e que tem um glifo para ele, como um navegador escolhe entre as regras `@font-face`, e uma letra e os seus sinais combinantes ficam num mesmo arquivo. Um caractere que nenhum intervalo contém é desenhado a partir do primeiro arquivo que tiver um glifo. Os arquivos sem esses dados, e todos os de um documento com `'host'`, são lidos na ordem dada, como antes, então um provedor que descreve os seus arquivos continua pondo primeiro o declarado por último. Uma variante descrita assim é pedida de novo para todos os caracteres para os quais ainda não tinha sido pedida, porque um arquivo declarado depois pode tê-los. O provedor do Sandbox descreve os seus arquivos.

Os caracteres para os quais nenhum arquivo da variante tem glifo são desenhados com o glifo `.notdef` da fonte (uma caixa vazia na maioria das fontes), e `renderToPdf` os informa uma vez por variante depois de desenhar as páginas:

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

O Sandbox lista esses avisos, e os dois descritos abaixo, no seu painel Verificações depois de cada PDF que gera. Quando o livro, as suas configurações ou os seus recursos mudam, eles passam a ser marcados como vindos de um PDF anterior até que o próximo os substitua; abrir outro livro os apaga.

- **O negrito precisa de um arquivo estático por peso.** O Fontsource serve cada peso da Noto Serif SC e TC como arquivos estáticos separados, então o negrito funciona no Sandbox. Os arquivos do Google Fonts (`NotoSerifSC[wght].ttf`, 25 MB) são fontes variáveis: o pdf-lib incorpora a instância padrão delas, de modo que uma variante 700 sai impressa em 400, e `renderToPdf` informa isso como `variableFontDefaultInstance`. Para um pacote, gere uma instância estática por peso com o fontTools (`fonttools varLib.instancer NotoSerifSC[wght].ttf wght=700`) e reduza-a aos caracteres do livro com `pyftsubset`.
- **Use as versões TrueType.** A Source Han Serif e os arquivos `.otf` da Noto Serif CJK têm contornos CFF, que o postext-pdf incorpora inteiros, de 8 a 25 MB por peso; uma fonte CFF com mais de 2 MB é informada como `cffEmbeddedWhole`. As versões TrueType (Google Fonts, Fontsource) são reduzidas aos glifos usados. Para japonês, a Noto Serif JP e a Noto Sans JP (Google Fonts, ou as fatias numeradas do Fontsource), a Shippori Mincho, a Zen Old Mincho e a BIZ UDMincho vêm em TrueType; a Source Han Serif JP e os arquivos `.otf` `JP` da Noto Serif CJK são CFF.
- **As formas japonesas de uma fonte pan-CJK.** Um mesmo ponto de código de Han, de pontuação ou de aspas pode ser desenhado de um jeito no Japão e de outro na China, e uma fonte pan-CJK (Source Han, Noto CJK) contém os dois. O PDF faz o shaping de um documento japonês (`locale: 'ja'`), e de um isolamento em japonês (`:ltr[…]{lang=ja}`) em qualquer documento, com o sistema de idioma OpenType `JAN `, de modo que o recurso `locl` da fonte imprime as formas japonesas que o canvas e o HTML imprimem por meio de `lang`. Um isolamento em outro idioma dentro de um livro japonês passa pelo shaping com as formas daquele idioma (as formas padrão da fonte, no caso do chinês com `'host'`) e é marcado como um `Span` com o seu `/Lang`. Com [`bodyText.shapingLanguage: 'document'`](https://postext.dev/pt/docs/configuration-text.md#texto-do-corpo), o padrão desde o postext 1.30, todo documento e todo isolamento passa pelo shaping no seu próprio idioma (`ZHS` para o chinês simplificado), como o layout o mediu; com `'host'`, documentos em chinês e em outros idiomas passam pelo shaping com as formas padrão da fonte, como antes. Uma fonte feita para o japonês, como a Noto Serif JP, tem formas padrão japonesas; mesmo assim, ela compõe as “ ” do texto japonês nas suas formas `JAN `.

Um caractere que falta em uma família não é tomado de outra: a Noto Serif TC não empresta da Noto Serif SC. Resolva a cobertura quando gerar os arquivos de fonte; a vitrine 红楼梦 copia da fonte SC os glifos que faltam aos seus subconjuntos TC.

### Provedor de fontes no servidor (Node, arquivos locais)

No Node você pode pular totalmente a etapa do WOFF2 e ler arquivos TTF/OTF do disco:

```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);
};
```

### Bytes de recursos e matrizes de impressão

`resourceBytes(fileId)` devolve os bytes brutos de uma imagem, e o renderizador identifica o formato:

- PNG, JPEG, GIF e WebP são incorporados como imagens;
- a marcação SVG é desenhada como caminhos vetoriais, com o seu `text` composto como texto real nas fontes incorporadas do documento, ou rasterizada a 600 dpi no navegador quando usa recursos fora do subconjunto vetorial. Um `<style>` que contém só regras `@font-face` (variantes que o autor incorporou) é ignorado e mantém a figura vetorial (desde o postext-pdf 1.25); qualquer outra folha de estilo a torna uma rasterização, feita com as variantes que o seu texto nomeia incorporadas a partir de `fontProvider` (a menos que `diagramStyle.inlineFonts` ou o `svg.inlineFonts` do recurso seja `false`);
- de um PDF, a primeira página é incorporada tal como está, como um form XObject.

Cada imagem é guardada no arquivo uma única vez, por mais vezes que seja desenhada. Um SVG desenhado como caminhos vetoriais vira um form XObject que todas as páginas pintam, de modo que uma moldura ou um logotipo no design de página de um documento de trinta páginas é escrito uma vez, não trinta; cada página a mais acrescenta algumas centenas de bytes. Até o postext-pdf 1.4, cada página levava a sua própria cópia dos caminhos.

Uma figura SVG pode nomear uma **matriz de impressão** em `svg.pdfFileId`: um PDF de uma página com a mesma figura, em geral o original do qual o SVG foi exportado. `renderToPdf` pede primeiro a `resourceBytes` o id da matriz. Ele incorpora essa página no lugar do SVG, com as suas fontes, degradês e espaços de cor intactos, em todo lugar onde o SVG é desenhado: como figura, como imagem de uma célula de tabela (`TableCell.image`), como imagem de design ou como ícone de boxe (o seu `marker` também). O VDT leva o id da matriz em cada um desses usos (`svg.pdfFileId` no recurso da figura, `pdfFileId` em uma imagem de célula e em um bloco de imagem de design), então, em um livro, todos os capítulos recebem a matriz. Os renderizadores de canvas e HTML continuam desenhando o SVG. Os bytes do próprio SVG são usados no lugar em três casos: a matriz está faltando, não é um PDF ou a tinta única está ativada (`diagramStyle.singleInk` só recolore marcação 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),
});
```

Um hospedeiro também pode devolver os bytes da matriz para o id do próprio SVG, como faz `bundleResourceBytes`; as duas formas funcionam.

### Texto vertical no PDF

Uma página vertical (`layout.writingMode: 'vertical-rl'`) é desenhada por meio de um referencial girado um quarto de volta, como o canvas a pinta, e o seu texto é composto coluna abaixo:

- **Os caracteres em pé** são mostrados por meio de uma segunda fonte Type0 do mesmo arquivo incorporado: a mesma CIDFont, as mesmas larguras e o mesmo mapa ToUnicode, com `Encoding /Identity-V` (modo vertical). Uma sequência de caracteres é um único objeto de texto cujos glifos avançam sozinhos um eme coluna abaixo (`DW2 [880 −1000]`), de modo que os leitores de PDF selecionam e extraem uma coluna como uma única linha. Os glifos passam pelo shaping com os recursos OpenType `vert` e `fwid`, que dão as formas verticais a parênteses, aspas, vírgulas de enumeração do chinês continental, reticências e travessões; um caractere que já fica em pé como é mantém o seu glifo horizontal. Nada da fonte é incorporado duas vezes.
- **Palavras latinas e números longos** correm de lado com a fonte horizontal; **um número em uma célula** fica em pé, comprimido até a largura de um eme quando é mais largo; uma marca para a qual a fonte não tem forma vertical é girada ou deslocada, como no canvas.
- **O tracking** entre caracteres é escrito como números de `TJ`, que no modo vertical movem a pena coluna abaixo.
- **Cada linha vertical** é marcada com um `/ActualText` do seu texto, para que a cópia e a extração de texto a leiam como foi escrita. O `pdftotext` e o pdf.js leem as colunas de cima para baixo, da direita para a esquerda; o pdf.js começa uma nova linha em um número composto em uma célula.
- **Links, marcadores e destinos** são mapeados sobre a folha: um link sobre uma linha vertical é um retângulo alto e estreito, e um marcador abre a página no topo da coluna do seu título.
- **Um PDF com tags** declara o modo de escrita no seu elemento `Document` (o atributo de Layout `WritingMode /TbRl`, que todos os elementos herdam); a validação PDF/UA-1 (veraPDF) passa em um capítulo vertical.
- **Visualizadores:** Acrobat, Preview, Chrome (PDFium), pdf.js e Poppler renderizam as fontes verticais. Um livro com encadernação à direita (`page.binding`) também pede aos visualizadores que mostrem as suas páginas duplas da direita para a esquerda (`/Direction /R2L`, `/PageLayout /TwoPageRight`); Acrobat e Foxit seguem isso, o Chrome não.

Um capítulo de 43 páginas composto em Noto Serif TC (os caracteres do livro, TrueType) tem cerca de 820 KB, a maior parte nos dois subconjuntos de fonte.

### Links no PDF

As palavras de um link Markdown (veja [Formato do documento › Links](https://postext.dev/pt/docs/document-format-inline.md#links)) viram anotações de link URI, uma por trecho de palavras com link em uma linha. Cada uma cobre a caixa da linha e não tem borda. Em uma renderização acessível, cada trecho é um elemento `Link` cujo `/Contents` é o seu texto. Só recebem link os destinos absolutos `http:`, `https:`, `mailto:`, `tel:` e `ftp:`, porque uma URL relativa não tem base dentro de um PDF. Os caracteres fora do ASCII imprimível são codificados com porcentagem. As citações `:ref` e as linhas do sumário mantêm os seus links dentro do documento.

### Exemplo completo no navegador: construir, renderizar, baixar

Juntando tudo: construir o VDT, renderizar em PDF e disparar o download a partir do navegador:

```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);
}
```

**Importante:** chame `ensureConfigFontsLoaded(config)` (ou equivalente) *antes* de `buildDocument` quando a sua configuração fizer referência a fontes web. O layout é medido com as métricas de fonte que o navegador tiver naquele momento para a família; se a fonte verdadeira ainda não chegou, o VDT é medido com uma substituta e o PDF não vai coincidir com a saída do canvas ou do HTML. O Sandbox faz isso explicitamente antes de cada renderização (veja `packages/postext-sandbox/src/viewport/PdfViewport.tsx`).

### Exemplo ao vivo: um PDF no navegador

O fluxo completo acima, rodando no navegador: o pen importa `postext` e `postext-pdf` de uma CDN, carrega as fontes web, constrói o documento, incorpora os cortes do Fontsource pelo provedor de fontes e entrega os bytes a um link que abre o arquivo em uma nova aba e a um link de download. O PDF resultante tem as mesmas quebras de linha que a saída do canvas e do HTML, fontes realmente incorporadas e marcadores de estrutura.

> **Exemplo executável: Postext · gerar um PDF no navegador** — Diagrame um documento Markdown com o postext e renderize-o em PDF com o postext-pdf. ([código](https://github.com/drnachio/postext/tree/main/docs/examples/render-pdf))

### Renderizar o PDF em um worker

`postext-pdf/worker` tira `renderToPdf` da thread principal. O worker escreve o texto, as figuras vetoriais, a árvore de estrutura e o próprio arquivo. Duas tarefas precisam da página, então o worker as pede à thread principal: buscar as fontes e rasterizar um SVG por meio de um `<img>`. Em um livro de centenas de páginas, a renderização leva segundos que, de outro modo, travariam a página; para poucas páginas, chamar `renderToPdf` diretamente é mais simples.

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

const pdfWorker = createPdfWorker();
const bytes = await pdfWorker.render(docs, {
  fontProvider,                                     // roda nesta thread
  resourceBytes: new Map([['map.svg', svgBytes]]),  // um Map; os seus buffers passam para o worker
  onProgress: ({ phase, pages, totalPages }) => showProgress(phase, pages, totalPages),
  onWarning: (w) => console.info(w.message),
});
pdfWorker.dispose();
```

- `render(docs, options)` recebe um documento ou a lista de documentos de capítulos de um livro. Aceita as opções de `renderToPdf`, com duas diferenças. `resourceBytes` é um `Map<string, Uint8Array>` cujos buffers são transferidos, então passe cópias dos bytes que você quer manter. `rasterizeSvg`, quando informado, roda na thread principal; por padrão, o `Image` e o canvas da própria página fazem o trabalho.
- Um handle renderiza um documento por vez. `dispose()` encerra o worker e rejeita qualquer renderização ainda pendente.
- `createPdfWorker({ worker })` aceita um `Worker` criado por você, para ferramentas de build que controlam as URLs de workers. Esse worker precisa executar `postext-pdf/worker/entry`.

**A partir de uma CDN.** Por padrão, o script do worker é carregado da URL do próprio pacote (`new URL('./pdf.worker.js', import.meta.url)`). Uma página em outra origem pode não conseguir iniciá-lo: importado do `esm.sh`, `createPdfWorker()` lança `Failed to construct 'Worker': Script at 'https://esm.sh/postext-pdf@…/pdf.worker.js' cannot be accessed from origin …`. Em vez disso, inicie um worker de módulo da mesma origem que importe o ponto de entrada (se fixar uma versão, fixe a mesma nas duas URLs):

```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' }) });
```

O worker de layout de `postext/worker` precisa do mesmo invólucro ao redor de `postext/worker/entry` (veja [Executar o layout em um Web Worker](https://postext.dev/pt/docs/configuration-programmatic-usage.md#executar-o-layout-em-um-web-worker)).

### PDFs prontos para impressão

Para fluxos de produção gráfica, ajuste estas opções de configuração antes de renderizar:

- **`page.cutLines.enabled: true`**: acrescenta a área de sangria e as marcas de corte ao redor do refile, e dá a cada página uma TrimBox e uma BleedBox. Veja [Marcas de corte](https://postext.dev/pt/docs/configuration-page-layout.md#marcas-de-corte).
- **`print: { standard: 'pdfx4', outputProfile: 'fogra51' }`** (ou `'pdfx1a'`): um arquivo PDF/X com a condição de saída, a identificação e as caixas que uma gráfica confere; cada cor e cada imagem RGB separadas com o perfil ICC, o preto 100 % K sobreimpresso e as áreas pretas grandes em preto composto. Veja [Produção gráfica (configuração)](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#produção-gráfica-configuração).
- **`colorSpace: 'cmyk'`** (ou `pdfGeneration: { forceColorSpace: true, colorSpace: 'cmyk' }`): a mesma separação, sem a identificação PDF/X (as marcas de corte ficam sempre na cor de registro). As matrizes de impressão em PDF são incorporadas como estão.
- **`page.dpi: 300`**: os px por polegada da diagramação; um bitmap sem resolução própria é impresso com essa resolução no seu tamanho natural. O [preflight](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#preflight) informa as imagens abaixo de 300 ppi no tamanho impresso.
- **`ColorValue.cmyk`**: uma cor definida em CMYK é impressa com os seus valores exatos.
- **`{ pageNegative: true }`** em `RenderToPdfOptions`: inverte a área de refile com um modo de mesclagem Difference (as marcas de corte não são invertidas). Útil em verificações de pré-impressão de tipografia escura sobre fundo claro.

### Implementação de referência

O componente `PdfViewport` do Sandbox (`packages/postext-sandbox/src/viewport/PdfViewport.tsx`) liga as peças acima em uma visualização ao vivo com botões para gerar de novo, baixar e imprimir, e é um bom ponto de partida para qualquer integração de PDF no navegador. Ele constrói o VDT pelo worker de layout compartilhado (veja [Executar o layout em um Web Worker](https://postext.dev/pt/docs/configuration-programmatic-usage.md#executar-o-layout-em-um-web-worker)), para que clicar em *Regenerar* não congele a interface enquanto o pipeline roda; a thread principal só cuida de `renderToPdf` (que já é rápido depois que o VDT existe).
