@jasy/pdf 1.0.0-alpha.5 → 1.0.0-alpha.7
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/api/content.d.ts +6 -3
- package/dist/api/content.js +12 -3
- package/dist/api/descriptor.js +24 -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 +112 -17
- package/dist/api/layout.js +118 -13
- package/dist/api/page-builder.d.ts +32 -0
- package/dist/api/page-builder.js +32 -0
- package/dist/api/structure.d.ts +5 -0
- package/dist/api/structure.js +1 -0
- package/dist/api/text.d.ts +24 -2
- package/dist/api/text.js +46 -4
- package/dist/elements/container-element.d.ts +26 -1
- package/dist/elements/container-element.js +79 -29
- package/dist/elements/image-element.d.ts +19 -1
- package/dist/elements/image-element.js +52 -11
- package/dist/elements/index.d.ts +8 -0
- package/dist/elements/index.js +8 -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/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +12 -0
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -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/positioned-element.d.ts +10 -7
- package/dist/elements/layout/positioned-element.js +21 -9
- 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/page-element.d.ts +19 -1
- package/dist/elements/page-element.js +54 -17
- package/dist/elements/pdf-element.d.ts +59 -0
- package/dist/elements/pdf-element.js +51 -0
- package/dist/elements/rectangle-element.d.ts +20 -1
- package/dist/elements/rectangle-element.js +75 -18
- package/dist/elements/row-element.d.ts +19 -1
- package/dist/elements/row-element.js +45 -8
- package/dist/elements/text-element.d.ts +34 -4
- package/dist/elements/text-element.js +40 -17
- package/dist/ir/display-list.d.ts +60 -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.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- 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/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -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 +37 -2
- package/dist/renderer/pdf-backend.d.ts +17 -0
- package/dist/renderer/pdf-backend.js +117 -7
- package/dist/renderer/pdf-document-class.js +3 -0
- package/dist/renderer/pdf-document-renderer.d.ts +16 -7
- package/dist/renderer/pdf-document-renderer.js +87 -36
- package/dist/renderer/pdf-renderer.js +53 -2
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/text-renderer.d.ts +1 -1
- package/dist/renderer/text-renderer.js +123 -54
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +42 -0
- package/dist/text/line-breaker.d.ts +9 -8
- package/dist/text/line-breaker.js +28 -21
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +13 -3
- package/dist/text/text-style.js +10 -3
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +73 -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/font-metrics.d.ts +14 -0
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +80 -0
- package/dist/utils/pdf-object-manager.d.ts +33 -1
- package/dist/utils/pdf-object-manager.js +0 -0
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/package.json +1 -1
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import { TextRenderer } from "../renderer/text-renderer.js";
|
|
6
6
|
import { DEFAULT_TEXT_STYLE } from "../text/text-style.js";
|
|
7
7
|
import { wrapStringIntoLines, breakSegmentsIntoLines, segmentLinesToSegments, } from "../text/line-breaker.js";
|
|
8
|
+
import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
|
|
9
|
+
import { runAdvance } from "../text/advance.js";
|
|
8
10
|
import { SizedPDFElement } from "./pdf-element.js";
|
|
9
11
|
export class TextElement extends SizedPDFElement {
|
|
10
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow = "clip", lineHeight, role, }) {
|
|
12
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, role, }) {
|
|
11
13
|
super({ x: 0, y: 0 });
|
|
12
14
|
this.role = role;
|
|
13
15
|
this.rawFontSize = fontSize;
|
|
@@ -16,6 +18,10 @@ export class TextElement extends SizedPDFElement {
|
|
|
16
18
|
this.rawColor = color;
|
|
17
19
|
this.rawTextAlignment = textAlignment;
|
|
18
20
|
this.rawLineHeight = lineHeight;
|
|
21
|
+
this.rawUnderline = underline;
|
|
22
|
+
this.rawStrikethrough = strikethrough;
|
|
23
|
+
this.rawSkipInk = skipInk;
|
|
24
|
+
this.rawLetterSpacing = letterSpacing;
|
|
19
25
|
this.content = content;
|
|
20
26
|
this.maxLines = maxLines;
|
|
21
27
|
this.overflow = overflow;
|
|
@@ -32,6 +38,10 @@ export class TextElement extends SizedPDFElement {
|
|
|
32
38
|
this.color = this.rawColor ?? ts.color;
|
|
33
39
|
this.textAlignment = this.rawTextAlignment ?? ts.textAlignment;
|
|
34
40
|
this.lineHeight = this.rawLineHeight ?? ts.lineHeight;
|
|
41
|
+
this.underline = this.rawUnderline ?? ts.underline;
|
|
42
|
+
this.strikethrough = this.rawStrikethrough ?? ts.strikethrough;
|
|
43
|
+
this.skipInk = this.rawSkipInk ?? ts.skipInk;
|
|
44
|
+
this.letterSpacing = this.rawLetterSpacing ?? ts.letterSpacing;
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
47
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
@@ -45,11 +55,12 @@ export class TextElement extends SizedPDFElement {
|
|
|
45
55
|
? this.fragmentString(this.content, maxHeight, width, ctx)
|
|
46
56
|
: this.fragmentSegments(this.content, maxHeight, width, ctx);
|
|
47
57
|
}
|
|
48
|
-
// Plain string: every wrapped line
|
|
49
|
-
// so `floor(maxHeight /
|
|
58
|
+
// Plain string: every wrapped line gets the same box (the same one calculateTextHeight uses),
|
|
59
|
+
// so `floor(maxHeight / lineBox)` lines fit.
|
|
50
60
|
fragmentString(content, maxHeight, width, ctx) {
|
|
51
|
-
const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow);
|
|
52
|
-
const
|
|
61
|
+
const lines = wrapStringIntoLines(content, this.fontFamily, this.fontSize, this.fontStyle, width, ctx.metrics, this.maxLines, this.overflow, this.letterSpacing);
|
|
62
|
+
const box = lineBoxForString(ctx.metrics, this.fontFamily, this.fontStyle, this.fontSize, this.lineHeight);
|
|
63
|
+
const fittedLineCount = Math.floor(maxHeight / box.height);
|
|
53
64
|
if (fittedLineCount <= 0)
|
|
54
65
|
return { fitted: null, remainder: this };
|
|
55
66
|
if (fittedLineCount >= lines.length)
|
|
@@ -59,18 +70,20 @@ export class TextElement extends SizedPDFElement {
|
|
|
59
70
|
remainder: this.cloneWithContent(lines.slice(fittedLineCount).join(" ")),
|
|
60
71
|
};
|
|
61
72
|
}
|
|
62
|
-
// Styled segments: each line's height
|
|
63
|
-
//
|
|
73
|
+
// Styled segments: each line's height comes from the fonts on it, so pack lines by cumulative
|
|
74
|
+
// box height. Rebuild the fitted/remainder halves back into TextSegment[].
|
|
64
75
|
fragmentSegments(content, maxHeight, width, ctx) {
|
|
65
|
-
const
|
|
76
|
+
const defaults = {
|
|
66
77
|
fontFamily: this.fontFamily,
|
|
67
78
|
fontSize: this.fontSize,
|
|
68
79
|
fontStyle: this.fontStyle,
|
|
69
|
-
|
|
80
|
+
letterSpacing: this.letterSpacing,
|
|
81
|
+
};
|
|
82
|
+
const lines = breakSegmentsIntoLines(content, defaults, width, ctx.metrics, this.maxLines, this.overflow);
|
|
70
83
|
let used = 0;
|
|
71
84
|
let fittedLineCount = 0;
|
|
72
85
|
for (const line of lines) {
|
|
73
|
-
const lineBox = line.
|
|
86
|
+
const lineBox = lineBoxForSegmentLine(line, defaults, ctx.metrics, this.lineHeight).height;
|
|
74
87
|
if (used + lineBox > maxHeight)
|
|
75
88
|
break;
|
|
76
89
|
used += lineBox;
|
|
@@ -98,6 +111,10 @@ export class TextElement extends SizedPDFElement {
|
|
|
98
111
|
maxLines: this.maxLines,
|
|
99
112
|
overflow: this.overflow,
|
|
100
113
|
lineHeight: this.lineHeight,
|
|
114
|
+
underline: this.underline,
|
|
115
|
+
strikethrough: this.strikethrough,
|
|
116
|
+
skipInk: this.skipInk,
|
|
117
|
+
letterSpacing: this.letterSpacing,
|
|
101
118
|
role: this.role,
|
|
102
119
|
}).adoptStructId(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
|
|
103
120
|
}
|
|
@@ -112,7 +129,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
112
129
|
? constraints.maxWidth
|
|
113
130
|
: this.naturalWidth(ctx.metrics);
|
|
114
131
|
const wrapWidth = this.width ?? 0;
|
|
115
|
-
this.height = TextRenderer.calculateTextHeight(this.content, this.fontSize, this.fontFamily, this.fontStyle, ctx.metrics, wrapWidth, this.maxLines, this.overflow, this.lineHeight);
|
|
132
|
+
this.height = TextRenderer.calculateTextHeight(this.content, this.fontSize, this.fontFamily, this.fontStyle, ctx.metrics, wrapWidth, this.maxLines, this.overflow, this.lineHeight, this.letterSpacing);
|
|
116
133
|
// Top-left coordinates (y = top of the text box). The baseline offset and the
|
|
117
134
|
// Y-flip are applied downstream (the line-builder positions baselines, the seam
|
|
118
135
|
// flips to PDF), so the element stays coordinate-system-blind.
|
|
@@ -127,22 +144,24 @@ export class TextElement extends SizedPDFElement {
|
|
|
127
144
|
* - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
|
|
128
145
|
* 'n' beats 'l') one bit over its own width, dropping the last word onto a second line. */
|
|
129
146
|
naturalWidth(metrics) {
|
|
130
|
-
const oneLine = (text, family, size, style) => {
|
|
147
|
+
const oneLine = (text, family, size, style, letterSpacing) => {
|
|
148
|
+
const font = { fontFamily: family, fontSize: size, fontStyle: style };
|
|
131
149
|
const words = text.split(" ");
|
|
132
|
-
const space = metrics
|
|
150
|
+
const space = runAdvance(metrics, " ", font, letterSpacing);
|
|
133
151
|
let width = 0;
|
|
134
152
|
words.forEach((word, i) => {
|
|
135
|
-
const w = metrics
|
|
136
|
-
// Group (word + space) as one term, exactly like the breaker - see the note above.
|
|
153
|
+
const w = runAdvance(metrics, word, font, letterSpacing);
|
|
154
|
+
// Group (word + space) as one term, exactly like the breaker - see the note above. Both use
|
|
155
|
+
// the same `runAdvance`, so the two agree bit for bit even with letterSpacing.
|
|
137
156
|
width += i < words.length - 1 ? w + space : w;
|
|
138
157
|
});
|
|
139
158
|
return width;
|
|
140
159
|
};
|
|
141
160
|
if (typeof this.content === "string") {
|
|
142
|
-
return oneLine(this.content, this.fontFamily, this.fontSize, this.fontStyle);
|
|
161
|
+
return oneLine(this.content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
|
|
143
162
|
}
|
|
144
163
|
return this.content.reduce((sum, seg) => sum +
|
|
145
|
-
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle), 0);
|
|
164
|
+
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), 0);
|
|
146
165
|
}
|
|
147
166
|
getProps() {
|
|
148
167
|
return {
|
|
@@ -159,6 +178,10 @@ export class TextElement extends SizedPDFElement {
|
|
|
159
178
|
maxLines: this.maxLines,
|
|
160
179
|
overflow: this.overflow,
|
|
161
180
|
lineHeight: this.lineHeight,
|
|
181
|
+
underline: this.underline,
|
|
182
|
+
strikethrough: this.strikethrough,
|
|
183
|
+
skipInk: this.skipInk,
|
|
184
|
+
letterSpacing: this.letterSpacing,
|
|
162
185
|
role: this.role,
|
|
163
186
|
};
|
|
164
187
|
}
|
|
@@ -35,6 +35,7 @@ export interface TextRun {
|
|
|
35
35
|
fontStyle: FontStyle;
|
|
36
36
|
fontSize: number;
|
|
37
37
|
color: Color;
|
|
38
|
+
letterSpacing?: number;
|
|
38
39
|
tag?: StructTag;
|
|
39
40
|
}
|
|
40
41
|
/** An axis-aligned rectangle. An absent `fill` or `stroke` means that part is not drawn. */
|
|
@@ -103,6 +104,64 @@ export interface ClipPush {
|
|
|
103
104
|
export interface ClipPop {
|
|
104
105
|
type: "clip-pop";
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* A hyperlink over a rectangle. Unlike every other IR node this draws NOTHING into the content stream -
|
|
109
|
+
* it becomes a PDF Link ANNOTATION on the page (`/Annots`), a side channel next to the drawing. Exactly
|
|
110
|
+
* one target is set: `href` is an external URL (a `/URI` action); `dest` is a named destination inside
|
|
111
|
+
* this document (a `/GoTo` action, resolved via the catalog `/Names /Dests` tree - see `Anchor`). The
|
|
112
|
+
* rect is the clickable region in top-left engine coords; the Y-flip converts it to PDF page space.
|
|
113
|
+
*
|
|
114
|
+
* NOTE: a Link inside a `TransformPush` is NOT yet rotated with it - the annotation rect lives in page
|
|
115
|
+
* space and ignores the content-stream matrix. Completing that is a planned follow-up (see todo.md).
|
|
116
|
+
*/
|
|
117
|
+
export interface Link {
|
|
118
|
+
type: "link";
|
|
119
|
+
x: number;
|
|
120
|
+
y: number;
|
|
121
|
+
width: number;
|
|
122
|
+
height: number;
|
|
123
|
+
href?: string;
|
|
124
|
+
dest?: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A named destination anchor - a jump TARGET for an internal `Link({ to })`. Like `Link` it draws
|
|
128
|
+
* NOTHING; it registers `name` -> (this page, `y`) in the catalog `/Names /Dests` name tree. `y` is the
|
|
129
|
+
* top of the target in top-left engine coords (the Y-flip turns it into the page-space scroll target).
|
|
130
|
+
*/
|
|
131
|
+
export interface Anchor {
|
|
132
|
+
type: "anchor";
|
|
133
|
+
y: number;
|
|
134
|
+
name: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A document-outline (bookmark) anchor. Like `Link` it draws NOTHING - it becomes an entry in the PDF
|
|
138
|
+
* outline tree (`/Outlines`), a side channel the viewer shows in its bookmark panel. `y` is the top of
|
|
139
|
+
* the target in top-left engine coords (the Y-flip turns it into the page-space scroll target); `level`
|
|
140
|
+
* (1-based) nests the entry under the nearest preceding entry of a smaller level.
|
|
141
|
+
*/
|
|
142
|
+
export interface Outline {
|
|
143
|
+
type: "outline";
|
|
144
|
+
y: number;
|
|
145
|
+
title: string;
|
|
146
|
+
level: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
|
|
150
|
+
* matching `TransformPop` is painted through `matrix`. This is what `Rotated` wraps its child's nodes
|
|
151
|
+
* in, so a subtree draws rotated (or scaled/translated) around a pivot without touching its layout.
|
|
152
|
+
*
|
|
153
|
+
* `matrix` is `[a, b, c, d, e, f]` in the engine's TOP-LEFT coordinates - the same space every other IR
|
|
154
|
+
* node uses. The Y-flip at the IR -> backend seam converts it to PDF's bottom-left once per page, so
|
|
155
|
+
* producers stay coordinate-system-blind (they never see a flipped matrix).
|
|
156
|
+
*/
|
|
157
|
+
export interface TransformPush {
|
|
158
|
+
type: "transform-push";
|
|
159
|
+
matrix: [number, number, number, number, number, number];
|
|
160
|
+
}
|
|
161
|
+
/** Closes the most recent `TransformPush` (restores the graphics state). */
|
|
162
|
+
export interface TransformPop {
|
|
163
|
+
type: "transform-pop";
|
|
164
|
+
}
|
|
106
165
|
/**
|
|
107
166
|
* One drawing command of a `Path`, in absolute page coordinates. Curves are CUBIC because PDF
|
|
108
167
|
* content streams have no quadratic operator - a producer converts TrueType quads to cubics before
|
|
@@ -168,4 +227,4 @@ export interface Path {
|
|
|
168
227
|
tag?: StructTag;
|
|
169
228
|
}
|
|
170
229
|
/** The closed set of primitives the PDF backend knows how to draw. */
|
|
171
|
-
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path;
|
|
230
|
+
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | TransformPush | TransformPop;
|
|
@@ -44,6 +44,14 @@ export declare class BoxConstraints {
|
|
|
44
44
|
/** Returns the constraints clamped to lie within `parent` (Flutter's enforce). */
|
|
45
45
|
enforce(parent: BoxConstraints): BoxConstraints;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolves a sized element's extent on one axis (relative sizing). An explicit point size (`fixed`)
|
|
49
|
+
* always wins; otherwise a `factor` (0..1) of the offered extent, but ONLY when that axis is bounded -
|
|
50
|
+
* a fraction of an unbounded axis has no meaning (Flutter's FractionallySizedBox under unbounded
|
|
51
|
+
* constraints). Returns `undefined` to mean "no explicit extent", i.e. fill the box or shrink-wrap.
|
|
52
|
+
* Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveExtent(fixed: number | undefined, factor: number | undefined, boundedMax: number, hasBounded: boolean): number | undefined;
|
|
47
55
|
/** The size an element resolves to and returns UP the tree. */
|
|
48
56
|
export interface Size {
|
|
49
57
|
width: number;
|
|
@@ -67,3 +67,17 @@ export class BoxConstraints {
|
|
|
67
67
|
return new BoxConstraints(Math.max(parent.minWidth, Math.min(this.minWidth, parent.maxWidth)), Math.max(parent.minWidth, Math.min(this.maxWidth, parent.maxWidth)), Math.max(parent.minHeight, Math.min(this.minHeight, parent.maxHeight)), Math.max(parent.minHeight, Math.min(this.maxHeight, parent.maxHeight)));
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolves a sized element's extent on one axis (relative sizing). An explicit point size (`fixed`)
|
|
72
|
+
* always wins; otherwise a `factor` (0..1) of the offered extent, but ONLY when that axis is bounded -
|
|
73
|
+
* a fraction of an unbounded axis has no meaning (Flutter's FractionallySizedBox under unbounded
|
|
74
|
+
* constraints). Returns `undefined` to mean "no explicit extent", i.e. fill the box or shrink-wrap.
|
|
75
|
+
* Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveExtent(fixed, factor, boundedMax, hasBounded) {
|
|
78
|
+
if (fixed !== undefined)
|
|
79
|
+
return fixed;
|
|
80
|
+
if (factor !== undefined && hasBounded)
|
|
81
|
+
return boundedMax * factor;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PDFElement } from "../elements/pdf-element.js";
|
|
2
|
+
import { ImageElement } from "../elements/image-element.js";
|
|
3
|
+
/**
|
|
4
|
+
* Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
|
|
5
|
+
* returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
|
|
6
|
+
* size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
|
|
7
|
+
* give a width-only image its proportional height. Cheap - images are few and this runs once.
|
|
8
|
+
*/
|
|
9
|
+
export declare function collectImageElements(root: PDFElement): ImageElement[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PDFElement, hasChildrenProp, hasChildProp } from "../elements/pdf-element.js";
|
|
2
|
+
import { ImageElement } from "../elements/image-element.js";
|
|
3
|
+
/**
|
|
4
|
+
* Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
|
|
5
|
+
* returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
|
|
6
|
+
* size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
|
|
7
|
+
* give a width-only image its proportional height. Cheap - images are few and this runs once.
|
|
8
|
+
*/
|
|
9
|
+
export function collectImageElements(root) {
|
|
10
|
+
const out = [];
|
|
11
|
+
const visit = (el) => {
|
|
12
|
+
if (el instanceof ImageElement)
|
|
13
|
+
out.push(el);
|
|
14
|
+
const props = el.getProps();
|
|
15
|
+
if (hasChildrenProp(props))
|
|
16
|
+
for (const c of props.children)
|
|
17
|
+
visit(c);
|
|
18
|
+
else if (hasChildProp(props))
|
|
19
|
+
visit(props.child);
|
|
20
|
+
// Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer and a
|
|
21
|
+
// RepeatingHeaderElement's `body` (a Table with a header wraps its rows there). A
|
|
22
|
+
// DeferredElement's `composed` is null until layout runs, so it cannot be reached from this
|
|
23
|
+
// pre-layout pass; an image inside a deferred subtree just skips aspect pre-resolution.
|
|
24
|
+
const w = props;
|
|
25
|
+
if (w.header instanceof PDFElement)
|
|
26
|
+
visit(w.header);
|
|
27
|
+
if (w.footer instanceof PDFElement)
|
|
28
|
+
visit(w.footer);
|
|
29
|
+
if (w.body instanceof PDFElement)
|
|
30
|
+
visit(w.body);
|
|
31
|
+
};
|
|
32
|
+
visit(root);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
@@ -12,6 +12,10 @@ import { LayoutContext, PDFElement } from "../elements/pdf-element.js";
|
|
|
12
12
|
export interface FragmentResult {
|
|
13
13
|
fitted: PDFElement | null;
|
|
14
14
|
remainder: PDFElement | null;
|
|
15
|
+
/** The split ended at a FORCED page break (a `PageBreak`), not because the region filled up. The
|
|
16
|
+
* remainder must start a fresh page, and an enclosing flow must stop packing too - so this bubbles
|
|
17
|
+
* up through nested `fragment()` calls. Absent/false for an ordinary height-driven break. */
|
|
18
|
+
forceBreak?: boolean;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* An element that knows how to split itself across regions. Width is already finalized
|
|
@@ -29,6 +33,13 @@ export declare function isFragmentable(element: PDFElement): element is PDFEleme
|
|
|
29
33
|
* logs and clips, `"ignore"` clips silently.
|
|
30
34
|
*/
|
|
31
35
|
export type OverflowPolicy = "error" | "warn" | "ignore";
|
|
36
|
+
export declare function reportOverflow(child: PDFElement, childHeight: number, maxHeight: number, policy: OverflowPolicy): void;
|
|
37
|
+
/**
|
|
38
|
+
* True if packing `children` will hit a forced cut: a child that IS a break, contains one deeper in its
|
|
39
|
+
* subtree, or asks to break before/after itself. A flow container reports this from `hasForcedBreak` so
|
|
40
|
+
* its parent fragments it (runs `packChildren`) instead of placing it whole and swallowing the cut.
|
|
41
|
+
*/
|
|
42
|
+
export declare function childrenForceBreak(children: PDFElement[]): boolean;
|
|
32
43
|
/**
|
|
33
44
|
* Packs a vertical stack of children into `maxHeight`, in order. Children are measured
|
|
34
45
|
* against `width` (unbounded height) and added until one would overflow; that straddling
|
|
@@ -46,4 +57,5 @@ export type OverflowPolicy = "error" | "warn" | "ignore";
|
|
|
46
57
|
export declare function packChildren(children: PDFElement[], maxHeight: number, width: number, ctx: LayoutContext, gap?: number): {
|
|
47
58
|
fitted: PDFElement[];
|
|
48
59
|
remainder: PDFElement[];
|
|
60
|
+
forceBreak: boolean;
|
|
49
61
|
};
|
|
@@ -10,7 +10,7 @@ export function isFragmentable(element) {
|
|
|
10
10
|
const canFragment = element.canFragment;
|
|
11
11
|
return typeof canFragment !== "function" || canFragment.call(element);
|
|
12
12
|
}
|
|
13
|
-
function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
13
|
+
export function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
14
14
|
if (policy === "ignore")
|
|
15
15
|
return;
|
|
16
16
|
const name = child.constructor.name.replace(/Element$/, "");
|
|
@@ -21,6 +21,14 @@ function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
|
21
21
|
throw new Error(`Layout overflow: ${detail}`);
|
|
22
22
|
console.warn(`Layout overflow (clipped): ${detail}`);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* True if packing `children` will hit a forced cut: a child that IS a break, contains one deeper in its
|
|
26
|
+
* subtree, or asks to break before/after itself. A flow container reports this from `hasForcedBreak` so
|
|
27
|
+
* its parent fragments it (runs `packChildren`) instead of placing it whole and swallowing the cut.
|
|
28
|
+
*/
|
|
29
|
+
export function childrenForceBreak(children) {
|
|
30
|
+
return children.some((child) => child.hasForcedBreak() || child.breaksBefore() || child.breaksAfter());
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* Packs a vertical stack of children into `maxHeight`, in order. Children are measured
|
|
26
34
|
* against `width` (unbounded height) and added until one would overflow; that straddling
|
|
@@ -39,30 +47,61 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
39
47
|
const fitted = [];
|
|
40
48
|
const remainder = [];
|
|
41
49
|
let usedHeight = 0;
|
|
50
|
+
// Everything from index `from` onward spills to the next region; `broke` says whether the cut was
|
|
51
|
+
// a FORCED page break (so an enclosing flow stops too) rather than the region filling up.
|
|
52
|
+
const spill = (from, broke) => {
|
|
53
|
+
for (let j = from; j < children.length; j++)
|
|
54
|
+
remainder.push(children[j]);
|
|
55
|
+
return { fitted, remainder, forceBreak: broke };
|
|
56
|
+
};
|
|
42
57
|
for (let i = 0; i < children.length; i++) {
|
|
43
58
|
const child = children[i];
|
|
59
|
+
// A forced break: consume the marker (it draws nothing) and send everything after it to a fresh
|
|
60
|
+
// page, regardless of how much room is left.
|
|
61
|
+
if (child.isPageBreak())
|
|
62
|
+
return spill(i + 1, true);
|
|
63
|
+
// break-before: this child wants a fresh page. Honour it only if something is already on this region
|
|
64
|
+
// (CSS ignores break-before at the top of a page); that guard also prevents an empty page + a loop.
|
|
65
|
+
if (child.breaksBefore() && fitted.length > 0)
|
|
66
|
+
return spill(i, true);
|
|
44
67
|
const childHeight = child.calculateLayout(BoxConstraints.loose(width, Infinity), { x: 0, y: 0 }, ctx).height;
|
|
45
68
|
// A gap precedes every child except the first one placed in this region.
|
|
46
69
|
const lead = fitted.length > 0 ? gap : 0;
|
|
47
|
-
if
|
|
70
|
+
// Place whole only if it fits AND holds no forced break: a child that fits by height but contains
|
|
71
|
+
// a `PageBreak` (or a break-before/after) in its subtree must still be fragmented, or the break
|
|
72
|
+
// would be swallowed.
|
|
73
|
+
if (usedHeight + lead + childHeight <= maxHeight && !child.hasForcedBreak()) {
|
|
48
74
|
fitted.push(child);
|
|
49
75
|
usedHeight += lead + childHeight;
|
|
76
|
+
// break-after: everything after this child starts a fresh page.
|
|
77
|
+
if (child.breaksAfter())
|
|
78
|
+
return spill(i + 1, true);
|
|
50
79
|
continue;
|
|
51
80
|
}
|
|
52
|
-
// `child` straddles the boundary
|
|
81
|
+
// `child` straddles the boundary, or carries a forced break. Split it; otherwise place/defer whole.
|
|
53
82
|
const remaining = maxHeight - usedHeight - lead;
|
|
54
83
|
let placedPart = false;
|
|
84
|
+
let childBroke = false;
|
|
55
85
|
if (isFragmentable(child)) {
|
|
56
86
|
const split = child.fragment(Math.max(0, remaining), width, ctx);
|
|
87
|
+
childBroke = split.forceBreak ?? false;
|
|
57
88
|
if (split.fitted) {
|
|
58
89
|
fitted.push(split.fitted);
|
|
90
|
+
// The child fully fit and triggered no cut: it only reached this path because its subtree
|
|
91
|
+
// FLAGGED a possible break (hasForcedBreak) that turned out to be a no-op here - e.g. a
|
|
92
|
+
// break-before at the top of the region, which CSS ignores. Keep packing the siblings after
|
|
93
|
+
// it instead of spilling them, so break-before does not accidentally push later content away.
|
|
94
|
+
if (!split.remainder && !childBroke) {
|
|
95
|
+
usedHeight += lead + childHeight;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
59
98
|
if (split.remainder)
|
|
60
99
|
remainder.push(split.remainder);
|
|
61
100
|
placedPart = true;
|
|
62
101
|
}
|
|
63
102
|
}
|
|
64
103
|
if (!placedPart) {
|
|
65
|
-
if (fitted.length === 0) {
|
|
104
|
+
if (fitted.length === 0 && !childBroke) {
|
|
66
105
|
// Taller than the whole region and unsplittable: force it on (it overflows and is clipped)
|
|
67
106
|
// so the next region still advances - and surface it per the overflow policy.
|
|
68
107
|
reportOverflow(child, childHeight, maxHeight, ctx.onOverflow ?? "ignore");
|
|
@@ -72,9 +111,7 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
72
111
|
remainder.push(child);
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
|
-
|
|
76
|
-
remainder.push(children[j]);
|
|
77
|
-
break;
|
|
114
|
+
return spill(i + 1, childBroke);
|
|
78
115
|
}
|
|
79
|
-
return { fitted, remainder };
|
|
116
|
+
return { fitted, remainder, forceBreak: false };
|
|
80
117
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { AnchorElement } from "../elements/layout/anchor-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
export declare class AnchorRenderer {
|
|
5
|
+
static render(element: AnchorElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class AnchorRenderer {
|
|
3
|
+
static async render(element, objectManager) {
|
|
4
|
+
const { name, child, y } = element.getProps();
|
|
5
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
+
const childNodes = renderer ? await renderer(child, objectManager) : [];
|
|
7
|
+
// The child draws normally; an Anchor IR node carries the name + scroll target for the page renderer
|
|
8
|
+
// to register as a named destination (it produces no content-stream ops itself).
|
|
9
|
+
return [...childNodes, { type: "anchor", y, name }];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
export declare class BookmarkRenderer {
|
|
5
|
+
static render(element: BookmarkElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class BookmarkRenderer {
|
|
3
|
+
static async render(element, objectManager) {
|
|
4
|
+
const { title, level, child, y } = element.getProps();
|
|
5
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
+
const childNodes = renderer ? await renderer(child, objectManager) : [];
|
|
7
|
+
// The child draws normally; an Outline IR node carries the title + nesting + scroll target for the
|
|
8
|
+
// page renderer to collect into the /Outlines tree (it produces no content-stream ops itself).
|
|
9
|
+
return [...childNodes, { type: "outline", y, title, level }];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
2
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* A `keepTogether` is layout-transparent: it just renders its child. All of its work happens in the
|
|
6
|
+
* layout pass (`fragment`), never here.
|
|
7
|
+
*/
|
|
8
|
+
export declare class KeepTogetherRenderer {
|
|
9
|
+
static render(group: KeepTogetherElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* A `keepTogether` is layout-transparent: it just renders its child. All of its work happens in the
|
|
4
|
+
* layout pass (`fragment`), never here.
|
|
5
|
+
*/
|
|
6
|
+
export class KeepTogetherRenderer {
|
|
7
|
+
static async render(group, objectManager) {
|
|
8
|
+
const { child } = group.getProps();
|
|
9
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
10
|
+
return renderer ? await renderer(child, objectManager) : [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { LinkElement } from "../elements/layout/link-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
export declare class LinkRenderer {
|
|
5
|
+
static render(element: LinkElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class LinkRenderer {
|
|
3
|
+
static async render(element, objectManager) {
|
|
4
|
+
const { href, dest, child, x, y, width, height } = element.getProps();
|
|
5
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
+
const childNodes = renderer ? await renderer(child, objectManager) : [];
|
|
7
|
+
// The child draws normally; a Link IR node carries the clickable rect + target (external href or
|
|
8
|
+
// internal dest) for the page renderer to emit as a /Link annotation (no content-stream ops itself).
|
|
9
|
+
return [...childNodes, { type: "link", x, y, width, height, href, dest }];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* A `PageBreak` draws nothing - the pagination packer consumes it at the cut, so in a vertical flow it
|
|
6
|
+
* never reaches this pass. Reaching it therefore means the break was ineffective: it sat somewhere that
|
|
7
|
+
* does not paginate, like inside a horizontal `Row` (a row is one line, you cannot split it across
|
|
8
|
+
* pages - CSS and react-pdf ignore a forced break there too). We degrade to drawing nothing, but warn
|
|
9
|
+
* once so the mistake is not silent. This one choke-point catches every ineffective break; no element
|
|
10
|
+
* needs a special case.
|
|
11
|
+
*/
|
|
12
|
+
export declare class PageBreakRenderer {
|
|
13
|
+
static render(_element: PageBreakElement, _objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `PageBreak` draws nothing - the pagination packer consumes it at the cut, so in a vertical flow it
|
|
3
|
+
* never reaches this pass. Reaching it therefore means the break was ineffective: it sat somewhere that
|
|
4
|
+
* does not paginate, like inside a horizontal `Row` (a row is one line, you cannot split it across
|
|
5
|
+
* pages - CSS and react-pdf ignore a forced break there too). We degrade to drawing nothing, but warn
|
|
6
|
+
* once so the mistake is not silent. This one choke-point catches every ineffective break; no element
|
|
7
|
+
* needs a special case.
|
|
8
|
+
*/
|
|
9
|
+
export class PageBreakRenderer {
|
|
10
|
+
static async render(_element, _objectManager) {
|
|
11
|
+
console.warn("PageBreak had no effect: it is not in a paginating flow (e.g. it is inside a Row, which is a " +
|
|
12
|
+
"single horizontal line). Put the break as a sibling in a Column to break after that content.");
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
export declare class PageBuilderRenderer {
|
|
5
|
+
static render(element: PageBuilderElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class PageBuilderRenderer {
|
|
3
|
+
static async render(element, objectManager) {
|
|
4
|
+
// `composed` is whatever the closure built for THIS page during the render pass's layout.
|
|
5
|
+
const { composed } = element.getProps();
|
|
6
|
+
if (!composed)
|
|
7
|
+
return [];
|
|
8
|
+
const renderer = RendererRegistry.getRenderer(composed);
|
|
9
|
+
return renderer ? renderer(composed, objectManager) : [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -28,7 +28,8 @@ export class PageRenderer {
|
|
|
28
28
|
// Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
|
|
29
29
|
// serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).
|
|
30
30
|
const structCtx = objectManager.struct.enabled ? objectManager.struct.beginPage() : undefined;
|
|
31
|
-
const
|
|
31
|
+
const flipped = PdfBackend.flipY(nodes, height);
|
|
32
|
+
const pageContent = PdfBackend.serialize(flipped, objectManager, structCtx);
|
|
32
33
|
// Add the page content as a new object (FlateDecode-compressed when enabled). The /Length is
|
|
33
34
|
// computed inside, with an explicit EOL before `endstream` (PDF/A clause 6.1.7.1).
|
|
34
35
|
const contentObjectNumber = objectManager.addContentStream(pageContent);
|
|
@@ -67,14 +68,48 @@ export class PageRenderer {
|
|
|
67
68
|
shadingReferences.push(`/${name} ${objectNumber} 0 R`);
|
|
68
69
|
});
|
|
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(" ")}]` : "";
|
|
70
89
|
// Tagging only: /StructParents links this page's marked content to the ParentTree, /Tabs /S makes the
|
|
71
90
|
// logical structure order the tab/reading order (a PDF/UA requirement).
|
|
72
91
|
const structAttrs = structCtx ? ` /StructParents ${structCtx.structParents} /Tabs /S` : "";
|
|
73
|
-
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} >>`;
|
|
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} >>`;
|
|
74
93
|
// Add page as new object; register its object number with the struct tree (for /Pg refs), then return it.
|
|
75
94
|
const pageObjNum = objectManager.addObject(pageObject);
|
|
76
95
|
if (structCtx)
|
|
77
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
|
+
}
|
|
78
113
|
return pageObjNum;
|
|
79
114
|
}
|
|
80
115
|
}
|