@jasy/pdf 1.0.0-alpha.4 → 1.0.0-alpha.6
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/README.md +5 -2
- package/dist/api/content.d.ts +9 -3
- package/dist/api/content.js +13 -3
- package/dist/api/descriptor.js +22 -1
- package/dist/api/dimension.d.ts +20 -0
- package/dist/api/dimension.js +16 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/layout.d.ts +65 -3
- package/dist/api/layout.js +71 -2
- package/dist/api/page-builder.d.ts +32 -0
- package/dist/api/page-builder.js +32 -0
- package/dist/api/structure.d.ts +19 -0
- package/dist/api/structure.js +25 -0
- package/dist/api/table.js +13 -5
- package/dist/api/text.d.ts +10 -1
- package/dist/api/text.js +32 -4
- package/dist/elements/container-element.d.ts +17 -1
- package/dist/elements/container-element.js +54 -22
- package/dist/elements/image-element.d.ts +23 -1
- package/dist/elements/image-element.js +54 -11
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/elements/layout/anchor-element.d.ts +23 -0
- package/dist/elements/layout/anchor-element.js +27 -0
- package/dist/elements/layout/bookmark-element.d.ts +27 -0
- package/dist/elements/layout/bookmark-element.js +30 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/page-builder-element.d.ts +32 -0
- package/dist/elements/layout/page-builder-element.js +43 -0
- package/dist/elements/layout/rotated-box-element.d.ts +35 -0
- package/dist/elements/layout/rotated-box-element.js +54 -0
- package/dist/elements/layout/rotated-element.d.ts +32 -0
- package/dist/elements/layout/rotated-element.js +41 -0
- package/dist/elements/layout/struct-group.d.ts +28 -0
- package/dist/elements/layout/struct-group.js +42 -0
- package/dist/elements/page-element.d.ts +8 -0
- package/dist/elements/page-element.js +13 -1
- package/dist/elements/pdf-element.d.ts +35 -0
- package/dist/elements/pdf-element.js +34 -0
- package/dist/elements/rectangle-element.d.ts +11 -1
- package/dist/elements/rectangle-element.js +49 -13
- package/dist/elements/row-element.d.ts +11 -1
- package/dist/elements/row-element.js +37 -8
- package/dist/elements/text-element.d.ts +11 -1
- package/dist/elements/text-element.js +5 -2
- package/dist/ir/display-list.d.ts +137 -1
- package/dist/layout/box-constraints.d.ts +8 -0
- package/dist/layout/box-constraints.js +14 -0
- package/dist/layout/collect-images.d.ts +9 -0
- package/dist/layout/collect-images.js +34 -0
- package/dist/layout/fragmentation.js +9 -2
- package/dist/renderer/anchor-renderer.d.ts +6 -0
- package/dist/renderer/anchor-renderer.js +11 -0
- package/dist/renderer/bookmark-renderer.d.ts +6 -0
- package/dist/renderer/bookmark-renderer.js +11 -0
- package/dist/renderer/emoji-image.d.ts +9 -0
- package/dist/renderer/emoji-image.js +68 -0
- package/dist/renderer/image-renderer.d.ts +1 -1
- package/dist/renderer/image-renderer.js +10 -2
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/page-builder-renderer.d.ts +6 -0
- package/dist/renderer/page-builder-renderer.js +11 -0
- package/dist/renderer/page-renderer.js +54 -4
- package/dist/renderer/pdf-backend.d.ts +16 -2
- package/dist/renderer/pdf-backend.js +135 -3
- package/dist/renderer/pdf-document-renderer.d.ts +16 -7
- package/dist/renderer/pdf-document-renderer.js +65 -33
- package/dist/renderer/pdf-renderer.js +58 -2
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/struct-group-renderer.d.ts +10 -0
- package/dist/renderer/struct-group-renderer.js +24 -0
- package/dist/renderer/text-renderer.d.ts +3 -0
- package/dist/renderer/text-renderer.js +206 -5
- package/dist/text/emoji-codepoints.d.ts +1 -0
- package/dist/text/emoji-codepoints.js +29 -0
- package/dist/text/line-breaker.js +6 -3
- package/dist/utils/afm-parser.js +6 -4
- package/dist/utils/dest-registry.d.ts +19 -0
- package/dist/utils/dest-registry.js +32 -0
- package/dist/utils/flex-layout.d.ts +10 -3
- package/dist/utils/flex-layout.js +26 -6
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +80 -0
- package/dist/utils/pdf-object-manager.d.ts +26 -1
- package/dist/utils/pdf-object-manager.js +0 -0
- package/dist/utils/struct-tree.d.ts +44 -0
- package/dist/utils/struct-tree.js +130 -0
- package/dist/utils/ttf-parser.d.ts +73 -0
- package/dist/utils/ttf-parser.js +456 -16
- package/dist/utils/ua-xmp.d.ts +9 -0
- package/dist/utils/ua-xmp.js +32 -0
- package/package.json +1 -1
|
@@ -25,7 +25,11 @@ export class PageRenderer {
|
|
|
25
25
|
nodes.push(...(await renderer(element, objectManager)));
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
// Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
|
|
29
|
+
// serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).
|
|
30
|
+
const structCtx = objectManager.struct.enabled ? objectManager.struct.beginPage() : undefined;
|
|
31
|
+
const flipped = PdfBackend.flipY(nodes, height);
|
|
32
|
+
const pageContent = PdfBackend.serialize(flipped, objectManager, structCtx);
|
|
29
33
|
// Add the page content as a new object (FlateDecode-compressed when enabled). The /Length is
|
|
30
34
|
// computed inside, with an explicit EOL before `endstream` (PDF/A clause 6.1.7.1).
|
|
31
35
|
const contentObjectNumber = objectManager.addContentStream(pageContent);
|
|
@@ -58,8 +62,54 @@ export class PageRenderer {
|
|
|
58
62
|
const extGStateCode = extGStateReferences.length > 0
|
|
59
63
|
? "/ExtGState <<\n" + extGStateReferences.join("\n") + "\n>>\n"
|
|
60
64
|
: "";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
// - Gradient shadings (color-glyph fills), registered during serialize above
|
|
66
|
+
const shadingReferences = [];
|
|
67
|
+
objectManager.getAllShadingsRaw().forEach((objectNumber, name) => {
|
|
68
|
+
shadingReferences.push(`/${name} ${objectNumber} 0 R`);
|
|
69
|
+
});
|
|
70
|
+
const shadingCode = shadingReferences.length > 0 ? "/Shading <<\n" + shadingReferences.join("\n") + "\n>>\n" : "";
|
|
71
|
+
// Hyperlinks: a `Link` IR node drew nothing into the content stream - here its (already Y-flipped)
|
|
72
|
+
// rect + href become a /Link annotation object. `/Border [0 0 0]` hides the default visible frame;
|
|
73
|
+
// the URL is escaped like any PDF literal string. Refs are collected into the page's /Annots below.
|
|
74
|
+
const num = (n) => Number(n.toFixed(2));
|
|
75
|
+
const annotRefs = [];
|
|
76
|
+
for (const node of flipped) {
|
|
77
|
+
if (node.type !== "link")
|
|
78
|
+
continue;
|
|
79
|
+
const rect = `[${num(node.x)} ${num(node.y)} ${num(node.x + node.width)} ${num(node.y + node.height)}]`;
|
|
80
|
+
// Either an external URL (/URI) or an internal named destination (/GoTo, resolved via /Names /Dests).
|
|
81
|
+
// The Link IR guarantees exactly one is set; prefer `dest` when present.
|
|
82
|
+
const action = node.dest !== undefined
|
|
83
|
+
? `/A << /S /GoTo /D (${PdfBackend.escapePdfString(node.dest)}) >>`
|
|
84
|
+
: `/A << /S /URI /URI (${PdfBackend.escapePdfString(node.href ?? "")}) >>`;
|
|
85
|
+
const annot = objectManager.addObject(`<< /Type /Annot /Subtype /Link /Rect ${rect} /Border [0 0 0] ${action} >>`);
|
|
86
|
+
annotRefs.push(`${annot} 0 R`);
|
|
87
|
+
}
|
|
88
|
+
const annotsCode = annotRefs.length > 0 ? ` /Annots [${annotRefs.join(" ")}]` : "";
|
|
89
|
+
// Tagging only: /StructParents links this page's marked content to the ParentTree, /Tabs /S makes the
|
|
90
|
+
// logical structure order the tab/reading order (a PDF/UA requirement).
|
|
91
|
+
const structAttrs = structCtx ? ` /StructParents ${structCtx.structParents} /Tabs /S` : "";
|
|
92
|
+
const pageObject = `<< /Type /Page /Parent ${parentObjectNumber} 0 R /Contents ${contentObjectNumber} 0 R /Resources <<\n/Font <<\n${fontReferences.join("\n")}\n>>\n${imageCode}${extGStateCode}${shadingCode}>>\n/MediaBox [0 0 ${width} ${height}]${structAttrs}${annotsCode} >>`;
|
|
93
|
+
// Add page as new object; register its object number with the struct tree (for /Pg refs), then return it.
|
|
94
|
+
const pageObjNum = objectManager.addObject(pageObject);
|
|
95
|
+
if (structCtx)
|
|
96
|
+
objectManager.struct.setPageObject(structCtx.structParents, pageObjNum);
|
|
97
|
+
// Bookmarks + named destinations: an `Outline`/`Anchor` IR node drew nothing; record it now that the
|
|
98
|
+
// page's object number is known. `node.y` is already Y-flipped to the page-space scroll target.
|
|
99
|
+
// PDFRenderer builds /Outlines and /Names /Dests at the end.
|
|
100
|
+
for (const node of flipped) {
|
|
101
|
+
if (node.type === "outline") {
|
|
102
|
+
objectManager.outline.add({
|
|
103
|
+
title: node.title,
|
|
104
|
+
level: node.level,
|
|
105
|
+
pageObjNum,
|
|
106
|
+
top: node.y,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else if (node.type === "anchor") {
|
|
110
|
+
objectManager.dests.add(node.name, { pageObjNum, top: node.y });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return pageObjNum;
|
|
64
114
|
}
|
|
65
115
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IRNode } from "../ir/display-list.js";
|
|
2
2
|
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
|
+
import type { PageStructContext } from "../utils/struct-tree.js";
|
|
3
4
|
/**
|
|
4
5
|
* PDF backend - turns display-list primitives into content-stream operators.
|
|
5
6
|
*
|
|
@@ -17,8 +18,21 @@ export declare class PdfBackend {
|
|
|
17
18
|
* directly. (image is migrated in a later slice and still arrives pre-flipped.)
|
|
18
19
|
*/
|
|
19
20
|
static flipY(nodes: IRNode[], pageHeight: number): IRNode[];
|
|
20
|
-
/** Serialize a whole display list into one content stream (page-level entry point).
|
|
21
|
-
|
|
21
|
+
/** Serialize a whole display list into one content stream (page-level entry point). When a
|
|
22
|
+
* `PageStructContext` is given (accessible tagging on), each drawable node is wrapped in a marked-content
|
|
23
|
+
* sequence (`/Role <</MCID n>> BDC … EMC`, or `/Artifact` when untagged); graphics-state and empty nodes
|
|
24
|
+
* pass through untouched, so untagged output stays byte-identical. */
|
|
25
|
+
/**
|
|
26
|
+
* Every number reaching the backend must be finite. A non-finite coordinate is a layout bug, but
|
|
27
|
+
* silence turns it into a corrupt PDF: `Infinity` is written into the operator stream verbatim, and a
|
|
28
|
+
* viewer then discards everything from that point on - including the footer, which is drawn last. That
|
|
29
|
+
* failure is invisible until somebody opens the file, so we refuse instead.
|
|
30
|
+
*
|
|
31
|
+
* Checks the node's numbers, not the serialized text: a document containing the WORD "Infinity" is
|
|
32
|
+
* perfectly legal and must not trip this.
|
|
33
|
+
*/
|
|
34
|
+
private static assertFinite;
|
|
35
|
+
static serialize(nodes: IRNode[], om: PDFObjectManager, struct?: PageStructContext): string;
|
|
22
36
|
/**
|
|
23
37
|
* Escapes a string for a PDF literal string `( ... )`. The backslash must be doubled
|
|
24
38
|
* first, then the parentheses that delimit the string. Without this, a ")" in the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Color } from "../common/color.js";
|
|
1
2
|
/**
|
|
2
3
|
* PDF backend - turns display-list primitives into content-stream operators.
|
|
3
4
|
*
|
|
@@ -44,6 +45,45 @@ export class PdfBackend {
|
|
|
44
45
|
return { ...node, y: pageHeight - node.y - node.height };
|
|
45
46
|
case "clip-pop":
|
|
46
47
|
return node;
|
|
48
|
+
case "path": {
|
|
49
|
+
// Flip every point's y directly (like a line endpoint); `z` carries no coordinates.
|
|
50
|
+
const commands = node.commands.map((c) => {
|
|
51
|
+
if (c.op === "z")
|
|
52
|
+
return c;
|
|
53
|
+
if (c.op === "c") {
|
|
54
|
+
return { ...c, y1: pageHeight - c.y1, y2: pageHeight - c.y2, y: pageHeight - c.y };
|
|
55
|
+
}
|
|
56
|
+
return { ...c, y: pageHeight - c.y };
|
|
57
|
+
});
|
|
58
|
+
// A gradient fill carries its own page-space anchor points; flip their y too (radii and
|
|
59
|
+
// x stay). A solid Color fill has no geometry.
|
|
60
|
+
const fill = node.fill instanceof Color
|
|
61
|
+
? node.fill
|
|
62
|
+
: { ...node.fill, y0: pageHeight - node.fill.y0, y1: pageHeight - node.fill.y1 };
|
|
63
|
+
return { ...node, commands, fill };
|
|
64
|
+
}
|
|
65
|
+
case "link":
|
|
66
|
+
// A link's clickable rect flips around its bottom edge, exactly like a rect.
|
|
67
|
+
return { ...node, y: pageHeight - node.y - node.height };
|
|
68
|
+
case "outline":
|
|
69
|
+
// An outline anchor is a single point (the target's top); flip it like a text baseline.
|
|
70
|
+
return { ...node, y: pageHeight - node.y };
|
|
71
|
+
case "anchor":
|
|
72
|
+
// A named-destination anchor is a single point (the target's top), flipped like a baseline.
|
|
73
|
+
return { ...node, y: pageHeight - node.y };
|
|
74
|
+
case "transform-push": {
|
|
75
|
+
// The child nodes are flipped from top-left to bottom-left by F = [1,0,0,-1,0,H] (F is its
|
|
76
|
+
// own inverse). For a matrix authored in top-left space to act correctly on them, conjugate
|
|
77
|
+
// it by that flip: M_pdf = F · M · F. Worked out for [a,b,c,d,e,f] with H = pageHeight:
|
|
78
|
+
const [a, b, c, d, e, f] = node.matrix;
|
|
79
|
+
const H = pageHeight;
|
|
80
|
+
return {
|
|
81
|
+
...node,
|
|
82
|
+
matrix: [a, -b, -c, d, H * c + e, H - H * d - f],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
case "transform-pop":
|
|
86
|
+
return node; // no coordinates to flip
|
|
47
87
|
default: {
|
|
48
88
|
const unknown = node;
|
|
49
89
|
return unknown;
|
|
@@ -51,9 +91,58 @@ export class PdfBackend {
|
|
|
51
91
|
}
|
|
52
92
|
});
|
|
53
93
|
}
|
|
54
|
-
/** Serialize a whole display list into one content stream (page-level entry point).
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
/** Serialize a whole display list into one content stream (page-level entry point). When a
|
|
95
|
+
* `PageStructContext` is given (accessible tagging on), each drawable node is wrapped in a marked-content
|
|
96
|
+
* sequence (`/Role <</MCID n>> BDC … EMC`, or `/Artifact` when untagged); graphics-state and empty nodes
|
|
97
|
+
* pass through untouched, so untagged output stays byte-identical. */
|
|
98
|
+
/**
|
|
99
|
+
* Every number reaching the backend must be finite. A non-finite coordinate is a layout bug, but
|
|
100
|
+
* silence turns it into a corrupt PDF: `Infinity` is written into the operator stream verbatim, and a
|
|
101
|
+
* viewer then discards everything from that point on - including the footer, which is drawn last. That
|
|
102
|
+
* failure is invisible until somebody opens the file, so we refuse instead.
|
|
103
|
+
*
|
|
104
|
+
* Checks the node's numbers, not the serialized text: a document containing the WORD "Infinity" is
|
|
105
|
+
* perfectly legal and must not trip this.
|
|
106
|
+
*/
|
|
107
|
+
static assertFinite(node) {
|
|
108
|
+
const finite = (value) => {
|
|
109
|
+
if (typeof value === "number")
|
|
110
|
+
return Number.isFinite(value);
|
|
111
|
+
if (Array.isArray(value))
|
|
112
|
+
return value.every(finite);
|
|
113
|
+
if (value !== null && typeof value === "object")
|
|
114
|
+
return Object.values(value).every(finite);
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
if (!finite(node)) {
|
|
118
|
+
throw new Error(`Layout produced a non-finite number on a "${node.type}" node, refusing to emit a corrupt ` +
|
|
119
|
+
`content stream. This is a bug in an element's calculateLayout (a size resolved to Infinity ` +
|
|
120
|
+
`or NaN), not in your document.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
static serialize(nodes, om, struct) {
|
|
124
|
+
return nodes
|
|
125
|
+
.map((node) => {
|
|
126
|
+
PdfBackend.assertFinite(node);
|
|
127
|
+
const ops = PdfBackend.serializeNode(node, om);
|
|
128
|
+
// Two kinds of node skip the marked-content wrapper, both because they carry no tag and never
|
|
129
|
+
// draw: GRAPHICS-STATE nodes (clip / transform push+pop) only save/restore state around the
|
|
130
|
+
// drawables they enclose, and SIDE-CHANNEL nodes (link, outline, anchor) become annotations /
|
|
131
|
+
// catalog entries instead of content-stream operators.
|
|
132
|
+
if (!struct ||
|
|
133
|
+
node.type === "clip-push" ||
|
|
134
|
+
node.type === "clip-pop" ||
|
|
135
|
+
node.type === "transform-push" ||
|
|
136
|
+
node.type === "transform-pop" ||
|
|
137
|
+
node.type === "link" ||
|
|
138
|
+
node.type === "outline" ||
|
|
139
|
+
node.type === "anchor" ||
|
|
140
|
+
ops === "")
|
|
141
|
+
return ops;
|
|
142
|
+
const { open, close } = struct.mark(node.tag);
|
|
143
|
+
return open + ops + close;
|
|
144
|
+
})
|
|
145
|
+
.join("");
|
|
57
146
|
}
|
|
58
147
|
/**
|
|
59
148
|
* Escapes a string for a PDF literal string `( ... )`. The backslash must be doubled
|
|
@@ -189,8 +278,51 @@ export class PdfBackend {
|
|
|
189
278
|
: `${node.x} ${node.y} ${node.width} ${node.height} re`;
|
|
190
279
|
return `q\n${path}\nW n\n`;
|
|
191
280
|
}
|
|
281
|
+
case "path": {
|
|
282
|
+
// A filled vector path (one color-glyph layer). Emit the subpath ops, then either a solid
|
|
283
|
+
// nonzero fill (a Color) or, for a gradient, clip to the path and paint a shading inside it.
|
|
284
|
+
const f = (n) => n.toFixed(3);
|
|
285
|
+
let path = "";
|
|
286
|
+
for (const c of node.commands) {
|
|
287
|
+
if (c.op === "m")
|
|
288
|
+
path += `${f(c.x)} ${f(c.y)} m\n`;
|
|
289
|
+
else if (c.op === "l")
|
|
290
|
+
path += `${f(c.x)} ${f(c.y)} l\n`;
|
|
291
|
+
else if (c.op === "c")
|
|
292
|
+
path += `${f(c.x1)} ${f(c.y1)} ${f(c.x2)} ${f(c.y2)} ${f(c.x)} ${f(c.y)} c\n`;
|
|
293
|
+
else
|
|
294
|
+
path += `h\n`;
|
|
295
|
+
}
|
|
296
|
+
if (!(node.fill instanceof Color)) {
|
|
297
|
+
// Gradient: clip to the path (W n keeps it as the clip without painting it), then paint
|
|
298
|
+
// the registered shading across that clip. The q/Q isolates the clip. Note: per-stop alpha
|
|
299
|
+
// is not represented (a DeviceRGB shading has no alpha channel - that would need a soft-mask
|
|
300
|
+
// group); COLR gradient stops are opaque in real fonts, so they render correctly.
|
|
301
|
+
const shading = om.registerShading(node.fill);
|
|
302
|
+
return `q\n${path}W n\n/${shading} sh\nQ\n`;
|
|
303
|
+
}
|
|
304
|
+
const body = `${node.fill.toPDFColorString()} rg\n${path}f\n`;
|
|
305
|
+
const gs = PdfBackend.alphaPrefix(om, node.fill.getAlpha(), 1);
|
|
306
|
+
return gs ? `q\n${gs}${body}Q\n` : body;
|
|
307
|
+
}
|
|
192
308
|
case "clip-pop":
|
|
193
309
|
return `Q\n`;
|
|
310
|
+
case "transform-push": {
|
|
311
|
+
// Save the graphics state, then apply the affine; everything until transform-pop paints through it.
|
|
312
|
+
const f = (n) => n.toFixed(3);
|
|
313
|
+
return `q\n${node.matrix.map(f).join(" ")} cm\n`;
|
|
314
|
+
}
|
|
315
|
+
case "transform-pop":
|
|
316
|
+
return `Q\n`;
|
|
317
|
+
case "link":
|
|
318
|
+
// A link draws nothing in the content stream - it becomes a page /Annot (built in PageRenderer).
|
|
319
|
+
return "";
|
|
320
|
+
case "outline":
|
|
321
|
+
// An outline anchor draws nothing - it becomes a /Outlines entry (built in PageRenderer/PDFRenderer).
|
|
322
|
+
return "";
|
|
323
|
+
case "anchor":
|
|
324
|
+
// A named destination draws nothing - it becomes a /Names /Dests entry (built in PageRenderer/PDFRenderer).
|
|
325
|
+
return "";
|
|
194
326
|
default: {
|
|
195
327
|
// Exhaustiveness guard: if a new IRNode variant is added, this fails to compile.
|
|
196
328
|
const unknown = node;
|
|
@@ -4,16 +4,25 @@ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
|
4
4
|
export declare class PDFDocumentRenderer {
|
|
5
5
|
static render(document: PDFDocumentElement, objectManager: PDFObjectManager, ctx: LayoutContext): Promise<number>;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* The page geometry a physical page will use. `PageElement.calculateLayout` has already folded the
|
|
8
|
+
* document defaults into every page's config by the time we get here (`PDFRenderer` lays the document
|
|
9
|
+
* out before it calls us, and it is our only caller). Merging again is therefore a no-op today - it is
|
|
10
|
+
* here so this stays correct BY CONSTRUCTION rather than by call order: were that order ever changed,
|
|
11
|
+
* an unmerged config would otherwise make `resolvePageSize` fall back to A4 and quietly report the
|
|
12
|
+
* wrong page size.
|
|
13
|
+
*/
|
|
14
|
+
private static configOf;
|
|
15
|
+
/**
|
|
16
|
+
* Splits one logical page into the physical pages it will occupy, without rendering.
|
|
8
17
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
18
|
+
* Only the common shape auto-paginates - a page whose single child is a fragmentation context
|
|
19
|
+
* (a Container). Anything else stays a single page on the unchanged path, so non-overflowing
|
|
20
|
+
* documents keep producing byte-identical output.
|
|
12
21
|
*/
|
|
13
|
-
private static
|
|
22
|
+
private static paginateLogicalPage;
|
|
14
23
|
/**
|
|
15
|
-
* Lays out one fragment
|
|
16
|
-
* header/footer are attached to every physical page so they repeat
|
|
24
|
+
* Lays out one physical page and renders it. A fragment gets a fresh `PageElement` of the same
|
|
25
|
+
* geometry; the header/footer are attached to every physical page so they repeat, and `PageElement`
|
|
17
26
|
* re-places them and the body in the band between.
|
|
18
27
|
*/
|
|
19
28
|
private static renderPhysicalPage;
|
|
@@ -1,21 +1,36 @@
|
|
|
1
|
-
import { layoutPageBands, PageElement } from "../elements/page-element.js";
|
|
1
|
+
import { layoutPageBands, PageElement, resolvePageSize, } from "../elements/page-element.js";
|
|
2
2
|
import { BoxConstraints } from "../layout/box-constraints.js";
|
|
3
3
|
import { isFragmentable } from "../layout/fragmentation.js";
|
|
4
4
|
import { PageRenderer } from "./page-renderer.js";
|
|
5
5
|
export class PDFDocumentRenderer {
|
|
6
6
|
static async render(document, objectManager, ctx) {
|
|
7
|
-
const pageNumbers = [];
|
|
8
7
|
// Add the pages object first... we need its object number (resources). The count is
|
|
9
8
|
// a placeholder; it is replaced below with the real (post-pagination) page count.
|
|
10
9
|
const pagesObject = `<< /Type /Pages /Kids [] /Count ${document.getProps().children.length} >>`;
|
|
11
10
|
const pagesObjectNumber = objectManager.addObject(pagesObject);
|
|
12
11
|
// Now set the given object number all its childs
|
|
13
12
|
objectManager.setParentObjectNumber(pagesObjectNumber);
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// Pass A - paginate the whole document WITHOUT drawing anything. Fragmentation is synchronous and
|
|
14
|
+
// pure (`{ fitted, remainder }`), so the full list of physical pages can be settled up front. Only
|
|
15
|
+
// then is the total page count known, which is what page numbers in a header/footer need.
|
|
16
|
+
const physicalPages = [];
|
|
17
|
+
for (const page of document.getProps().children) {
|
|
18
|
+
physicalPages.push(...PDFDocumentRenderer.paginateLogicalPage(page, ctx));
|
|
19
|
+
}
|
|
20
|
+
// Pass B - draw them, in order, so the emitted page objects keep their previous numbering. Each page
|
|
21
|
+
// is laid out again with its own PageInfo, which is how a `PageBuilder` anywhere in the tree (header,
|
|
22
|
+
// footer or body) learns its final page number and the now-known document total.
|
|
23
|
+
const pageNumbers = [];
|
|
24
|
+
for (const [index, physical] of physicalPages.entries()) {
|
|
25
|
+
const pageInfo = {
|
|
26
|
+
pageNumber: index + 1,
|
|
27
|
+
pageCount: physicalPages.length,
|
|
28
|
+
pageSize: resolvePageSize(PDFDocumentRenderer.configOf(physical, ctx)),
|
|
29
|
+
};
|
|
30
|
+
pageNumbers.push(await PDFDocumentRenderer.renderPhysicalPage(physical, objectManager, {
|
|
31
|
+
...ctx,
|
|
32
|
+
pageInfo,
|
|
33
|
+
}));
|
|
19
34
|
}
|
|
20
35
|
// We must update the pages object with the real physical page numbers and count.
|
|
21
36
|
const updatedPagesObject = `<< /Type /Pages /Kids [${pageNumbers
|
|
@@ -26,16 +41,28 @@ export class PDFDocumentRenderer {
|
|
|
26
41
|
return pagesObjectNumber;
|
|
27
42
|
}
|
|
28
43
|
/**
|
|
29
|
-
*
|
|
44
|
+
* The page geometry a physical page will use. `PageElement.calculateLayout` has already folded the
|
|
45
|
+
* document defaults into every page's config by the time we get here (`PDFRenderer` lays the document
|
|
46
|
+
* out before it calls us, and it is our only caller). Merging again is therefore a no-op today - it is
|
|
47
|
+
* here so this stays correct BY CONSTRUCTION rather than by call order: were that order ever changed,
|
|
48
|
+
* an unmerged config would otherwise make `resolvePageSize` fall back to A4 and quietly report the
|
|
49
|
+
* wrong page size.
|
|
50
|
+
*/
|
|
51
|
+
static configOf(physical, ctx) {
|
|
52
|
+
const config = physical.kind === "whole" ? physical.page.getProps().config : physical.config;
|
|
53
|
+
return { ...ctx.pageConfig, ...config };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Splits one logical page into the physical pages it will occupy, without rendering.
|
|
30
57
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
58
|
+
* Only the common shape auto-paginates - a page whose single child is a fragmentation context
|
|
59
|
+
* (a Container). Anything else stays a single page on the unchanged path, so non-overflowing
|
|
60
|
+
* documents keep producing byte-identical output.
|
|
34
61
|
*/
|
|
35
|
-
static
|
|
62
|
+
static paginateLogicalPage(page, ctx) {
|
|
36
63
|
const { children, config, header, footer } = page.getProps();
|
|
37
64
|
if (children.length !== 1 || !isFragmentable(children[0])) {
|
|
38
|
-
return [
|
|
65
|
+
return [{ kind: "whole", page }];
|
|
39
66
|
}
|
|
40
67
|
// Header/footer repeat on every physical page, so the body only ever flows into the
|
|
41
68
|
// band between them. Resolve that band once (config is already merged by pass 1).
|
|
@@ -46,45 +73,50 @@ export class PDFDocumentRenderer {
|
|
|
46
73
|
onOverflow: ctx.onOverflow,
|
|
47
74
|
};
|
|
48
75
|
const { bodyWidth: width, bodyHeight: height } = layoutPageBands(config, header, footer, pageCtx);
|
|
49
|
-
const
|
|
76
|
+
const pages = [];
|
|
50
77
|
let region = children[0];
|
|
51
78
|
let isFirstRegion = true;
|
|
52
79
|
while (region) {
|
|
53
80
|
if (!isFragmentable(region)) {
|
|
54
81
|
// A non-fragmentable remainder is placed whole on its own page.
|
|
55
|
-
|
|
82
|
+
pages.push({ kind: "fragment", config, content: region, header, footer });
|
|
56
83
|
break;
|
|
57
84
|
}
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
//
|
|
85
|
+
// pageCtx, not the document ctx: a descendant that reads `pageConfig` while being measured (a
|
|
86
|
+
// `PageBuilder` sizing its provisional build) must see THIS page's geometry, not the document default.
|
|
87
|
+
const { fitted, remainder } = region.fragment(height, width, pageCtx);
|
|
88
|
+
// Everything fits on one page: keep the ORIGINAL page so output is unchanged. Measuring inside
|
|
89
|
+
// fragment() left its children at the measuring origin; pass B lays the page out again, which
|
|
90
|
+
// restores their real positions (layout is deterministic).
|
|
62
91
|
if (isFirstRegion && remainder === null) {
|
|
63
|
-
|
|
64
|
-
numbers.push(await PageRenderer.render(page, objectManager));
|
|
92
|
+
pages.push({ kind: "whole", page });
|
|
65
93
|
break;
|
|
66
94
|
}
|
|
67
95
|
if (fitted) {
|
|
68
|
-
|
|
96
|
+
pages.push({ kind: "fragment", config, content: fitted, header, footer });
|
|
69
97
|
}
|
|
70
98
|
region = remainder;
|
|
71
99
|
isFirstRegion = false;
|
|
72
100
|
}
|
|
73
|
-
return
|
|
101
|
+
return pages;
|
|
74
102
|
}
|
|
75
103
|
/**
|
|
76
|
-
* Lays out one fragment
|
|
77
|
-
* header/footer are attached to every physical page so they repeat
|
|
104
|
+
* Lays out one physical page and renders it. A fragment gets a fresh `PageElement` of the same
|
|
105
|
+
* geometry; the header/footer are attached to every physical page so they repeat, and `PageElement`
|
|
78
106
|
* re-places them and the body in the band between.
|
|
79
107
|
*/
|
|
80
|
-
static async renderPhysicalPage(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
static async renderPhysicalPage(physical, objectManager, ctx) {
|
|
109
|
+
if (physical.kind === "whole") {
|
|
110
|
+
physical.page.calculateLayout(new BoxConstraints(), { x: 0, y: 0 }, ctx);
|
|
111
|
+
return PageRenderer.render(physical.page, objectManager);
|
|
112
|
+
}
|
|
113
|
+
const page = new PageElement({
|
|
114
|
+
config: physical.config,
|
|
115
|
+
header: physical.header,
|
|
116
|
+
footer: physical.footer,
|
|
117
|
+
children: [physical.content],
|
|
86
118
|
});
|
|
87
|
-
|
|
88
|
-
return PageRenderer.render(
|
|
119
|
+
page.calculateLayout(new BoxConstraints(), { x: 0, y: 0 }, ctx);
|
|
120
|
+
return PageRenderer.render(page, objectManager);
|
|
89
121
|
}
|
|
90
122
|
}
|
|
@@ -19,7 +19,22 @@ import { DeferredElement } from "../elements/layout/deferred-element.js";
|
|
|
19
19
|
import { DeferredRenderer } from "./deferred-renderer.js";
|
|
20
20
|
import { PositionedElement } from "../elements/layout/positioned-element.js";
|
|
21
21
|
import { PositionedRenderer } from "./positioned-renderer.js";
|
|
22
|
+
import { StructGroup } from "../elements/layout/struct-group.js";
|
|
23
|
+
import { StructGroupRenderer } from "./struct-group-renderer.js";
|
|
24
|
+
import { LinkElement } from "../elements/layout/link-element.js";
|
|
25
|
+
import { LinkRenderer } from "./link-renderer.js";
|
|
26
|
+
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
27
|
+
import { BookmarkRenderer } from "./bookmark-renderer.js";
|
|
28
|
+
import { AnchorElement } from "../elements/layout/anchor-element.js";
|
|
29
|
+
import { AnchorRenderer } from "./anchor-renderer.js";
|
|
30
|
+
import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
|
|
31
|
+
import { PageBuilderRenderer } from "./page-builder-renderer.js";
|
|
32
|
+
import { RotatedElement } from "../elements/layout/rotated-element.js";
|
|
33
|
+
import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
|
|
34
|
+
import { RotatedRenderer } from "./rotated-renderer.js";
|
|
35
|
+
import { PdfBackend } from "./pdf-backend.js";
|
|
22
36
|
import { BoxConstraints } from "../layout/box-constraints.js";
|
|
37
|
+
import { collectImageElements } from "../layout/collect-images.js";
|
|
23
38
|
import { DEFAULT_TEXT_STYLE, mergeTextStyle } from "../text/text-style.js";
|
|
24
39
|
export class PDFRenderer {
|
|
25
40
|
static async render(document, objectManager) {
|
|
@@ -36,6 +51,13 @@ export class PDFRenderer {
|
|
|
36
51
|
RendererRegistry.register(RepeatingHeaderElement, RepeatingHeaderRenderer.render);
|
|
37
52
|
RendererRegistry.register(DeferredElement, DeferredRenderer.render);
|
|
38
53
|
RendererRegistry.register(PositionedElement, PositionedRenderer.render);
|
|
54
|
+
RendererRegistry.register(StructGroup, StructGroupRenderer.render);
|
|
55
|
+
RendererRegistry.register(LinkElement, LinkRenderer.render);
|
|
56
|
+
RendererRegistry.register(BookmarkElement, BookmarkRenderer.render);
|
|
57
|
+
RendererRegistry.register(AnchorElement, AnchorRenderer.render);
|
|
58
|
+
RendererRegistry.register(PageBuilderElement, PageBuilderRenderer.render);
|
|
59
|
+
RendererRegistry.register(RotatedElement, RotatedRenderer.render);
|
|
60
|
+
RendererRegistry.register(RotatedBoxElement, RotatedRenderer.render);
|
|
39
61
|
let pdfContent = "";
|
|
40
62
|
// Header: version line + the PDF/A binary marker (the object manager owns it so its length
|
|
41
63
|
// matches the xref offset calculation).
|
|
@@ -48,6 +70,11 @@ export class PDFRenderer {
|
|
|
48
70
|
textStyle: mergeTextStyle(DEFAULT_TEXT_STYLE, document.getDefaultTextStyle()),
|
|
49
71
|
onOverflow: objectManager.getOverflowPolicy(),
|
|
50
72
|
};
|
|
73
|
+
// Pre-layout: resolve every image's intrinsic pixel size (async), so the synchronous layout can
|
|
74
|
+
// give a width-only image a proportional height. init() is idempotent, so the renderer reuses it.
|
|
75
|
+
const images = collectImageElements(document);
|
|
76
|
+
if (images.length > 0)
|
|
77
|
+
await Promise.all(images.map((img) => img.resolveIntrinsicSize()));
|
|
51
78
|
document.calculateLayout(new BoxConstraints(), { x: 0, y: 0 }, ctx);
|
|
52
79
|
// Render pages and contents (the driver paginates overflowing pages).
|
|
53
80
|
await PDFDocumentRenderer.render(document, objectManager, ctx);
|
|
@@ -63,12 +90,41 @@ export class PDFRenderer {
|
|
|
63
90
|
if (outputIntent) {
|
|
64
91
|
catalogParts.push(`/OutputIntents [${outputIntent} 0 R]`);
|
|
65
92
|
}
|
|
93
|
+
// The catalog has exactly one /Names dict; embedded files and named destinations are two entries in
|
|
94
|
+
// it, so collect both and emit the /Names once (below).
|
|
95
|
+
const namesParts = [];
|
|
66
96
|
const attachments = objectManager.getAttachments();
|
|
67
97
|
if (attachments.length > 0) {
|
|
68
|
-
|
|
98
|
+
// The name-tree key is a PDF literal string, so it needs the same escaping the /Filespec's /F and
|
|
99
|
+
// /UF get in attachFile(). Unescaped, a ")" or "\" in a file name closes the string early and the
|
|
100
|
+
// rest of it leaks out as raw operators.
|
|
101
|
+
const names = attachments
|
|
102
|
+
.map((a) => `(${PdfBackend.escapePdfString(a.name)}) ${a.filespec} 0 R`)
|
|
103
|
+
.join(" ");
|
|
69
104
|
const af = attachments.map((a) => `${a.filespec} 0 R`).join(" ");
|
|
70
|
-
catalogParts.push(`/AF [${af}]
|
|
105
|
+
catalogParts.push(`/AF [${af}]`);
|
|
106
|
+
namesParts.push(`/EmbeddedFiles << /Names [${names}] >>`);
|
|
71
107
|
}
|
|
108
|
+
// Accessible tagging: finalize the structure tree (emits StructTreeRoot + StructElems + ParentTree) and
|
|
109
|
+
// fold /MarkInfo + /StructTreeRoot + /Lang into the catalog. Returns "" (no-op) when tagging is off.
|
|
110
|
+
const structCatalog = objectManager.struct.finalize(objectManager);
|
|
111
|
+
if (structCatalog) {
|
|
112
|
+
catalogParts.push(structCatalog);
|
|
113
|
+
// PDF/UA requires the viewer to show the document title (not the file name).
|
|
114
|
+
catalogParts.push("/ViewerPreferences << /DisplayDocTitle true >>");
|
|
115
|
+
}
|
|
116
|
+
// Document outline (bookmarks): emit the /Outlines tree collected during page rendering. Returns
|
|
117
|
+
// "" (no-op) when no Bookmark was placed, so a plain document's catalog is unchanged.
|
|
118
|
+
const outlineCatalog = objectManager.outline.finalize(objectManager);
|
|
119
|
+
if (outlineCatalog)
|
|
120
|
+
catalogParts.push(outlineCatalog);
|
|
121
|
+
// Named destinations (internal-link targets): a /Dests entry in the shared /Names dict.
|
|
122
|
+
const destsNames = objectManager.dests.finalize();
|
|
123
|
+
if (destsNames)
|
|
124
|
+
namesParts.push(destsNames);
|
|
125
|
+
// Emit the single /Names dict if either embedded files or destinations contributed to it.
|
|
126
|
+
if (namesParts.length > 0)
|
|
127
|
+
catalogParts.push(`/Names << ${namesParts.join(" ")} >>`);
|
|
72
128
|
const catalogObject = `<< ${catalogParts.join(" ")} >>`;
|
|
73
129
|
objectManager.addObject(catalogObject);
|
|
74
130
|
// Now that the render pass has revealed which glyphs each embedded font uses, fill the reserved
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { RotatedElement } from "../elements/layout/rotated-element.js";
|
|
3
|
+
import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
|
|
4
|
+
import { IRNode } from "../ir/display-list.js";
|
|
5
|
+
export declare class RotatedRenderer {
|
|
6
|
+
static render(element: RotatedElement | RotatedBoxElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class RotatedRenderer {
|
|
3
|
+
// Both `Rotated` (paint-only) and `RotatedBox` (layout-aware quarter-turns) expose the same
|
|
4
|
+
// `{ angle, child, x, y, width, height }` and rotate the child around the box center - shared here.
|
|
5
|
+
static async render(element, objectManager) {
|
|
6
|
+
const { angle, child, x, y, width, height } = element.getProps();
|
|
7
|
+
// Rotate around the box CENTER, in the engine's top-left coordinates (the IR->backend seam flips
|
|
8
|
+
// the matrix to PDF space per page). A positive angle reads clockwise on the page.
|
|
9
|
+
const cx = x + width / 2;
|
|
10
|
+
const cy = y + height / 2;
|
|
11
|
+
const theta = (angle * Math.PI) / 180;
|
|
12
|
+
const cos = Math.cos(theta);
|
|
13
|
+
const sin = Math.sin(theta);
|
|
14
|
+
// M = T(cx,cy) · R(theta) · T(-cx,-cy) as a PDF cm [a,b,c,d,e,f]: the linear part is the rotation,
|
|
15
|
+
// the translation keeps the center fixed.
|
|
16
|
+
const matrix = [
|
|
17
|
+
cos,
|
|
18
|
+
sin,
|
|
19
|
+
-sin,
|
|
20
|
+
cos,
|
|
21
|
+
cx - cos * cx + sin * cy,
|
|
22
|
+
cy - sin * cx - cos * cy,
|
|
23
|
+
];
|
|
24
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
25
|
+
const childNodes = renderer ? await renderer(child, objectManager) : [];
|
|
26
|
+
// Wrap the child's drawing between a push (apply the matrix) and a pop (restore).
|
|
27
|
+
return [{ type: "transform-push", matrix }, ...childNodes, { type: "transform-pop" }];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StructGroup } from "../elements/layout/struct-group.js";
|
|
2
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* Renders a StructGroup: opens its structure element, renders the child under it, closes it. With accessible
|
|
6
|
+
* tagging off it is fully transparent - just the child's IR.
|
|
7
|
+
*/
|
|
8
|
+
export declare class StructGroupRenderer {
|
|
9
|
+
static render(group: StructGroup, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Renders a StructGroup: opens its structure element, renders the child under it, closes it. With accessible
|
|
4
|
+
* tagging off it is fully transparent - just the child's IR.
|
|
5
|
+
*/
|
|
6
|
+
export class StructGroupRenderer {
|
|
7
|
+
static async render(group, objectManager) {
|
|
8
|
+
const { role, child } = group.getProps();
|
|
9
|
+
const renderChild = async () => {
|
|
10
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
11
|
+
return renderer ? await renderer(child, objectManager) : [];
|
|
12
|
+
};
|
|
13
|
+
if (!objectManager.struct.enabled)
|
|
14
|
+
return renderChild();
|
|
15
|
+
const key = objectManager.struct.openElement(group.structId, role);
|
|
16
|
+
objectManager.struct.push(key);
|
|
17
|
+
try {
|
|
18
|
+
return await renderChild();
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
objectManager.struct.pop(); // keep the parent stack balanced even if a child throws
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -6,5 +6,8 @@ import { TextOverflow } from "../text/line-breaker.js";
|
|
|
6
6
|
export declare class TextRenderer {
|
|
7
7
|
static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number, maxLines?: number, overflow?: TextOverflow, lineHeight?: number): number;
|
|
8
8
|
static render(textElement: TextElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
9
|
+
private static _expandColorGlyphs;
|
|
10
|
+
private static _glyphToPath;
|
|
11
|
+
private static _toGradient;
|
|
9
12
|
private static _buildRuns;
|
|
10
13
|
}
|