跳到主要内容
食谱编号175

排版食谱 · 第5章 · 图书结构

链接印成二维码的博物馆导览

一本口袋博物馆导览:网页链接在页脚印成编号注释,附短网址和二维码,用的是 links.print 设为 notes。

本页内容
输出
Canvas · PDF
难度
中级
Postext
已用Postext 1.65.0测试
需要≥ 1.65.0 · postext-pdf ≥ 1.65.0
许可证
更新于2026年10月31日
代码MIT · 文本CC BY 4.0
  • 英文样例:尚无中文版本
  • 成品尺寸160 × 240 mm
  • 1栏
  • Newsreader 10.2/14
  • Archivo
  • Instrument Serif
  • 7页
  • 难度
  • Postext 1.65.0
  • 排版用时167 ms
  • 177行代码

简单来说

一本介绍五幅画的小导览。正文提到网页的地方有一个小号数字,指向页脚;页脚有一个方形码,用手机一扫就能打开那个网页。

成品一览

一本大都会艺术博物馆荷兰展厅的口袋导览:五幅画,步行一小时,一页一幅。封面是维米尔窗边的少女,画面在上边缘被裁切。每一站先是展厅号、画名和画家,再是画作,旁边放展签,然后是三段短文。正文像普通网页那样链接到博物馆的藏品目录。印在纸上时,每个链接变成一个小号数字;页脚的注释给出短网址和一个二维码,参观者站在画前用手机对准它即可。

这道食谱解答

  • 怎样把书中链接的网址印出来,做成编号注释或读者可扫描的二维码?

简短回答

script.js · 第39–50行在完整代码中
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.jpg
  • rembrandt-1200.jpg
  • ruisdael-1400.jpg
  • steen-1200.jpg
  • vermeer-1600.jpg
  • vermeer-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 里,二维码以矢量绘制,本身也是一个链接。

第一站的页脚:注释 3,画作的目录页以短网址印在一个 16 毫米的二维码旁。

#2 · 一站一页

script.js · 第74–82行在完整代码中
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 · 展签放在画旁

script.js · 第219–259行在完整代码中
// 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 · 书眉写出这一站

script.js · 第86–96行在完整代码中
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
图片
字体
Newsreader (SIL OFL 1.1) · Instrument Serif (SIL OFL 1.1) · Archivo (SIL OFL 1.1)
沙盒PDF