postext-pdf 1.8.6 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/accessibility.test.js +44 -0
  3. package/dist/__tests__/accessibility.test.js.map +1 -1
  4. package/dist/__tests__/chinesePageLabels.test.d.ts +2 -0
  5. package/dist/__tests__/chinesePageLabels.test.d.ts.map +1 -0
  6. package/dist/__tests__/chinesePageLabels.test.js +58 -0
  7. package/dist/__tests__/chinesePageLabels.test.js.map +1 -0
  8. package/dist/__tests__/cjkAnnotations.test.d.ts +2 -0
  9. package/dist/__tests__/cjkAnnotations.test.d.ts.map +1 -0
  10. package/dist/__tests__/cjkAnnotations.test.js +83 -0
  11. package/dist/__tests__/cjkAnnotations.test.js.map +1 -0
  12. package/dist/__tests__/cjkDashRule.test.d.ts +2 -0
  13. package/dist/__tests__/cjkDashRule.test.d.ts.map +1 -0
  14. package/dist/__tests__/cjkDashRule.test.js +101 -0
  15. package/dist/__tests__/cjkDashRule.test.js.map +1 -0
  16. package/dist/__tests__/cjkFontFiles.test.d.ts +2 -0
  17. package/dist/__tests__/cjkFontFiles.test.d.ts.map +1 -0
  18. package/dist/__tests__/cjkFontFiles.test.js +302 -0
  19. package/dist/__tests__/cjkFontFiles.test.js.map +1 -0
  20. package/dist/__tests__/cjkGrid.test.d.ts +2 -0
  21. package/dist/__tests__/cjkGrid.test.d.ts.map +1 -0
  22. package/dist/__tests__/cjkGrid.test.js +57 -0
  23. package/dist/__tests__/cjkGrid.test.js.map +1 -0
  24. package/dist/__tests__/cjkPunctuation.test.d.ts +2 -0
  25. package/dist/__tests__/cjkPunctuation.test.d.ts.map +1 -0
  26. package/dist/__tests__/cjkPunctuation.test.js +143 -0
  27. package/dist/__tests__/cjkPunctuation.test.js.map +1 -0
  28. package/dist/__tests__/cjkTracking.test.d.ts +2 -0
  29. package/dist/__tests__/cjkTracking.test.d.ts.map +1 -0
  30. package/dist/__tests__/cjkTracking.test.js +100 -0
  31. package/dist/__tests__/cjkTracking.test.js.map +1 -0
  32. package/dist/__tests__/documentLanguage.test.js +13 -3
  33. package/dist/__tests__/documentLanguage.test.js.map +1 -1
  34. package/dist/__tests__/fallbackSpaces.test.js +49 -0
  35. package/dist/__tests__/fallbackSpaces.test.js.map +1 -1
  36. package/dist/__tests__/outlineTitles.test.js +87 -0
  37. package/dist/__tests__/outlineTitles.test.js.map +1 -1
  38. package/dist/__tests__/pdfWorker.test.js +17 -0
  39. package/dist/__tests__/pdfWorker.test.js.map +1 -1
  40. package/dist/__tests__/renderOptionsDocs.test.d.ts +2 -0
  41. package/dist/__tests__/renderOptionsDocs.test.d.ts.map +1 -0
  42. package/dist/__tests__/renderOptionsDocs.test.js +32 -0
  43. package/dist/__tests__/renderOptionsDocs.test.js.map +1 -0
  44. package/dist/__tests__/rightBinding.test.d.ts +2 -0
  45. package/dist/__tests__/rightBinding.test.d.ts.map +1 -0
  46. package/dist/__tests__/rightBinding.test.js +112 -0
  47. package/dist/__tests__/rightBinding.test.js.map +1 -0
  48. package/dist/__tests__/verticalDesignImage.test.d.ts +2 -0
  49. package/dist/__tests__/verticalDesignImage.test.d.ts.map +1 -0
  50. package/dist/__tests__/verticalDesignImage.test.js +152 -0
  51. package/dist/__tests__/verticalDesignImage.test.js.map +1 -0
  52. package/dist/__tests__/verticalText.test.d.ts +2 -0
  53. package/dist/__tests__/verticalText.test.d.ts.map +1 -0
  54. package/dist/__tests__/verticalText.test.js +380 -0
  55. package/dist/__tests__/verticalText.test.js.map +1 -0
  56. package/dist/faceFiles.d.ts +70 -0
  57. package/dist/faceFiles.d.ts.map +1 -0
  58. package/dist/faceFiles.js +154 -0
  59. package/dist/faceFiles.js.map +1 -0
  60. package/dist/fontCache.d.ts +101 -7
  61. package/dist/fontCache.d.ts.map +1 -1
  62. package/dist/fontCache.js +178 -26
  63. package/dist/fontCache.js.map +1 -1
  64. package/dist/fontFamilies.d.ts +12 -0
  65. package/dist/fontFamilies.d.ts.map +1 -0
  66. package/dist/fontFamilies.js +12 -0
  67. package/dist/fontFamilies.js.map +1 -0
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/pdf-backend/annotations.d.ts +29 -0
  72. package/dist/pdf-backend/annotations.d.ts.map +1 -0
  73. package/dist/pdf-backend/annotations.js +133 -0
  74. package/dist/pdf-backend/annotations.js.map +1 -0
  75. package/dist/pdf-backend/blockRender.d.ts.map +1 -1
  76. package/dist/pdf-backend/blockRender.js +160 -30
  77. package/dist/pdf-backend/blockRender.js.map +1 -1
  78. package/dist/pdf-backend/fallbackSpaces.d.ts +21 -2
  79. package/dist/pdf-backend/fallbackSpaces.d.ts.map +1 -1
  80. package/dist/pdf-backend/fallbackSpaces.js +43 -6
  81. package/dist/pdf-backend/fallbackSpaces.js.map +1 -1
  82. package/dist/pdf-backend/fontHelpers.d.ts +12 -0
  83. package/dist/pdf-backend/fontHelpers.d.ts.map +1 -1
  84. package/dist/pdf-backend/fontHelpers.js +49 -11
  85. package/dist/pdf-backend/fontHelpers.js.map +1 -1
  86. package/dist/pdf-backend/headerFooter.d.ts +8 -3
  87. package/dist/pdf-backend/headerFooter.d.ts.map +1 -1
  88. package/dist/pdf-backend/headerFooter.js +74 -16
  89. package/dist/pdf-backend/headerFooter.js.map +1 -1
  90. package/dist/pdf-backend/index.d.ts +74 -8
  91. package/dist/pdf-backend/index.d.ts.map +1 -1
  92. package/dist/pdf-backend/index.js +121 -25
  93. package/dist/pdf-backend/index.js.map +1 -1
  94. package/dist/pdf-backend/inkScale.d.ts +12 -0
  95. package/dist/pdf-backend/inkScale.d.ts.map +1 -0
  96. package/dist/pdf-backend/inkScale.js +16 -0
  97. package/dist/pdf-backend/inkScale.js.map +1 -0
  98. package/dist/pdf-backend/outlines.d.ts +12 -1
  99. package/dist/pdf-backend/outlines.d.ts.map +1 -1
  100. package/dist/pdf-backend/outlines.js +46 -16
  101. package/dist/pdf-backend/outlines.js.map +1 -1
  102. package/dist/pdf-backend/pageDecorations.d.ts +5 -1
  103. package/dist/pdf-backend/pageDecorations.d.ts.map +1 -1
  104. package/dist/pdf-backend/pageDecorations.js +22 -1
  105. package/dist/pdf-backend/pageDecorations.js.map +1 -1
  106. package/dist/pdf-backend/pageLabels.d.ts +3 -1
  107. package/dist/pdf-backend/pageLabels.d.ts.map +1 -1
  108. package/dist/pdf-backend/pageLabels.js +12 -6
  109. package/dist/pdf-backend/pageLabels.js.map +1 -1
  110. package/dist/pdf-backend/primitives.d.ts +153 -6
  111. package/dist/pdf-backend/primitives.d.ts.map +1 -1
  112. package/dist/pdf-backend/primitives.js +269 -28
  113. package/dist/pdf-backend/primitives.js.map +1 -1
  114. package/dist/pdf-backend/renderResourceBlock.d.ts +4 -0
  115. package/dist/pdf-backend/renderResourceBlock.d.ts.map +1 -1
  116. package/dist/pdf-backend/renderResourceBlock.js +105 -26
  117. package/dist/pdf-backend/renderResourceBlock.js.map +1 -1
  118. package/dist/pdf-backend/structureFlow.d.ts +5 -0
  119. package/dist/pdf-backend/structureFlow.d.ts.map +1 -1
  120. package/dist/pdf-backend/structureFlow.js +8 -0
  121. package/dist/pdf-backend/structureFlow.js.map +1 -1
  122. package/dist/pdf-backend/svgVector.d.ts.map +1 -1
  123. package/dist/pdf-backend/svgVector.js +13 -2
  124. package/dist/pdf-backend/svgVector.js.map +1 -1
  125. package/dist/pdf-backend/tagging.d.ts +8 -1
  126. package/dist/pdf-backend/tagging.d.ts.map +1 -1
  127. package/dist/pdf-backend/tagging.js +7 -1
  128. package/dist/pdf-backend/tagging.js.map +1 -1
  129. package/dist/pdf-backend/verticalText.d.ts +43 -0
  130. package/dist/pdf-backend/verticalText.d.ts.map +1 -0
  131. package/dist/pdf-backend/verticalText.js +284 -0
  132. package/dist/pdf-backend/verticalText.js.map +1 -0
  133. package/dist/verticalFonts.d.ts +75 -0
  134. package/dist/verticalFonts.d.ts.map +1 -0
  135. package/dist/verticalFonts.js +136 -0
  136. package/dist/verticalFonts.js.map +1 -0
  137. package/dist/worker/client.d.ts +1 -1
  138. package/dist/worker/client.d.ts.map +1 -1
  139. package/dist/worker/client.js +7 -5
  140. package/dist/worker/client.js.map +1 -1
  141. package/dist/worker/pdf.worker.js +8 -4
  142. package/dist/worker/pdf.worker.js.map +1 -1
  143. package/dist/worker/protocol.d.ts +10 -5
  144. package/dist/worker/protocol.d.ts.map +1 -1
  145. package/package.json +3 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rightBinding.test.js","sourceRoot":"","sources":["../../src/__tests__/rightBinding.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=verticalDesignImage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verticalDesignImage.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/verticalDesignImage.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import { PDFArray, PDFDocument, PDFRawStream, decodePDFRawStream } from 'pdf-lib';
