postext-pdf 1.46.4 → 1.48.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 (56) hide show
  1. package/dist/__tests__/boxShape.test.d.ts +2 -0
  2. package/dist/__tests__/boxShape.test.d.ts.map +1 -0
  3. package/dist/__tests__/boxShape.test.js +52 -0
  4. package/dist/__tests__/boxShape.test.js.map +1 -0
  5. package/dist/__tests__/connectorOver.test.d.ts +2 -0
  6. package/dist/__tests__/connectorOver.test.d.ts.map +1 -0
  7. package/dist/__tests__/connectorOver.test.js +85 -0
  8. package/dist/__tests__/connectorOver.test.js.map +1 -0
  9. package/dist/__tests__/figureLeader.test.d.ts +2 -0
  10. package/dist/__tests__/figureLeader.test.d.ts.map +1 -0
  11. package/dist/__tests__/figureLeader.test.js +86 -0
  12. package/dist/__tests__/figureLeader.test.js.map +1 -0
  13. package/dist/__tests__/figureLegend.test.d.ts +2 -0
  14. package/dist/__tests__/figureLegend.test.d.ts.map +1 -0
  15. package/dist/__tests__/figureLegend.test.js +102 -0
  16. package/dist/__tests__/figureLegend.test.js.map +1 -0
  17. package/dist/__tests__/labelGlyph.test.d.ts +2 -0
  18. package/dist/__tests__/labelGlyph.test.d.ts.map +1 -0
  19. package/dist/__tests__/labelGlyph.test.js +94 -0
  20. package/dist/__tests__/labelGlyph.test.js.map +1 -0
  21. package/dist/__tests__/pathBlock.test.d.ts +2 -0
  22. package/dist/__tests__/pathBlock.test.d.ts.map +1 -0
  23. package/dist/__tests__/pathBlock.test.js +83 -0
  24. package/dist/__tests__/pathBlock.test.js.map +1 -0
  25. package/dist/__tests__/pathElement.test.d.ts +2 -0
  26. package/dist/__tests__/pathElement.test.d.ts.map +1 -0
  27. package/dist/__tests__/pathElement.test.js +84 -0
  28. package/dist/__tests__/pathElement.test.js.map +1 -0
  29. package/dist/pdf-backend/blockRender.d.ts.map +1 -1
  30. package/dist/pdf-backend/blockRender.js +3 -3
  31. package/dist/pdf-backend/blockRender.js.map +1 -1
  32. package/dist/pdf-backend/fontHelpers.d.ts.map +1 -1
  33. package/dist/pdf-backend/fontHelpers.js +4 -0
  34. package/dist/pdf-backend/fontHelpers.js.map +1 -1
  35. package/dist/pdf-backend/headerFooter.d.ts +15 -3
  36. package/dist/pdf-backend/headerFooter.d.ts.map +1 -1
  37. package/dist/pdf-backend/headerFooter.js +12 -2
  38. package/dist/pdf-backend/headerFooter.js.map +1 -1
  39. package/dist/pdf-backend/index.d.ts.map +1 -1
  40. package/dist/pdf-backend/index.js +20 -5
  41. package/dist/pdf-backend/index.js.map +1 -1
  42. package/dist/pdf-backend/inlineIcon.d.ts +18 -0
  43. package/dist/pdf-backend/inlineIcon.d.ts.map +1 -0
  44. package/dist/pdf-backend/inlineIcon.js +71 -0
  45. package/dist/pdf-backend/inlineIcon.js.map +1 -0
  46. package/dist/pdf-backend/pathBlock.d.ts +5 -0
  47. package/dist/pdf-backend/pathBlock.d.ts.map +1 -0
  48. package/dist/pdf-backend/pathBlock.js +110 -0
  49. package/dist/pdf-backend/pathBlock.js.map +1 -0
  50. package/dist/pdf-backend/primitives.d.ts +4 -0
  51. package/dist/pdf-backend/primitives.d.ts.map +1 -1
  52. package/dist/pdf-backend/primitives.js.map +1 -1
  53. package/dist/pdf-backend/renderResourceBlock.d.ts.map +1 -1
  54. package/dist/pdf-backend/renderResourceBlock.js +35 -11
  55. package/dist/pdf-backend/renderResourceBlock.js.map +1 -1
  56. package/package.json +3 -3
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=boxShape.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boxShape.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/boxShape.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PDFDocument } from 'pdf-lib';
3
+ import { boxOutline, shapePathBlocks } from 'postext';
4
+ import { FontCache } from '../fontCache';
5
+ import { renderHeaderFooterSlot } from '../pdf-backend/headerFooter';
6
+ import { makeScale } from '../pdf-backend/primitives';
7
+ import { PageTagger } from '../pdf-backend/tagging';
8
+ // #1059: box shapes in PDF — corners of their own, a border per side and a
9
+ // card joined to its tab, as vector paths.
10
+ function pageContent(page) {
11
+ const stream = page.getContentStream();
12
+ return new TextDecoder('latin1').decode(stream.getUnencodedContents());
13
+ }
14
+ async function paint(blocks, tagged = false) {
15
+ const pdfDoc = await PDFDocument.create();
16
+ const page = pdfDoc.addPage([400, 400]);
17
+ const ctx = { page, pageHeightPt: 400, scale: makeScale(72), colorSpace: 'rgb', ...(tagged ? { tags: new PageTagger(page, 0) } : {}) };
18
+ const slot = { bbox: { x: 0, y: 0, width: 400, height: 400 }, blocks };
19
+ renderHeaderFooterSlot(ctx, slot, new FontCache(pdfDoc, async () => new Uint8Array()), undefined, tagged ? { artifact: { type: 'Layout' } } : undefined);
20
+ ctx.tags?.close();
21
+ return pageContent(page);
22
+ }
23
+ const box = (style) => ({ kind: 'box', bbox: { x: 10, y: 20, width: 100, height: 50 }, box: style });
24
+ describe('box shapes in the PDF backend (#1059)', () => {
25
+ it('fills a box with a border on its top only: the fill, then the top piece', async () => {
26
+ const content = await paint([box({
27
+ backgroundColor: '#ffffff', borderColor: '#ff0000', borderWidthPx: 2, borderRadiusPx: 0,
28
+ borderSides: { top: { color: '#ff0000', widthPx: 2 }, right: null, bottom: null, left: null },
29
+ })]);
30
+ // The fill: the box's rectangle (y up from 400).
31
+ expect(content).toMatch(/1 1 1 rg\n10 380 m\n110 380 l\n110 330 l\n10 330 l\n10 380 l\nh\nf\n/);
32
+ // The border: the top edge, then back along the inner edge 2 pt down.
33
+ expect(content).toMatch(/1 0 0 rg\n10 380 m\n110 380 l\n110 378 l\n10 378 l\nh\nf\n/);
34
+ });
35
+ it('rounds only the corners a style names, with cubic curves', async () => {
36
+ const content = await paint([box({ backgroundColor: '#00ff00', borderWidthPx: 0, borderRadiusPx: 8, borderRadiiPx: [8, 0, 8, 0] })]);
37
+ expect(content.match(/ c\n/g)).toHaveLength(2);
38
+ // A square corner is a plain point of the outline: the top right.
39
+ expect(content).toContain('110 380 l\n');
40
+ });
41
+ it('paints a card joined to its tab, inside a Layout artifact', async () => {
42
+ const outline = boxOutline({ x: 10, y: 30, width: 100, height: 50 }, [6, 6, 6, 6], { edge: 'top', x0: 20, x1: 60, height: 12, radius: 6, fillet: 6 });
43
+ const content = await paint(shapePathBlocks(outline, '#fafafa', {
44
+ top: { color: '#336699', widthPx: 1 }, right: { color: '#336699', widthPx: 1 }, bottom: { color: '#336699', widthPx: 1 }, left: { color: '#336699', widthPx: 1 },
45
+ }), true);
46
+ expect(content).toContain('/Artifact <<');
47
+ expect(content).toMatch(/h\nf\*\n/);
48
+ // The tab's top stands 12 pt over the box: y = 400 - 18.
49
+ expect(content).toMatch(/ 382 [lc]\n/);
50
+ });
51
+ });
52
+ //# sourceMappingURL=boxShape.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boxShape.test.js","sourceRoot":"","sources":["../../src/__tests__/boxShape.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=connectorOver.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectorOver.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/connectorOver.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } 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
+ // #1076: a connector that ends at an anchor of a picture is painted after
9
+ // every frame, so the line shows over the picture's frame at its default
10
+ // layer; left to right and through the mirrored flow of a right-to-left
11
+ // page; a Layout artifact in a tagged PDF. With `over: false` it is
12
+ // painted with the decoration, before the picture.
13
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
14
+ const face = fontkit.create(fontBytes);
15
+ const fontProvider = async () => new Uint8Array(fontBytes);
16
+ class StubCtx {
17
+ font = '';
18
+ measureText(s) {
19
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
20
+ return { width: (face.layout(s).advanceWidth / face.unitsPerEm) * sizePx };
21
+ }
22
+ }
23
+ globalThis.OffscreenCanvas = class {
24
+ getContext() {
25
+ return new StubCtx();
26
+ }
27
+ };
28
+ const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'));
29
+ const mm = (value) => ({ value, unit: 'mm' });
30
+ const PLAN = {
31
+ id: 'plan', typeId: 'figure', kind: 'bitmap', altText: 'A plan of a garden', caption: '', createdAt: 0, updatedAt: 0,
32
+ bitmap: { fileId: 'plan.png', format: 'png', width: 400, height: 300 },
33
+ anchors: [{ id: 'pond', x: 0.4, y: 0.5 }],
34
+ };
35
+ const config = (direction, over) => ({
36
+ locale: 'en-us',
37
+ direction,
38
+ header: { elements: [] },
39
+ footer: { elements: [] },
40
+ pageTemplates: [{
41
+ id: 'plan',
42
+ frames: [
43
+ { name: 'note', width: mm(40), height: mm(20), placement: { anchor: { to: 'container', edge: 'top-left' } } },
44
+ { name: 'picture', width: mm(90), height: mm(70), placement: { anchor: { to: 'container', edge: 'top-right' } } },
45
+ ],
46
+ decoration: { elements: [{
47
+ kind: 'connector', id: 'pointer', routing: 'straight', from: { to: '#note', side: 'end' }, to: { to: '#picture', anchor: 'pond' },
48
+ endCap: 'dot', color: { hex: '#c0392b', model: 'hex' }, ...(over === undefined ? {} : { over }),
49
+ }] },
50
+ }],
51
+ });
52
+ const markdown = '::::page{template="plan"}\n:::frame{slot="note"}\nThe pond\n:::\n:::frame{slot="picture"}\n:::figure{resource="plan"}\n:::\n:::\n::::\n';
53
+ async function pageOps(direction, over, accessible = false) {
54
+ const doc = buildDocument({ markdown, resources: [PLAN] }, config(direction, over));
55
+ const pdf = await PDFDocument.load(await renderToPdf(doc, { fontProvider, accessible, resourceBytes: (id) => (id === 'plan.png' ? PNG : undefined) }));
56
+ const contents = pdf.getPage(0).node.Contents();
57
+ const streams = contents instanceof PDFArray ? contents.asArray().map((r) => pdf.context.lookup(r)) : [contents];
58
+ return streams.map((s) => (s instanceof PDFRawStream ? Buffer.from(decodePDFRawStream(s).decode()).toString('latin1') : '')).join('\n').split(/\s+/);
59
+ }
60
+ /** The first stroking colour op of the pointer's red (#c0392b). */
61
+ const red = (ops) => ops.findIndex((op, i) => op === 'RG' && Math.abs(Number(ops[i - 3]) - 0xc0 / 255) < 0.002 && Math.abs(Number(ops[i - 2]) - 0x39 / 255) < 0.002);
62
+ /** The picture's paint (`/ImN Do`). */
63
+ const image = (ops) => ops.findIndex((op, i) => op === 'Do' && /^\/Im|^\/I\d|^\/X/.test(ops[i - 1] ?? ''));
64
+ describe('a connector into a picture in the PDF (#1076)', () => {
65
+ for (const direction of ['ltr', 'rtl']) {
66
+ it(`${direction}: stroked after the picture by default, before it with over: false`, async () => {
67
+ const ops = await pageOps(direction);
68
+ expect(image(ops)).toBeGreaterThan(0);
69
+ expect(red(ops)).toBeGreaterThan(image(ops));
70
+ const under = await pageOps(direction, false);
71
+ expect(red(under)).toBeGreaterThan(-1);
72
+ expect(red(under)).toBeLessThan(image(under));
73
+ }, 60_000);
74
+ }
75
+ it('is a Layout artifact in a tagged PDF', async () => {
76
+ const ops = await pageOps('ltr', undefined, true);
77
+ const at = red(ops);
78
+ expect(at).toBeGreaterThan(image(ops));
79
+ // An artifact opened after the picture and still open at the stroke.
80
+ const open = ops.lastIndexOf('/Artifact', at);
81
+ expect(open).toBeGreaterThan(image(ops));
82
+ expect(ops.slice(open, at)).not.toContain('EMC');
83
+ }, 60_000);
84
+ });
85
+ //# sourceMappingURL=connectorOver.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectorOver.test.js","sourceRoot":"","sources":["../../src/__tests__/connectorOver.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=figureLeader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figureLeader.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/figureLeader.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import zlib from 'node:zlib';
4
+ import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRawStream, PDFRef } from 'pdf-lib';
5
+ import fontkit from '@pdf-lib/fontkit';
6
+ import { buildDocument, figureLabelLeaderPath } from 'postext';
7
+ import { renderToPdf } from '../pdf-backend';
8
+ import { parseFontString } from '../fontString';
9
+ // #1057: a label set away from its point draws its leader in the PDF as
10
+ // vector lines with a dot on the point, under the label, a layout artifact
11
+ // in the tagged PDF (the label's words are still read once).
12
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
13
+ const face = fontkit.create(fontBytes);
14
+ class StubCtx {
15
+ font = '';
16
+ measureText(s) {
17
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
18
+ return { width: (face.layout(s).advanceWidth / face.unitsPerEm) * sizePx };
19
+ }
20
+ }
21
+ globalThis.OffscreenCanvas = class {
22
+ getContext() {
23
+ return new StubCtx();
24
+ }
25
+ };
26
+ const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'));
27
+ const hex = (h) => ({ hex: h, model: 'hex' });
28
+ const pt = (value) => ({ value, unit: 'pt' });
29
+ const MAP = {
30
+ id: 'mapa', typeId: 'figure', kind: 'bitmap', altText: 'A map', createdAt: 0, updatedAt: 0,
31
+ bitmap: { fileId: 'mapa.png', format: 'png', width: 400, height: 300 },
32
+ anchors: [{ id: 'lake', x: 0.3, y: 0.7, labelAt: { x: 0.7, y: 0.25 }, style: 'name', leader: true }],
33
+ };
34
+ const config = {
35
+ labelStyles: [{ id: 'name', fontSize: pt(9), leader: { color: hex('#7a3b10'), width: pt(1) } }],
36
+ };
37
+ let content;
38
+ let tree;
39
+ let block;
40
+ function readElem(pdf, ref) {
41
+ const dict = pdf.context.lookup(ref, PDFDict);
42
+ const elem = { type: dict.get(PDFName.of('S')).decodeText(), kids: [] };
43
+ const k = dict.get(PDFName.of('K'));
44
+ for (const kid of k instanceof PDFArray ? k.asArray() : k ? [k] : [])
45
+ if (kid instanceof PDFRef)
46
+ elem.kids.push(readElem(pdf, kid));
47
+ return elem;
48
+ }
49
+ const shape = (e) => (e.kids.length ? `${e.type}(${e.kids.map(shape).join(' ')})` : e.type);
50
+ beforeAll(async () => {
51
+ const doc = buildDocument({ markdown: ':::figure{resource="mapa"}\n::label[Lake Orla]{at="lake"}\n:::\n', resources: [MAP] }, config);
52
+ block = [...doc.blocks, ...doc.pages.flatMap((p) => [...p.columns.flatMap((c) => c.blocks), ...(p.floats ?? [])])].find((b) => b.resourceBlock?.labels);
53
+ const bytes = await renderToPdf(doc, {
54
+ fontProvider: async () => new Uint8Array(fontBytes),
55
+ accessible: true,
56
+ resourceBytes: (id) => (id === 'mapa.png' ? PNG : undefined),
57
+ });
58
+ const pdf = await PDFDocument.load(bytes);
59
+ const root = pdf.catalog.lookup(PDFName.of('StructTreeRoot'), PDFDict);
60
+ tree = shape(readElem(pdf, root.get(PDFName.of('K')).get(0)));
61
+ const page = pdf.getPages()[0];
62
+ const contents = page.node.Contents();
63
+ const streams = contents instanceof PDFArray ? contents.asArray().map((r) => pdf.context.lookup(r)) : [contents];
64
+ content = streams.map((st) => (st instanceof PDFRawStream ? zlib.inflateSync(Buffer.from(st.contents)).toString('latin1') : '')).join('\n');
65
+ }, 60_000);
66
+ describe('a label leader in the PDF (#1057)', () => {
67
+ it('strokes the line in its colour and width and fills a dot on the point, as an artifact before the label', () => {
68
+ const rb = block.resourceBlock;
69
+ const path = figureLabelLeaderPath(rb.labels[0].layout, block.bbox.x + rb.bodyRect.x, block.bbox.y + rb.bodyRect.y);
70
+ expect(path).toBeDefined();
71
+ // The leader's colour (#7a3b10) as a stroking colour, a 1 pt width.
72
+ expect(content).toMatch(/0\.478\d* 0\.231\d* 0\.0627?\d* RG/);
73
+ expect(content).toMatch(/ RG\n(0\.99\d*|1) w\n[\d.]+ [\d.]+ m\n[\d.]+ [\d.]+ l\nS\n/);
74
+ const stroke = content.search(/ RG\n/);
75
+ // A dot is four curves, filled.
76
+ expect(content.slice(stroke)).toMatch(/( c\n[^]*){4}h?\n?f/);
77
+ // Marked as a layout artifact, before the label's words.
78
+ expect(content.slice(0, stroke)).toMatch(/\/Artifact <<\n\/Type \/Layout\n>> BDC\nq\n[^\n]*$/);
79
+ expect(content.indexOf('Tj', stroke) > stroke || content.indexOf('TJ', stroke) > stroke).toBe(true);
80
+ });
81
+ it('leaves the structure as it was: the figure and its label read once', () => {
82
+ expect(tree).toContain('Figure');
83
+ expect((tree.match(/\bP\b/g) ?? []).length).toBeGreaterThanOrEqual(1);
84
+ });
85
+ });
86
+ //# sourceMappingURL=figureLeader.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figureLeader.test.js","sourceRoot":"","sources":["../../src/__tests__/figureLeader.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=figureLegend.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figureLegend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/figureLegend.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import zlib from 'node:zlib';
4
+ import { PDFArray, PDFDict, PDFDocument, PDFHexString, PDFName, PDFRawStream, PDFRef, PDFString } from 'pdf-lib';
5
+ import fontkit from '@pdf-lib/fontkit';
6
+ import { buildDocument } from 'postext';
7
+ import { renderToPdf } from '../pdf-backend';
8
+ import { parseFontString } from '../fontString';
9
+ // #1058: a map's key in the PDF — its symbols drawn (a glyph, a dashed line
10
+ // sample, a picture), its plate outlined, and its rows read as a list in
11
+ // the tagged PDF, each symbol read as its name in brackets.
12
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
13
+ const face = fontkit.create(fontBytes);
14
+ class StubCtx {
15
+ font = '';
16
+ measureText(s) {
17
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
18
+ return { width: (face.layout(s).advanceWidth / face.unitsPerEm) * sizePx };
19
+ }
20
+ }
21
+ globalThis.OffscreenCanvas = class {
22
+ getContext() {
23
+ return new StubCtx();
24
+ }
25
+ };
26
+ // A 1×1 PNG: the map, and the picture of the town symbol.
27
+ const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64'));
28
+ const hex = (h) => ({ hex: h, model: 'hex' });
29
+ const pt = (value) => ({ value, unit: 'pt' });
30
+ const MAP = {
31
+ id: 'mapa', typeId: 'figure', kind: 'bitmap', altText: 'A political map', createdAt: 0, updatedAt: 0,
32
+ bitmap: { fileId: 'mapa.png', format: 'png', width: 400, height: 300 },
33
+ anchors: [{ id: 'key', x: 0.05, y: 0.05, align: 'start', verticalAlign: 'top', style: 'key' }],
34
+ };
35
+ const TOWN = {
36
+ id: 'town', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
37
+ bitmap: { fileId: 'town.png', format: 'png', width: 10, height: 10 },
38
+ };
39
+ const config = {
40
+ labelStyles: [{ id: 'key', kind: 'legend', fontSize: pt(9), background: hex('#ffffff'), border: { color: hex('#333333'), width: pt(1) }, radius: pt(2), padding: pt(3) }],
41
+ };
42
+ function readElem(pdf, ref) {
43
+ const dict = pdf.context.lookup(ref, PDFDict);
44
+ const text = (key) => {
45
+ const v = dict.get(PDFName.of(key));
46
+ return v instanceof PDFHexString || v instanceof PDFString ? v.decodeText() : undefined;
47
+ };
48
+ const alt = text('Alt');
49
+ const actualText = text('ActualText');
50
+ const elem = { type: dict.get(PDFName.of('S')).decodeText(), ...(alt !== undefined ? { alt } : {}), ...(actualText !== undefined ? { actualText } : {}), kids: [] };
51
+ const k = dict.get(PDFName.of('K'));
52
+ for (const kid of k instanceof PDFArray ? k.asArray() : k ? [k] : [])
53
+ if (kid instanceof PDFRef)
54
+ elem.kids.push(readElem(pdf, kid));
55
+ return elem;
56
+ }
57
+ const shape = (e) => (e.type === 'Figure' ? `Figure[${e.alt}]` : e.type === 'Span' ? `Span[${e.actualText ?? ''}]` : `${e.type}(${e.kids.map(shape).join(' ')})`);
58
+ let tree;
59
+ let content;
60
+ let images;
61
+ beforeAll(async () => {
62
+ const markdown = [
63
+ ':::figure{resource="mapa"}',
64
+ ':::label{at="key"}',
65
+ ':icon[Capital]{glyph="o"} Capital',
66
+ ':icon[Border]{line="dashed" color="#8a3b52"} Border',
67
+ ':icon[Town]{resource="town"} Town',
68
+ ':icon[]{line="solid"} Road',
69
+ ':::',
70
+ ':::',
71
+ '',
72
+ ].join('\n');
73
+ const doc = buildDocument({ markdown, resources: [MAP, TOWN] }, config);
74
+ const bytes = await renderToPdf(doc, {
75
+ fontProvider: async () => new Uint8Array(fontBytes),
76
+ accessible: true,
77
+ resourceBytes: (id) => (id === 'mapa.png' || id === 'town.png' ? PNG : undefined),
78
+ });
79
+ const pdf = await PDFDocument.load(bytes);
80
+ const root = pdf.catalog.lookup(PDFName.of('StructTreeRoot'), PDFDict);
81
+ const top = readElem(pdf, root.get(PDFName.of('K')).get(0));
82
+ tree = top.kids.map(shape).join(' ');
83
+ const page = pdf.getPages()[0];
84
+ const contents = page.node.Contents();
85
+ const streams = contents instanceof PDFArray ? contents.asArray().map((r) => pdf.context.lookup(r)) : [contents];
86
+ content = streams.map((st) => (st instanceof PDFRawStream ? zlib.inflateSync(Buffer.from(st.contents)).toString('latin1') : '')).join('\n');
87
+ images = (content.match(/ Do\b/g) ?? []).length;
88
+ }, 60_000);
89
+ describe('a legend in the PDF (#1058)', () => {
90
+ it('reads the key as a list after the figure, each symbol as its name in brackets', () => {
91
+ expect(tree).toContain('Figure[A political map] Div(L(LI(LBody(Span[[Capital]])) LI(LBody(Span[[Border]])) LI(LBody(Span[[Town]])) LI(LBody())))');
92
+ });
93
+ it('draws the dashed sample, the picture of the town and the outlined plate', () => {
94
+ // A dash pattern of three and two stroke widths.
95
+ expect(content).toMatch(/\[[\d.]+ [\d.]+\] 0 d/);
96
+ // The map and the town's picture.
97
+ expect(images).toBeGreaterThanOrEqual(2);
98
+ // The plate's rounded outline is a path filled even-odd or stroked.
99
+ expect(content).toMatch(/ c\n/);
100
+ });
101
+ });
102
+ //# sourceMappingURL=figureLegend.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figureLegend.test.js","sourceRoot":"","sources":["../../src/__tests__/figureLegend.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=labelGlyph.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labelGlyph.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/labelGlyph.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } 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
+ // #1054: a label tab's glyph icon is drawn as text in the PDF, in its own
9
+ // colour; a tagged PDF marks it a layout artifact, so the tab's tagged
10
+ // content is its words alone; a glyph the face lacks is reported.
11
+ const fontBytes = fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url));
12
+ const fontProvider = async () => new Uint8Array(fontBytes);
13
+ const face = fontkit.create(fontBytes);
14
+ class StubCtx {
15
+ font = '';
16
+ measureText(s) {
17
+ const sizePx = parseFontString(this.font)?.sizePx ?? 16;
18
+ return { width: (face.layout(s).advanceWidth / face.unitsPerEm) * sizePx };
19
+ }
20
+ }
21
+ globalThis.OffscreenCanvas = class {
22
+ getContext() {
23
+ return new StubCtx();
24
+ }
25
+ };
26
+ const pt = (value) => ({ value, unit: 'pt' });
27
+ const hex = (h) => ({ hex: h, model: 'hex' });
28
+ // The words in #123456, the glyph in #cc0000 (a bullet, which Lora has).
29
+ const config = (glyph) => ({
30
+ page: { width: pt(300), height: pt(400), dpi: 72, margins: { top: pt(40), bottom: pt(30), left: pt(30), right: pt(30) } },
31
+ locale: 'en-us',
32
+ layout: { layoutType: 'single' },
33
+ header: { elements: [] },
34
+ footer: { elements: [] },
35
+ bodyText: { fontFamily: 'Lora', fontSize: pt(10), lineHeight: pt(14) },
36
+ calloutStyles: [{
37
+ id: 'tab', title: '',
38
+ label: {
39
+ fontFamily: 'Lora', fontSize: pt(10), fontWeight: 400, position: 'top-left', height: pt(14), paddingX: pt(6), offset: pt(7),
40
+ color: hex('#123456'), background: hex('#eeeeee'),
41
+ icon: { kind: 'glyph', glyph, placement: 'inside', gap: pt(3), color: hex('#cc0000') },
42
+ },
43
+ }],
44
+ });
45
+ function pageContent(pdf) {
46
+ const contents = pdf.getPage(0).node.Contents();
47
+ const refs = contents instanceof PDFArray ? contents.asArray() : [contents];
48
+ return refs
49
+ .map((ref) => {
50
+ const s = pdf.context.lookup(ref);
51
+ return s instanceof PDFRawStream ? new TextDecoder('latin1').decode(decodePDFRawStream(s).decode()) : '';
52
+ })
53
+ .join('\n');
54
+ }
55
+ async function render(glyph, accessible) {
56
+ const doc = buildDocument({ markdown: ':::callout{type="tab" label="Money"}\nBody of the box.\n:::' }, config(glyph));
57
+ const warnings = [];
58
+ const bytes = await renderToPdf(doc, { fontProvider, accessible, onWarning: (w) => warnings.push(w) });
59
+ return { content: pageContent(await PDFDocument.load(bytes)), warnings };
60
+ }
61
+ /** `#cc0000` and `#123456` as fill colours of the content stream. */
62
+ const GLYPH_FILL = /0\.8 0 0 rg/;
63
+ const WORDS_FILL = /0\.0\d+ 0\.20\d* 0\.33\d* rg/;
64
+ describe('a glyph tab icon in the PDF (#1054)', () => {
65
+ it('draws the glyph as text, in its own colour', async () => {
66
+ const { content, warnings } = await render('•', false);
67
+ const at = content.search(GLYPH_FILL);
68
+ expect(at).toBeGreaterThan(-1);
69
+ // A text object follows the colour: the glyph is shown, not drawn as a path.
70
+ const after = content.slice(at);
71
+ expect(after.indexOf('Tj') === -1 ? after.indexOf('TJ') : after.indexOf('Tj')).toBeGreaterThan(-1);
72
+ expect(content).toMatch(WORDS_FILL);
73
+ expect(warnings.filter((w) => w.kind === 'missingGlyph')).toEqual([]);
74
+ });
75
+ it('marks the glyph a layout artifact in a tagged PDF and tags the words', async () => {
76
+ const { content } = await render('•', true);
77
+ const glyphAt = content.search(GLYPH_FILL);
78
+ const wordsAt = content.search(WORDS_FILL);
79
+ expect(glyphAt).toBeGreaterThan(-1);
80
+ expect(wordsAt).toBeGreaterThan(-1);
81
+ const beforeGlyph = content.slice(0, glyphAt);
82
+ expect(beforeGlyph.lastIndexOf('/Artifact')).toBeGreaterThan(beforeGlyph.lastIndexOf('/MCID'));
83
+ expect(beforeGlyph.slice(beforeGlyph.lastIndexOf('/Artifact'))).toMatch(/^\/Artifact\s*<<[^>]*\/Layout/);
84
+ const beforeWords = content.slice(0, wordsAt);
85
+ expect(beforeWords.lastIndexOf('/MCID')).toBeGreaterThan(beforeWords.lastIndexOf('/Artifact'));
86
+ });
87
+ it('reports a glyph the face does not have', async () => {
88
+ const { warnings } = await render('◉', false);
89
+ const missing = warnings.filter((w) => w.kind === 'missingGlyph');
90
+ expect(missing).toHaveLength(1);
91
+ expect(missing[0]).toMatchObject({ family: 'Lora', characters: ['◉'] });
92
+ });
93
+ });
94
+ //# sourceMappingURL=labelGlyph.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labelGlyph.test.js","sourceRoot":"","sources":["../../src/__tests__/labelGlyph.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pathBlock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathBlock.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/pathBlock.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PDFDocument } from 'pdf-lib';
3
+ import { FontCache } from '../fontCache';
4
+ import { renderHeaderFooterSlot } from '../pdf-backend/headerFooter';
5
+ import { makeScale } from '../pdf-backend/primitives';
6
+ import { PageTagger } from '../pdf-backend/tagging';
7
+ /** The page's content stream operators, before pdf-lib deflates them. */
8
+ function pageContent(page) {
9
+ const stream = page.getContentStream();
10
+ return new TextDecoder('latin1').decode(stream.getUnencodedContents());
11
+ }
12
+ /** Paint `blocks` as a design slot on a 400 × 400 pt page at 72 dpi (1 px
13
+ * = 1 pt, y up from 400), optionally tagged. */
14
+ async function paint(blocks, tagged = false) {
15
+ const pdfDoc = await PDFDocument.create();
16
+ const page = pdfDoc.addPage([400, 400]);
17
+ const ctx = { page, pageHeightPt: 400, scale: makeScale(72), colorSpace: 'rgb', ...(tagged ? { tags: new PageTagger(page, 0) } : {}) };
18
+ const slot = { bbox: { x: 0, y: 0, width: 400, height: 400 }, blocks };
19
+ renderHeaderFooterSlot(ctx, slot, new FontCache(pdfDoc, async () => new Uint8Array()), undefined, tagged ? { artifact: { type: 'Layout' } } : undefined);
20
+ ctx.tags?.close();
21
+ return pageContent(page);
22
+ }
23
+ describe('path blocks in the PDF backend (#1055)', () => {
24
+ it('fills an outline with m l h and the even-odd rule', async () => {
25
+ const content = await paint([{
26
+ kind: 'path',
27
+ bbox: { x: 10, y: 10, width: 40, height: 30 },
28
+ subpaths: [[{ op: 'M', x: 10, y: 40 }, { op: 'L', x: 30, y: 10 }, { op: 'L', x: 50, y: 40 }, { op: 'Z' }]],
29
+ fill: '#ff0000',
30
+ fillRule: 'evenodd',
31
+ }]);
32
+ expect(content).toMatch(/1 0 0 rg\n10 360 m\n30 390 l\n50 360 l\nh\nf\*\n/);
33
+ expect(content).not.toMatch(/\nS\n/);
34
+ });
35
+ it('strokes with its width, dash, cap and join, and draws a quadratic as a cubic', async () => {
36
+ const content = await paint([{
37
+ kind: 'path',
38
+ bbox: { x: 0, y: 0, width: 60, height: 30 },
39
+ subpaths: [[{ op: 'M', x: 0, y: 0 }, { op: 'Q', x1: 30, y1: 30, x: 60, y: 0 }]],
40
+ stroke: { color: '#0000ff', widthPx: 2, dash: [6, 3], cap: 'round', join: 'bevel' },
41
+ }]);
42
+ expect(content).toContain('0 0 1 RG\n2 w\n1 J\n2 j\n[6 3] 0 d\n0 400 m\n20 380 40 380 60 400 c\nS\n');
43
+ });
44
+ it('fills and strokes in one operator when it has both', async () => {
45
+ const content = await paint([{
46
+ kind: 'path',
47
+ bbox: { x: 0, y: 0, width: 10, height: 10 },
48
+ subpaths: [[{ op: 'M', x: 0, y: 0 }, { op: 'L', x: 10, y: 0 }, { op: 'L', x: 10, y: 10 }, { op: 'Z' }]],
49
+ fill: '#00ff00',
50
+ stroke: { color: '#000000', widthPx: 1 },
51
+ }]);
52
+ expect(content).toMatch(/h\nB\n/);
53
+ });
54
+ it('fills a dot as a circle of four cubics and an arrow as a triangle pointing along its angle', async () => {
55
+ const content = await paint([{
56
+ kind: 'path',
57
+ bbox: { x: 0, y: 0, width: 100, height: 100 },
58
+ subpaths: [],
59
+ markers: [
60
+ { kind: 'dot', x: 50, y: 50, angle: 0, sizePx: 10, color: '#00ff00' },
61
+ { kind: 'arrow', x: 90, y: 50, angle: 0, sizePx: 10, color: '#ff0000' },
62
+ ],
63
+ }]);
64
+ // The dot: from its right point, four curves, closed and filled.
65
+ expect(content).toMatch(/0 1 0 rg\n55 350 m\n(.* c\n){4}h\nf\n/);
66
+ // The arrow: tip at (90, 50), base 10 px back, 7 px wide.
67
+ expect(content).toContain('1 0 0 rg\n90 350 m\n80 346.5 l\n80 353.5 l\nh\nf\n');
68
+ });
69
+ it('is a Layout artifact in a tagged PDF, like a rule', async () => {
70
+ const content = await paint([{
71
+ kind: 'path',
72
+ bbox: { x: 0, y: 0, width: 10, height: 10 },
73
+ subpaths: [[{ op: 'M', x: 0, y: 0 }, { op: 'L', x: 10, y: 10 }]],
74
+ stroke: { color: '#000000', widthPx: 1 },
75
+ }], true);
76
+ const artifact = content.indexOf('/Artifact <<');
77
+ expect(artifact).toBeGreaterThanOrEqual(0);
78
+ expect(content.slice(artifact, content.indexOf('BDC', artifact))).toContain('/Type /Layout');
79
+ expect(content.indexOf(' m\n')).toBeGreaterThan(artifact);
80
+ expect(content).toContain('EMC');
81
+ });
82
+ });
83
+ //# sourceMappingURL=pathBlock.test.js.map