Aller au contenu principal
Recette numéro 121

Recettes · Chapitre 6 · Encadrés et notes

Notes à côté d’un texte japonais vertical, par double page

Le premier chapitre de Takekurabe d’Ichiyō avec sept notes en 傍注 : (1) à droite de la ligne, les notes des deux pages en marge extérieure de la page de gauche.

Sur cette page
Sortie
Canvas · PDF
Postext
Testé avec Postext 1.31.0
Nécessite ≥ 1.25.0 · postext-pdf ≥ 1.25.0
Licence
Mis à jour le 10 oct. 2026
Code MIT · Texte CC BY 4.0

p. 二–三 · 2–3 sur 4

  • Exemple en anglais : pas encore d’édition française
  • Format 127 × 188 mm
  • 1 colonne
  • Shippori Mincho 9,3/16,3
  • Noto Sans JP
  • 4 pages
  • Niveau
  • Postext 1.30.0
  • Mis en page en 78 ms
  • 126 lignes de code

En bref

Un récit japonais classique avec de courtes explications sur ses noms de lieux anciens. Les petits numéros sont à côté du texte, et les explications des deux pages ouvertes sont réunies au bord extérieur de la page de gauche.

Ce que vous allez composer

Quatre pages d’une édition 四六判 de Takekurabe de Higuchi Ichiyō (たけくらべ, 1895–96), son premier chapitre, avec sept notes sur l’ancien quartier du Yoshiwara qu’il décrit. Les notes sont des 傍注 (bōchū), les notes latérales des classiques japonais en édition de poche : un petit (1) se tient à droite de la ligne après le mot qu’il explique, et les notes appelées sur les deux pages d’une double page se composent en marge extérieure de la page de gauche, sous un filet d’un tiers de la longueur de ligne, dans un corps plus petit d’un cinquième. Le lecteur trouve chaque note sans tourner la page. Le texte, 42 caractères sur 17 lignes en Shippori Mincho à 9,25 pt, garde l’unique long paragraphe d’Ichiyō et ses points sésame sur ぎつちよんちよん. Les notes au pied de la colonne d’un roman européen sont dans Nº 071.

Cette recette répond à

  • Comment composer les notes d’un livre japonais : (1) en fin de chapitre, en pied de page ou à côté du texte sur la double page ?
  • Comment faire des notes de bas de page ?
  • Comment marquer un texte japonais avec des points d’emphase sésame ou un trait latéral au lieu de l’italique ?

La réponse courte

script.js · lignes 32–48dans le code complet
// placement 'spread' gathers the notes cited on the two pages of a spread at the end of the
// left-hand page's text, the fore-edge in a book bound on the right, under a rule a third of
// the line long, and numbers them per spread. The marker (1) stands small at the right of
// the line, its digits upright, and the note opens with its number and a full note-em.
const footnotes = {
  placement: 'spread', // 傍注; 'chapterEnd' sets 後注 after the chapter instead
  numbering: 'spread',
  markerPosition: 'right', markerTemplate: '({n})', // written out: 'ja' sets them so
  numberGap: 'em', hangingIndent: em(2), // turnovers clear the number
  fontSize: em(0.8), lineHeight: em(1.5),
  separator: { width: 1 / 3, lineWidth: pt(0.5), color: col('plum') },
};
// Ichiyō's sesame marks (傍点) over ぎつちよんちよん: the mark Japan uses, right of the line.
const cjk = {
  grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
  emphasisMark: { style: 'sesame', position: 'over' },
};

Ingrédients

Typographie
Shippori Mincho, Noto Sans JP (SIL OFL 1.1)
Ressources
Aucune : chaque image est dessinée en code

Préparation

#1 · Réunir les notes de la double page en marge extérieure de la page de gauche

Le code est dans la réponse courte plus haut. footnotes.placement: 'spread' compose les notes appelées pages 2 et 3 à la fin du texte de la page 3, qui dans un livre relié à droite est sa marge extérieure, et numbering: 'spread' fait repartir les notes de chaque double page à (1) (Notes de bas de page). Une note appelée sur la page de droite attend la page de gauche ; si les notes laissent à la page de gauche moins d’une ligne de texte, le reste demeure au pied de la page de droite, comme le permet JLReq. Un document étiqueté ja compose déjà les appels d’un livre vertical sous la forme ({n}) à droite de la ligne, à 0,7 em, chiffres debout : les deux lignes écrites en toutes lettres le montrent. numberGap: 'em' place un cadratin du corps des notes après le numéro, et hangingIndent: em(2) aligne les lignes de suite au-delà.

