简单来说
一本介绍五幅画的小导览。正文提到网页的地方有一个小号数字,指向页脚;页脚有一个方形码,用手机一扫就能打开那个网页。
成品一览
一本大都会艺术博物馆荷兰展厅的口袋导览:五幅画,步行一小时,一页一幅。封面是维米尔窗边的少女,画面在上边缘被裁切。每一站先是展厅号、画名和画家,再是画作,旁边放展签,然后是三段短文。正文像普通网页那样链接到博物馆的藏品目录。印在纸上时,每个链接变成一个小号数字;页脚的注释给出短网址和一个二维码,参观者站在画前用手机对准它即可。
这道食谱解答
- 怎样把书中链接的网址印出来,做成编号注释或读者可扫描的二维码?
简短回答
const links = {
print: 'notes', // a note marker after the link text, the address in a note at the foot
show: 'both', // the short address for reading, the code for the phone
urlDisplay: 'short', // metmuseum.org/art/collection/search/436616: no https://, no www.
repeat: 'first', // an address noted once is cited again by its number, never noted twice
qr: { size: mm(16), side: 'start', gap: em(1.2), color: col('ink') },
};
// Link notes are footnotes: they take the note style and the numbering below. 'document'
// numbers them through the walk, so the five stops print notes 3 to 7, not five notes 1.
const footnotes = { placement: 'column', numbering: 'document', fontSize: pt(8),
lineHeight: pt(10.5), color: col('muted'), spaceAbove: pt(LEAD), spaceBetween: pt(5),
separator: { enabled: true, width: 0.18, lineWidth: pt(0.5), color: col('rule') } };
用料
- 类型
- Newsreader, Instrument Serif, Archivo(SIL OFL 1.1)
- 素材
hals-1200.jpgrembrandt-1200.jpgruisdael-1400.jpgsteen-1200.jpgvermeer-1600.jpgvermeer-cover-1800.jpg- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (the cover, cropped) (The Metropolitan Museum of Art, CC0 1.0)
- Frans Hals, Young Man and Woman in an Inn, 1623 (stop 1) (The Metropolitan Museum of Art, CC0 1.0)
- Jan Steen, Merry Company on a Terrace, about 1670 (stop 2) (The Metropolitan Museum of Art, CC0 1.0)
- Rembrandt, Aristotle with a Bust of Homer, 1653 (stop 3) (The Metropolitan Museum of Art, CC0 1.0)
- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (stop 4) (The Metropolitan Museum of Art, CC0 1.0)
- Jacob van Ruisdael, Wheat Fields, about 1670 (stop 5) (The Metropolitan Museum of Art, CC0 1.0)
做法
#1 · 写链接,印注释
代码就是上面的简短回答。Markdown 里保留普通链接 [The catalogue entry](https://www.metmuseum.org/…/436616),links.print: 'notes' 把每个链接变成带网址和二维码的编号注释(印刷版中的链接)。这些注释就是脚注:numbering: 'document' 在整条路线上连续编号,读者看到的是 1 到 7;repeat: 'first' 让最后一站用注释 3 引用哈尔斯那幅画,不必把同一个码印两次。在 PDF 里,二维码以矢量绘制,本身也是一个链接。

#2 · 一站一页
const stop = { enabled: true, minHeight: pt(4 * LEAD), slot: { elements: [
{ kind: 'text', id: 'stop', content: '{attr.stop}', ...caps(7.5), color: col('delft'),
align: 'left', placement: at('container', 'top-left', 0, 0) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(22),
lineHeight: 1.05, color: col('ink'), align: 'left', placement: at('#stop', 'below', 0, 1.6) },
{ kind: 'text', id: 'artist', content: '{attr.artist}', fontFamily: TEXT, italic: true,
fontSize: pt(10.5), color: col('muted'), align: 'left',
placement: at('#title', 'below', 0, 0.8) },
] } };
每一站是一个二级标题,它的 breakBefore 另起一页,奇偶不限,参观者每看一幅画翻一页。展厅号和画家是标题属性;版式把它们放在画名的上方和下方,minHeight 让每页的画作从同一高度开始。
#3 · 展签放在画旁
// Paintings are not numbered: their own type, with no caption prefix and no number.
const paintingType = { id: 'painting', name: t({ en: 'Painting', es: 'Cuadro' }),
shortLabel: t({ en: 'painting', es: 'cuadro' }), captionPrefix: '', numberingTemplate: '',
resetOn: 'never', counterFormat: 'decimal' };
// Set where the ::resource line stands, under the stop's heading; caption and note beside it.
const HERE = { position: 'here', captionSide: 'inside' };
const cm = (n) => (LANG === 'es' ? String(n).replace('.', ',') : String(n)); // decimal comma
// The wall label: medium, size in cm (no-break spaces) and the museum's credit line.
const painting = (id, w, h, [cmH, cmW], credit, alt, width = 0.5) => ({
id, typeId: 'painting', kind: 'bitmap', createdAt: 0, updatedAt: 0, placement: { ...HERE, width },
bitmap: { fileId: `${id}-${Math.max(w, h)}.jpg`, format: 'jpeg', width: w, height: h },
caption: `${OIL}, ${cm(cmH)}\u00a0×\u00a0${cm(cmW)}\u00a0cm. ${credit}`, altText: alt,
note: 'The Metropolitan Museum of Art, Open Access (CC0)' });
const OIL = t({ en: 'Oil on canvas', es: 'Óleo sobre lienzo' });
const ALTMAN = t({ en: 'Bequest of Benjamin Altman, 1913', es: 'Legado de Benjamin Altman, 1913' });
const resources = [
painting('hals', 908, 1200, [105.4, 79.4], `${ALTMAN} (14.40.602)`, t({
en: 'A laughing young man raises a glass; a young woman leans on his shoulder.',
es: 'Un joven ríe y alza una copa; una joven se apoya en su hombro.' }), 0.4), // portrait
painting('steen', 1115, 1200, [141, 131.4], 'Fletcher Fund, 1958 (58.89)', t({
en: 'A crowded party under a vine, with a lute player, a child and a dog.',
es: 'Una fiesta concurrida bajo una parra, con un laudista, un niño y un perro.' })),
painting('rembrandt', 1139, 1200, [143.5, 136.5], t({
en: 'Purchase, special contributions and funds given or bequeathed by friends of the Museum',
es: 'Compra con aportaciones y fondos donados o legados por amigos del Museo' })
+ ', 1961 (61.198)', t({ en: 'A bearded man in a black hat rests his hand on a marble bust.',
es: 'Un hombre barbado con sombrero negro apoya la mano en un busto de mármol.' })),
painting('vermeer', 1422, 1600, [45.7, 40.6], t({ en: 'Marquand Collection, gift of',
es: 'Colección Marquand, donación de' }) + ' Henry G. Marquand, 1889 (89.15.21)', t({
en: 'A young woman at an open window holds a gilt pitcher over a basin.',
es: 'Una joven junto a una ventana abierta sujeta una jarra dorada sobre una jofaina.' })),
painting('ruisdael', 1400, 1084, [100, 130.2], `${ALTMAN} (14.40.623)`, t({
en: 'A sandy road between wheat fields under a tall bank of cloud.',
es: 'Un camino de arena entre campos de trigo bajo un alto banco de nubes.' }), 0.6),
// The cover: the Vermeer cut to the box's 4 : 3 (160 × 120 mm), since the opener fits a
// picture inside its box. Drawn by the cover design, never cited.
{ ...painting('vermeer-cover', 1800, 1350, [], '', t({
en: 'Detail of Vermeer’s young woman at the window, in blue and white.',
es: 'Detalle de la joven de Vermeer junto a la ventana, en azul y blanco.' })),
caption: '', note: '' },
];
captionSide: 'inside' 把图注和来源注释放在画的旁边,就像展厅里挂的展签;width 决定每幅画占版心的比例:竖长的哈尔斯少一些,横宽的雷斯达尔多一些,这样五页的正文高度相近。展签由材质、厘米尺寸(用不换行空格连接)和博物馆的入藏说明组成,西班牙语版用逗号作小数点。
#4 · 书眉写出这一站
const head = (id, content, parity, edge, x, extra = {}) => ({
kind: 'text', id, content, parity, pages: 'body', ...caps(7, 600), color: col('muted'),
placement: at('page', edge, x, 13), ...extra,
});
const folio = { ...caps(8, 700), color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
head('recto-stop', '{firstMark.h2}', 'odd', 'top-right', -(OUTER + 9)),
head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
{firstMark.h2} 印出本页开始的那一站的标题,所以奇数页写画名,偶数页写导览名。书眉不会出现在封面上,因为封面的页面角色是开篇页。
完整食谱
// ═══ Postext Cookbook · Nº 175 · A museum walk whose links print as QR notes ═════
// https://postext.dev/en/cookbook/museum-guide-qr-link-notes
// Code: MIT · Text: original (CC BY 4.0) · Paintings: The Metropolitan Museum of Art (CC0)
// Fonts: Newsreader, Instrument Serif, Archivo (SIL OFL 1.1) · Needs postext ≥ 1.65.0
// A pocket gallery guide: five paintings, one to a page. The Markdown writes the museum's
// catalogue pages as ordinary links; the print edition turns each one into a numbered note at
// the foot of the page, with the short address and a code a phone can scan.
import {
buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'museum-guide-qr-link-notes';
// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: Delft blue on warm paper, every colour linked by id
const palette = {
ink: '#1f1d1a', // text: a warm near-black
delft: '#2b4a7a', // the one accent: kickers, stop numbers, rules (8.6:1 on paper)
rule: '#c6c9cf', // hairlines
muted: '#5f6168', // running heads, notes, credits (6.1:1 on paper)
paper: '#ffffff',
};
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' } })),
// The engine's defaults link to main-color: point it at the accent, so nothing prints blue.
{ id: 'main-color', name: 'delft (defaults)', value: { hex: palette.delft, model: 'hex' } },
];
// #endregion
const TRIM = { w: 160, h: 240 }; // a large pocket guide, in mm
const TOP = 22; // top margin in mm: the cover's container starts here
const OUTER = 22; // outer margin in mm: folios and running heads align to it
const LEAD = 14; // body leading in pt: the baseline grid
const [TEXT, DISPLAY, LABEL] = ['Newsreader', 'Instrument Serif', 'Archivo']; // three voices
// #region answer: every web link of the guide prints as a numbered note with a code to scan
const links = {
print: 'notes', // a note marker after the link text, the address in a note at the foot
show: 'both', // the short address for reading, the code for the phone
urlDisplay: 'short', // metmuseum.org/art/collection/search/436616: no https://, no www.
repeat: 'first', // an address noted once is cited again by its number, never noted twice
qr: { size: mm(16), side: 'start', gap: em(1.2), color: col('ink') },
};
// Link notes are footnotes: they take the note style and the numbering below. 'document'
// numbers them through the walk, so the five stops print notes 3 to 7, not five notes 1.
const footnotes = { placement: 'column', numbering: 'document', fontSize: pt(8),
组合好的script.js可以直接运行:把它粘贴到任何页面的模块脚本中,或在CodePen上打开这道食谱。 GitHub上的食谱文件夹 ↗ (在新标签页中打开)
变化
#把网址印在行内
在家里读的导览,可用 'inline' 在链接文字后加上括号里的网址,不设注释也不印二维码。
- print: 'notes', // a note marker after the link text, the address in a note at the foot
- show: 'both', // the short address for reading, the code for the phone
+ print: 'inline', // (metmuseum.org/…) after the link text#只印二维码
show: 'qr' 只在注释编号旁印二维码,适合海报或展签,那里印网址只会占地方。
- show: 'both', // the short address for reading, the code for the phone
+ show: 'qr', // the code alone常见问题
带二维码的注释至少和二维码一样高。如果载有链接的那一行在本页下方放不下它的注释,这一行会连同注释移到下一页。所以在必须停在固定位置的页面上,别把链接放在最后几行,本导览封面的第一段就是这样处理的。
致谢
- 文本
- 原创文字, CC BY 4.0
- 图片
- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (the cover, cropped) · The Metropolitan Museum of Art · CC0 1.0
- Frans Hals, Young Man and Woman in an Inn, 1623 (stop 1) · The Metropolitan Museum of Art · CC0 1.0
- Jan Steen, Merry Company on a Terrace, about 1670 (stop 2) · The Metropolitan Museum of Art · CC0 1.0
- Rembrandt, Aristotle with a Bust of Homer, 1653 (stop 3) · The Metropolitan Museum of Art · CC0 1.0
- Johannes Vermeer, Young Woman with a Water Pitcher, about 1662 (stop 4) · The Metropolitan Museum of Art · CC0 1.0
- Jacob van Ruisdael, Wheat Fields, about 1670 (stop 5) · The Metropolitan Museum of Art · CC0 1.0
- 字体
- Newsreader (SIL OFL 1.1) · Instrument Serif (SIL OFL 1.1) · Archivo (SIL OFL 1.1)


