Em poucas palavras
Um fotolivro de paisagem que conta um dia na serra em seis imagens. Mostra como encher páginas inteiras com fotos até a borda e estender uma imagem sobre duas páginas lado a lado.
O que você vai compor
Sierra, um fotolivro de paisagem em formato 280 × 210 mm: um dia numa serra de granito em seis pranchas, da primeira luz até a manhã seguinte à primeira neve. Cada prancha enche a página até o refile e traz só um numeral pequeno e a hora no canto inferior esquerdo; a primeira traz também o título, SIERRA em Syne de 72 pt, vazado em branco no céu do amanhecer. A tempestade cruza a medianiz das páginas 4 e 5 como uma única imagem. Três textos curtos, compostos numa coluna de 100 mm junto à lombada, ficam de frente para as pranchas que vêm depois deles. O texto da tempestade termina numa prancha menor, a do entardecer, que flutua abaixo dele com o numeral na legenda. A última prancha mostra o circo glaciar da segunda, do mesmo matacão, coberto de neve. As pranchas são fotografias geradas com modelos de difusão; a da neve foi feita como edição da prancha do meio-dia, para que as duas vistas coincidam.
Esta receita responde a
- Como dou a cada capítulo sua própria foto, cor ou variante de abertura?
- Como insiro páginas em branco de propósito, ou começo uma seção em uma nova página dupla?
- Como oculto os cabeços nas aberturas e nas páginas em branco, ou pinto uma página par em branco com a cor da parte?
- Como componho ilustrações sem numeração: ornamentos, vinhetas, logotipos?
A resposta curta
// '{attr.art}' is filled in per heading, so a plain plate names its picture on its line:
// # Noon {style="lamina" art="mediodia" …}. Only the cover and the last plate draw extras.
const PLATES = [ // the style id, its picture, the ink of its caption, anything extra it draws
{ id: 'alba', art: 'alba', extra: (colour) => cover(colour) }, // an arrow: cover() is below
{ id: 'lamina', art: '{attr.art}' }, // any plate on a page of its own
{ id: 'pliego', art: '{attr.art}', half: 'verso' }, // one picture across a spread:
{ id: 'pliego-recto', art: '{attr.art}', half: 'recto' }, // the left half, then the right
{ id: 'nieve', art: 'nieve', ink: 'ink', extra: (colour) => colophon(colour) },
];
const plate = ({ id, art, half, ink = 'white', extra = () => [] }) => ({
id, span: 'page', // an opener: a span heading always starts a page of its own
// The left half opens on an even page, a verso, so the right half faces it across the
// gutter (gotcha: parity-page1-recto).
...(half === 'verso' && { breakBefore: { enabled: true, parity: 'even' } }),
// No margins: the plate's column is the page, and minHeight fills it, so what follows starts
// on the next page. Images reserve no room (gotcha: opener-image-no-reserve), and a minHeight
// taller than the column is dropped whole (gotcha: opener-taller-than-column).
margins: { top: mm(0), bottom: mm(0), left: mm(0), right: mm(0) },
advancedDesign: { enabled: true, minHeight: mm(PAGE.height), slot: { elements: [
{ kind: 'image', id: 'picture', resourceId: art, // the recto half is the same picture,
placement: at('page', 'top-left', half === 'recto' ? -PAGE.width : 0, 0, // moved left
{ width: mm(half ? 2 * PAGE.width : PAGE.width), height: mm(PAGE.height) }) },
...(half === 'recto' ? [] : caption(col(ink))), ...extra(col(ink)),
] } },
});
Ingredientes
- Funcionalidades
- Imagens nos designs de páginaEstilos de títuloAberturas desenhadasAncoragem de elementos de designGeometria por seçãoAtributos de títuloCapítulos que abrem em página ímparCabeços por tipo de páginaCitações que posicionam as figurasTipos de recurso personalizadosEstilo de legendaCapas, folhas de rosto e colofõesCor do papelMargens espelhadasTextos, fios e caixas nos designs de páginaHifenização e idioma do documentoPaleta de cores semânticaFiguras e tabelas como recursosPáginas em um canvas
- Tipografia
- Andada Pro, Syne, Syne Mono (SIL OFL 1.1)
- Materiais
alba-1600.jpgatardecer-2000.jpgmediodia-1600.jpgnieve-1600.jpgnoche-1600.jpgtormenta-2400.jpg- Plate I, dawn: ridges fading into a peach haze under a dark blue sky (Generated With Diffusion Models, original)
- Plate II, noon: a granite cirque and its lake (Generated With Diffusion Models, original)
- Plate III, storm: rain, lightning and a break of sun across a spread (Generated With Diffusion Models, original)
- Plate IV, dusk: the crest lit orange under a violet sky (Generated With Diffusion Models, original)
- Plate V, night: stars over the ridges and a lit refuge (Generated With Diffusion Models, original)
- Plate VI, snow: the cirque of plate II the morning after (Generated With Diffusion Models, original)
Preparo
#1 · Um estilo de título para cada prancha, gerado a partir de uma lista
O código é a resposta curta logo acima. plate() gera um estilo de título a partir de cada linha de PLATES, e o Markdown define a ordem do ensaio, uma linha por prancha: # Night {style="lamina" art="noche" n="V" hora="22:10"}. O resourceId do elemento de imagem é '{attr.art}', um marcador como os de um texto, então todas as pranchas simples compartilham o estilo lamina e indicam a sua imagem na linha do título; só a capa e a última prancha, que desenham elementos a mais, mantêm um estilo próprio (elementos de imagem). span: 'page' abre cada prancha numa página própria. As margens zeradas deixam a coluna da prancha da altura da página, e minHeight preenche essa coluna, de modo que o que vem depois de uma prancha começa na página seguinte. As duas configurações são necessárias. Sem minHeight, a metade direita da tempestade, que não desenha nenhum texto, fica só com a linha de 7,6 mm do próprio título, e o texto seguinte começa nessa página, por cima da imagem. Com as margens das páginas de texto, a coluna tem 158 mm de altura, menos que os 210 mm de minHeight, então o postext 1.4.1 descarta a reserva inteira e os três textos saem impressos por cima das pranchas que vêm antes deles.
Os dois estilos da página dupla, pliego e pliego-recto, desenham a mesma imagem de 560 mm, aqui art="tormenta": a metade do verso a partir da borda esquerda da página, a metade do anverso 280 mm mais à esquerda, de modo que cada página mostra a sua metade e o canvas corta o resto no refile. parity: 'even' mantém as duas metades frente a frente. Sem isso, tirar a prancha do meio-dia do Markdown poria a metade esquerda na página 3, uma página ímpar, e a metade direita no verso, na página 4; com isso, a página 3 fica em branco e a tempestade continua enchendo as páginas 4 e 5.
#2 · O numeral e a hora vêm da linha do título
const NUMERAL = { x: 16, y: PAGE.height - 21, size: 13 }; // mm from the top left; size in pt
// A design text's baseline sits 0.8 of its line box below its top: set a smaller label beside
// a larger one this much lower and the two share a baseline.
const dropTo = (big, small, lineHeight = 1.2) => (0.8 * lineHeight * (big - small) * 25.4) / 72;
const caption = (colour) => [
{ kind: 'text', id: 'numeral', content: '{attr.n}', fontFamily: 'Syne', fontWeight: 700,
fontSize: pt(NUMERAL.size), color: colour, align: 'left',
placement: at('page', 'top-left', NUMERAL.x, NUMERAL.y) },
{ kind: 'text', id: 'hour', content: '· {attr.hora}', fontFamily: 'Syne Mono', fontSize: pt(8),
letterSpacing: pt(0.8), color: colour, align: 'left', // the dot: a lone I reads as a bar
placement: at('#numeral', 'right-of', 1.6, dropTo(NUMERAL.size, 8)) },
];
O numeral e a hora são atributos do título, então o conteúdo fica no Markdown e um só design serve para todas as pranchas. A hora fica pendurada right-of do numeral, o que alinha o topo dos dois. A linha de base de um texto de design fica a 0,8 da sua caixa de linha abaixo do topo do elemento, e a caixa de linha mede por padrão 1,2 vez o corpo, então dropTo(13, 8) desce a hora 0,8 × 1,2 × (13 − 8) pt e apoia a hora de 8 pt na linha de base do numeral de 13 pt, a 193,4 mm do topo da página. A hora começa com um ponto médio, como a legenda da prancha IV, porque o I da Syne é uma barra simples que, sozinha ao lado da hora, parece um fio. A metade direita da tempestade não tem legenda, e a prancha da neve compõe a sua na cor da tinta, porque o primeiro plano é branco.
#3 · Um título e um colofão nas próprias pranchas
const cover = (colour) => [
{ kind: 'text', id: 'title', content: '{title}', fontFamily: 'Syne', fontWeight: 800,
fontSize: pt(72), lineHeight: 1, letterSpacing: pt(6), textTransform: 'uppercase',
color: colour, align: 'left', placement: at('page', 'top-left', 22, 26) },
{ kind: 'text', id: 'subtitle', content: '{subtitle}', fontFamily: 'Syne Mono',
fontSize: pt(10), letterSpacing: pt(2), textTransform: 'uppercase', color: colour,
align: 'left', placement: at('#title', 'below', 1.5, 3) },
];
// The colophon: one element per line, 10.5 pt apart, so the break falls after the licence, and
// the second line on the baseline of the plate's numeral.
const COLOPHON = { y: NUMERAL.y + dropTo(NUMERAL.size, 7.5), leading: (10.5 * 25.4) / 72 };
const colophon = (colour) => ['colofon', 'tipos'].map((key, line) => ({ kind: 'text', id: key,
content: `{attr.${key}}`, fontFamily: 'Syne Mono', fontSize: pt(7.5), letterSpacing: pt(0.2),
color: colour, align: 'right',
placement: at('page', 'top-right', -16, COLOPHON.y - (1 - line) * COLOPHON.leading) }));
A primeira prancha é a capa. {title} e {subtitle} leem o frontmatter, e extra em PLATES acrescenta os dois elementos ao design dessa prancha, na cor da legenda. O colofão vai na última prancha do mesmo jeito, na cor da tinta sobre a neve: dois elementos de 7,5 pt, um por linha, a partir dos atributos colofon e tipos, alinhados à direita, o segundo na linha de base do numeral. Um único elemento com quebra automática quebraria onde a largura acabasse; com um elemento por linha, a quebra cai depois da licença.
#4 · Os textos seguem as pranchas sem quebra
const textOpener = { enabled: true, slot: { elements: [
{ kind: 'text', id: 'hours', content: '{attr.hora}', fontFamily: 'Syne Mono', fontSize: pt(8),
letterSpacing: pt(0.8), color: col('accent'), align: 'left',
placement: at('container', 'top-left', 0, 0) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Syne', fontWeight: 700,
fontSize: pt(28), lineHeight: 1.05, // a multiple (gotcha: design-lineheight-multiple)
color: col('ink'), align: 'left', overflow: 'wrap',
placement: at('#hours', 'below', 0, 2.5, { width: 'fill' }) },
] } };
// The folio and the book's title under the text block, flush with its left edge, on the
// baseline of the plates' numerals. Not on the plates: a span heading opens their pages, so
// they are opener pages, and 'body' leaves them out.
const foot = (id, content, extra) => ({ kind: 'text', id, content, pages: 'body',
fontFamily: 'Syne Mono', fontSize: pt(7.5), letterSpacing: pt(0.8), color: col('muted'),
align: 'left', ...extra });
const FOOT = NUMERAL.y + dropTo(NUMERAL.size, 7.5) - (PAGE.height - MARGIN.bottom); // from the foot
const footer = { elements: [
foot('folio', '{pageNumber}', { color: col('ink'),
placement: at('container', 'top-left', 0, FOOT) }),
foot('book', '{title}', { textTransform: 'uppercase', placement: at('#folio', 'right-of', 5) }),
] };
A prancha antes de um texto enche a sua página, então o texto começa na página seguinte sem quebra própria. Uma página cujo primeiro bloco é um título com quebra é uma página de abertura, e o pages: 'body' do fólio a deixa de fora. Sem quebra, as páginas de texto mantêm o fólio; as pranchas, abertas por títulos na largura da página, ficam sem ele. O postext 1.4.1 já remove a quebra do nível 1 assim que existe um objeto headings, então apagar breakBefore: { enabled: false } hoje não muda nada. A linha passa a importar quando uma versão restaurar a quebra padrão para página ímpar: ela impede que os textos quebrem, e assim as páginas deles continuam sendo páginas de texto, com fólio. FOOT põe o fólio na linha de base dos numerais das pranchas, a 193,4 mm do topo da página, de modo que o fólio de um texto e o numeral da prancha em frente ficam na mesma linha.
#5 · Declare todas as pranchas e cite só a que flutua
// Plate IV is the one plate that floats, so a counter of its type would number it 1. The type
// prints no number (no caption prefix, an empty template) and the caption carries the numeral.
// The text names the plate with :ref's text, since a bare :ref prints 'lámina' and nothing else.
const lamina = { id: 'lamina', name: t({ en: 'Plate', es: 'Lámina' }),
shortLabel: t({ en: 'plate', es: 'lámina' }), numberingTemplate: '',
resetOn: 'never', counterFormat: 'decimal' };
// JPEGs in assets/, cut to the page's shape: 1600 × 1200 px for 280 × 210 mm (145 dpi).
const picture = (id, [w, h], alt, extra = {}) => ({ id, typeId: 'lamina', kind: 'bitmap',
bitmap: { fileId: `${id}-${w}.jpg`, format: 'jpeg', width: w, height: h },
createdAt: 0, updatedAt: 0, altText: t(alt), ...extra });
const resources = [ // the five the heading styles draw, never cited, and plate IV
picture('alba', [1600, 1200], { en: 'Eight ridges fading into a peach dawn haze.',
es: 'Ocho crestas que se pierden en la bruma del alba.' }),
picture('mediodia', [1600, 1200], { en: 'A granite cirque and its lake under a pale noon sky.',
es: 'Un circo de granito y su laguna bajo el cielo pálido del mediodía.' }),
picture('tormenta', [2400, 900], { en: 'A storm over the range: rain on the left, lightning '
+ 'and a break of sun on the right.', es: 'Una tormenta sobre la sierra: lluvia a la '
+ 'izquierda; un rayo y un claro de sol a la derecha.' }),
picture('noche', [1600, 1200], { en: 'Stars over dark ridges and two lit windows at a refuge.',
es: 'Estrellas sobre crestas oscuras y dos ventanas encendidas en un refugio.' }),
picture('nieve', [1600, 1200], { en: 'The cirque the morning after, white with new snow.',
es: 'El circo a la mañana siguiente, blanco de nieve nueva.' }),
// 2000 px, 339 mm at the layout's 150 dpi, which the float fits to the 100 mm measure.
picture('atardecer', [2000, 800], { en: 'A crest lit orange under strips of cloud, violet sky.',
es: 'Una cresta encendida de naranja bajo franjas de nube, en un cielo violeta.' },
{ caption: t({ en: 'IV · Dusk · 19:41', es: 'IV · Atardecer · 19:41' }) }),
];
Um elemento de imagem de um design desenha um recurso declarado, então as seis imagens estão em resources: JPEGs da pasta assets/ da receita, cada um recortado no formato da página ou da página dupla que preenche e declarado com o seu tamanho em pixels, que é só o que a diagramação lê. loadImage() registra cada arquivo antes da composição. A prancha IV é a única citada com :ref e, por isso, a única que flutua. Ela ocupa o primeiro espaço livre depois da citação, na página do seu texto, uma linha abaixo do último parágrafo. Um tipo numerado lhe daria a legenda Plate 1, já que ela é o único recurso do seu tipo que flutua. lamina, em vez disso, tem um numberingTemplate vazio, e a própria legenda começa com IV. Na frase, o text do :ref dá nome à prancha, porque um :ref sem ele imprime só a palavra plate.
A receita completa
// ═══ Postext Cookbook · Nº 068 · Photo essay with full-bleed plates ═══════════════ // https://postext.dev/en/cookbook/photo-essay-full-bleed // Code: MIT · Text: original (CC BY 4.0) · Plates: diffusion models // Fonts: Andada Pro, Syne, Syne Mono (SIL OFL 1.1) · Needs postext ≥ 1.8.0 // Sierra, a landscape photobook: one day in a mountain range in six plates, each on a page of // its own and one across the gutter of a spread, with three short texts between them. import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'photo-essay-full-bleed'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: the paper between the plates, the ink and one rust for the labels const palette = { ink: '#1c1f27', // text: the blue-black of the night plate paper: '#f3f0ea', // every page's ground: a pale stone, seen only between the plates accent: '#94462e', // the times over each text: the dusk plate's rust, dark enough for 8 pt muted: '#5f646d', // the running foot and the caption of the small plate white: '#fbfaf7', // type set on the plates }; // Design elements read the hex, not the palette, in 1.4.1 (gotcha: palette-skips-designs). const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries(palette) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const PAGE = { width: 280, height: 210 }; // a landscape photobook, in mm const MARGIN = { top: 28, bottom: 24, inner: 40, outer: 140 }; // text pages: a 100 mm measure const LEAD = 15; // body leading in pt const at = (to, edge, x = 0, y = 0, size) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) }, ...(size && { size }) }); // #region answer: the plates' heading styles, generated from a list // '{attr.art}' is filled in per heading, so a plain plate names its picture on its line: // # Noon {style="lamina" art="mediodia" …}. Only the cover and the last plate draw extras. const PLATES = [ // the style id, its picture, the ink of its caption, anything extra it draws { id: 'alba', art: 'alba', extra: (colour) => cover(colour) }, // an arrow: cover() is below { id: 'lamina', art: '{attr.art}' }, // any plate on a page of its own { id: 'pliego', art: '{attr.art}', half: 'verso' }, // one picture across a spread: { id: 'pliego-recto', art: '{attr.art}', half: 'recto' }, // the left half, then the right { id: 'nieve', art: 'nieve', ink: 'ink', extra: (colour) => colophon(colour) }, ]; const plate = ({ id, art, half, ink = 'white', extra = () => [] }) => ({ id, span: 'page', // an opener: a span heading always starts a page of its own // The left half opens on an even page, a verso, so the right half faces it across the // gutter (gotcha: parity-page1-recto). ...(half === 'verso' && { breakBefore: { enabled: true, parity: 'even' } }), // No margins: the plate's column is the page, and minHeight fills it, so what follows starts // on the next page. Images reserve no room (gotcha: opener-image-no-reserve), and a minHeight // taller than the column is dropped whole (gotcha: opener-taller-than-column). margins: { top: mm(0), bottom: mm(0), left: mm(0), right: mm(0) }, advancedDesign: { enabled: true, minHeight: mm(PAGE.height), slot: { elements: [ { kind: 'image', id: 'picture', resourceId: art, // the recto half is the same picture, placement: at('page', 'top-left', half === 'recto' ? -PAGE.width : 0, 0, // moved left { width: mm(half ? 2 * PAGE.width : PAGE.width), height: mm(PAGE.height) }) }, ...(half === 'recto' ? [] : caption(col(ink))), ...extra(col(ink)), ] } }, }); // #endregion // #region caption: the plate's numeral and hour, lower left, from the heading's attributes const NUMERAL = { x: 16, y: PAGE.height - 21, size: 13 }; // mm from the top left; size in pt // A design text's baseline sits 0.8 of its line box below its top: set a smaller label beside // a larger one this much lower and the two share a baseline. const dropTo = (big, small, lineHeight = 1.2) => (0.8 * lineHeight * (big - small) * 25.4) / 72; const caption = (colour) => [ { kind: 'text', id: 'numeral', content: '{attr.n}', fontFamily: 'Syne', fontWeight: 700, fontSize: pt(NUMERAL.size), color: colour, align: 'left', placement: at('page', 'top-left', NUMERAL.x, NUMERAL.y) }, { kind: 'text', id: 'hour', content: '· {attr.hora}', fontFamily: 'Syne Mono', fontSize: pt(8), letterSpacing: pt(0.8), color: colour, align: 'left', // the dot: a lone I reads as a bar placement: at('#numeral', 'right-of', 1.6, dropTo(NUMERAL.size, 8)) }, ]; // #endregion // #region cover: the book's title reversed out of the dawn sky of the first plate const cover = (colour) => [ { kind: 'text', id: 'title', content: '{title}', fontFamily: 'Syne', fontWeight: 800, fontSize: pt(72), lineHeight: 1, letterSpacing: pt(6), textTransform: 'uppercase', color: colour, align: 'left', placement: at('page', 'top-left', 22, 26) }, { kind: 'text', id: 'subtitle', content: '{subtitle}', fontFamily: 'Syne Mono', fontSize: pt(10), letterSpacing: pt(2), textTransform: 'uppercase', color: colour, align: 'left', placement: at('#title', 'below', 1.5, 3) }, ]; // The colophon: one element per line, 10.5 pt apart, so the break falls after the licence, and // the second line on the baseline of the plate's numeral. const COLOPHON = { y: NUMERAL.y + dropTo(NUMERAL.size, 7.5), leading: (10.5 * 25.4) / 72 }; const colophon = (colour) => ['colofon', 'tipos'].map((key, line) => ({ kind: 'text', id: key, content: `{attr.${key}}`, fontFamily: 'Syne Mono', fontSize: pt(7.5), letterSpacing: pt(0.2), color: colour, align: 'right', placement: at('page', 'top-right', -16, COLOPHON.y - (1 - line) * COLOPHON.leading) })); // #endregion // #region texts: a text between plates opens on the next page with its hours above it const textOpener = { enabled: true, slot: { elements: [ { kind: 'text', id: 'hours', content: '{attr.hora}', fontFamily: 'Syne Mono', fontSize: pt(8), letterSpacing: pt(0.8), color: col('accent'), align: 'left', placement: at('container', 'top-left', 0, 0) }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Syne', fontWeight: 700, fontSize: pt(28), lineHeight: 1.05, // a multiple (gotcha: design-lineheight-multiple) color: col('ink'), align: 'left', overflow: 'wrap', placement: at('#hours', 'below', 0, 2.5, { width: 'fill' }) }, ] } }; // The folio and the book's title under the text block, flush with its left edge, on the // baseline of the plates' numerals. Not on the plates: a span heading opens their pages, so // they are opener pages, and 'body' leaves them out. const foot = (id, content, extra) => ({ kind: 'text', id, content, pages: 'body', fontFamily: 'Syne Mono', fontSize: pt(7.5), letterSpacing: pt(0.8), color: col('muted'), align: 'left', ...extra }); const FOOT = NUMERAL.y + dropTo(NUMERAL.size, 7.5) - (PAGE.height - MARGIN.bottom); // from the foot const footer = { elements: [ foot('folio', '{pageNumber}', { color: col('ink'), placement: at('container', 'top-left', 0, FOOT) }), foot('book', '{title}', { textTransform: 'uppercase', placement: at('#folio', 'right-of', 5) }), ] }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object // The English sample is British English, set with the US patterns: 1.4.1 ships no en-gb. locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, resourceTypes: [lamina], page: { width: mm(PAGE.width), height: mm(PAGE.height), dpi: 150, backgroundColor: col('paper'), // the ground of every page; the plates cover it margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, // left is the inner margin layout: { layoutType: 'single' }, bodyText: { fontFamily: 'Andada Pro', fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), // both default to blue referenceColor: col('ink'), referenceBold: false, // 'lámina IV' reads as a word of the text firstLineIndent: mm(4), indentAfterHeading: false, // justified and hyphenated by default minWordSpacing: 0.7, maxWordSpacing: 1.6, // a narrower range than the defaults, 0.6 to 2 maxRuntTracking: 0 }, // tracking 1.4.1 never paints (gotcha: runt-tracking-unpainted) // A heading's own line is measured even where its design paints the title: set it in a face // the page loads, or the kit fetches Open Sans for it. headings: { fontFamily: 'Syne', levels: [ // A text follows its plate with no forced break (gotcha: headings-drop-h1-break): the // plate fills its page, so the text still starts a page, and that page stays a body page, // with its running foot. { level: 1, breakBefore: { enabled: false }, advancedDesign: textOpener }, ] }, headingStyles: PLATES.map(plate), captionStyle: { fontFamily: 'Syne Mono', fontSize: pt(7.5), color: col('muted') }, header: { elements: [] }, footer, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Amostra em Markdown · 43 linhas · content.en.md
title: "Sierra" subtitle: "An essay in six lights" --- # Dawn {style="alba" n="I" hora="08:12"} # The climb {hora="06:40–14:06 · 1770–2180 m"} We left the car park at 6:40 with head torches, on the paved track that climbs between the broom. At that hour the granite still holds the night’s cold, and our boots sound louder than they should. From the first bend we can see the lights of the village below, eleven of them, counted twice; from the second we can’t. At 8:12 the sun touches the highest point of the crest. It is a pink edge that lasts as long as it takes to get the camera out; then the light works its way down the gullies, slowly, and takes almost an hour to reach the path. The meadow grass is white with frost and crunches under our boots. An ibex watches us from a boulder and does not move, so we are the ones who go round. We stop at the pass to eat. There is a spring that is not on the map, an iron spout driven into the rock, and the water is cold enough to make our teeth ache. Andrés notes the time and the altitude of every photograph in a notebook; at one o’clock the thermometer hanging from his rucksack reads eleven degrees in the sun. Solar noon falls after two here, on summer time. By then the shadows have gone under the stones, and the lake, seen from the pass, looks like a sheet of tin someone has left behind between the walls. # Noon {style="lamina" art="mediodia" n="II" hora="14:06"} # Storm {style="pliego" art="tormenta" n="III" hora="16:40"} # Storm {style="pliego-recto" art="tormenta"} # Eleven seconds {hora="15:00–19:50 · 2180–1950 m"} At three o’clock the air grows heavy. A cloud that at first we take for another mountain is building in the west. We pack in a hurry. The first thunder comes eleven seconds after the flash: nearly four kilometres away, Andrés reckons, because he always counts. The next one comes after four. We take shelter under the overhang of a boulder as big as a house. First small hail, then rain; the wall opposite fades behind the curtain and comes back darker, washed. Nobody speaks for a good hour. Around half past seven the storm drifts away down the valley and leaves the sky broken into strips. For a moment the sun comes out below the cloud, low in the west, and turns the whole crest a colour that lasts less than three minutes (:ref{id="atardecer" text="plate IV"}). We go down to the refuge soaked through, beside the stream, which is running full. # Night {style="lamina" art="noche" n="V" hora="22:10"} # First snow {hora="22:10–09:00 · 1950 m"} At ten the sky is clear, and the refuge window cannot hold all its stars. At midnight somebody opens the door and says it is snowing. We go out in our socks. The snow falls straight down, with no wind, in large flakes that are slow to melt on our sleeves, and the torch lights only a cone of white points coming towards us. In the morning the thermometer at the window reads two below. There is a hand’s depth of snow at the door and a rim of thin ice along the lake. The ridges that were grey yesterday are white with black streaks, the ribs where the snow will not stick, and the sky is the same colour as the ground. Andrés takes the last photograph at 8:31, from the same boulder as at noon yesterday. It takes him some time to find it, because the stones he used as markers are now identical white mounds. In the end he places it by matching the crest to yesterday’s picture on the camera screen. It is the first snow of the autumn, two weeks early, the warden says. We set off at nine. The paved track is under the snow, so we go down from one cairn to the next. # Snow {style="nieve" n="VI" hora="08:31" colofon="Sierra. An essay in six lights · Plates generated with diffusion models · Text: CC BY 4.0" tipos="Set in Andada Pro, Syne and Syne Mono (SIL OFL)"}`; // content.<lang>.md, inlined by the Cookbook // #region plates: every picture is a resource; only plate IV is cited, so only it floats // Plate IV is the one plate that floats, so a counter of its type would number it 1. The type // prints no number (no caption prefix, an empty template) and the caption carries the numeral. // The text names the plate with :ref's text, since a bare :ref prints 'lámina' and nothing else. const lamina = { id: 'lamina', name: t({ en: 'Plate', es: 'Lámina' }), shortLabel: t({ en: 'plate', es: 'lámina' }), numberingTemplate: '', resetOn: 'never', counterFormat: 'decimal' }; // JPEGs in assets/, cut to the page's shape: 1600 × 1200 px for 280 × 210 mm (145 dpi). const picture = (id, [w, h], alt, extra = {}) => ({ id, typeId: 'lamina', kind: 'bitmap', bitmap: { fileId: `${id}-${w}.jpg`, format: 'jpeg', width: w, height: h }, createdAt: 0, updatedAt: 0, altText: t(alt), ...extra }); const resources = [ // the five the heading styles draw, never cited, and plate IV picture('alba', [1600, 1200], { en: 'Eight ridges fading into a peach dawn haze.', es: 'Ocho crestas que se pierden en la bruma del alba.' }), picture('mediodia', [1600, 1200], { en: 'A granite cirque and its lake under a pale noon sky.', es: 'Un circo de granito y su laguna bajo el cielo pálido del mediodía.' }), picture('tormenta', [2400, 900], { en: 'A storm over the range: rain on the left, lightning ' + 'and a break of sun on the right.', es: 'Una tormenta sobre la sierra: lluvia a la ' + 'izquierda; un rayo y un claro de sol a la derecha.' }), picture('noche', [1600, 1200], { en: 'Stars over dark ridges and two lit windows at a refuge.', es: 'Estrellas sobre crestas oscuras y dos ventanas encendidas en un refugio.' }), picture('nieve', [1600, 1200], { en: 'The cirque the morning after, white with new snow.', es: 'El circo a la mañana siguiente, blanco de nieve nueva.' }), // 2000 px, 339 mm at the layout's 150 dpi, which the float fits to the 100 mm measure. picture('atardecer', [2000, 800], { en: 'A crest lit orange under strips of cloud, violet sky.', es: 'Una cresta encendida de naranja bajo franjas de nube, en un cielo violeta.' }, { caption: t({ en: 'IV · Dusk · 19:41', es: 'IV · Atardecer · 19:41' }) }), ]; // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces, loaded before the build (gotcha: fonts-first) 'Andada Pro': ['400'], Syne: ['700', '800'], 'Syne Mono': ['400'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const files = resources.map((r) => r.bitmap.fileId); // a resource names a file: load each await Promise.all(files.map((f) => loadImage(f, asset(f)))); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'Sierra: a photo essay in landscape', es: 'Sierra: un ensayo fotográfico apaisado' }) });Kit · core, fonts, viewer, images: igual em todas as receitas · 271 linhas
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v2 ── the same in every recipe · postext.dev/cookbook // Postext measures with the loaded faces and caches the widths: load every face // before the first build, from Fontsource, the files the PDF embeds too. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` and loads any face the pages use that FONTS missed (a regular * one with a warning), then clears the measurement cache and builds again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout; `base` marks a block's own face. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · images v1 ── recipes with pictures · postext.dev/cookbook /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
O script.js montado funciona como está: cole-o como script de módulo em qualquer página ou abra a receita no CodePen. Pasta da receita no GitHub ↗ (abre em uma nova aba)
Variações
#Faça as pranchas flutuarem de frente para os verbetes
Para pranchas que flutuam até a página ímpar em frente a um verbete de catálogo, com a largura da figura calculada pelas proporções, veja Verbetes de catálogo de frente para as pranchas.
#Abra uma matéria sobre uma foto sangrada
Para uma foto sangrada no alto de uma matéria, com o título e o olho por cima, veja a reportagem de revista.
Erros comuns
Erro comum
As imagens de uma abertura nunca contam para a altura que ela reserva
No postext 1.4.1, um título com design avançado mede a altura que reserva sem as imagens: textos, fios e caixas contam, mesmo quando ancorados na página, mas uma imagem, como uma ilustração sangrada no alto da página, não reserva nada, então o texto pode começar por cima dela. Defina com minHeight onde o texto deve começar. Aberturas desenhadas →
Erro comum
Uma abertura mais alta que a sua coluna perde toda a reserva
No postext 1.4.1, quando a altura que um título com design avançado reserva (o seu minHeight, ou o seu elemento de design mais baixo) é maior que a da coluna que ele abre, o título fica só com a altura da própria linha, sem nenhum aviso, e o texto é composto por cima do design. Uma prancha de página inteira precisa de uma coluna tão alta quanto a página: zere as margens do estilo do título e defina minHeight com a altura da página. Aberturas desenhadas →
Erro comum
A página 1 é ímpar: planeje com os números físicos
A página 1 fica à direita e a 2 é a primeira página par, então planeje as páginas duplas com os números físicos de página: uma abertura em página par fica de frente para a ímpar que vem depois dela. Quebras de página e de coluna →
Erro comum
Qualquer objeto headings desativa a quebra de página do H1
Por padrão, um H1 salta para uma página ímpar (always-odd), mas passar qualquer objeto headings redefine esse padrão, então os capítulos ficam emendados e span: 'page' não faz nada. Declare de novo headings.levels[0].breakBefore: { enabled: true, parity } em toda configuração. Capítulos que abrem em página ímpar →
Erro comum
Uma paleta trocada não chega aos elementos de design nem à cor das referências
postext 1.4.1 aplica colorPalette aos estilos de texto (corpo, títulos, listas, legendas, tabelas, boxes), mas não aos elementos de cabeçalhos, rodapés, aberturas e páginas de parte, nem a bodyText.referenceColor: eles mantêm o hex escrito ao lado do seu paletteId. Se você trocar a paleta, para uma edição de tela escura ou para mudar as cores, reescreva cada cor vinculada a partir de colorPalette antes de compor. Paleta de cores semântica →
Erro comum
O lineHeight de um texto de design é um múltiplo, nunca uma medida
Num slot de design, o lineHeight de um elemento de texto multiplica o tamanho da fonte (lineHeight: 1.05). No postext 1.4.1, uma medida como pt(15) não é rejeitada: a altura da abertura dá NaN, o espaço que ela reserva, minHeight incluído, se perde sem aviso e o texto passa por baixo do título. Textos, fios e caixas nos designs de página →
Erro comum
Sem <marker> nem filtros na arte SVG (vira bitmap)
Uma figura SVG só continua vetorial no PDF sem <marker>, filtros e máscaras; caso contrário, é rasterizada, e filtros muito aninhados podem deixá-la em branco no Chrome. Desenhe as pontas de seta como caminhos (paths). Figuras e tabelas como recursos →
Erro comum
O ajuste de linhas curtas pode apertar um tracking que nunca é pintado
No postext 1.4.1, quando um parágrafo termina numa linha curta, a diagramação o compõe com uma linha a menos: primeiro aperta o espaçamento entre palavras, depois aplica até maxRuntTracking milésimos de em de tracking negativo. Os renderizadores de canvas e PDF só pintam tracking acima de zero, então o parágrafo sai impresso sem ele: as linhas justificadas perdem essa diferença nos espaços entre palavras, que ficam esmagados, e a última linha pode passar da medida e ser cortada na borda da coluna. Defina bodyText.maxRuntTracking: 0, que mantém o ajuste pelo espaçamento entre palavras, e reescreva os parágrafos que voltarem a terminar numa linha curta. Viúvas, órfãs e linhas curtas →
Erro comum
Só 8 idiomas têm hifenização, com o código exato
A hifenização existe para en-us, es, fr, de, it, pt, ca e nl, com o código exato: 'es-ES' ou qualquer outro idioma passa sem aviso para o inglês americano. Hifenização e idioma do documento →
Erro comum
Carregue todas as fontes antes do layout
O motor de layout mede o texto com as fontes que o navegador carregou e guarda as larguras em cache, então uma fonte que chega depois da primeira composição deixa quebras de linha erradas e um PDF que não corresponde mais à tela. Carregue antes todos os pesos e estilos e chame clearMeasurementCache() antes de recompor quando alguma chegar atrasada. Fontes antes da diagramação →
- Nunca cite uma prancha que um design já desenha. Citada, ela também flutua como figura: um
:refà prancha do meio-dia no primeiro texto põe uma segunda cópia dela na página 3, e a tempestade anda duas páginas, depois de uma página em branco. - A prancha do entardecer fica uma linha abaixo do seu texto, não no pé da página. Na última página antes de uma prancha, o postext 1.4.1 sobe até o texto um flutuante que esteja abaixo dele, e
position: 'bottom'não muda nada ali. - Cada texto foi ajustado para caber numa página nas duas edições. Se o primeiro passar para a página 3, a prancha do meio-dia vai para a página 4 e a tempestade para as páginas 6 e 7, depois de uma página 5 em branco.
- A metade direita da tempestade é um segundo título
# Storm. A página não imprime esse título, mas um:::tocmontado a partir dos títulos lista a tempestade duas vezes, nas páginas 4 e 5. - As pranchas terminam no refile, que é a borda do canvas. Com
page.cutLinesligado, o canvas cresce em volta do refile, e a sangria em volta de cada prancha mostra a cor do papel. Para impressão, ancore as pranchas em'bleed'e aumente cada uma pela medida da sangria em todos os lados.
Créditos
- Receita
- Ignacio Ferro
- Texto
- The three texts, the captions and the colophon, in Spanish and English, and the six plates, drawn in code · Postext Cookbook · CC BY 4.0
- Imagens
- Plate I, dawn: ridges fading into a peach haze under a dark blue sky · Generated With Diffusion Models · original
- Plate II, noon: a granite cirque and its lake · Generated With Diffusion Models · original
- Plate III, storm: rain, lightning and a break of sun across a spread · Generated With Diffusion Models · original
- Plate IV, dusk: the crest lit orange under a violet sky · Generated With Diffusion Models · original
- Plate V, night: stars over the ridges and a lit refuge · Generated With Diffusion Models · original
- Plate VI, snow: the cirque of plate II the morning after · Generated With Diffusion Models · original
- Fontes
- Andada Pro (SIL OFL 1.1) · Syne (SIL OFL 1.1) · Syne Mono (SIL OFL 1.1)
- Código
- MIT, como o Postext
Editar este texto ↗ (abre em uma nova aba)Pasta da receita no GitHub ↗ (abre em uma nova aba)