#2 · Écrire les appels là où le japonais les place

Le Markdown écrit 見返り柳[^1] après le mot que la note explique, et les notes sous forme de paragraphes [^1]: … à la fin du chapitre. L’appel suit le mot que la note explique ; un appel qui termine une phrase se place avant son 。 (…先生[^1]。), et le moteur ne laisse jamais un 。 ni un appel commencer une ligne. Les sept notes ont été écrites pour cette recette : ce qu’étaient le 見返り柳 et le お齒ぐろ溝, où se trouvait 大音寺前, ce qu’on vendait au 酉の市, en japonais moderne simple.

#3 · Garder les points sésame de la source

cjk.emphasisMark décide de ce qu’impriment *…* et :dots[…] dans ce livre : des points sésame, à droite d’une ligne verticale. Un document ja les emploie déjà ; écrits en toutes lettres, ils activent les kenten dans la liste des fonctions. Le texte d’Aozora marque ぎつちよんちよん de 傍点, et aozora.py l’a écrit :dots[ぎつちよんちよん]{style=sesame} ; les livres chinois placent au contraire les points sous la ligne (Signes, ruby et warichu).

#4 · Ouvrir sur une double page

script.js · lignes 52–71dans le code complet
// Page-anchored design on a vertical page: x runs down the page from its top edge, y across
// it leftwards from its right edge. The title stands on the page's axis, the author lower.
const PT = 25.4 / 72; // mm in a point
const page = (down, across) => ({ anchor: { to: 'page', edge: 'top-left' },
  offset: { x: mm(down), y: mm(across) } });
const TITLE = 26; // pt
// parity 'any': the story opens on the next page, either side (the default waits for a recto).
const story = { level: 1, breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, minHeight: lines(LINES), slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: MINCHO, fontWeight: 700,
      fontSize: pt(TITLE), lineHeight: 1, letterSpacing: pt(TITLE / 2), color: col('ink'),
      placement: page(42, 127 / 2 - (TITLE * PT) / 2) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.6), color: col('plum'),
      placement: { ...page(42, 127 / 2 + (TITLE * PT) / 2 + 5), size: { width: mm(24) } } },
    { kind: 'text', id: 'author', content: '{author}', fontFamily: MINCHO, fontSize: pt(12),
      letterSpacing: pt(4), color: col('ink'),
      placement: page(118, 127 / 2 + (TITLE * PT) / 2 + 3) },
  ] } } };
const section = { level: 2, fontFamily: MINCHO, fontWeight: 700, fontSize: pt(11),
  lineSpan: 3, indent: em(7) }; // 3行取り, seven characters down, as the Aozora text marks it

La première phrase du chapitre appelle quatre notes dans ses trois premières lignes. Composées sous un titre en page 1, elles laisseraient la place de trois lignes de texte : la page de gauche porte ses propres notes et celles de personne d’autre. Une page de titre seule en page 1 repousse le chapitre en page 2, une page de droite, si bien que la première double page contient le texte et toutes ses notes. La page de titre est un titre de niveau 1 dont la maquette remplit la page, ancrée à la coupe pour que le titre se tienne sur l’axe de la page.

#5 · Placer les hashira et les folios le long de la marge extérieure

script.js · lignes 75–89dans le code complet
const foreEdge = (id, content, parity, edge, y, pages) => ({
  kind: 'text', id, content, parity, pages, writingMode: 'vertical-rl',
  fontFamily: GOTHIC, fontSize: pt(7), letterSpacing: pt(1), color: col('muted'),
  overflow: 'clip', placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } },
});
const header = { elements: [
  foreEdge('hashira', '{title}', 'odd', 'top', 2, 'body'),
  foreEdge('folio', '{pageNumber}', 'all', 'bottom', -2, 'body'),
] };
// The title page carries the colophon across the foot, in the edition's language.
const footer = { elements: [{ kind: 'text', id: 'colophon', content: '{attr.colophon}',
  pages: 'opener', fontFamily: GOTHIC, fontSize: pt(5.5), lineHeight: 1.45,
  color: col('muted'), overflow: 'wrap', align: 'center',
  placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { y: mm(-6) },
    size: { width: mm(100) } } }] };

Le titre descend la marge extérieure des pages de gauche, au-delà des notes, et les folios en kanji se placent au pied de chaque page sauf la page de titre.

La recette complète

