postext-pdf 1.22.3 → 1.23.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.
- package/dist/__tests__/codeBlocks.test.d.ts +2 -0
- package/dist/__tests__/codeBlocks.test.d.ts.map +1 -0
- package/dist/__tests__/codeBlocks.test.js +135 -0
- package/dist/__tests__/codeBlocks.test.js.map +1 -0
- package/dist/__tests__/dropCap.test.d.ts +2 -0
- package/dist/__tests__/dropCap.test.d.ts.map +1 -0
- package/dist/__tests__/dropCap.test.js +125 -0
- package/dist/__tests__/dropCap.test.js.map +1 -0
- package/dist/__tests__/hardBreaks.test.d.ts +2 -0
- package/dist/__tests__/hardBreaks.test.d.ts.map +1 -0
- package/dist/__tests__/hardBreaks.test.js +107 -0
- package/dist/__tests__/hardBreaks.test.js.map +1 -0
- package/dist/__tests__/lineNumbers.test.d.ts +2 -0
- package/dist/__tests__/lineNumbers.test.d.ts.map +1 -0
- package/dist/__tests__/lineNumbers.test.js +106 -0
- package/dist/__tests__/lineNumbers.test.js.map +1 -0
- package/dist/__tests__/tabStops.test.d.ts +2 -0
- package/dist/__tests__/tabStops.test.d.ts.map +1 -0
- package/dist/__tests__/tabStops.test.js +105 -0
- package/dist/__tests__/tabStops.test.js.map +1 -0
- package/dist/__tests__/verseLines.test.d.ts +2 -0
- package/dist/__tests__/verseLines.test.d.ts.map +1 -0
- package/dist/__tests__/verseLines.test.js +112 -0
- package/dist/__tests__/verseLines.test.js.map +1 -0
- package/dist/pdf-backend/blockRender.d.ts.map +1 -1
- package/dist/pdf-backend/blockRender.js +79 -18
- package/dist/pdf-backend/blockRender.js.map +1 -1
- package/dist/pdf-backend/fontHelpers.d.ts.map +1 -1
- package/dist/pdf-backend/fontHelpers.js +7 -1
- package/dist/pdf-backend/fontHelpers.js.map +1 -1
- package/dist/pdf-backend/headerFooter.d.ts +7 -2
- package/dist/pdf-backend/headerFooter.d.ts.map +1 -1
- package/dist/pdf-backend/headerFooter.js +11 -6
- package/dist/pdf-backend/headerFooter.js.map +1 -1
- package/dist/pdf-backend/index.d.ts.map +1 -1
- package/dist/pdf-backend/index.js +6 -0
- package/dist/pdf-backend/index.js.map +1 -1
- package/dist/pdf-backend/structureFlow.d.ts.map +1 -1
- package/dist/pdf-backend/structureFlow.js +6 -0
- package/dist/pdf-backend/structureFlow.js.map +1 -1
- package/dist/pdf-backend/tagging.d.ts +1 -1
- package/dist/pdf-backend/tagging.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"lineNumbers.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/lineNumbers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
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, PDFDocument, PDFRawStream, decodePDFRawStream } 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
|
+
// #621: line numbers in the margin. They are painted beside their lines,
|
|
12
|
+
// marked as layout artifacts in a tagged PDF, and set under an empty
|
|
13
|
+
// `/ActualText`, so extracted and copied text runs from line to line
|
|
14
|
+
// without them.
|
|
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(48), right: pt(48) } },
|
|
34
|
+
locale: 'en-us',
|
|
35
|
+
layout: { layoutType: 'single' },
|
|
36
|
+
header: { elements: [] },
|
|
37
|
+
footer: { elements: [] },
|
|
38
|
+
lineNumbers: { enabled: true, interval: 5 },
|
|
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
|
+
function pageContent(pdf) {
|
|
52
|
+
const contents = pdf.getPage(0).node.Contents();
|
|
53
|
+
const refs = contents instanceof PDFArray ? contents.asArray() : [contents];
|
|
54
|
+
return refs
|
|
55
|
+
.map((ref) => {
|
|
56
|
+
const s = pdf.context.lookup(ref);
|
|
57
|
+
return s instanceof PDFRawStream ? new TextDecoder('latin1').decode(decodePDFRawStream(s).decode()) : '';
|
|
58
|
+
})
|
|
59
|
+
.join('\n');
|
|
60
|
+
}
|
|
61
|
+
const POEM = [
|
|
62
|
+
':::verse{align=start}',
|
|
63
|
+
'Yet once more, O ye laurels, and once more',
|
|
64
|
+
'Ye myrtles brown, with ivy never sere,',
|
|
65
|
+
'I come to pluck your berries harsh and crude,',
|
|
66
|
+
'And with forced fingers rude',
|
|
67
|
+
'Shatter your leaves before the mellowing year.',
|
|
68
|
+
'Bitter constraint and sad occasion dear',
|
|
69
|
+
'Compels me to disturb your season due;',
|
|
70
|
+
'For Lycidas is dead, dead ere his prime,',
|
|
71
|
+
'Young Lycidas, and hath not left his peer.',
|
|
72
|
+
'Who would not sing for Lycidas? he knew',
|
|
73
|
+
':::',
|
|
74
|
+
].join('\n');
|
|
75
|
+
describe('line numbers in the PDF (#621)', () => {
|
|
76
|
+
const doc = buildDocument({ markdown: POEM, metadata: { title: 'Lycidas' } }, config);
|
|
77
|
+
it('paints the numbers as layout artifacts, each under an empty /ActualText', async () => {
|
|
78
|
+
expect(doc.pages[0].lineNumberMarks.map((m) => m.label)).toEqual(['5', '10']);
|
|
79
|
+
const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider }));
|
|
80
|
+
const content = pageContent(pdf);
|
|
81
|
+
// Two numbers: two empty spans, inside a layout artifact (no marked
|
|
82
|
+
// content of the structure between them).
|
|
83
|
+
expect(content.match(/\/Span\s*<<\s*\/ActualText\s*<FEFF>\s*>>\s*BDC/g) ?? []).toHaveLength(2);
|
|
84
|
+
const before = content.slice(0, content.indexOf('<FEFF>'));
|
|
85
|
+
expect(before.lastIndexOf('/Artifact')).toBeGreaterThan(before.lastIndexOf('/MCID'));
|
|
86
|
+
expect(before.slice(before.lastIndexOf('/Artifact'))).toMatch(/^\/Artifact\s*<<[^>]*\/Layout/);
|
|
87
|
+
const tail = content.slice(content.indexOf('<FEFF>'));
|
|
88
|
+
expect(tail.slice(0, tail.lastIndexOf('<FEFF>'))).not.toContain('/MCID');
|
|
89
|
+
});
|
|
90
|
+
it.skipIf(!hasPdftotext)('leaves them out of the extracted text', async () => {
|
|
91
|
+
const file = path.join(os.tmpdir(), `postext-linenumbers-${process.pid}.pdf`);
|
|
92
|
+
fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
|
|
93
|
+
const text = execFileSync('pdftotext', ['-raw', file, '-'], { encoding: 'utf8' });
|
|
94
|
+
fs.unlinkSync(file);
|
|
95
|
+
expect(text).toContain('Shatter your leaves before the mellowing year.');
|
|
96
|
+
expect(text).not.toMatch(/\b(5|10)\b/);
|
|
97
|
+
});
|
|
98
|
+
it.skipIf(!hasVerapdf)('passes veraPDF (PDF/UA-1)', async () => {
|
|
99
|
+
const file = path.join(os.tmpdir(), `postext-linenumbers-ua-${process.pid}.pdf`);
|
|
100
|
+
fs.writeFileSync(file, await renderToPdf(doc, { fontProvider }));
|
|
101
|
+
const out = spawnSync('verapdf', ['--flavour', 'ua1', '--format', 'text', file], { encoding: 'utf8' });
|
|
102
|
+
fs.unlinkSync(file);
|
|
103
|
+
expect(out.stdout).toMatch(/^PASS /m);
|
|
104
|
+
}, 120_000);
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=lineNumbers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineNumbers.test.js","sourceRoot":"","sources":["../../src/__tests__/lineNumbers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabStops.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tabStops.test.ts"],"names":[],"mappings":""}
|