4
+ import { buildDocument } from 'postext';
5
+ import { renderToPdf } from '../pdf-backend';
6
+ // A picture a design draws in the flow of a vertical page (a chapter's plate
7
+ // in the 紅樓夢 showcase, #200) stands upright on the sheet, as the canvas and
8
+ // the HTML draw it: the layout sizes its box with the picture's width and
9
+ // height swapped (`VDTDesignImageBlock.upright`), and the PDF turns it back.
10
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
11
+ const fontProvider = async () => new Uint8Array(fontBytes);
12
+ class StubCtx {
13
+ font = '';
14
+ measureText(s) {
15
+ return { width: s.length * 7 };
16
+ }
17
+ }
18
+ globalThis.OffscreenCanvas = class {
19
+ getContext() {
20
+ return new StubCtx();
21
+ }
22
+ };
23
+ const pt = (value) => ({ value, unit: 'pt' });
24
+ // A 2 × 1 PNG: twice as wide as it is tall.
25
+ const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAIAAAB7QOjdAAAAC0lEQVR4nGNgAAMAAAcAAbKGrPQAAAAASUVORK5CYII=', 'base64'));
26
+ const resources = [
27
+ { id: 'plate', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'plate.png', format: 'png', width: 200, height: 100 } },
28
+ ];
29
+ const described = [{ ...resources[0], altText: '石頭與美玉' }];
30
+ const config = (writingMode) => ({
31
+ page: { dpi: 72, width: pt(300), height: pt(420), margins: { top: pt(30), bottom: pt(30), left: pt(30), right: pt(30) } },
32
+ layout: { layoutType: 'single', writingMode },
33
+ locale: 'zh-Hant',
34
+ header: { elements: [] },
35
+ footer: { elements: [] },
36
+ headings: {
37
+ levels: [{
38
+ level: 1,
39
+ span: 'page',
40
+ breakBefore: { enabled: true, parity: 'any' },
41
+ advancedDesign: {
42
+ enabled: true,
43
+ slot: { elements: [{ kind: 'image', id: 'plate', resourceId: 'plate', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: pt(20), y: pt(10) }, size: { width: pt(60), height: 'auto' } } }] },
44
+ },
45
+ }],
46
+ },
47
+ });
48
+ function pageContent(pdf, index) {
49
+ const contents = pdf.getPage(index).node.Contents();
50
+ const refs = contents instanceof PDFArray ? contents.asArray() : [contents];
51
+ let out = '';
52
+ for (const ref of refs) {
53
+ const stream = pdf.context.lookup(ref);
54
+ if (!(stream instanceof PDFRawStream))
55
+ throw new Error('page content is not a raw stream');
56
+ out += new TextDecoder('latin1').decode(decodePDFRawStream(stream).decode());
57
+ }
58
+ return out;
59
+ }
60
+ const mul = (m, n) => [
61
+ m[0] * n[0] + m[1] * n[2], m[0] * n[1] + m[1] * n[3],
62
+ m[2] * n[0] + m[3] * n[2], m[2] * n[1] + m[3] * n[3],
63
+ m[4] * n[0] + m[5] * n[2] + n[4], m[4] * n[1] + m[5] * n[3] + n[5],
64
+ ];
65
+ /** The transformation matrix in force at each image `Do` of a content
66
+ * stream (`q` / `Q` / `cm` followed; text objects skipped). */
67
+ function imageMatrices(content) {
68
+ const out = [];
69
+ const stack = [];
70
+ let ctm = [1, 0, 0, 1, 0, 0];
71
+ const tokens = content.replace(/\((?:\\.|[^\\)])*\)|<[0-9A-Fa-f\s]*>/g, ' ').split(/\s+/).filter(Boolean);
72
+ const nums = [];
73
+ for (let i = 0; i < tokens.length; i++) {
74
+ const t = tokens[i];
75
+ if (/^-?[\d.]+$/.test(t)) {
76
+ nums.push(parseFloat(t));
77
+ continue;
78
+ }
79
+ if (t === 'q')
80
+ stack.push(ctm);
81
+ else if (t === 'Q')
82
+ ctm = stack.pop() ?? [1, 0, 0, 1, 0, 0];
83
+ else if (t === 'cm')
84
+ ctm = mul(nums.slice(-6), ctm);
85
+ else if (t === 'Do' && /^\/Image/.test(tokens[i - 1] ?? ''))
86
+ out.push(ctm);
87
+ nums.length = 0;
88
+ }
89
+ return out;
90
+ }
91
+ async function plateMatrix(writingMode) {
92
+ const doc = buildDocument({ markdown: '# 甄士隱\n\n此開卷第一回也。', resources }, config(writingMode));
93
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider, accessible: false, resourceBytes: (id) => (id === 'plate.png' ? PNG : undefined) }));
94
+ const found = imageMatrices(pageContent(pdf, 0));
95
+ expect(found).toHaveLength(1);
96
+ return found[0];
97
+ }
98
+ describe('a design picture in the flow of a vertical page (#200)', () => {
99
+ it('stands upright on the sheet, as wide as it is drawn on the canvas', async () => {
100
+ const [a, b, c, d] = await plateMatrix('vertical-rl');
101
+ // The unit square of the image onto the sheet: no turn, no mirror.
102
+ expect(Math.abs(b)).toBeLessThan(1e-6);
103
+ expect(Math.abs(c)).toBeLessThan(1e-6);
104
+ expect(a).toBeGreaterThan(0);
105
+ expect(d).toBeGreaterThan(0);
106
+ // 60 pt down the sheet, twice that across: the picture's own proportions.
107
+ expect(d).toBeCloseTo(60, 3);
108
+ expect(a).toBeCloseTo(120, 3);
109
+ });
110
+ it('leaves a horizontal page as it was', async () => {
111
+ const [a, b, c, d] = await plateMatrix('horizontal-tb');
112
+ expect([Math.abs(b), Math.abs(c)]).toEqual([0, 0]);
113
+ expect(a).toBeCloseTo(60, 3);
114
+ expect(d).toBeCloseTo(30, 3);
115
+ });
116
+ });
117
+ /** The marked-content sequence open at each image `Do` of a content
118
+ * stream (its tag, `/Figure` or `/Artifact`), or `null` outside any. */
119
+ function imageTags(content) {
120
+ const out = [];
121
+ const open = [];
122
+ const tokens = content.replace(/\((?:\\.|[^\\)])*\)|<[0-9A-Fa-f\s]*>/g, ' ').replace(/<<[^>]*>>/g, ' ').split(/\s+/).filter(Boolean);
123
+ for (let i = 0; i < tokens.length; i++) {
124
+ const t = tokens[i];
125
+ if (t === 'BDC' || t === 'BMC') {
126
+ // The tag is the last name before the operator (and its properties).
127
+ const name = tokens.slice(0, i).reverse().find((x) => x.startsWith('/'));
128
+ open.push(name ?? '?');
129
+ }
130
+ else if (t === 'EMC')
131
+ open.pop();
132
+ else if (t === 'Do' && /^\/Image/.test(tokens[i - 1] ?? ''))
133
+ out.push(open[open.length - 1] ?? null);
134
+ }
135
+ return out;
136
+ }
137
+ describe('a design picture on a vertical page in a tagged PDF (#200, #213)', () => {
138
+ const render = async (writingMode, res) => {
139
+ const doc = buildDocument({ markdown: '# 甄士隱\n\n此開卷第一回也。', resources: res }, config(writingMode));
140
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider, resourceBytes: (id) => (id === 'plate.png' ? PNG : undefined) }));
141
+ return imageTags(pageContent(pdf, 0));
142
+ };
143
+ it('paints a picture with alternative text inside its Figure', async () => {
144
+ expect(await render('vertical-rl', described)).toEqual(['/Figure']);
145
+ expect(await render('horizontal-tb', described)).toEqual(['/Figure']);
146
+ });
147
+ it('paints a picture without alternative text inside an artifact', async () => {
148
+ expect(await render('vertical-rl', resources)).toEqual(['/Artifact']);
149
+ expect(await render('horizontal-tb', resources)).toEqual(['/Artifact']);
150
+ });
151
+ });
152
+ //# sourceMappingURL=verticalDesignImage.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verticalDesignImage.test.js","sourceRoot":"","sources":["../../src/__tests__/verticalDesignImage.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=verticalText.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verticalText.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/verticalText.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,380 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import { PDFArray, PDFDict, PDFDocument, PDFHexString, PDFName, PDFRawStream, decodePDFRawStream } from 'pdf-lib';
4
+ import fontkit from '@pdf-lib/fontkit';
5
+ import { buildDocument } from 'postext';
6
+ import { renderToPdf } from '../pdf-backend';
7
+ import { parseFontString } from '../fontString';
8
+ // Issue #191: vertical text in the PDF. Upright characters are shown
9
+ // through an Identity-V twin of their font (the same CIDFont, the same
10
+ // ToUnicode), in their vertical forms where the font has one; Latin words
11
+ // and long numbers run sideways; a short number stands in one cell.
12
+ const FIXTURES = new URL('./fixtures/cjk/', import.meta.url);
13
+ const TC = new Uint8Array(fs.readFileSync(new URL('noto-serif-tc-vertical.ttf', FIXTURES)));
14
+ const SC = new Uint8Array(fs.readFileSync(new URL('noto-serif-sc-vertical.ttf', FIXTURES)));
15
+ const faces = { tc: fontkit.create(Buffer.from(TC)), sc: fontkit.create(Buffer.from(SC)) };
16
+ const fontProvider = async (family) => (family.includes('SC') ? SC : TC);
17
+ /** Measures with the fixture fonts, as a browser would. */
18
+ class StubCtx {
19
+ font = '';
20
+ letterSpacing = '0px';
21
+ measureText(s) {
22
+ const f = parseFontString(this.font);
23
+ const face = f?.family.includes('SC') ? faces.sc : faces.tc;
24
+ const run = face.layout(s);
25
+ const k = (f?.sizePx ?? 16) / face.unitsPerEm;
26
+ let w = 0;
27
+ for (const p of run.positions)
28
+ w += p.xAdvance;
29
+ const b = run.glyphs[0]?.bbox;
30
+ return {
31
+ width: w * k,
32
+ actualBoundingBoxAscent: b ? b.maxY * k : 0,
33
+ actualBoundingBoxDescent: b ? -b.minY * k : 0,
34
+ actualBoundingBoxLeft: 0,
35
+ actualBoundingBoxRight: w * k,
36
+ };
37
+ }
38
+ }
39
+ globalThis.OffscreenCanvas = class {
40
+ getContext() {
41
+ return new StubCtx();
42
+ }
43
+ };
44
+ const pt = (value) => ({ value, unit: 'pt' });
45
+ const config = (locale, extra = {}) => ({
46
+ locale,
47
+ page: { width: pt(300), height: pt(420), dpi: 72, margins: { top: pt(40), bottom: pt(40), left: pt(30), right: pt(30) } },
48
+ layout: { writingMode: 'vertical-rl', layoutType: 'single' },
49
+ bodyText: { fontFamily: locale === 'zh-Hant' ? 'Noto Serif TC' : 'Noto Serif SC', fontSize: pt(16), lineHeight: pt(24), firstLineIndent: pt(0), textAlign: 'left', hyphenation: { enabled: false } },
50
+ headings: { fontFamily: locale === 'zh-Hant' ? 'Noto Serif TC' : 'Noto Serif SC' },
51
+ header: { elements: [] },
52
+ footer: { elements: [] },
53
+ cjk: { punctuationWidth: 'fullwidth', compressAdjacent: false, trimLineStart: false, latinSpacing: { value: 0, unit: 'em' } },
54
+ ...extra,
55
+ });
56
+ async function render(markdown, cfg, accessible = false) {
57
+ const doc = buildDocument({ markdown }, cfg);
58
+ const bytes = await renderToPdf(doc, { fontProvider, accessible });
59
+ const pdf = await PDFDocument.load(bytes);
60
+ return { doc, pdf, bytes };
61
+ }
62
+ function pageOps(pdf, index = 0) {
63
+ const contents = pdf.getPage(index).node.Contents();
64
+ const refs = contents instanceof PDFArray ? contents.asArray() : [contents];
65
+ return refs
66
+ .map((ref) => {
67
+ const s = pdf.context.lookup(ref);
68
+ return s instanceof PDFRawStream ? new TextDecoder('latin1').decode(decodePDFRawStream(s).decode()) : '';
69
+ })
70
+ .join('\n');
71
+ }
72
+ /** The page's fonts by resource name: their dictionaries. */
73
+ function pageFonts(pdf, index = 0) {
74
+ const fonts = pdf.getPage(index).node.Resources().lookup(PDFName.of('Font'), PDFDict);
75
+ const out = new Map();
76
+ for (const [name, ref] of fonts.entries())
77
+ out.set(name.asString().replace(/^\//, ''), pdf.context.lookup(ref, PDFDict));
78
+ return out;
79
+ }
80
+ /** The embedded font file of a Type0 font, opened with fontkit. */
81
+ function embeddedFace(pdf, type0) {
82
+ const cid = type0.lookup(PDFName.of('DescendantFonts'), PDFArray).lookup(0, PDFDict);
83
+ const descriptor = cid.lookup(PDFName.of('FontDescriptor'), PDFDict);
84
+ const file = descriptor.lookup(PDFName.of('FontFile2'));
85
+ const bytes = decodePDFRawStream(file).decode();
86
+ return fontkit.create(Buffer.from(bytes));
87
+ }
88
+ /** Every upright run shown through a vertical twin: its font name, text
89
+ * matrix and the glyph ids it shows. */
90
+ function uprightShows(ops, fonts) {
91
+ const out = [];
92
+ let font = '';
93
+ let tm = [];
94
+ for (const line of ops.split('\n')) {
95
+ const tf = /^\/(\S+) [\d.]+ Tf$/.exec(line);
96
+ if (tf)
97
+ font = tf[1];
98
+ const m = /^([-\d. ]+) Tm$/.exec(line);
99
+ if (m)
100
+ tm = m[1].trim().split(/\s+/).map(Number);
101
+ if (!/ (Tj|TJ)$/.test(line))
102
+ continue;
103
+ if (fonts.get(font)?.get(PDFName.of('Encoding')) !== PDFName.of('Identity-V'))
104
+ continue;
105
+ const hex = [...line.matchAll(/<([0-9a-fA-F]+)>/g)].map((h) => h[1]).join('');
106
+ const cids = [];
107
+ for (let i = 0; i < hex.length; i += 4)
108
+ cids.push(parseInt(hex.slice(i, i + 4), 16));
109
+ const tj = [...line.replace(/<[0-9a-fA-F]+>/g, ' ').replace(/ T[jJ]$/, '').matchAll(/-?\d+(?:\.\d+)?/g)].map((n) => Number(n[0]));
110
+ out.push({ font, tm, cids, tj });
111
+ }
112
+ return out;
113
+ }
114
+ describe('vertical text in the PDF (#191)', () => {
115
+ it('shows upright characters through an Identity-V twin sharing the font', async () => {
116
+ const { pdf } = await render('此開卷第一回也。', config('zh-Hant'));
117
+ const fonts = pageFonts(pdf);
118
+ const twins = [...fonts.values()].filter((f) => f.get(PDFName.of('Encoding')) === PDFName.of('Identity-V'));
119
+ expect(twins).toHaveLength(1);
120
+ const horizontal = [...fonts.values()].find((f) => f.get(PDFName.of('Encoding')) === PDFName.of('Identity-H'));
121
+ // The twin is written with its font: same CIDFont, same ToUnicode.
122
+ const cidOf = (f) => f.lookup(PDFName.of('DescendantFonts'), PDFArray).get(0);
123
+ if (horizontal) {
124
+ expect(cidOf(twins[0])).toBe(cidOf(horizontal));
125
+ expect(twins[0].get(PDFName.of('ToUnicode'))).toBe(horizontal.get(PDFName.of('ToUnicode')));
126
+ }
127
+ const cid = twins[0].lookup(PDFName.of('DescendantFonts'), PDFArray).lookup(0, PDFDict);
128
+ expect(cid.lookup(PDFName.of('DW2'), PDFArray).asArray().map(String)).toEqual(['880', '-1000']);
129
+ // One show for the whole line, its text matrix turned back upright.
130
+ const shows = uprightShows(pageOps(pdf), fonts);
131
+ expect(shows).toHaveLength(1);
132
+ expect(shows[0].tm.slice(0, 4)).toEqual([0, 1, -1, 0]);
133
+ expect(shows[0].cids).toHaveLength(8);
134
+ });
135
+ it('sets a mainland 。 in its vertical form and a Taiwan one as it is; 「 in its vertical form in both', async () => {
136
+ for (const [locale, face] of [['zh-Hans', faces.sc], ['zh-Hant', faces.tc]]) {
137
+ const { pdf } = await render('此「一」也。', config(locale));
138
+ const fonts = pageFonts(pdf);
139
+ const shows = uprightShows(pageOps(pdf), fonts);
140
+ const cids = shows.flatMap((s) => s.cids);
141
+ expect(cids).toHaveLength(6);
142
+ const embedded = embeddedFace(pdf, fonts.get(shows[0].font));
143
+ const bbox = (cid) => embedded.getGlyph(cid).bbox;
144
+ // 「: the vertical form spans the cell across, its hook at the top.
145
+ const bracket = bbox(cids[1]);
146
+ expect(bracket.maxX - bracket.minX).toBeGreaterThan(600);
147
+ const vBracket = face.layout('「', ['vert']).glyphs[0].bbox;
148
+ expect(bracket.minY).toBe(vBracket.minY);
149
+ const stop = bbox(cids[5]);
150
+ if (locale === 'zh-Hans') {
151
+ // The top right quadrant of the cell.
152
+ expect(stop.minX).toBeGreaterThan(500);
153
+ expect(stop.minY).toBeGreaterThan(400);
154
+ }
155
+ else {
156
+ // Centred.
157
+ expect((stop.minX + stop.maxX) / 2).toBeCloseTo(500, -2);
158
+ expect((stop.minY + stop.maxY) / 2).toBeCloseTo(380, -2);
159
+ }
160
+ }
161
+ });
162
+ it('sets a single dash in its vertical form (vert with fwid), a 破折号 as one rule turned down the column', async () => {
163
+ const isRule = (face) => (cid) => {
164
+ const b = face.getGlyph(cid).bbox;
165
+ return b.maxY - b.minY > 700 && b.maxX - b.minX < 100;
166
+ };
167
+ // A single dash: one show of the vertical form, a rule down the middle
168
+ // of its cell.
169
+ const single = await render('此—也', config('zh-Hant'));
170
+ const singleFonts = pageFonts(single.pdf);
171
+ const singleShows = uprightShows(pageOps(single.pdf), singleFonts);
172
+ const embedded = embeddedFace(single.pdf, singleFonts.get(singleShows[0].font));
173
+ const dashes = singleShows.filter((s) => s.cids.length === 1 && isRule(embedded)(s.cids[0]));
174
+ expect(dashes).toHaveLength(1);
175
+ const bbox = embedded.getGlyph(dashes[0].cids[0]).bbox;
176
+ expect((bbox.minX + bbox.maxX) / 2).toBeCloseTo(500, -1);
177
+ // A 破折号: its vertical forms would leave blank at both ends of each
178
+ // cell (0.07 em), so each dash is shown turned with the frame, the
179
+ // horizontal glyph under an unturned text matrix, stretched down the
180
+ // column (Tz) into one rule.
181
+ const { doc, pdf } = await render('此——也', config('zh-Hant'));
182
+ const ops = pageOps(pdf);
183
+ const fonts = pageFonts(pdf);
184
+ const shows = uprightShows(ops, fonts);
185
+ expect(shows.some((s) => s.cids.some(isRule(embeddedFace(pdf, fonts.get(s.font)))))).toBe(false);
186
+ const stretched = [...ops.matchAll(/([\d.]+) Tz\s+q[\s\S]*?\/(\S+) [\d.]+ Tf[\s\S]*?(-?[\d.]+) (-?[\d.]+) (-?[\d.]+) (-?[\d.]+) (-?[\d.]+) (-?[\d.]+) Tm[\s\S]*?ET\s+Q\s+100 Tz/g)];
187
+ expect(stretched).toHaveLength(2);
188
+ const segs = doc.pages[0].columns[0].blocks[0].lines[0].segments.filter((s) => s.text === '—');
189
+ for (const [i, m] of stretched.entries()) {
190
+ expect(Number(m[1])).toBeCloseTo(segs[i].inkScale * 100, 2);
191
+ expect(Number(m[1])).toBeGreaterThan(100);
192
+ expect(fonts.get(m[2])?.get(PDFName.of('Encoding'))).toBe(PDFName.of('Identity-H'));
193
+ expect([m[3], m[4], m[5], m[6]].map(Number)).toEqual([1, 0, 0, 1]);
194
+ }
195
+ expect(Number(stretched[1][7]) - Number(stretched[0][7])).toBeCloseTo(16 + segs[1].inkOffset - segs[0].inkOffset, 3);
196
+ });
197
+ it('places the pen so every em box is centred on the column axis, one em apart', async () => {
198
+ const { doc, pdf } = await render('此開卷也', config('zh-Hant'));
199
+ const page = doc.pages[0];
200
+ const line = page.columns[0].blocks[0].lines[0];
201
+ const show = uprightShows(pageOps(pdf), pageFonts(pdf))[0];
202
+ const axisFlowY = line.baseline - (page.flow.centralBaselines?.['Noto Serif TC'] ?? 0.38) * 16;
203
+ // Flow points: x along the line, y from the frame's top (y up).
204
+ expect(show.tm[5]).toBeCloseTo(420 - axisFlowY, 3);
205
+ // The pen at the vertical origin (0.88 em above the baseline): the cell
206
+ // top when the em box's centre is 0.38 em above it.
207
+ const central = page.flow.centralBaselines?.['Noto Serif TC'] ?? 0.38;
208
+ expect(show.tm[4]).toBeCloseTo(line.bbox.x + 16 * (0.5 - (0.88 - central)), 3);
209
+ });
210
+ it('stands each column in the middle of its line box, and a design title in the middle of its own line (#188)', async () => {
211
+ const { doc, pdf } = await render('此開卷也', config('zh-Hant'));
212
+ const line = doc.pages[0].columns[0].blocks[0].lines[0];
213
+ const show = uprightShows(pageOps(pdf), pageFonts(pdf))[0];
214
+ expect(show.tm[5]).toBeCloseTo(420 - (line.bbox.y + line.bbox.height / 2), 3);
215
+ // An opener title of 40 pt on a line of 40 pt: the same, in its own box.
216
+ const opened = await render('# 桃園結義\n\n此開卷也', config('zh-Hant', {
217
+ headings: { fontFamily: 'Noto Serif TC', levels: [{ level: 1, breakBefore: { enabled: false }, advancedDesign: { enabled: true, minHeight: pt(95), slot: { elements: [
218
+ { kind: 'text', id: 't', content: '{titleText}', fontFamily: 'Noto Serif TC', fontSize: pt(40), lineHeight: 1, overflow: 'clip', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: pt(20), y: pt(19) } } },
219
+ ] } } }] },
220
+ }));
221
+ const page = opened.doc.pages[0];
222
+ const slot = page.openerBand ?? page.columns[0].blocks.find((b) => b.designOverlay).designOverlay;
223
+ const title = slot.blocks.find((b) => b.kind === 'text');
224
+ const sized = [];
225
+ let size = 0;
226
+ for (const op of pageOps(opened.pdf).split('\n')) {
227
+ const tf = /^\/\S+ ([\d.]+) Tf$/.exec(op);
228
+ if (tf)
229
+ size = Number(tf[1]);
230
+ const tm = /^([-\d. ]+) Tm$/.exec(op);
231
+ if (tm && size === 40)
232
+ sized.push(Number(tm[1].trim().split(/\s+/)[5]));
233
+ }
234
+ expect(sized.length).toBeGreaterThan(0);
235
+ for (const y of sized)
236
+ expect(y).toBeCloseTo(420 - (title.bbox.y + title.bbox.height / 2), 3);
237
+ });
238
+ it('sets a cluster of several glyphs in one cell, the characters after it where they were measured (#191 review)', async () => {
239
+ // 此 with a combining acute the font does not compose: two glyphs (the
240
+ // fixture has no acute: .notdef). In one show the second would push
241
+ // every glyph after it down one em.
242
+ const { doc, pdf } = await render('此\u0301開卷也', config('zh-Hant'));
243
+ const page = doc.pages[0];
244
+ const line = page.columns[0].blocks[0].lines[0];
245
+ expect(line.bbox.width).toBeCloseTo(4 * 16, 3);
246
+ const ops = pageOps(pdf);
247
+ const shows = uprightShows(ops, pageFonts(pdf));
248
+ // The twin shows only the three characters after the cluster, from the
249
+ // second cell on.
250
+ expect(shows.flatMap((s) => s.cids)).toHaveLength(3);
251
+ const central = page.flow.centralBaselines?.['Noto Serif TC'] ?? 0.38;
252
+ expect(shows[0].tm[4]).toBeCloseTo(line.bbox.x + 16 + 16 * (0.5 - (0.88 - central)), 3);
253
+ // The cluster: shaped horizontally, stood upright in the first cell.
254
+ const cluster = [...ops.matchAll(/0 1 -1 0 ([-\d.]+) [-\d.]+ Tm/g)].map((m) => Number(m[1]));
255
+ expect(cluster.some((x) => Math.abs(x - (line.bbox.x + 8 + central * 16)) < 0.01)).toBe(true);
256
+ });
257
+ it('sets each letter of an :upright run in its own cell, never a ligature of two', async () => {
258
+ // Fraunces ligates f+f+i, f+i and f+f by default: shaped together, the
259
+ // six letters of "office" would be four glyphs of one em each, and the
260
+ // two cells after them empty.
261
+ const fraunces = new Uint8Array(fs.readFileSync(new URL('../../../../apps/web/public/fonts/Fraunces-Regular.ttf', import.meta.url)));
262
+ expect(fontkit.create(Buffer.from(fraunces)).layout('office').glyphs).toHaveLength(4);
263
+ const doc = buildDocument({ markdown: ':upright[office]' }, config('zh-Hant'));
264
+ const line = doc.pages[0].columns[0].blocks[0].lines[0];
265
+ expect(line.bbox.width).toBeCloseTo(6 * 16, 3);
266
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider: async () => fraunces, accessible: false }));
267
+ const cids = uprightShows(pageOps(pdf), pageFonts(pdf)).flatMap((s) => s.cids);
268
+ // o f f i c e: one glyph per cell, the two f's the same glyph.
269
+ expect(cids).toHaveLength(6);
270
+ expect(cids[1]).toBe(cids[2]);
271
+ expect(new Set(cids).size).toBe(5);
272
+ });
273
+ it('spreads a justified vertical line with TJ numbers that move the pen down', async () => {
274
+ const cfg = config('zh-Hant');
275
+ cfg.bodyText = { ...cfg.bodyText, textAlign: 'justify' };
276
+ // A full line is one em short of the measure: the gaps take it.
277
+ const text = '此開卷第一回也作者自云因曾歷過一番夢幻之後故將真事隱去而借通靈之說撰石頭記書';
278
+ const { pdf } = await render(text, cfg);
279
+ const shows = uprightShows(pageOps(pdf), pageFonts(pdf));
280
+ const spread = shows.find((s) => s.tj.length > 0);
281
+ expect(spread).toBeDefined();
282
+ expect(spread.tj.every((n) => n > 0)).toBe(true);
283
+ });
284
+ it('sets Latin words and long numbers sideways and short numbers upright in one cell', async () => {
285
+ const { pdf } = await render('今天是2026年9月28日,用iPhone拍照,:tcy[12345]。', config('zh-Hant'));
286
+ const ops = pageOps(pdf);
287
+ // Sideways: an unturned text matrix inside the page's frame.
288
+ expect(ops).toMatch(/1 0 0 1 [-\d.]+ [-\d.]+ Tm/);
289
+ // Upright cells of a horizontal font: 28 at full width, 12345 squeezed.
290
+ const cells = [...ops.matchAll(/0 ([\d.]+) -1 0 [-\d.]+ [-\d.]+ Tm\s+(<[0-9a-fA-F]+> Tj|\[[^\]]*\] TJ)/g)].map((m) => Number(m[1]));
291
+ expect(cells).toContain(1);
292
+ expect(cells.some((k) => k < 1)).toBe(true);
293
+ });
294
+ it('reads every vertical line as written (/ActualText)', async () => {
295
+ const { pdf } = await render('今天是2026年9月28日,用iPhone拍照。', config('zh-Hant'));
296
+ const ops = pageOps(pdf);
297
+ const texts = [...ops.matchAll(/\/ActualText <([0-9A-Fa-f]+)>/g)].map((m) => PDFHexString.of(m[1]).decodeText());
298
+ expect(texts.join('')).toContain('今天是2026年9月28日,用iPhone拍照。');
299
+ });
300
+ it('declares the vertical writing mode on the Document element and passes the page’s headings to the outline at the column top', async () => {
301
+ const { doc, pdf } = await render('# 第一回\n\n此開卷第一回也。', config('zh-Hant'), true);
302
+ const root = pdf.catalog.lookup(PDFName.of('StructTreeRoot'), PDFDict);
303
+ const docElem = root.lookup(PDFName.of('K'), PDFArray).lookup(0, PDFDict);
304
+ const attrs = docElem.lookup(PDFName.of('A'), PDFDict);
305
+ expect(attrs.get(PDFName.of('O'))).toBe(PDFName.of('Layout'));
306
+ expect(attrs.get(PDFName.of('WritingMode'))).toBe(PDFName.of('TbRl'));
307
+ const outlines = pdf.catalog.lookup(PDFName.of('Outlines'), PDFDict);
308
+ const first = outlines.lookup(PDFName.of('First'), PDFDict);
309
+ const dest = first.lookup(PDFName.of('Dest'), PDFArray).asArray();
310
+ const heading = doc.pages[0].columns[0].blocks.find((b) => b.type === 'heading');
311
+ expect(Number(String(dest[3]))).toBeCloseTo(420 - heading.bbox.x, 3);
312
+ });
313
+ it('sets a vertical running head down the fore-edge in its own turned frame (#192)', async () => {
314
+ const cfg = config('zh-Hant', {
315
+ page: { width: pt(300), height: pt(420), dpi: 72, margins: { top: pt(40), bottom: pt(40), left: pt(30), right: pt(50), mirror: true } },
316
+ header: { elements: [{ kind: 'text', id: 'h', content: '第一回', writingMode: 'vertical-rl', fontFamily: 'Noto Serif TC', fontSize: pt(8), overflow: 'clip', placement: { anchor: { to: 'outer', edge: 'top' }, offset: { y: { value: 4, unit: 'em' } } } }] },
317
+ });
318
+ const { doc, pdf } = await render('此開卷第一回也。', cfg);
319
+ const head = doc.pages[0].header.blocks.find((b) => b.kind === 'text');
320
+ const { x, y, width } = head.bbox;
321
+ const ops = pageOps(pdf);
322
+ // The block's frame: a quarter turn clockwise about its box's top right.
323
+ const frame = `0 -1 1 0 ${+(x + width - 420).toFixed(4)} ${+(420 - y).toFixed(4)} cm`;
324
+ expect(ops.replace(/(\d+\.\d{4})\d+/g, '$1')).toContain(frame.replace(/(\d+\.\d{4})\d+/g, '$1'));
325
+ const after = ops.slice(ops.indexOf(' cm', ops.lastIndexOf('0 -1 1 0 ')));
326
+ const shows = uprightShows(after, pageFonts(pdf));
327
+ expect(shows.flatMap((sh) => sh.cids)).toHaveLength(3);
328
+ });
329
+ it('sets the orientation marks of a vertical running head (#190, #192 review)', async () => {
330
+ const cfg = config('zh-Hant', {
331
+ page: { width: pt(300), height: pt(420), dpi: 72, margins: { top: pt(40), bottom: pt(40), left: pt(30), right: pt(50), mirror: true } },
332
+ header: { elements: [{ kind: 'text', id: 'h', content: '第:tcy[12345]回:upright[GDP]', inlineMarks: true, writingMode: 'vertical-rl', fontFamily: 'Noto Serif TC', fontSize: pt(8), overflow: 'clip', placement: { anchor: { to: 'outer', edge: 'top' } } }] },
333
+ });
334
+ const { doc, pdf } = await render('此開卷第一回也。', cfg);
335
+ const head = doc.pages[0].header.blocks.find((b) => b.kind === 'text');
336
+ expect(head.kind === 'text' && head.lines[0].runs?.some((r) => r.tcy)).toBe(true);
337
+ const ops = pageOps(pdf);
338
+ const after = ops.slice(ops.indexOf(' cm', ops.lastIndexOf('0 -1 1 0 ')));
339
+ // 12345 upright in one cell, squeezed across by its text matrix.
340
+ const cells = [...after.matchAll(/0 ([\d.]+) -1 0 [-\d.]+ [-\d.]+ Tm/g)].map((m) => Number(m[1]));
341
+ expect(cells.some((k) => k < 1)).toBe(true);
342
+ // 第, 回 and G, D, P upright through the twin, one glyph a cell.
343
+ expect(uprightShows(after, pageFonts(pdf)).flatMap((sh) => sh.cids)).toHaveLength(5);
344
+ });
345
+ it('sets readings, note rows and marks down the column (#193–#195)', async () => {
346
+ const cfg = config('zh-Hant', { bodyText: { fontFamily: 'Noto Serif TC', fontSize: pt(16), lineHeight: pt(32), firstLineIndent: pt(0), textAlign: 'left', hyphenation: { enabled: false } } });
347
+ const { doc, pdf } = await render('此:dots[真事]隱:ruby[開卷]{rt="GD PA"}:ruby[回]{rt="ㄏㄨㄟˊ"}:warichu[甲乙丙丁]也', cfg);
348
+ const line = doc.pages[0].columns[0].blocks[0].lines[0];
349
+ const ops = pageOps(pdf);
350
+ const fonts = pageFonts(pdf);
351
+ // Everything after the flow's frame is entered.
352
+ const flow = ops.slice(ops.indexOf(' cm', ops.indexOf('0 -1 1 0 ')));
353
+ const shows = uprightShows(flow, fonts);
354
+ const sizeOf = (font) => Number(new RegExp(`/${font} ([\\d.]+) Tf`).exec(flow)[1]);
355
+ // The note: two rows through the twin at half the size, the upper one
356
+ // (read first) nearer the flow's top, the right of the column.
357
+ const rows = shows.filter((sh) => /V-/.test(sh.font) && sh.cids.length === 2 && flow.includes(`/${sh.font} 8 Tf`)).filter((sh) => sh.tm[4] > line.bbox.x + 16 * 6);
358
+ expect(rows).toHaveLength(2);
359
+ expect(rows[0].tm[5]).toBeGreaterThan(rows[1].tm[5]);
360
+ // Pinyin runs sideways with the horizontal font at the ruby size.
361
+ expect(flow).toMatch(/Tf\n[^\n]*\n?1 0 0 1 [\d.]+ [\d.]+ Tm\n\[? ?<[0-9A-F]{4}>/);
362
+ expect(sizeOf(shows[0].font)).toBe(16);
363
+ // Zhuyin at 0.3 em through the twin: the symbols' column, then the
364
+ // tone mark standing upright in a show of its own (UAX #50 would turn
365
+ // it sideways).
366
+ const zhuyin = [...flow.matchAll(/V-\d+ 4\.8 Tf\n[^\n]*\n?0 1 -1 0 [\d.]+ [\d.]+ Tm\n<([0-9a-fA-F]+)> Tj/g)].map((m) => m[1].length / 4);
367
+ expect(zhuyin).toEqual([3, 1]);
368
+ // The dots: two filled circles drawn in the flow's frame after the
369
+ // line's text.
370
+ const after = flow.slice(flow.lastIndexOf(' Tj'));
371
+ expect(after.match(/ c\n/g).length).toBe(8);
372
+ expect(after.match(/\nf\n/g).length).toBe(2);
373
+ });
374
+ it('leaves horizontal Chinese text without a twin', async () => {
375
+ const { pdf } = await render('此開卷第一回也。', config('zh-Hant', { layout: { writingMode: 'horizontal-tb', layoutType: 'single' } }));
376
+ const fonts = pageFonts(pdf);
377
+ expect([...fonts.values()].some((f) => f.get(PDFName.of('Encoding')) === PDFName.of('Identity-V'))).toBe(false);
378
+ });
379
+ });
380
+ //# sourceMappingURL=verticalText.test.js.map