Sandbox
// ═══ Postext Cookbook · Nº 121 · Notes beside a vertical Japanese text, across the spread ══
// https://postext.dev/en/cookbook/vertical-notes-and-sidenotes
// Code: MIT · Text: 樋口一葉『たけくらべ』, Aozora Bunko 389 (PD); notes CC BY 4.0 · Pictures: none
// Fonts: Shippori Mincho, Noto Sans JP (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the colophon; the story is Japanese in both
const RECIPE = 'vertical-notes-and-sidenotes';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: ink, a muted plum for the note numbers' rule, cream paper
const palette = {
  ink: '#211d1b', // the text and the notes
  plum: '#6b3a55', // 紫: the rule over the notes, the engine's defaults
  muted: '#6b645d', // the hashira and the folios
  paper: '#fbf7ef',
};
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' } })),
  { id: 'main-color', name: 'defaults', value: { hex: palette.plum, model: 'hex' } },
];
// #endregion
const [MINCHO, GOTHIC] = ['Shippori Mincho', 'Noto Sans JP'];
const [BODY, LEAD, CHARS, LINES] = [9.25, 16.25, 42, 17]; // pt, pt: 四六判 42字 × 17行
const lines = (n) => pt(n * LEAD);

// #region answer: 傍注, the notes of both pages of a spread at the fore-edge of the left one
// placement 'spread' gathers the notes cited on the two pages of a spread at the end of the
// left-hand page's text, the fore-edge in a book bound on the right, under a rule a third of
// the line long, and numbers them per spread. The marker (1) stands small at the right of
// the line, its digits upright, and the note opens with its number and a full note-em.
const footnotes = {
  placement: 'spread', // 傍注; 'chapterEnd' sets 後注 after the chapter instead
  numbering: 'spread',
  markerPosition: 'right', markerTemplate: '({n})', // written out: 'ja' sets them so
  numberGap: 'em', hangingIndent: em(2), // turnovers clear the number
  fontSize: em(0.8), lineHeight: em(1.5),
  separator: { width: 1 / 3, lineWidth: pt(0.5), color: col('plum') },
};
// Ichiyō's sesame marks (傍点) over ぎつちよんちよん: the mark Japan uses, right of the line.
const cjk = {
  grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
  emphasisMark: { style: 'sesame', position: 'over' },
};
// #endregion

// #region opener: a title page (扉) on its own, so the story opens on a spread
// Page-anchored design on a vertical page: x runs down the page from its top edge, y across
// it leftwards from its right edge. The title stands on the page's axis, the author lower.
const PT = 25.4 / 72; // mm in a point
const page = (down, across) => ({ anchor: { to: 'page', edge: 'top-left' },
  offset: { x: mm(down), y: mm(across) } });
const TITLE = 26; // pt
// parity 'any': the story opens on the next page, either side (the default waits for a recto).
const story = { level: 1, breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, minHeight: lines(LINES), slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: MINCHO, fontWeight: 700,
      fontSize: pt(TITLE), lineHeight: 1, letterSpacing: pt(TITLE / 2), color: col('ink'),
      placement: page(42, 127 / 2 - (TITLE * PT) / 2) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.6), color: col('plum'),
      placement: { ...page(42, 127 / 2 + (TITLE * PT) / 2 + 5), size: { width: mm(24) } } },
    { kind: 'text', id: 'author', content: '{author}', fontFamily: MINCHO, fontSize: pt(12),
      letterSpacing: pt(4), color: col('ink'),
      placement: page(118, 127 / 2 + (TITLE * PT) / 2 + 3) },
  ] } } };
const section = { level: 2, fontFamily: MINCHO, fontWeight: 700, fontSize: pt(11),
  lineSpan: 3, indent: em(7) }; // 3行取り, seven characters down, as the Aozora text marks it
// #endregion

// #region furniture: the hashira and kanji folios down the fore-edge
const foreEdge = (id, content, parity, edge, y, pages) => ({
  kind: 'text', id, content, parity, pages, writingMode: 'vertical-rl',
  fontFamily: GOTHIC, fontSize: pt(7), letterSpacing: pt(1), color: col('muted'),
  overflow: 'clip', placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } },
});
const header = { elements: [
  foreEdge('hashira', '{title}', 'odd', 'top', 2, 'body'),
  foreEdge('folio', '{pageNumber}', 'all', 'bottom', -2, 'body'),
] };
// The title page carries the colophon across the foot, in the edition's language.
const footer = { elements: [{ kind: 'text', id: 'colophon', content: '{attr.colophon}',
  pages: 'opener', fontFamily: GOTHIC, fontSize: pt(5.5), lineHeight: 1.45,
  color: col('muted'), overflow: 'wrap', align: 'center',
  placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { y: mm(-6) },
    size: { width: mm(100) } } }] };
