postext-pdf 1.22.3 → 1.24.0

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 (54) hide show
  1. package/dist/__tests__/booktabsTable.test.d.ts +2 -0
  2. package/dist/__tests__/booktabsTable.test.d.ts.map +1 -0
  3. package/dist/__tests__/booktabsTable.test.js +101 -0
  4. package/dist/__tests__/booktabsTable.test.js.map +1 -0
  5. package/dist/__tests__/codeBlocks.test.d.ts +2 -0
  6. package/dist/__tests__/codeBlocks.test.d.ts.map +1 -0
  7. package/dist/__tests__/codeBlocks.test.js +135 -0
  8. package/dist/__tests__/codeBlocks.test.js.map +1 -0
  9. package/dist/__tests__/dropCap.test.d.ts +2 -0
  10. package/dist/__tests__/dropCap.test.d.ts.map +1 -0
  11. package/dist/__tests__/dropCap.test.js +125 -0
  12. package/dist/__tests__/dropCap.test.js.map +1 -0
  13. package/dist/__tests__/hardBreaks.test.d.ts +2 -0
  14. package/dist/__tests__/hardBreaks.test.d.ts.map +1 -0
  15. package/dist/__tests__/hardBreaks.test.js +107 -0
  16. package/dist/__tests__/hardBreaks.test.js.map +1 -0
  17. package/dist/__tests__/lineNumbers.test.d.ts +2 -0
  18. package/dist/__tests__/lineNumbers.test.d.ts.map +1 -0
  19. package/dist/__tests__/lineNumbers.test.js +106 -0
  20. package/dist/__tests__/lineNumbers.test.js.map +1 -0
  21. package/dist/__tests__/tabStops.test.d.ts +2 -0
  22. package/dist/__tests__/tabStops.test.d.ts.map +1 -0
  23. package/dist/__tests__/tabStops.test.js +105 -0
  24. package/dist/__tests__/tabStops.test.js.map +1 -0
  25. package/dist/__tests__/textWrap.test.d.ts +2 -0
  26. package/dist/__tests__/textWrap.test.d.ts.map +1 -0
  27. package/dist/__tests__/textWrap.test.js +116 -0
  28. package/dist/__tests__/textWrap.test.js.map +1 -0
  29. package/dist/__tests__/verseLines.test.d.ts +2 -0
  30. package/dist/__tests__/verseLines.test.d.ts.map +1 -0
  31. package/dist/__tests__/verseLines.test.js +140 -0
  32. package/dist/__tests__/verseLines.test.js.map +1 -0
  33. package/dist/pdf-backend/blockRender.d.ts.map +1 -1
  34. package/dist/pdf-backend/blockRender.js +79 -18
  35. package/dist/pdf-backend/blockRender.js.map +1 -1
  36. package/dist/pdf-backend/fontHelpers.d.ts.map +1 -1
  37. package/dist/pdf-backend/fontHelpers.js +7 -1
  38. package/dist/pdf-backend/fontHelpers.js.map +1 -1
  39. package/dist/pdf-backend/headerFooter.d.ts +7 -2
  40. package/dist/pdf-backend/headerFooter.d.ts.map +1 -1
  41. package/dist/pdf-backend/headerFooter.js +11 -6
  42. package/dist/pdf-backend/headerFooter.js.map +1 -1
  43. package/dist/pdf-backend/index.d.ts.map +1 -1
  44. package/dist/pdf-backend/index.js +6 -0
  45. package/dist/pdf-backend/index.js.map +1 -1
  46. package/dist/pdf-backend/renderResourceBlock.d.ts.map +1 -1
  47. package/dist/pdf-backend/renderResourceBlock.js +6 -1
  48. package/dist/pdf-backend/renderResourceBlock.js.map +1 -1
  49. package/dist/pdf-backend/structureFlow.d.ts.map +1 -1
  50. package/dist/pdf-backend/structureFlow.js +6 -0
  51. package/dist/pdf-backend/structureFlow.js.map +1 -1
  52. package/dist/pdf-backend/tagging.d.ts +1 -1
  53. package/dist/pdf-backend/tagging.d.ts.map +1 -1
  54. package/package.json +3 -3
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=booktabsTable.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booktabsTable.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/booktabsTable.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import { PDFArray, PDFDocument, 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
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
9
+ const fontProvider = async () => new Uint8Array(fontBytes);
10
+ const face = fontkit.create(fontBytes);
11
+ class StubCtx {
12
+ font = '';
13
+ measureText(s) {
14
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
15
+ const run = face.layout(s);
16
+ return { width: (run.advanceWidth / face.unitsPerEm) * sizePx };
17
+ }
18
+ }
19
+ globalThis.OffscreenCanvas = class {
20
+ getContext() {
21
+ return new StubCtx();
22
+ }
23
+ };
24
+ const pt = (value) => ({ value, unit: 'pt' });
25
+ const cell = (content, extra = {}) => ({ content, ...extra });
26
+ // A journal table (#625): booktabs rules, a 0.4pt light rule, a head over
27
+ // two columns, and a long table split across pages.
28
+ const config = {
29
+ page: { width: pt(400), height: pt(500), margins: { top: pt(20), bottom: pt(20), left: pt(20), right: pt(20) } },
30
+ headings: { balancing: { enabled: false }, levels: [{ level: 1, breakBefore: { enabled: false } }] },
31
+ tableStyle: { rules: 'booktabs', bodyFontSize: pt(10), heavyRuleWidth: pt(0.8), lightRuleWidth: pt(0.4), headerBackgroundEnabled: false },
32
+ };
33
+ const tableOf = (id, rows) => ({
34
+ id,
35
+ typeId: 'table',
36
+ kind: 'table',
37
+ caption: `Table ${id}.`,
38
+ createdAt: 0,
39
+ updatedAt: 0,
40
+ table: {
41
+ model: {
42
+ headerRowCount: 2,
43
+ rows: [
44
+ [cell('Model', { isHeader: true, rowSpan: 2 }), cell('Accuracy', { isHeader: true, colSpan: 2 }), cell('', { hiddenBy: { row: 0, col: 1 } })],
45
+ [cell('', { hiddenBy: { row: 0, col: 0 } }), cell('Train', { isHeader: true }), cell('Test', { isHeader: true })],
46
+ ...Array.from({ length: rows }, (_, i) => [cell(`Model ${i + 1}`), cell(`${90 + (i % 9)}.1`), cell(`${80 + (i % 9)}.4`)]),
47
+ ],
48
+ },
49
+ },
50
+ placement: { span: 'page' },
51
+ });
52
+ const resources = [tableOf('short', 4), tableOf('long', 60)];
53
+ const filler = (n) => Array.from({ length: n }, (_, i) => `Paragraph ${i} carries enough words to take a few lines of the narrow column so the flow advances.`).join('\n\n');
54
+ const markdown = `See :ref{id="short"} and :ref{id="long"}.\n\n${filler(30)}`;
55
+ let doc;
56
+ let pdf;
57
+ const pageText = (index) => {
58
+ const contents = pdf.getPage(index).node.Contents();
59
+ const streams = contents instanceof PDFArray ? contents.asArray().map((r) => pdf.context.lookup(r)) : [contents];
60
+ return streams
61
+ .map((s) => (s instanceof PDFRawStream ? Buffer.from(decodePDFRawStream(s).decode()).toString('latin1') : ''))
62
+ .join('\n');
63
+ };
64
+ const tableBlocks = (id) => doc.pages.flatMap((p) => [...p.columns.flatMap((c) => c.blocks), ...(p.floats ?? [])]
65
+ .filter((b) => b.resourceBlock?.resource.id === id)
66
+ .map((block) => ({ page: p.index, block })));
67
+ beforeAll(async () => {
68
+ doc = buildDocument({ markdown, resources }, config);
69
+ const bytes = await renderToPdf(doc, { fontProvider });
70
+ // POSTEXT_PDF_OUT=/path/out.pdf keeps the file for a visual check.
71
+ if (process.env.POSTEXT_PDF_OUT)
72
+ fs.writeFileSync(process.env.POSTEXT_PDF_OUT, bytes);
73
+ pdf = await PDFDocument.load(bytes);
74
+ }, 60_000);
75
+ describe('booktabs tables in the PDF', () => {
76
+ it('strokes every layout stroke at its own width, fractional ones included', () => {
77
+ const [{ page, block }] = tableBlocks('short');
78
+ const strokes = block.resourceBlock.table.strokes;
79
+ // Top, span rule, header rule, bottom.
80
+ expect(strokes).toHaveLength(4);
81
+ const text = pageText(page);
82
+ const widths = [...text.matchAll(/([\d.]+) w\b/g)].map((m) => Number(m[1]));
83
+ expect(widths).toContainEqual(0.8);
84
+ expect(widths).toContainEqual(0.4);
85
+ expect(widths).toContainEqual(0.3);
86
+ });
87
+ it('closes a part that continues with the light rule and the last part with the heavy one', () => {
88
+ const parts = tableBlocks('long');
89
+ expect(parts.length).toBeGreaterThan(1);
90
+ // In px at the page dpi: compare against the short table's heavy rule.
91
+ const heavy = tableBlocks('short')[0].block.resourceBlock.table.strokes[0].widthPx;
92
+ const scale = 0.8 / heavy;
93
+ for (const [i, { block }] of parts.entries()) {
94
+ const s = block.resourceBlock.table.strokes;
95
+ const last = i === parts.length - 1;
96
+ expect(s[0].widthPx * scale).toBeCloseTo(0.8, 6);
97
+ expect(s[s.length - 1].widthPx * scale).toBeCloseTo(last ? 0.8 : 0.4, 6);
98
+ }
99
+ });
100
+ });
101
+ //# sourceMappingURL=booktabsTable.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booktabsTable.test.js","sourceRoot":"","sources":["../../src/__tests__/booktabsTable.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=codeBlocks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codeBlocks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/codeBlocks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { execFileSync, spawnSync } from 'node:child_process';
6
+ import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef } from 'pdf-lib';
7
+ import fontkit from '@pdf-lib/fontkit';
8
+ import { buildDocument } from 'postext';
9
+ import { renderToPdf } from '../pdf-backend';
10
+ import { parseFontString } from '../fontString';
11
+ // #624: code listings in the PDF. Each listing is a `P` holding one `Code`
12
+ // element; its spaces are real space glyphs, so a copy keeps the
13
+ // indentation; its line numbers and wrap markers are artifacts, out of the
14
+ // extracted text.
15
+ const lora = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
16
+ const mono = fs.readFileSync(new URL('../../../../apps/web/public/fonts/JetBrainsMono-Regular.ttf', import.meta.url));
17
+ const monoBold = fs.readFileSync(new URL('../../../../apps/web/public/fonts/JetBrainsMono-Bold.ttf', import.meta.url));
18
+ const MONO = 'JetBrains Mono';
19
+ const fontProvider = async (family, weight) => new Uint8Array(family === MONO ? (weight >= 600 ? monoBold : mono) : lora);
20
+ const faces = { text: fontkit.create(lora), mono: fontkit.create(mono) };
21
+ class StubCtx {
22
+ font = '';
23
+ measureText(s) {
24
+ const parsed = parseFontString(this.font);
25
+ const face = this.font.includes(MONO) ? faces.mono : faces.text;
26
+ const run = face.layout(s);
27
+ return { width: (run.advanceWidth / face.unitsPerEm) * (parsed?.sizePx ?? 16) };
28
+ }
29
+ }
30
+ globalThis.OffscreenCanvas = class {
31
+ getContext() {
32
+ return new StubCtx();
33
+ }
34
+ };
35
+ const pt = (value) => ({ value, unit: 'pt' });
36
+ const config = {
37
+ page: { width: pt(300), height: pt(480), margins: { top: pt(24), bottom: pt(24), left: pt(24), right: pt(24) } },
38
+ locale: 'en-us',
39
+ layout: { layoutType: 'single' },
40
+ header: { elements: [] },
41
+ footer: { elements: [] },
42
+ codeStyle: { fontFamily: MONO },
43
+ };
44
+ const has = (cmd, arg) => spawnSync(cmd, [arg], { encoding: 'utf8' }).status === 0;
45
+ const hasPdftotext = (() => {
46
+ try {
47
+ execFileSync('pdftotext', ['-v'], { stdio: 'ignore' });
48
+ return true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ })();
54
+ const hasVerapdf = process.env.VERAPDF === '1' && has('verapdf', '--version');
55
+ /** The structure types of the tree, depth first. */
56
+ function structTypes(pdf) {
57
+ const out = [];
58
+ const visit = (ref) => {
59
+ const dict = ref instanceof PDFRef ? pdf.context.lookup(ref) : ref;
60
+ if (dict instanceof PDFArray) {
61
+ for (const kid of dict.asArray())
62
+ visit(kid);
63
+ return;
64
+ }
65
+ if (!(dict instanceof PDFDict))
66
+ return;
67
+ const s = dict.get(PDFName.of('S'));
68
+ if (s instanceof PDFName)
69
+ out.push(s.decodeText());
70
+ const k = dict.get(PDFName.of('K'));
71
+ for (const kid of k instanceof PDFArray ? k.asArray() : k ? [k] : [])
72
+ visit(kid);
73
+ };
74
+ const root = pdf.catalog.lookup(PDFName.of('StructTreeRoot'), PDFDict);
75
+ visit(root.get(PDFName.of('K')));
76
+ return out;
77
+ }
78
+ const TEXT = [
79
+ 'A listing:',
80
+ '',
81
+ '```py {lineNumbers title="f.py"}',
82
+ 'def f(x):',
83
+ ' return x # twice',
84
+ '',
85
+ 'print(f(1))',
86
+ '```',
87
+ '',
88
+ 'After it.',
89
+ '',
90
+ '```js {highlight="1"}',
91
+ `const s = "${'a'.repeat(70)}";`,
92
+ '```',
93
+ ].join('\n');
94
+ describe('code listings in the PDF (#624)', () => {
95
+ const doc = buildDocument({ markdown: TEXT, metadata: { title: 'Code' } }, config);
96
+ it('tags a listing as a paragraph holding Code, in its box', async () => {
97
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider }));
98
+ const types = structTypes(pdf);
99
+ const at = types.indexOf('Code');
100
+ expect(at).toBeGreaterThan(0);
101
+ expect(types[at - 1]).toBe('P');
102
+ expect(types.filter((t) => t === 'Code')).toHaveLength(2);
103
+ });
104
+ it.skipIf(!hasPdftotext)('copies the code with its indentation, without its line numbers', async () => {
105
+ const file = path.join(os.tmpdir(), `postext-code-${process.pid}.pdf`);
106
+ fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
107
+ const text = execFileSync('pdftotext', ['-layout', file, '-'], { encoding: 'utf8' });
108
+ fs.unlinkSync(file);
109
+ // The indentation survives: `return` four columns in from `def`.
110
+ const lines = text.split('\n');
111
+ const def = lines.find((l) => l.includes('def f(x):'));
112
+ const ret = lines.find((l) => l.includes('return x'));
113
+ expect(ret.indexOf('return') - def.indexOf('def')).toBeGreaterThanOrEqual(4);
114
+ expect(ret).toMatch(/return x {2,}# twice/);
115
+ expect(text).not.toContain('\u00bb');
116
+ expect(text).not.toMatch(/\b1\s+def f/);
117
+ expect(text).not.toMatch(/\b2\s+return/);
118
+ });
119
+ it('embeds the code face', async () => {
120
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider }));
121
+ const names = pdf.context.enumerateIndirectObjects()
122
+ .map(([, obj]) => (obj instanceof PDFDict ? obj.get(PDFName.of('BaseFont')) : undefined))
123
+ .filter((n) => n instanceof PDFName)
124
+ .map((n) => n.decodeText());
125
+ expect(names.some((n) => n.includes('JetBrainsMono-Regular'))).toBe(true);
126
+ });
127
+ it.skipIf(!hasVerapdf)('passes veraPDF (PDF/UA-1)', async () => {
128
+ const file = path.join(os.tmpdir(), `postext-code-ua-${process.pid}.pdf`);
129
+ fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
130
+ const out = spawnSync('verapdf', ['--flavour', 'ua1', '--format', 'text', file], { encoding: 'utf8' });
131
+ fs.unlinkSync(file);
132
+ expect(out.stdout).toMatch(/^PASS /m);
133
+ }, 120_000);
134
+ });
135
+ //# sourceMappingURL=codeBlocks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codeBlocks.test.js","sourceRoot":"","sources":["../../src/__tests__/codeBlocks.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dropCap.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropCap.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dropCap.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { execFileSync, spawnSync } from 'node:child_process';
6
+ import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef, PDFString, PDFHexString } from 'pdf-lib';
7
+ import fontkit from '@pdf-lib/fontkit';
8
+ import { buildDocument } from 'postext';
9
+ import { renderToPdf } from '../pdf-backend';
10
+ import { parseFontString } from '../fontString';
11
+ // #623: a drop cap in a body paragraph. The initial is drawn in its own face
12
+ // (embedded), as part of the paragraph's `P`; with the rest of its word it is
13
+ // a `Span` whose `/ActualText` is the word, and the text extraction reads the
14
+ // word once, whole, where the first line is.
15
+ const lora = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
16
+ const fraunces = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Fraunces-Bold.ttf', import.meta.url));
17
+ const fontProvider = async (family) => new Uint8Array(family === 'Fraunces' ? fraunces : lora);
18
+ const faces = { lora: fontkit.create(lora), fraunces: fontkit.create(fraunces) };
19
+ class StubCtx {
20
+ font = '';
21
+ measureText(s) {
22
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
23
+ const face = /Fraunces/.test(this.font) ? faces.fraunces : faces.lora;
24
+ const run = face.layout(s);
25
+ return {
26
+ width: (run.advanceWidth / face.unitsPerEm) * sizePx,
27
+ actualBoundingBoxAscent: (face.capHeight / face.unitsPerEm) * sizePx,
28
+ actualBoundingBoxDescent: 0,
29
+ };
30
+ }
31
+ }
32
+ globalThis.OffscreenCanvas = class {
33
+ getContext() {
34
+ return new StubCtx();
35
+ }
36
+ };
37
+ const pt = (value) => ({ value, unit: 'pt' });
38
+ const config = {
39
+ page: { width: pt(300), height: pt(480), margins: { top: pt(24), bottom: pt(24), left: pt(24), right: pt(24) } },
40
+ locale: 'en-us',
41
+ bodyText: { fontFamily: 'Lora', textAlign: 'justify' },
42
+ headings: { levels: [{ level: 1, fontFamily: 'Lora', dropCap: { lines: 3, fontFamily: 'Fraunces', fontWeight: 700 } }] },
43
+ layout: { layoutType: 'single' },
44
+ header: { elements: [] },
45
+ footer: { elements: [] },
46
+ };
47
+ const has = (cmd, arg) => spawnSync(cmd, [arg], { encoding: 'utf8' }).status === 0;
48
+ const hasPdftotext = (() => {
49
+ try {
50
+ execFileSync('pdftotext', ['-v'], { stdio: 'ignore' });
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ })();
57
+ const hasVerapdf = process.env.VERAPDF === '1' && has('verapdf', '--version');
58
+ /** The structure elements of the tree, depth first: type and ActualText. */
59
+ function structElems(pdf) {
60
+ const out = [];
61
+ const visit = (ref, parent) => {
62
+ const dict = ref instanceof PDFRef ? pdf.context.lookup(ref) : ref;
63
+ if (dict instanceof PDFArray) {
64
+ for (const kid of dict.asArray())
65
+ visit(kid, parent);
66
+ return;
67
+ }
68
+ if (!(dict instanceof PDFDict))
69
+ return;
70
+ const s = dict.get(PDFName.of('S'));
71
+ const type = s instanceof PDFName ? s.decodeText() : undefined;
72
+ if (type) {
73
+ const at = dict.get(PDFName.of('ActualText'));
74
+ out.push({ type, ...(at instanceof PDFString || at instanceof PDFHexString ? { actualText: at.decodeText() } : {}), ...(parent ? { parent } : {}) });
75
+ }
76
+ const k = dict.get(PDFName.of('K'));
77
+ for (const kid of k instanceof PDFArray ? k.asArray() : k ? [k] : [])
78
+ visit(kid, type ?? parent);
79
+ };
80
+ const root = pdf.catalog.lookup(PDFName.of('StructTreeRoot'), PDFDict);
81
+ visit(root.get(PDFName.of('K')));
82
+ return out;
83
+ }
84
+ const TEXT = 'Long before there were title pages there were readers of the scrolls who kept their place with a finger and a lamp, and the first letter of a chapter was painted large so the eye could find it again.';
85
+ describe('drop caps in the PDF (#623)', () => {
86
+ const doc = buildDocument({ markdown: `# The scrolls\n\n${TEXT}`, metadata: { title: 'Scrolls' } }, config);
87
+ const p = doc.blocks.find((b) => b.type === 'paragraph');
88
+ it('the paragraph opens with the initial in its own face', () => {
89
+ expect(p.dropCap).toMatchObject({ text: 'L', word: 'Long', wordRest: 3 });
90
+ expect(p.dropCap.fontString).toContain('Fraunces');
91
+ });
92
+ it('draws the initial in its face, embedded, inside the paragraph’s P, a Span reading the whole word', async () => {
93
+ const bytes = await renderToPdf(doc, { fontProvider });
94
+ const pdf = await PDFDocument.load(bytes);
95
+ const fonts = new Set();
96
+ pdf.context.enumerateIndirectObjects().forEach(([, obj]) => {
97
+ if (obj instanceof PDFDict && obj.get(PDFName.of('Type')) === PDFName.of('Font')) {
98
+ const base = obj.get(PDFName.of('BaseFont'));
99
+ if (base instanceof PDFName)
100
+ fonts.add(base.decodeText());
101
+ }
102
+ });
103
+ expect([...fonts].some((f) => /Fraunces/.test(f))).toBe(true);
104
+ const span = structElems(pdf).find((e) => e.type === 'Span' && e.actualText === 'Long');
105
+ expect(span).toBeDefined();
106
+ expect(span.parent).toBe('P');
107
+ });
108
+ it.skipIf(!hasPdftotext)('reads the first word whole in the extracted text', async () => {
109
+ const file = path.join(os.tmpdir(), `postext-dropcap-${process.pid}.pdf`);
110
+ fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
111
+ const text = execFileSync('pdftotext', ['-raw', file, '-'], { encoding: 'utf8' });
112
+ fs.unlinkSync(file);
113
+ expect(text).toMatch(/Long before there/);
114
+ expect(text).not.toMatch(/\bL\s+ong\b/);
115
+ expect(text.match(/Long/g)).toHaveLength(1);
116
+ });
117
+ it.skipIf(!hasVerapdf)('passes veraPDF (PDF/UA-1)', async () => {
118
+ const file = path.join(os.tmpdir(), `postext-dropcap-ua-${process.pid}.pdf`);
119
+ fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
120
+ const out = spawnSync('verapdf', ['--flavour', 'ua1', '--format', 'text', file], { encoding: 'utf8' });
121
+ fs.unlinkSync(file);
122
+ expect(out.stdout).toMatch(/^PASS /m);
123
+ }, 120_000);
124
+ });
125
+ //# sourceMappingURL=dropCap.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropCap.test.js","sourceRoot":"","sources":["../../src/__tests__/dropCap.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hardBreaks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardBreaks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hardBreaks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { execFileSync, spawnSync } from 'node:child_process';
6
+ import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef } from 'pdf-lib';
7
+ import fontkit from '@pdf-lib/fontkit';
8
+ import { buildDocument } from 'postext';
9
+ import { renderToPdf } from '../pdf-backend';
10
+ import { parseFontString } from '../fontString';
11
+ // #620: a forced line break inside a paragraph (a backslash ending a line
12
+ // of the source). The paragraph stays one `P` of the structure tree, the
13
+ // line before the break is not stretched to the measure, and the extracted
14
+ // text breaks there.
15
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
16
+ const fontProvider = async () => new Uint8Array(fontBytes);
17
+ const face = fontkit.create(fontBytes);
18
+ class StubCtx {
19
+ font = '';
20
+ measureText(s) {
21
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
22
+ const run = face.layout(s);
23
+ return { width: (run.advanceWidth / face.unitsPerEm) * sizePx };
24
+ }
25
+ }
26
+ globalThis.OffscreenCanvas = class {
27
+ getContext() {
28
+ return new StubCtx();
29
+ }
30
+ };
31
+ const pt = (value) => ({ value, unit: 'pt' });
32
+ const config = {
33
+ page: { width: pt(300), height: pt(480), margins: { top: pt(24), bottom: pt(24), left: pt(24), right: pt(24) } },
34
+ locale: 'en-us',
35
+ bodyText: { textAlign: 'justify' },
36
+ layout: { layoutType: 'single' },
37
+ header: { elements: [] },
38
+ footer: { elements: [] },
39
+ };
40
+ const has = (cmd, arg) => spawnSync(cmd, [arg], { encoding: 'utf8' }).status === 0;
41
+ const hasPdftotext = (() => {
42
+ try {
43
+ execFileSync('pdftotext', ['-v'], { stdio: 'ignore' });
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ })();
50
+ const hasVerapdf = process.env.VERAPDF === '1' && has('verapdf', '--version');
51
+ /** The structure types of the tree, depth first. */
52
+ function structTypes(pdf) {
53
+ const out = [];
54
+ const visit = (ref) => {
55
+ const dict = ref instanceof PDFRef ? pdf.context.lookup(ref) : ref;
56
+ if (dict instanceof PDFArray) {
57
+ for (const kid of dict.asArray())
58
+ visit(kid);
59
+ return;
60
+ }
61
+ if (!(dict instanceof PDFDict))
62
+ return;
63
+ const s = dict.get(PDFName.of('S'));
64
+ if (s instanceof PDFName)
65
+ out.push(s.decodeText());
66
+ const k = dict.get(PDFName.of('K'));
67
+ for (const kid of k instanceof PDFArray ? k.asArray() : k ? [k] : [])
68
+ visit(kid);
69
+ };
70
+ const root = pdf.catalog.lookup(PDFName.of('StructTreeRoot'), PDFDict);
71
+ visit(root.get(PDFName.of('K')));
72
+ return out;
73
+ }
74
+ const TEXT = [
75
+ 'To the keeper of the lighthouse,\\',
76
+ 'at the end of the northern pier, who climbs the stairs every evening and lights the lamp before the boats come home.',
77
+ '',
78
+ 'After the letter.',
79
+ ].join('\n');
80
+ describe('forced line breaks in the PDF (#620)', () => {
81
+ const doc = buildDocument({ markdown: TEXT, metadata: { title: 'A letter' } }, config);
82
+ const paragraph = doc.blocks.find((b) => b.type === 'paragraph');
83
+ const lines = paragraph.lines;
84
+ it('ends the line at the break, set at its natural width, in one paragraph', async () => {
85
+ expect(lines[0].text.trim()).toBe('To the keeper of the lighthouse,');
86
+ expect(lines[0].hardBreak).toBe(true);
87
+ expect(lines[0].justifiedSpaceRatio).toBeUndefined();
88
+ expect(lines.length).toBeGreaterThan(2);
89
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider }));
90
+ expect(structTypes(pdf).filter((t) => t === 'P')).toHaveLength(2);
91
+ });
92
+ it.skipIf(!hasPdftotext)('breaks the extracted text there, its words spaced as written', async () => {
93
+ const file = path.join(os.tmpdir(), `postext-hardbreak-${process.pid}.pdf`);
94
+ fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
95
+ const text = execFileSync('pdftotext', ['-raw', file, '-'], { encoding: 'utf8' });
96
+ fs.unlinkSync(file);
97
+ expect(text).toMatch(/To the keeper of the lighthouse,\s*\n\s*at the end/);
98
+ });
99
+ it.skipIf(!hasVerapdf)('passes veraPDF (PDF/UA-1)', async () => {
100
+ const file = path.join(os.tmpdir(), `postext-hardbreak-ua-${process.pid}.pdf`);
101
+ fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
102
+ const out = spawnSync('verapdf', ['--flavour', 'ua1', '--format', 'text', file], { encoding: 'utf8' });
103
+ fs.unlinkSync(file);
104
+ expect(out.stdout).toMatch(/^PASS /m);
105
+ }, 120_000);
106
+ });
107
+ //# sourceMappingURL=hardBreaks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardBreaks.test.js","sourceRoot":"","sources":["../../src/__tests__/hardBreaks.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lineNumbers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineNumbers.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/lineNumbers.test.ts"],"names":[],"mappings":""}