@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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { resolveExtent } from "../layout/box-constraints.js";
|
|
1
2
|
import { FlexLayoutHelper, HORIZONTAL_AXIS } from "../utils/flex-layout.js";
|
|
2
|
-
import { SizedPDFElement } from "./pdf-element.js";
|
|
3
|
+
import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
|
|
3
4
|
/**
|
|
4
5
|
* Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
|
|
5
6
|
* left-to-right via the shared `FlexLayoutHelper` on the horizontal axis; fixed children
|
|
@@ -13,26 +14,54 @@ import { SizedPDFElement } from "./pdf-element.js";
|
|
|
13
14
|
* Grid/Table concern.
|
|
14
15
|
*/
|
|
15
16
|
export class RowElement extends SizedPDFElement {
|
|
16
|
-
constructor({ children, gap, main, cross }) {
|
|
17
|
+
constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, }) {
|
|
17
18
|
super({ x: 0, y: 0 });
|
|
18
19
|
this.children = children;
|
|
19
20
|
this.gap = gap ?? 0;
|
|
20
21
|
this.main = main ?? "start";
|
|
21
22
|
this.cross = cross ?? "stretch";
|
|
23
|
+
this.requested = { width, height, widthFactor, heightFactor };
|
|
24
|
+
}
|
|
25
|
+
relativeSizeFactor(horizontal) {
|
|
26
|
+
return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
|
|
27
|
+
}
|
|
28
|
+
/** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
|
|
29
|
+
* descendant propagates on both axes (see the Column for the reasoning). */
|
|
30
|
+
needsBoundedMain(horizontal) {
|
|
31
|
+
const requested = horizontal
|
|
32
|
+
? [this.requested.width, this.requested.widthFactor]
|
|
33
|
+
: [this.requested.height, this.requested.heightFactor];
|
|
34
|
+
if (requested.some((v) => v !== undefined))
|
|
35
|
+
return false;
|
|
36
|
+
const ownFlexChild = horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
|
|
37
|
+
return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
|
|
22
38
|
}
|
|
23
39
|
calculateLayout(constraints, offset, ctx) {
|
|
24
40
|
this.x = offset.x;
|
|
25
41
|
this.y = offset.y;
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
const
|
|
42
|
+
// Relative sizing: a pinned extent (fixed or a fraction of the offered box, clamped) wins; else
|
|
43
|
+
// width fills the offered space (flex children split the leftover) and height is the tallest child.
|
|
44
|
+
const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
|
|
45
|
+
const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
|
|
46
|
+
const boundedWidth = explicitWidth !== undefined
|
|
47
|
+
? constraints.constrainWidth(explicitWidth)
|
|
48
|
+
: constraints.hasBoundedWidth
|
|
49
|
+
? constraints.maxWidth
|
|
50
|
+
: undefined;
|
|
51
|
+
const boundedHeight = explicitHeight !== undefined
|
|
52
|
+
? constraints.constrainHeight(explicitHeight)
|
|
53
|
+
: constraints.hasBoundedHeight
|
|
54
|
+
? constraints.maxHeight
|
|
55
|
+
: undefined;
|
|
56
|
+
// Horizontal stack: main = width (children fill it), cross = height (children can stretch to it).
|
|
57
|
+
const mainAvail = boundedWidth ?? Infinity;
|
|
58
|
+
const crossAvail = boundedHeight ?? Infinity;
|
|
30
59
|
let result = { mainUsed: 0, crossUsed: 0 };
|
|
31
60
|
if (this.children.length > 0) {
|
|
32
61
|
result = FlexLayoutHelper.layout(this.children, HORIZONTAL_AXIS, mainAvail, crossAvail, this.x, this.y, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
|
|
33
62
|
}
|
|
34
|
-
this.width =
|
|
35
|
-
this.height =
|
|
63
|
+
this.width = boundedWidth ?? result.mainUsed;
|
|
64
|
+
this.height = boundedHeight ?? result.crossUsed;
|
|
36
65
|
return { width: this.width, height: this.height };
|
|
37
66
|
}
|
|
38
67
|
getProps() {
|
|
@@ -10,7 +10,13 @@ export interface TextSegment {
|
|
|
10
10
|
fontColor?: Color;
|
|
11
11
|
fontFamily?: string;
|
|
12
12
|
fontSize?: number;
|
|
13
|
+
/** External URL: this segment becomes an inline hyperlink (a /Link annotation over its glyphs). */
|
|
14
|
+
href?: string;
|
|
15
|
+
/** Internal named destination (an `Anchor`): this segment links to it (a /GoTo /Link annotation). */
|
|
16
|
+
dest?: string;
|
|
13
17
|
}
|
|
18
|
+
/** Accessibility role for the tagged structure tree: a heading level or a paragraph (the default). */
|
|
19
|
+
export type TextRole = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
|
|
14
20
|
interface TextElementParams {
|
|
15
21
|
id?: string;
|
|
16
22
|
/** Unset (undefined) inherits the cascaded size; see ResolvedTextStyle. */
|
|
@@ -26,6 +32,8 @@ interface TextElementParams {
|
|
|
26
32
|
overflow?: TextOverflow;
|
|
27
33
|
/** Line-height multiplier: each line is `fontSize * lineHeight` tall (default `1`). */
|
|
28
34
|
lineHeight?: number;
|
|
35
|
+
/** Accessibility role for the tagged structure tree (heading level or paragraph; default `"p"`). */
|
|
36
|
+
role?: TextRole;
|
|
29
37
|
}
|
|
30
38
|
export declare class TextElement extends SizedPDFElement implements Fragmentable {
|
|
31
39
|
private readonly rawFontSize?;
|
|
@@ -43,7 +51,8 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
43
51
|
private content;
|
|
44
52
|
private maxLines?;
|
|
45
53
|
private overflow;
|
|
46
|
-
|
|
54
|
+
private readonly role?;
|
|
55
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow, lineHeight, role, }: TextElementParams);
|
|
47
56
|
private resolveStyle;
|
|
48
57
|
private applyStyle;
|
|
49
58
|
/**
|
|
@@ -80,6 +89,7 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
80
89
|
maxLines: number | undefined;
|
|
81
90
|
overflow: TextOverflow;
|
|
82
91
|
lineHeight: number;
|
|
92
|
+
role: TextRole | undefined;
|
|
83
93
|
};
|
|
84
94
|
}
|
|
85
95
|
export {};
|
|
@@ -7,8 +7,9 @@ import { DEFAULT_TEXT_STYLE } from "../text/text-style.js";
|
|
|
7
7
|
import { wrapStringIntoLines, breakSegmentsIntoLines, segmentLinesToSegments, } from "../text/line-breaker.js";
|
|
8
8
|
import { SizedPDFElement } from "./pdf-element.js";
|
|
9
9
|
export class TextElement extends SizedPDFElement {
|
|
10
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow = "clip", lineHeight, }) {
|
|
10
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, overflow = "clip", lineHeight, role, }) {
|
|
11
11
|
super({ x: 0, y: 0 });
|
|
12
|
+
this.role = role;
|
|
12
13
|
this.rawFontSize = fontSize;
|
|
13
14
|
this.rawFontFamily = fontFamily;
|
|
14
15
|
this.rawFontStyle = fontStyle;
|
|
@@ -97,7 +98,8 @@ export class TextElement extends SizedPDFElement {
|
|
|
97
98
|
maxLines: this.maxLines,
|
|
98
99
|
overflow: this.overflow,
|
|
99
100
|
lineHeight: this.lineHeight,
|
|
100
|
-
|
|
101
|
+
role: this.role,
|
|
102
|
+
}).adoptStructId(this); // a wrapped remainder is the SAME logical paragraph (one P across pages)
|
|
101
103
|
}
|
|
102
104
|
calculateLayout(constraints, offset, ctx) {
|
|
103
105
|
this.resolveStyle(ctx);
|
|
@@ -157,6 +159,7 @@ export class TextElement extends SizedPDFElement {
|
|
|
157
159
|
maxLines: this.maxLines,
|
|
158
160
|
overflow: this.overflow,
|
|
159
161
|
lineHeight: this.lineHeight,
|
|
162
|
+
role: this.role,
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -11,6 +11,16 @@ import { FontStyle } from "../utils/pdf-object-manager.js";
|
|
|
11
11
|
* Coordinates are in PDF points (1/72"). Which origin they use is still the
|
|
12
12
|
* producer's concern for now; centralizing the Y-flip at this seam is Phase 3.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Structure tag for accessible (PDF/UA) tagging. `role` is the PDF structure type the content maps to
|
|
16
|
+
* (`P`, `H1`..`H6`, `Figure`, `TD`, …); `key` groups the IR nodes of one logical element (e.g. every line
|
|
17
|
+
* of a paragraph) into a single structure element; `alt` is the alternate text for a figure. A drawable
|
|
18
|
+
* node WITHOUT a tag is emitted as an Artifact (decoration, skipped by screen readers).
|
|
19
|
+
*/
|
|
20
|
+
export interface StructTag {
|
|
21
|
+
role: string;
|
|
22
|
+
key: number;
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* A single positioned run of text in one font / size / color. Line wrapping has
|
|
16
26
|
* already happened upstream: the producer emits one `TextRun` per laid-out line or
|
|
@@ -25,6 +35,7 @@ export interface TextRun {
|
|
|
25
35
|
fontStyle: FontStyle;
|
|
26
36
|
fontSize: number;
|
|
27
37
|
color: Color;
|
|
38
|
+
tag?: StructTag;
|
|
28
39
|
}
|
|
29
40
|
/** An axis-aligned rectangle. An absent `fill` or `stroke` means that part is not drawn. */
|
|
30
41
|
export interface Rect {
|
|
@@ -38,6 +49,7 @@ export interface Rect {
|
|
|
38
49
|
strokeWidth: number;
|
|
39
50
|
/** Corner radius in points; absent/0 = sharp corners (plain `re`). */
|
|
40
51
|
radius?: number;
|
|
52
|
+
tag?: StructTag;
|
|
41
53
|
}
|
|
42
54
|
/** A straight line segment between two points. */
|
|
43
55
|
export interface Line {
|
|
@@ -48,6 +60,7 @@ export interface Line {
|
|
|
48
60
|
y2: number;
|
|
49
61
|
stroke: Color;
|
|
50
62
|
strokeWidth: number;
|
|
63
|
+
tag?: StructTag;
|
|
51
64
|
}
|
|
52
65
|
/** A raster image, already resolved to bytes by the producer (no `CustomImage` here). */
|
|
53
66
|
export interface Image {
|
|
@@ -71,6 +84,7 @@ export interface Image {
|
|
|
71
84
|
};
|
|
72
85
|
/** Corner radius in points for the image box; absent/0 = sharp corners. */
|
|
73
86
|
radius?: number;
|
|
87
|
+
tag?: StructTag;
|
|
74
88
|
}
|
|
75
89
|
/**
|
|
76
90
|
* Pushes a clipping region (a rectangle, rounded when `radius` is set). Everything between this and
|
|
@@ -89,5 +103,127 @@ export interface ClipPush {
|
|
|
89
103
|
export interface ClipPop {
|
|
90
104
|
type: "clip-pop";
|
|
91
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* A hyperlink over a rectangle. Unlike every other IR node this draws NOTHING into the content stream -
|
|
108
|
+
* it becomes a PDF Link ANNOTATION on the page (`/Annots`), a side channel next to the drawing. Exactly
|
|
109
|
+
* one target is set: `href` is an external URL (a `/URI` action); `dest` is a named destination inside
|
|
110
|
+
* this document (a `/GoTo` action, resolved via the catalog `/Names /Dests` tree - see `Anchor`). The
|
|
111
|
+
* rect is the clickable region in top-left engine coords; the Y-flip converts it to PDF page space.
|
|
112
|
+
*
|
|
113
|
+
* NOTE: a Link inside a `TransformPush` is NOT yet rotated with it - the annotation rect lives in page
|
|
114
|
+
* space and ignores the content-stream matrix. Completing that is a planned follow-up (see todo.md).
|
|
115
|
+
*/
|
|
116
|
+
export interface Link {
|
|
117
|
+
type: "link";
|
|
118
|
+
x: number;
|
|
119
|
+
y: number;
|
|
120
|
+
width: number;
|
|
121
|
+
height: number;
|
|
122
|
+
href?: string;
|
|
123
|
+
dest?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A named destination anchor - a jump TARGET for an internal `Link({ to })`. Like `Link` it draws
|
|
127
|
+
* NOTHING; it registers `name` -> (this page, `y`) in the catalog `/Names /Dests` name tree. `y` is the
|
|
128
|
+
* top of the target in top-left engine coords (the Y-flip turns it into the page-space scroll target).
|
|
129
|
+
*/
|
|
130
|
+
export interface Anchor {
|
|
131
|
+
type: "anchor";
|
|
132
|
+
y: number;
|
|
133
|
+
name: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A document-outline (bookmark) anchor. Like `Link` it draws NOTHING - it becomes an entry in the PDF
|
|
137
|
+
* outline tree (`/Outlines`), a side channel the viewer shows in its bookmark panel. `y` is the top of
|
|
138
|
+
* the target in top-left engine coords (the Y-flip turns it into the page-space scroll target); `level`
|
|
139
|
+
* (1-based) nests the entry under the nearest preceding entry of a smaller level.
|
|
140
|
+
*/
|
|
141
|
+
export interface Outline {
|
|
142
|
+
type: "outline";
|
|
143
|
+
y: number;
|
|
144
|
+
title: string;
|
|
145
|
+
level: number;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
|
|
149
|
+
* matching `TransformPop` is painted through `matrix`. This is what `Rotated` wraps its child's nodes
|
|
150
|
+
* in, so a subtree draws rotated (or scaled/translated) around a pivot without touching its layout.
|
|
151
|
+
*
|
|
152
|
+
* `matrix` is `[a, b, c, d, e, f]` in the engine's TOP-LEFT coordinates - the same space every other IR
|
|
153
|
+
* node uses. The Y-flip at the IR -> backend seam converts it to PDF's bottom-left once per page, so
|
|
154
|
+
* producers stay coordinate-system-blind (they never see a flipped matrix).
|
|
155
|
+
*/
|
|
156
|
+
export interface TransformPush {
|
|
157
|
+
type: "transform-push";
|
|
158
|
+
matrix: [number, number, number, number, number, number];
|
|
159
|
+
}
|
|
160
|
+
/** Closes the most recent `TransformPush` (restores the graphics state). */
|
|
161
|
+
export interface TransformPop {
|
|
162
|
+
type: "transform-pop";
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* One drawing command of a `Path`, in absolute page coordinates. Curves are CUBIC because PDF
|
|
166
|
+
* content streams have no quadratic operator - a producer converts TrueType quads to cubics before
|
|
167
|
+
* emitting. `z` closes the current subpath (a glyph contour).
|
|
168
|
+
*/
|
|
169
|
+
export type PathCommand = {
|
|
170
|
+
op: "m";
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
} | {
|
|
174
|
+
op: "l";
|
|
175
|
+
x: number;
|
|
176
|
+
y: number;
|
|
177
|
+
} | {
|
|
178
|
+
op: "c";
|
|
179
|
+
x1: number;
|
|
180
|
+
y1: number;
|
|
181
|
+
x2: number;
|
|
182
|
+
y2: number;
|
|
183
|
+
x: number;
|
|
184
|
+
y: number;
|
|
185
|
+
} | {
|
|
186
|
+
op: "z";
|
|
187
|
+
};
|
|
188
|
+
/** One color stop of a gradient: a position 0..1 along the axis and its color. */
|
|
189
|
+
export interface GradientStop {
|
|
190
|
+
offset: number;
|
|
191
|
+
color: Color;
|
|
192
|
+
}
|
|
193
|
+
/** An axial (linear) gradient between two points, in absolute page coordinates. */
|
|
194
|
+
export interface LinearGradient {
|
|
195
|
+
type: "linear";
|
|
196
|
+
x0: number;
|
|
197
|
+
y0: number;
|
|
198
|
+
x1: number;
|
|
199
|
+
y1: number;
|
|
200
|
+
stops: GradientStop[];
|
|
201
|
+
extend: "pad" | "repeat" | "reflect";
|
|
202
|
+
}
|
|
203
|
+
/** A radial gradient between two circles, in absolute page coordinates. */
|
|
204
|
+
export interface RadialGradient {
|
|
205
|
+
type: "radial";
|
|
206
|
+
x0: number;
|
|
207
|
+
y0: number;
|
|
208
|
+
r0: number;
|
|
209
|
+
x1: number;
|
|
210
|
+
y1: number;
|
|
211
|
+
r1: number;
|
|
212
|
+
stops: GradientStop[];
|
|
213
|
+
extend: "pad" | "repeat" | "reflect";
|
|
214
|
+
}
|
|
215
|
+
export type Gradient = LinearGradient | RadialGradient;
|
|
216
|
+
/**
|
|
217
|
+
* A filled vector path - one or more closed subpaths sharing a single fill, using the nonzero
|
|
218
|
+
* winding rule (so a glyph's inner contour cuts a hole). This is what a color glyph's layers draw
|
|
219
|
+
* onto: each COLR layer is one `Path` filled with a solid `Color` (v0) or a `Gradient` (v1).
|
|
220
|
+
*/
|
|
221
|
+
export interface Path {
|
|
222
|
+
type: "path";
|
|
223
|
+
commands: PathCommand[];
|
|
224
|
+
fill: Color | Gradient;
|
|
225
|
+
/** Accessibility structure tag (absent = drawn as an artifact); color-emoji layers stay untagged. */
|
|
226
|
+
tag?: StructTag;
|
|
227
|
+
}
|
|
92
228
|
/** The closed set of primitives the PDF backend knows how to draw. */
|
|
93
|
-
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop;
|
|
229
|
+
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
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { BoxConstraints } from "./box-constraints.js";
|
|
2
2
|
export function isFragmentable(element) {
|
|
3
|
-
|
|
4
|
-
typeof element.fragment
|
|
3
|
+
if (!("fragment" in element) ||
|
|
4
|
+
typeof element.fragment !== "function") {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
// An element may veto splitting at runtime via canFragment() - a transparent StructGroup only fragments
|
|
8
|
+
// when its wrapped child does, so a non-splittable wrapped child (e.g. a table row) is moved whole to the
|
|
9
|
+
// next page instead of being clipped at the boundary. Elements without canFragment() are always splittable.
|
|
10
|
+
const canFragment = element.canFragment;
|
|
11
|
+
return typeof canFragment !== "function" || canFragment.call(element);
|
|
5
12
|
}
|
|
6
13
|
function reportOverflow(child, childHeight, maxHeight, policy) {
|
|
7
14
|
if (policy === "ignore")
|
|
@@ -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,9 @@
|
|
|
1
|
+
import { Image } from "../ir/display-list.js";
|
|
2
|
+
export declare function emojiImageUrl(codePoint: number, source: {
|
|
3
|
+
url: string;
|
|
4
|
+
format: string;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function emojiImageNode(codePoint: number, source: {
|
|
7
|
+
url: string;
|
|
8
|
+
format: string;
|
|
9
|
+
}, x: number, baselineY: number, em: number): Promise<Image | null>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CustomBytesImage } from "../elements/image-element.js";
|
|
2
|
+
import { decodePngToRgbFlate } from "../utils/image-helper.js";
|
|
3
|
+
import { bytesFromLatin1 } from "../utils/bytes.js";
|
|
4
|
+
// Per-URL fetch cache (process-wide): an emoji image is fetched once, however many times it appears.
|
|
5
|
+
const cache = new Map();
|
|
6
|
+
const FETCH_TIMEOUT_MS = 5000; // a slow CDN must not stall the whole render
|
|
7
|
+
async function fetchBytes(url) {
|
|
8
|
+
const cached = cache.get(url);
|
|
9
|
+
if (cached)
|
|
10
|
+
return cached;
|
|
11
|
+
const pending = (async () => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
14
|
+
try {
|
|
15
|
+
const r = await fetch(url, { signal: controller.signal });
|
|
16
|
+
return r.ok ? new Uint8Array(await r.arrayBuffer()) : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null; // offline / 404 / timeout -> null; the caller leaves a blank gap (graceful)
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
cache.set(url, pending);
|
|
26
|
+
// Cache only SUCCESSFUL results: drop a failure so a transient error (offline) can be retried later.
|
|
27
|
+
void pending.then((bytes) => {
|
|
28
|
+
if (!bytes && cache.get(url) === pending)
|
|
29
|
+
cache.delete(url);
|
|
30
|
+
});
|
|
31
|
+
return pending;
|
|
32
|
+
}
|
|
33
|
+
// Twemoji-style URL: base + lowercase hex code point + "." + format (e.g. ".../1f600.png"). Single
|
|
34
|
+
// code points only - multi-code-point sequences (flags, ZWJ) are out of scope for now.
|
|
35
|
+
export function emojiImageUrl(codePoint, source) {
|
|
36
|
+
return `${source.url}${codePoint.toString(16)}.${source.format}`;
|
|
37
|
+
}
|
|
38
|
+
// Builds an `Image` IR node for an emoji drawn as a 1em square seated on the baseline (mostly above
|
|
39
|
+
// it, a little below - like a text glyph), or null when the fetch/decode fails.
|
|
40
|
+
export async function emojiImageNode(codePoint, source, x, baselineY, em) {
|
|
41
|
+
const bytes = await fetchBytes(emojiImageUrl(codePoint, source));
|
|
42
|
+
if (!bytes)
|
|
43
|
+
return null;
|
|
44
|
+
const img = new CustomBytesImage(bytes);
|
|
45
|
+
const imageType = await img.getImageType();
|
|
46
|
+
const fileData = await img.getFileData();
|
|
47
|
+
const dims = await img.getImageDimensions();
|
|
48
|
+
// JPEG embeds raw; PNG is decoded to DeviceRGB + an /SMask alpha channel (transparency).
|
|
49
|
+
let data = fileData;
|
|
50
|
+
let smask;
|
|
51
|
+
if (imageType === "FlateDecode") {
|
|
52
|
+
const decoded = await decodePngToRgbFlate(bytesFromLatin1(fileData));
|
|
53
|
+
data = decoded.data;
|
|
54
|
+
smask = decoded.smask;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
type: "image",
|
|
58
|
+
x,
|
|
59
|
+
y: baselineY - em * 0.82,
|
|
60
|
+
width: em,
|
|
61
|
+
height: em,
|
|
62
|
+
intrinsicWidth: dims.width,
|
|
63
|
+
intrinsicHeight: dims.height,
|
|
64
|
+
data,
|
|
65
|
+
imageType,
|
|
66
|
+
...(smask ? { smask } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -2,5 +2,5 @@ import { ImageElement } from "../elements/image-element.js";
|
|
|
2
2
|
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
3
|
import { IRNode } from "../ir/display-list.js";
|
|
4
4
|
export declare class ImageRenderer {
|
|
5
|
-
static render(imageElement: ImageElement,
|
|
5
|
+
static render(imageElement: ImageElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
6
|
}
|
|
@@ -2,9 +2,9 @@ import { BoxFit } from "../elements/image-element.js";
|
|
|
2
2
|
import { bytesFromLatin1 } from "../utils/bytes.js";
|
|
3
3
|
import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, } from "../utils/image-helper.js";
|
|
4
4
|
export class ImageRenderer {
|
|
5
|
-
static async render(imageElement,
|
|
5
|
+
static async render(imageElement, objectManager) {
|
|
6
6
|
// Load the image and convert it in a binary string
|
|
7
|
-
let { x, y, width, height, image, fit, radius } = imageElement.getProps();
|
|
7
|
+
let { x, y, width, height, image, fit, radius, alt } = imageElement.getProps();
|
|
8
8
|
await image.init(); // Load and initialize the image
|
|
9
9
|
const imageType = await image.getImageType(); // For the moment we can handle `png` and `jpg/jpeg` files
|
|
10
10
|
const fileData = await image.getFileData();
|
|
@@ -82,6 +82,14 @@ export class ImageRenderer {
|
|
|
82
82
|
}
|
|
83
83
|
: {}),
|
|
84
84
|
};
|
|
85
|
+
// Accessible tagging: an image WITH alt text is a Figure; without, it stays untagged and the backend
|
|
86
|
+
// treats it as decoration (an Artifact). One struct element per image.
|
|
87
|
+
if (objectManager.struct.enabled && alt) {
|
|
88
|
+
node.tag = {
|
|
89
|
+
role: "Figure",
|
|
90
|
+
key: objectManager.struct.openElement(imageElement.structId, "Figure", { alt }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
85
93
|
return [node];
|
|
86
94
|
}
|
|
87
95
|
}
|
|
@@ -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,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
|
+
}
|