// #endregion

const config = () => ({
  locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag)
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(127), height: mm(188), dpi: 150, // 四六判
    backgroundColor: col('paper'),
    margins: { top: mm(27), bottom: mm(21), left: mm(13), right: mm(16), mirror: true },
    pageNumbering: { format: 'japanese-informal' },
  },
  layout: { layoutType: 'single', writingMode: 'vertical-rl' },
  cjk,
  footnotes,
  bodyText: {
    fontFamily: MINCHO, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: em(1), indentAfterHeading: true,
  },
  headings: { fontFamily: MINCHO, color: col('ink'), levels: [story, section] },
  header,
  footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Exemple Markdown · 23 lignes · content.en.mdtitle: "たけくらべ" author: "樋口一葉" --- # たけくらべ {colophon="Higuchi Ichiyō, たけくらべ (Child’s Play, 1895–96), chapter one; the notes are this recipe’s. Text: Aozora Bunko 389, public domain. Set in Shippori Mincho and Noto Sans JP (SIL OFL)."} ## 一 廻れば大門の見返り柳[^1]いと長けれど、お齒ぐろ溝[^2]に{燈火|ともしび}うつる三階[^3]の騷ぎも手に取る如く、明けくれなしの車の{行來|ゆきゝ}にはかり知られぬ全盛をうらなひて、{大音寺前|だいおんじまへ}[^4]と名は佛くさけれど、さりとは陽氣の町と住みたる人の申き、{三嶋神社|みしまさま}の角をまがりてより是れぞと見ゆる{大厦|いへ}もなく、かたぶく軒端の十軒長屋二十軒長や、商ひはかつふつ利かぬ處とて半さしたる雨戸の外に、あやしき{形|なり}に紙を切りなして、胡粉ぬりくり彩色のある{田樂|でんがく}みるやう、裏にはりたる串のさまもをかし、一軒ならず二軒ならず、朝日に干して夕日に仕舞ふ手當こと〴〵しく、一家内これにかゝりて夫れは何ぞと問ふに、知らずや霜月{酉|とり}の日[^5]例の神社に欲深樣のかつぎ給ふ是れぞ熊手の下ごしらへといふ、正月門松とりすつるよりかゝりて、一年うち通しの夫れは誠の商賣人、片手わざにも夏より手足を色どりて、{新年着|はるぎ}の支度もこれをば當てぞかし、南無や大鳥大明神、買ふ人にさへ大福をあたへ給へば製造もとの我等萬倍の利益をと人ごとに言ふめれど、さりとは思ひのほかなるもの、此あたりに大長者のうわさも聞かざりき、住む人の多くは{廓者|くるわもの}にて良人は小格子[^6]の何とやら、下足札そろへてがらんがらんの音もいそがしや夕暮より羽織引かけて立出れば、うしろに切火打かくる女房の顏もこれが見納めか十人ぎりの側杖無理{情死|しんぢう}のしそこね、恨みはかゝる身のはて危ふく、すはと言はゞ命がけの勤めに{遊山|ゆさん}らしく見ゆるもをかし、娘は{大籬|おほまがき}の{下新造|したしんぞ}とやら、七軒の何屋が客廻しとやら、{提燈|かんばん}さげてちよこちよこ走りの修業、卒業して何にかなる、とかくは檜舞臺と見たつるもをかしからずや、垢ぬけのせし三十あまりの年増、小ざつぱりとせし唐棧ぞろひに紺足袋はきて、雪駄ちやら〳〵忙がしげに横抱きの小包はとはでもしるし、茶屋が棧橋とんと沙汰して、廻り遠や此處からあげまする、誂へ物の仕事やさんと此あたりには言ふぞかし、一體の風俗よそと變りて、{女子|おなご}の後帶きちんとせし人少なく、がらを好みて巾廣の卷帶、年増はまだよし、十五六の小癪なるが{酸漿|ほゝづき}ふくんで此{姿|なり}はと目をふさぐ人もあるべし、所がら是非もなや、昨日河岸店に何紫の源氏名耳に殘れど、けふは地廻りの吉と手馴れぬ燒鳥の夜店を出して、身代たゝき骨になれば再び古巣への{内儀|かみさま}姿、どこやら素人よりは見よげに覺えて、これに染まらぬ子供もなし、秋は九月{仁和賀|にわか}[^7]の頃の大路を見給へ、さりとは宜くも學びし{露八|ろはち}が物眞似、{榮喜|えいき}が{處|しよ}作、孟子の母やおどろかん上達の速やかさ、うまいと褒められて今宵も一廻りと生意氣は七つ八つよりつのりて、やがては肩に置手ぬぐひ、鼻歌のそゝり節、十五の少年がませかた恐ろし、學校の唱歌にも:dots[ぎつちよんちよん]{style=sesame}と拍子を取りて、運動會に木やり音頭もなしかねまじき風情、さらでも教育はむづかしきに教師の苦心さこそと思はるゝ{入谷|いりや}ぢかくに育英舍とて、私立なれども生徒の數は千人近く、狹き校舍に目白押の窮屈さも教師が人望いよ〳〵あらはれて、唯學校と一ト口にて此あたりには呑込みのつくほど成るがあり、通ふ子供の數々に或は火消鳶人足、おとつさんは{刎橋|はねばし}の番屋に居るよと習はずして知る其道のかしこさ、梯子のりのまねびにアレ忍びがへしを折りましたと訴へのつべこべ、三百といふ代言の子もあるべし、お前の父さんは馬だねへと言はれて、名のりや{愁|つ}らき子心にも顏あからめるしほらしさ、出入りの{貸座敷|いへ}の祕藏息子寮住居に華族さまを氣取りて、ふさ付き帽子面もちゆたかに洋服かる〴〵と花々敷を、坊ちやん坊ちやんとて此子の追從するもをかし、多くの中に{龍華|りうげ}寺の{信如|しんによ}とて、{千筋|ちすぢ}となづる黒髮も今いく{歳|とせ}のさかりにか、やがては墨染にかへぬべき袖の色、{發心|ほつしん}は腹からか、坊は親ゆづりの勉強ものあり、性來をとなしきを友達いぶせく思ひて、さま〴〵の惡戲をしかけ、猫の死骸を繩にくゝりてお役目なれば{引導|いんだう}をたのみますと投げつけし事も有りしが、それは昔、今は校内一の人とて假にも{侮|あなど}りての處業はなかりき、歳は十五、{並背|なみぜい}にていが栗の{頭髮|つむり}も思ひなしか俗とは變りて、{藤本信如|ふぢもとのぶゆき}と{訓|よみ}にてすませど、何處やら{釋|しやく}といひたげの素振なり。 [^1]: 吉原遊廓の入口、大門へ下る衣紋坂の上の日本堤に立っていた柳。帰りの客がここで名残を惜しんで振り返ったという。 [^2]: 遊廓のまわりをめぐる堀。遊女が逃げ出さないように掘られたもので、水が黒く濁っていたのでこう呼ばれた。 [^3]: 廓の中に建ち並ぶ三階建ての大きな妓楼。 [^4]: 吉原の裏手、下谷龍泉寺町のあたりの通称。大音寺の門前にあたる。 [^5]: 十一月の酉の日に鷲(おおとり)神社で開かれる酉の市。縁起物の熊手が売られる。 [^6]: 格の低い、小さな遊女屋。 [^7]: 吉原で秋に催された俄(にわか)。芸者や幇間が茶番や踊りを見せて廓の中を練り歩いた。
`; // content.<lang>.md: the same Japanese text in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Shippori Mincho': ['400', '700'], // the text and the notes; the title and 一 'Noto Sans JP': ['400'], // the hashira, the folios and the colophon }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const title = markdown.match(/^title: "(.*)"$/m)[1]; const author = markdown.match(/^author: "(.*)"$/m)[1]; await loadFonts(FONTS, markdown); // the Latin files: the colophon // The notes' (1) and their upright digits come from the text face too. await loadCjkFonts({ [MINCHO]: ['400'] }, `${markdown}()0123456789`, { vertical: true }); await loadCjkFonts({ [MINCHO]: ['700'] }, `${title}${author}一`); await loadCjkFonts({ [GOTHIC]: ['400'] }, `${title}${markdown.match(/colophon="([^"]*)"/)[1]}一二三四五六七八九十`, { vertical: true }); const doc = await withLoadedFonts(() => buildDocument({ markdown }, config()), { ...kitFonts(FONTS), text: markdown }); showBook(doc, { title: t({ en: 'Notes beside a vertical Japanese text, across the spread', es: 'Notas junto a un texto japonés vertical, en la doble página' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf, cjk : identique dans toutes les recettes · 458 lignes// ─── 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 v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { 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', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `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. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; 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 at = fontsourceAt(id); 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 = `${await at}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().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** 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, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** 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 v2 ── 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. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { 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, { ...render, 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 · pdf v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── Kit · cjk v2 ── Chinese, Japanese and Korean books · postext.dev/cookbook // Fontsource ships a CJK family as about a hundred files per weight, each // declared in its stylesheet with the unicode-range it covers. The screen // loads the files the sample touches; the PDF gets the same files for the // characters its pages set in each face, and embeds each as a subset. // A book bound on the right (vertical text) is shown with its spreads // mirrored: page 1 alone on the left of the spine, then [3 | 2]. /** The files of a Fontsource face, read from its stylesheet: { url, range, * ranges }, the last declared first (the order the browser tries them in). */ function cjkSlices(family, weight, style) { cjkSlices.cache ??= new Map(); const id = fontsourceId(family); const css = `${id}/${weight}${style === 'italic' ? '-italic' : ''}.css`; if (!cjkSlices.cache.has(css)) { let at; cjkSlices.cache.set(css, fontsourceAt(id).then((a) => fetch(at = a + css.slice(id.length + 1))) .then((res) => { if (!res.ok) throw new Error(`Fontsource has no ${family} ${weight} ${style} (${res.status})`); return res.text(); }) .then((text) => [...text.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => { const range = /unicode-range:\s*([^;]+);/.exec(rule)?.[1].trim() ?? 'U+0-10FFFF'; const ranges = range.split(',').map((part) => { const [lo, hi = lo] = part.trim().slice(2).split('-'); return [parseInt(lo, 16), parseInt(hi, 16)]; }); return { url: new URL(/url\(([^)]+?\.woff2)\)/.exec(rule)[1], at).href, range, ranges }; }).reverse())); } return cjkSlices.cache.get(css); } /** The file of `slices` the screen sets code point `cp` in, if any: the * last one loadCjkFonts adds whose range holds it. A numbered file that * repeats a letter of latin is added after latin, and wins it. */ function cjkSliceFor(slices, cp) { const holds = (slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi); return slices.reduce((last, slice) => (holds(slice) ? slice : last), undefined); } /** Whether Fontsource serves `family` as a Chinese, Japanese or Korean * family (its subsets name the script). Fails when the API does not * answer: a CJK face taken for a Latin one would paint in a system face. */ async function isCjkFamily(family) { const meta = await fontsourceMeta(family); if (!meta) throw new Error(`api.fontsource.org did not describe ${family}: reload to try again`); return !!meta.subsets?.some((subset) => /^(chinese|japanese|korean)/.test(subset)); } /** faces = { 'Noto Serif TC': ['400', '700'] }, as for loadFonts: the * whole FONTS object may be passed, its other families are left to * loadFonts. Adds one FontFace per file of each CJK face with its * unicodeRange, then loads the files `text` touches. `text` is what the * faces set: the sample for the text face; a book in several voices calls * it once per voice (loadCjkFonts({ 'LXGW WenKai TC': ['400'] }, quotes)), * so the heading and quotation faces fetch and check only their own * characters. Fails when a character of `text` is in no file of a face. * List every weight the pages use: a weight left to the engine's later round * gets the latin file only. With { vertical: true } it also loads each * family's vertical forms (brackets, quotes, pause marks) for the canvas, * which needs loadVerticalAlternates imported from postext. Resolves to * the number of files loaded. */ async function loadCjkFonts(faces, text, { vertical = false } = {}) { kitStatus('Loading fonts…'); let loaded = 0; try { if (vertical && typeof loadVerticalAlternates !== 'function') { throw new Error('loadCjkFonts(…, { vertical: true }) needs loadVerticalAlternates imported from postext'); } for (const [family, specs] of Object.entries(faces)) { if (!(await isCjkFamily(family))) continue; const twin = []; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; const slices = await cjkSlices(family, weight, style); const missing = [...new Set(text)].filter((ch) => /\S/.test(ch) && !cjkSliceFor(slices, ch.codePointAt(0))); if (missing.length) { throw new Error(`${family} ${spec} has no file for ${missing.slice(0, 12).join(' ')}: ` + `give each face the text it sets (loadCjkFonts({ '${family}': ['${spec}'] }, text))`); } for (const slice of slices) { document.fonts.add(new FontFace(family, `url(${slice.url}) format('woff2')`, { weight: String(weight), style, unicodeRange: slice.range })); twin.push({ source: slice.url, weight: String(weight), style, unicodeRange: slice.range }); } const font = `${style === 'italic' ? 'italic ' : ''}${weight} 16px "${family}"`; loaded += (await document.fonts.load(font, text)).length; if (!document.fonts.check(font, text)) throw new Error(`${family} ${spec} did not load for the sample`); } // The same files under a twin name with the `vert` feature on: the // canvas paints the punctuation of vertical lines with it. if (vertical && twin.length) await loadVerticalAlternates(family, twin); } } catch (error) { kitFail(error); throw error; } return loaded; } /** The PDF font provider for recipes with CJK faces: a family whose * Fontsource subsets are Chinese, Japanese or Korean gets the files the * screen set its pages' characters in (`request.codePoints`), each marked * with its unicode-range and its place in the order loadCjkFonts added * them: postext-pdf sets a character two files hold in the one added * last, as the browser does. Any other family gets the latin file * fontsourceProvider fetches (the "pdf" block) and, when the face sets * letters only latin-ext has, that file too. */ async function cjkPdfProvider(family, weight, style, request) { if (!(await isCjkFamily(family))) return cjkLatinPdfFiles(family, weight, style, request); const meta = await fontsourceMeta(family); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const slices = await cjkSlices(family, w, s); const picked = new Set(); for (const cp of request?.codePoints ?? []) { const slice = cjkSliceFor(slices, cp); if (slice) picked.add(slice); } if (!picked.size) picked.add(slices[0]); return Promise.all(slices.map(async (slice, order) => { if (!picked.has(slice)) return null; const res = await fetch(slice.url); if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`); const bytes = await decompressWoff2(new Uint8Array(await res.arrayBuffer())); return Object.assign(bytes, { unicodeRange: slice.range, order }); })).then((files) => files.filter(Boolean)); } /** A Latin family set next to the CJK faces: its latin file, then its * latin-ext file when the face sets letters only latin-ext has (ō ū in * Hepburn rōmaji, ǎ in pinyin), the file loadFonts adds on screen for * them, each marked with its unicode-range and its place in that order: * the PDF sets a character in the file the browser took it from. A face * Fontsource ships without latin-ext, or whose file does not come, gets * latin alone, and the PDF names the letters it lacks. */ async function cjkLatinPdfFiles(family, weight, style, request) { const meta = await fontsourceMeta(family); const beyond = [...(request?.codePoints ?? [])].some(cjkLatinExtOnly); if (!beyond || !meta?.subsets?.includes('latin-ext')) return fontsourceProvider(family, weight, style); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const id = fontsourceId(family); const url = `${await fontsourceAt(id)}files/${id}-latin-ext-${w}-${s}.woff2`; const [latin, ext] = await Promise.all([fontsourceProvider(family, weight, style), fetch(url) .then(async (res) => (res.ok ? decompressWoff2(new Uint8Array(await res.arrayBuffer())) : null), () => null)]); const ranges = kitRanges(); return ext ? [Object.assign(latin, { unicodeRange: ranges.latin, order: 0 }), Object.assign(ext, { unicodeRange: ranges['latin-ext'], order: 1 })] : latin; } /** Whether code point `cp` is in Fontsource's latin-ext file and not in * its latin file: Latin Extended-A and -B, IPA, the spacing modifiers and * Latin Extended Additional (loadFonts's test for latin-ext), less the * few latin holds too (ı Œ œ ʻ ʼ ˆ ˚ ˜). */ function cjkLatinExtOnly(cp) { if (!((cp >= 0x100 && cp <= 0x2ff) || (cp >= 0x1e00 && cp <= 0x1eff))) return false; return ![0x131, 0x152, 0x153, 0x2bb, 0x2bc, 0x2c6, 0x2da, 0x2dc].includes(cp); } /** showPages for a book bound on either edge. A right-bound book (the * document says so: doc.binding is 'right' for page.binding 'right' and * for vertical text) lies on the desk as it opens: page 1 alone on the * left of the spine, then [3 | 2], the spine shade on each page's inner * edge. `binding` ('left' | 'right') overrides the document's. */ function showBook(docs, { binding, ...options } = {}) { const count = showPages(docs, options); const right = (binding ?? [docs].flat()[0]?.binding) === 'right'; if (!document.getElementById('pt-kit-cjk')) { // The pages keep direction ltr: a canvas draws text in the direction its // element inherits, and under rtl each run would end where the engine // starts it, its brackets mirrored. document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit-cjk"> .pt-spread[dir="rtl"] canvas { direction: ltr; } .pt-spread[dir="rtl"] 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); } </style>`); } // Each pair stays [verso, recto] in the page; right to left, the verso // sits on the right. Phones stack the pages in reading order either way. for (const spread of document.querySelectorAll('#pages > .pt-spread')) spread.dir = right ? 'rtl' : 'ltr'; document.getElementById('pages').dataset.binding = right ? 'right' : 'left'; return count; } // ─── /Kit ───────────────────────────────────────────────────────────────────────

Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)

Variantes

#Réunir les notes à la fin du chapitre

Sans placement, un livre ja vertical compose ses notes en 後注 (kōchū) : après le texte du chapitre, sous le même filet, numérotées sur tout le chapitre. Ici, elles remplissent la moitié gauche de la page 4.

-  placement: 'spread', // 傍注; 'chapterEnd' sets 後注 after the chapter instead
-  numbering: 'spread',
+  placement: 'chapterEnd', // 後注, the default for a vertical ja book
+  numbering: 'chapter',

#Placer l’appel à côté du mot

markerPosition: 'side' compose un appel plus petit dans l’espace à côté de la ligne, comme un ruby interlinéaire, si bien qu’il ne prend aucune place dans la ligne.

-  markerPosition: 'right', markerTemplate: '({n})', // written out: 'ja' sets them so
+  markerPosition: 'side', markerTemplate: '{n}',

Erreurs fréquentes

Erreur fréquente

Balisez un texte japonais 'ja', jamais zh-Hans ni LANG

Les éditions d’une recette sont en et es, mais un exemple japonais est en japonais dans les deux : `locale: LANG` le baliserait en anglais ou en espagnol, et une balise chinoise le composerait selon les règles chinoises (ponctuation Kaiming, petits kana libres de commencer une ligne, 图 pour 図, formes de glyphes chinoises dans le PDF). Depuis postext 1.30, la balise est aussi la langue dans laquelle le texte est mis en forme (mesuré, peint et incorporé) : toute balise autre que 'ja' donne d’autres formes de glyphes que les japonaises (les chinoises sous une balise chinoise) et d’autres coupures de ligne. Écrivez 'ja' : elle choisit la région Japon (coupure des lignes et ponctuation JLReq, points d’emphase sésame, espacement des furigana, étiquettes 図 et 表) et désactive la césure. Le lint fait échouer un texte contenant des kana sous une balise zh ou ko. Coupure des lignes en japonais (kinsoku) →

Erreur fréquente

Les polices chinoises se chargent par tranches, avec le bloc cjk

Fontsource sert une famille chinoise, japonaise ou coréenne sous la forme d’une centaine de fichiers par graisse, chacun couvrant une plage de caractères. loadFonts ne récupère que le fichier latin : à l’écran, les caractères han viennent d’une police système et sont mal mesurés, et fontsourceProvider remet ce fichier latin au PDF, qui les imprime en cases vides. Listez le bloc cjk du kit, appelez loadCjkFonts(FONTS, markdown) après loadFonts (une fois par voix, avec le texte qu’elle compose, quand le livre utilise plusieurs polices CJK) et donnez à renderToPdf fontProvider: cjkPdfProvider : les deux prennent les fichiers qui contiennent les caractères du texte. Polices chinoises, japonaises et coréennes →

Erreur fréquente

Un livre relié à droite montre ses doubles pages avec showBook

Dans un livre relié à droite (texte arabe, hébreu ou persan, chinois vertical, ou page.binding 'right'), la page 1 reste le recto, mais elle se trouve à gauche du dos, et les paires se lisent [3 | 2]. showPages présente tout livre relié à gauche ; showBook, du bloc book (le bloc cjk contient la même fonction), lit doc.binding et inverse les paires. capture.hero désigne toujours une double page dans l’ordre de lecture, [verso, recto] : [2, 3]. Livres reliés à droite →

Crédits

Texte
  • 樋口一葉「たけくらべ」 (Child’s Play, 1895–96), chapter one; Aozora Bunko card 389, from 「日本現代文學全集10 樋口一葉集」 (Kōdansha, 1962), input 青空文庫, proofreading 米田進 and 小林繁雄, converted with aozora.py; the seven notes were written for this recipe (CC BY 4.0) · Higuchi Ichiyō; Aozora Bunko volunteers · domaine public
Polices
Shippori Mincho (SIL OFL 1.1) · Noto Sans JP (SIL OFL 1.1)
SandboxPDF