@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.1
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 +29 -10
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +45 -5
- package/dist/api/content.js +96 -7
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +57 -0
- package/dist/api/dimension.js +27 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +135 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +18 -7
- package/dist/api/layout.d.ts +20 -6
- package/dist/api/layout.js +31 -6
- package/dist/api/page-builder.d.ts +8 -0
- package/dist/api/page-builder.js +12 -0
- package/dist/api/structure.d.ts +12 -0
- package/dist/api/structure.js +54 -7
- package/dist/api/text.d.ts +65 -8
- package/dist/api/text.js +38 -5
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/edit/document.d.ts +14 -4
- package/dist/edit/document.js +15 -22
- package/dist/edit/fill.d.ts +19 -2
- package/dist/edit/fill.js +63 -34
- package/dist/edit/flatten.d.ts +13 -1
- package/dist/edit/flatten.js +38 -13
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +5 -0
- package/dist/edit/objects.js +27 -0
- package/dist/edit/writer.d.ts +7 -0
- package/dist/edit/writer.js +23 -1
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +23 -1
- package/dist/elements/container-element.js +31 -10
- package/dist/elements/image-element.d.ts +18 -4
- package/dist/elements/image-element.js +27 -12
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +5 -1
- package/dist/elements/layout/expanded-element.js +16 -3
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +9 -2
- package/dist/elements/layout/padding-element.js +12 -2
- package/dist/elements/layout/page-builder-element.js +2 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +75 -1
- package/dist/elements/pdf-element.js +80 -1
- package/dist/elements/rectangle-element.d.ts +22 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +26 -1
- package/dist/elements/row-element.js +31 -10
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +92 -1
- package/dist/elements/text-element.js +208 -33
- package/dist/ir/display-list.d.ts +67 -11
- package/dist/ir/display-list.js +8 -1
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +53 -0
- package/dist/layout/box-constraints.js +72 -0
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/image-renderer.js +2 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +21 -4
- package/dist/renderer/pdf-backend.js +198 -61
- package/dist/renderer/pdf-document-renderer.js +8 -1
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +9 -5
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.d.ts +2 -2
- package/dist/renderer/text-renderer.js +176 -57
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +8 -4
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +43 -1
- package/dist/text/line-breaker.js +181 -45
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +91 -0
- package/dist/text/text-style.d.ts +38 -0
- package/dist/text/text-style.js +50 -1
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/flex-layout.d.ts +24 -4
- package/dist/utils/flex-layout.js +218 -7
- package/dist/utils/font-metrics.d.ts +13 -2
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/pdf-object-manager.d.ts +44 -3
- package/dist/utils/pdf-object-manager.js +176 -14
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/ttf-parser.d.ts +6 -0
- package/dist/utils/ttf-parser.js +19 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/validators/element-validator.js +9 -2
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +6 -2
|
@@ -41,11 +41,22 @@ export declare class PdfBackend {
|
|
|
41
41
|
static escapePdfString(text: string): string;
|
|
42
42
|
/**
|
|
43
43
|
* Builds the `[ ... ]` operand of a `TJ` from a run and its per-gap kern adjustments (em/1000, the
|
|
44
|
-
* sign the font declares
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* sign the font declares). Generic over the UNIT so the same chunking serves both paths: characters
|
|
45
|
+
* for a WinAnsi run, glyph ids for a shaped one - a shaped run cannot be expressed as text.
|
|
46
|
+
* A `TJ` number moves the pen LEFT, so it is the NEGATED kern. Consecutive un-kerned glyphs stay in
|
|
47
|
+
* one encoded chunk, so `[(T) 40 (otal)]` rather than one chunk per glyph. `kerns[i]` is the
|
|
48
|
+
* adjustment AFTER code point `i`.
|
|
47
49
|
*/
|
|
48
|
-
static kernedArray(
|
|
50
|
+
static kernedArray<T>(units: readonly T[], kerns: number[], encode: (chunk: T[]) => string): string;
|
|
51
|
+
/** The path-construction operators for a command list. Shared by a filled path and by a clip. */
|
|
52
|
+
private static pathOps;
|
|
53
|
+
/**
|
|
54
|
+
* The graphics-state operators for a stroke, in the order a content stream wants them. Only what
|
|
55
|
+
* differs from the PDF default is emitted, EXCEPT the miter limit: SVG's initial value is 4 and
|
|
56
|
+
* PDF's is 10, so a mitered join has to say so or a sharp corner grows a spike the source never had.
|
|
57
|
+
* The caller isolates these in a q/Q - `d` in particular would otherwise dash every later stroke.
|
|
58
|
+
*/
|
|
59
|
+
private static strokeState;
|
|
49
60
|
/**
|
|
50
61
|
* Returns the `/GSx gs` operator that selects a transparency state, or `""` when both
|
|
51
62
|
* alphas are fully opaque. Opaque draws emit nothing here, so output stays
|
|
@@ -58,6 +69,12 @@ export declare class PdfBackend {
|
|
|
58
69
|
* (kappa ≈ 0.5523). Returns m/l/c/h ops WITHOUT the paint operator.
|
|
59
70
|
*/
|
|
60
71
|
private static roundedRectPath;
|
|
72
|
+
/**
|
|
73
|
+
* Clamp the four radii into the box. Each is capped at half the box, and two radii sharing an edge
|
|
74
|
+
* are scaled down together when they would overlap - the CSS rule, and what keeps the path from
|
|
75
|
+
* folding back on itself.
|
|
76
|
+
*/
|
|
77
|
+
private static clampRadii;
|
|
61
78
|
/**
|
|
62
79
|
* Serialize a single display-list node to its content-stream operators.
|
|
63
80
|
* `om` is used only by primitives that allocate PDF resources (images, fonts).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRounded, } from "../ir/display-list.js";
|
|
1
2
|
import { Color } from "../common/color.js";
|
|
2
3
|
/**
|
|
3
4
|
* PDF backend - turns display-list primitives into content-stream operators.
|
|
@@ -18,8 +19,13 @@ export class PdfBackend {
|
|
|
18
19
|
static flipY(nodes, pageHeight) {
|
|
19
20
|
return nodes.map((node) => {
|
|
20
21
|
switch (node.type) {
|
|
21
|
-
case "rect":
|
|
22
|
-
|
|
22
|
+
case "rect": {
|
|
23
|
+
// A gradient fill carries its own page-space anchors; flip them with the box (see "path").
|
|
24
|
+
const fill = node.fill === undefined || node.fill instanceof Color
|
|
25
|
+
? node.fill
|
|
26
|
+
: { ...node.fill, y0: pageHeight - node.fill.y0, y1: pageHeight - node.fill.y1 };
|
|
27
|
+
return { ...node, y: pageHeight - node.y - node.height, fill };
|
|
28
|
+
}
|
|
23
29
|
case "line":
|
|
24
30
|
return {
|
|
25
31
|
...node,
|
|
@@ -40,6 +46,19 @@ export class PdfBackend {
|
|
|
40
46
|
}
|
|
41
47
|
return flipped;
|
|
42
48
|
}
|
|
49
|
+
case "clip-path-push":
|
|
50
|
+
// Its commands live in the same space as a Path's, so they flip the same way.
|
|
51
|
+
return {
|
|
52
|
+
...node,
|
|
53
|
+
commands: node.commands.map((c) => {
|
|
54
|
+
if (c.op === "z")
|
|
55
|
+
return c;
|
|
56
|
+
if (c.op === "c") {
|
|
57
|
+
return { ...c, y1: pageHeight - c.y1, y2: pageHeight - c.y2, y: pageHeight - c.y };
|
|
58
|
+
}
|
|
59
|
+
return { ...c, y: pageHeight - c.y };
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
43
62
|
case "clip-push":
|
|
44
63
|
// Flip the clip rect around its bottom edge, like a rect.
|
|
45
64
|
return { ...node, y: pageHeight - node.y - node.height };
|
|
@@ -57,7 +76,7 @@ export class PdfBackend {
|
|
|
57
76
|
});
|
|
58
77
|
// A gradient fill carries its own page-space anchor points; flip their y too (radii and
|
|
59
78
|
// x stay). A solid Color fill has no geometry.
|
|
60
|
-
const fill = node.fill instanceof Color
|
|
79
|
+
const fill = node.fill === undefined || node.fill instanceof Color
|
|
61
80
|
? node.fill
|
|
62
81
|
: { ...node.fill, y0: pageHeight - node.fill.y0, y1: pageHeight - node.fill.y1 };
|
|
63
82
|
return { ...node, commands, fill };
|
|
@@ -134,6 +153,7 @@ export class PdfBackend {
|
|
|
134
153
|
// catalog entries instead of content-stream operators.
|
|
135
154
|
if (!struct ||
|
|
136
155
|
node.type === "clip-push" ||
|
|
156
|
+
node.type === "clip-path-push" ||
|
|
137
157
|
node.type === "clip-pop" ||
|
|
138
158
|
node.type === "transform-push" ||
|
|
139
159
|
node.type === "transform-pop" ||
|
|
@@ -158,25 +178,72 @@ export class PdfBackend {
|
|
|
158
178
|
}
|
|
159
179
|
/**
|
|
160
180
|
* Builds the `[ ... ]` operand of a `TJ` from a run and its per-gap kern adjustments (em/1000, the
|
|
161
|
-
* sign the font declares
|
|
162
|
-
*
|
|
163
|
-
*
|
|
181
|
+
* sign the font declares). Generic over the UNIT so the same chunking serves both paths: characters
|
|
182
|
+
* for a WinAnsi run, glyph ids for a shaped one - a shaped run cannot be expressed as text.
|
|
183
|
+
* A `TJ` number moves the pen LEFT, so it is the NEGATED kern. Consecutive un-kerned glyphs stay in
|
|
184
|
+
* one encoded chunk, so `[(T) 40 (otal)]` rather than one chunk per glyph. `kerns[i]` is the
|
|
185
|
+
* adjustment AFTER code point `i`.
|
|
164
186
|
*/
|
|
165
|
-
static kernedArray(
|
|
166
|
-
const
|
|
187
|
+
static kernedArray(units, kerns, encode) {
|
|
188
|
+
const gaps = Math.max(0, units.length - 1);
|
|
189
|
+
if (kerns.length !== gaps) {
|
|
190
|
+
// The loop walks the GAPS, so a short list would drop the units past it - silently, and only the
|
|
191
|
+
// last glyph, which is exactly the kind of thing nobody notices until a word reads wrong. An
|
|
192
|
+
// empty run has no gaps at all, so any adjustment for one is a disagreement too.
|
|
193
|
+
throw new Error(`@jasy/pdf: ${kerns.length} kern adjustments for ${units.length} units - expected ${gaps}.`);
|
|
194
|
+
}
|
|
167
195
|
let out = "";
|
|
168
|
-
let chunk =
|
|
196
|
+
let chunk = units.length > 0 ? [units[0]] : [];
|
|
169
197
|
for (let i = 0; i < kerns.length; i++) {
|
|
170
198
|
if (kerns[i] === 0) {
|
|
171
|
-
chunk
|
|
199
|
+
chunk.push(units[i + 1]);
|
|
172
200
|
}
|
|
173
201
|
else {
|
|
174
202
|
out += encode(chunk) + ` ${-kerns[i]} `;
|
|
175
|
-
chunk =
|
|
203
|
+
chunk = [units[i + 1]];
|
|
176
204
|
}
|
|
177
205
|
}
|
|
178
206
|
return `[${out}${encode(chunk)}]`;
|
|
179
207
|
}
|
|
208
|
+
/** The path-construction operators for a command list. Shared by a filled path and by a clip. */
|
|
209
|
+
static pathOps(commands) {
|
|
210
|
+
const f = (n) => n.toFixed(3);
|
|
211
|
+
let out = "";
|
|
212
|
+
for (const c of commands) {
|
|
213
|
+
if (c.op === "m")
|
|
214
|
+
out += `${f(c.x)} ${f(c.y)} m\n`;
|
|
215
|
+
else if (c.op === "l")
|
|
216
|
+
out += `${f(c.x)} ${f(c.y)} l\n`;
|
|
217
|
+
else if (c.op === "c")
|
|
218
|
+
out += `${f(c.x1)} ${f(c.y1)} ${f(c.x2)} ${f(c.y2)} ${f(c.x)} ${f(c.y)} c\n`;
|
|
219
|
+
else
|
|
220
|
+
out += `h\n`;
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The graphics-state operators for a stroke, in the order a content stream wants them. Only what
|
|
226
|
+
* differs from the PDF default is emitted, EXCEPT the miter limit: SVG's initial value is 4 and
|
|
227
|
+
* PDF's is 10, so a mitered join has to say so or a sharp corner grows a spike the source never had.
|
|
228
|
+
* The caller isolates these in a q/Q - `d` in particular would otherwise dash every later stroke.
|
|
229
|
+
*/
|
|
230
|
+
static strokeState(stroke) {
|
|
231
|
+
const CAPS = { butt: 0, round: 1, square: 2 };
|
|
232
|
+
const JOINS = { miter: 0, round: 1, bevel: 2 };
|
|
233
|
+
let ops = `${stroke.width} w\n${stroke.color.toPDFColorString()} RG\n`;
|
|
234
|
+
if (stroke.cap && stroke.cap !== "butt")
|
|
235
|
+
ops += `${CAPS[stroke.cap]} J\n`;
|
|
236
|
+
const join = stroke.join ?? "miter";
|
|
237
|
+
if (join !== "miter")
|
|
238
|
+
ops += `${JOINS[join]} j\n`;
|
|
239
|
+
else
|
|
240
|
+
ops += `${stroke.miterLimit ?? 4} M\n`;
|
|
241
|
+
// A dash array of all zeros means "solid" in SVG but is an error in PDF, so it is dropped.
|
|
242
|
+
if (stroke.dash && stroke.dash.length > 0 && stroke.dash.some((n) => n > 0)) {
|
|
243
|
+
ops += `[${stroke.dash.join(" ")}] ${stroke.dashOffset ?? 0} d\n`;
|
|
244
|
+
}
|
|
245
|
+
return ops;
|
|
246
|
+
}
|
|
180
247
|
/**
|
|
181
248
|
* Returns the `/GSx gs` operator that selects a transparency state, or `""` when both
|
|
182
249
|
* alphas are fully opaque. Opaque draws emit nothing here, so output stays
|
|
@@ -193,26 +260,45 @@ export class PdfBackend {
|
|
|
193
260
|
* (kappa ≈ 0.5523). Returns m/l/c/h ops WITHOUT the paint operator.
|
|
194
261
|
*/
|
|
195
262
|
static roundedRectPath(x, y, w, h, radius) {
|
|
196
|
-
|
|
197
|
-
|
|
263
|
+
// Per corner, in PDF coordinates (y up). With all four equal this emits exactly the operators the
|
|
264
|
+
// single-radius version did - the byte-identity guard for every document that never asks for one.
|
|
265
|
+
const r = PdfBackend.clampRadii(radius, w, h);
|
|
266
|
+
const k = 0.5523; // control-point offset that approximates a quarter circle
|
|
198
267
|
const f = (n) => n.toFixed(3);
|
|
199
|
-
const xr = x + r;
|
|
200
|
-
const xwr = x + w - r;
|
|
201
268
|
const xw = x + w;
|
|
202
|
-
const yr = y + r;
|
|
203
|
-
const yhr = y + h - r;
|
|
204
269
|
const yh = y + h;
|
|
205
|
-
return (`${f(
|
|
206
|
-
`${f(
|
|
207
|
-
`${f(
|
|
208
|
-
`${f(xw)} ${f(
|
|
209
|
-
`${f(xw)} ${f(
|
|
210
|
-
`${f(
|
|
211
|
-
`${f(
|
|
212
|
-
`${f(x)} ${f(
|
|
213
|
-
`${f(x)} ${f(
|
|
270
|
+
return (`${f(x + r.bl)} ${f(y)} m\n` +
|
|
271
|
+
`${f(xw - r.br)} ${f(y)} l\n` +
|
|
272
|
+
`${f(xw - r.br + r.br * k)} ${f(y)} ${f(xw)} ${f(y + r.br - r.br * k)} ${f(xw)} ${f(y + r.br)} c\n` +
|
|
273
|
+
`${f(xw)} ${f(yh - r.tr)} l\n` +
|
|
274
|
+
`${f(xw)} ${f(yh - r.tr + r.tr * k)} ${f(xw - r.tr + r.tr * k)} ${f(yh)} ${f(xw - r.tr)} ${f(yh)} c\n` +
|
|
275
|
+
`${f(x + r.tl)} ${f(yh)} l\n` +
|
|
276
|
+
`${f(x + r.tl - r.tl * k)} ${f(yh)} ${f(x)} ${f(yh - r.tl + r.tl * k)} ${f(x)} ${f(yh - r.tl)} c\n` +
|
|
277
|
+
`${f(x)} ${f(y + r.bl)} l\n` +
|
|
278
|
+
`${f(x)} ${f(y + r.bl - r.bl * k)} ${f(x + r.bl - r.bl * k)} ${f(y)} ${f(x + r.bl)} ${f(y)} c\n` +
|
|
214
279
|
`h`);
|
|
215
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Clamp the four radii into the box. Each is capped at half the box, and two radii sharing an edge
|
|
283
|
+
* are scaled down together when they would overlap - the CSS rule, and what keeps the path from
|
|
284
|
+
* folding back on itself.
|
|
285
|
+
*/
|
|
286
|
+
static clampRadii(radius, w, h) {
|
|
287
|
+
const n = typeof radius === "number" ? radius : 0;
|
|
288
|
+
const cap = Math.max(0, Math.min(w, h) / 2);
|
|
289
|
+
const at = (v) => Math.max(0, Math.min(v ?? n, cap));
|
|
290
|
+
const r = typeof radius === "number"
|
|
291
|
+
? { tl: at(radius), tr: at(radius), br: at(radius), bl: at(radius) }
|
|
292
|
+
: { tl: at(radius.tl), tr: at(radius.tr), br: at(radius.br), bl: at(radius.bl) };
|
|
293
|
+
// Per-edge overlap: scale the whole set by the tightest edge, so the corners stay proportional.
|
|
294
|
+
// An edge whose two corners are both 0 constrains NOTHING - it must not contribute a ratio of 0,
|
|
295
|
+
// which would scale every other corner away with it.
|
|
296
|
+
const ratio = (extent, a, b) => a + b > 0 ? extent / (a + b) : Infinity;
|
|
297
|
+
const scale = Math.min(1, ratio(w, r.tl, r.tr), ratio(w, r.bl, r.br), ratio(h, r.tl, r.bl), ratio(h, r.tr, r.br));
|
|
298
|
+
if (scale >= 1)
|
|
299
|
+
return r;
|
|
300
|
+
return { tl: r.tl * scale, tr: r.tr * scale, br: r.br * scale, bl: r.bl * scale };
|
|
301
|
+
}
|
|
216
302
|
/**
|
|
217
303
|
* Serialize a single display-list node to its content-stream operators.
|
|
218
304
|
* `om` is used only by primitives that allocate PDF resources (images, fonts).
|
|
@@ -237,22 +323,38 @@ export class PdfBackend {
|
|
|
237
323
|
const doStroke = !!node.stroke && (node.strokeWidth ?? 0) > 0;
|
|
238
324
|
if (!node.fill && !doStroke)
|
|
239
325
|
return "";
|
|
240
|
-
let ops = "";
|
|
241
|
-
if (doStroke) {
|
|
242
|
-
ops += `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n`;
|
|
243
|
-
}
|
|
244
|
-
if (node.fill)
|
|
245
|
-
ops += `${node.fill.toPDFColorString()} rg\n`;
|
|
246
|
-
const paint = node.fill ? (doStroke ? "B" : "f") : "S";
|
|
247
326
|
// Rounded corners emit a Bézier path; sharp corners keep the plain `re`
|
|
248
327
|
// (byte-identical when no radius is set).
|
|
249
|
-
const path = (node.radius
|
|
328
|
+
const path = isRounded(node.radius)
|
|
250
329
|
? PdfBackend.roundedRectPath(node.x, node.y, node.width, node.height, node.radius)
|
|
251
330
|
: `${node.x} ${node.y} ${node.width} ${node.height} re`;
|
|
331
|
+
// A gradient cannot be a fill COLOUR - PDF paints one with `sh`, which floods the current
|
|
332
|
+
// clip. So the box becomes the clip, the shading is painted inside it, and a border is
|
|
333
|
+
// stroked afterwards on its own. Solid fills keep the plain operators, byte for byte.
|
|
334
|
+
const gradient = node.fill !== undefined && !(node.fill instanceof Color) ? node.fill : undefined;
|
|
335
|
+
if (gradient) {
|
|
336
|
+
const shading = om.registerShading(gradient);
|
|
337
|
+
const gsFill = PdfBackend.alphaPrefix(om, gradient.alpha ?? 1, 1);
|
|
338
|
+
let out = `q\n${gsFill}${path} W n\n/${shading} sh\nQ\n`;
|
|
339
|
+
if (doStroke) {
|
|
340
|
+
const gsStroke = PdfBackend.alphaPrefix(om, 1, node.stroke.getAlpha());
|
|
341
|
+
const stroke = `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n` + `${path} S\n`;
|
|
342
|
+
out += gsStroke ? `q\n${gsStroke}${stroke}Q\n` : stroke;
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
const fill = node.fill;
|
|
347
|
+
let ops = "";
|
|
348
|
+
if (doStroke) {
|
|
349
|
+
ops += `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n`;
|
|
350
|
+
}
|
|
351
|
+
if (fill)
|
|
352
|
+
ops += `${fill.toPDFColorString()} rg\n`;
|
|
353
|
+
const paint = fill ? (doStroke ? "B" : "f") : "S";
|
|
252
354
|
const body = ops + `${path} ${paint}\n`;
|
|
253
355
|
// Transparency needs an isolating q/Q so the state does not leak; opaque rects
|
|
254
356
|
// keep their bare operators (byte-identical).
|
|
255
|
-
const gs = PdfBackend.alphaPrefix(om,
|
|
357
|
+
const gs = PdfBackend.alphaPrefix(om, fill?.getAlpha() ?? 1, doStroke ? node.stroke.getAlpha() : 1);
|
|
256
358
|
return gs ? `q\n${gs}${body}Q\n` : body;
|
|
257
359
|
}
|
|
258
360
|
case "text": {
|
|
@@ -268,17 +370,35 @@ export class PdfBackend {
|
|
|
268
370
|
? om.getCustomFontResource(node.fontFamily, node.fontStyle)
|
|
269
371
|
: om.registerFont(node.fontFamily, node.fontStyle);
|
|
270
372
|
const encode = (t) => isCustom
|
|
271
|
-
? `<${om.encodeCustomText(node.fontFamily, t, node.fontStyle)}>`
|
|
373
|
+
? `<${om.encodeCustomText(node.fontFamily, t, node.fontStyle, false)}>`
|
|
272
374
|
: `(${PdfBackend.escapePdfString(t)})`;
|
|
375
|
+
// A shaped run brings its own glyphs, already in drawing order - they cannot be derived from
|
|
376
|
+
// the text here.
|
|
377
|
+
const shapedHex = node.glyphs && isCustom
|
|
378
|
+
? om.registerGlyphs(node.fontFamily, node.glyphs, node.fontStyle)
|
|
379
|
+
: undefined;
|
|
273
380
|
// Kerning (if the document has it on): a `TJ` array with the font's per-pair adjustments
|
|
274
381
|
// between glyph chunks, measured into the layout by the SAME numbers (runAdvance). A plain
|
|
275
382
|
// `Tj` when the run has no kerning, byte-identical.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
383
|
+
// A shaped run brings its glyphs along, so kern those directly rather than re-deriving them
|
|
384
|
+
// from the text - which would also have to guess the run's ligature setting.
|
|
385
|
+
const kerns = !om.kerningEnabled
|
|
386
|
+
? []
|
|
387
|
+
: node.glyphs && isCustom
|
|
388
|
+
? om.getGlyphKernPairs(node.glyphs, node.fontFamily, node.fontStyle)
|
|
389
|
+
: // No glyph list means the run was NOT shaped, whatever the document asks for - so take the
|
|
390
|
+
// character path explicitly. Letting it default would shape here, and a pair count that
|
|
391
|
+
// does not match the character count silently drops the last one.
|
|
392
|
+
om.getKernPairs(node.text, node.fontFamily, node.fontStyle, false);
|
|
393
|
+
// A shaped run kerns too, over its SHAPED glyphs - `getKernPairs` returns one adjustment per
|
|
394
|
+
// adjacent drawn pair either way, so both paths use the same numbers `runAdvance` measured.
|
|
395
|
+
const showOp = node.glyphs && isCustom
|
|
396
|
+
? kerns.some((k) => k !== 0)
|
|
397
|
+
? `${PdfBackend.kernedArray(node.glyphs, kerns, (chunk) => `<${om.registerGlyphs(node.fontFamily, chunk, node.fontStyle)}>`)} TJ`
|
|
398
|
+
: `<${shapedHex}> Tj`
|
|
399
|
+
: kerns.some((k) => k !== 0)
|
|
400
|
+
? `${PdfBackend.kernedArray([...node.text], kerns, (chunk) => encode(chunk.join("")))} TJ`
|
|
401
|
+
: `${encode(node.text)} Tj`;
|
|
282
402
|
// letterSpacing is the `Tc` operator: extra advance after EVERY glyph (an embedded
|
|
283
403
|
// Identity-H font too - that is `Tw`, word spacing, which only touches single-byte code 32).
|
|
284
404
|
const spacing = node.letterSpacing ? `${node.letterSpacing.toFixed(3)} Tc\n` : "";
|
|
@@ -303,7 +423,7 @@ export class PdfBackend {
|
|
|
303
423
|
// Clip to the frame (re … W n); rounded when a radius is set. The rectangular
|
|
304
424
|
// path is byte-identical to before.
|
|
305
425
|
const c = node.clip;
|
|
306
|
-
const clipPath = (node.radius
|
|
426
|
+
const clipPath = isRounded(node.radius)
|
|
307
427
|
? PdfBackend.roundedRectPath(c.x, c.y, c.width, c.height, node.radius)
|
|
308
428
|
: `${c.x} ${c.y} ${c.width} ${c.height} re `;
|
|
309
429
|
return `q\n${clipPath}\nW n \n` + draw + `Q\n`;
|
|
@@ -311,38 +431,55 @@ export class PdfBackend {
|
|
|
311
431
|
case "clip-push": {
|
|
312
432
|
// Save the graphics state and intersect the clip with this (rounded) rect. Everything
|
|
313
433
|
// drawn until the matching clip-pop is cropped to it.
|
|
314
|
-
const path = (node.radius
|
|
434
|
+
const path = isRounded(node.radius)
|
|
315
435
|
? PdfBackend.roundedRectPath(node.x, node.y, node.width, node.height, node.radius)
|
|
316
436
|
: `${node.x} ${node.y} ${node.width} ${node.height} re`;
|
|
317
437
|
return `q\n${path}\nW n\n`;
|
|
318
438
|
}
|
|
319
439
|
case "path": {
|
|
320
|
-
// A
|
|
321
|
-
//
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
else if (c.op === "l")
|
|
328
|
-
path += `${f(c.x)} ${f(c.y)} l\n`;
|
|
329
|
-
else if (c.op === "c")
|
|
330
|
-
path += `${f(c.x1)} ${f(c.y1)} ${f(c.x2)} ${f(c.y2)} ${f(c.x)} ${f(c.y)} c\n`;
|
|
331
|
-
else
|
|
332
|
-
path += `h\n`;
|
|
333
|
-
}
|
|
334
|
-
if (!(node.fill instanceof Color)) {
|
|
440
|
+
// A vector path: a color-glyph layer (fill only) or an SVG shape (fill and/or stroke). Emit
|
|
441
|
+
// the subpath ops once, then pick the painting operator from what the node actually carries.
|
|
442
|
+
const path = PdfBackend.pathOps(node.commands);
|
|
443
|
+
// The even-odd variants of the fill and clip operators are the same letter plus a star.
|
|
444
|
+
const star = node.fillRule === "evenodd" ? "*" : "";
|
|
445
|
+
const stroke = node.stroke;
|
|
446
|
+
if (node.fill !== undefined && !(node.fill instanceof Color)) {
|
|
335
447
|
// Gradient: clip to the path (W n keeps it as the clip without painting it), then paint
|
|
336
448
|
// the registered shading across that clip. The q/Q isolates the clip. Note: per-stop alpha
|
|
337
449
|
// is not represented (a DeviceRGB shading has no alpha channel - that would need a soft-mask
|
|
338
450
|
// group); COLR gradient stops are opaque in real fonts, so they render correctly.
|
|
451
|
+
// A stroke cannot share that q/Q: the clip would cut the outline in half, because a stroke
|
|
452
|
+
// straddles the edge. So it is drawn afterwards, over the same path.
|
|
339
453
|
const shading = om.registerShading(node.fill);
|
|
340
|
-
|
|
454
|
+
const gsFill = PdfBackend.alphaPrefix(om, node.fill.alpha ?? 1, 1);
|
|
455
|
+
const filled = `q\n${gsFill}${path}W${star} n\n/${shading} sh\nQ\n`;
|
|
456
|
+
if (!stroke)
|
|
457
|
+
return filled;
|
|
458
|
+
const gsStroke = PdfBackend.alphaPrefix(om, 1, stroke.color.getAlpha());
|
|
459
|
+
return `${filled}q\n${gsStroke}${PdfBackend.strokeState(stroke)}${path}S\nQ\n`;
|
|
460
|
+
}
|
|
461
|
+
// No fill and no stroke: a shape that paints nothing. Emitting the path without an operator
|
|
462
|
+
// would leave it pending in the content stream and swallow whatever is drawn next.
|
|
463
|
+
if (node.fill === undefined && !stroke)
|
|
464
|
+
return "";
|
|
465
|
+
if (stroke) {
|
|
466
|
+
// A stroked path always gets its own q/Q. Unlike a rect it can set `J`, `j`, `M` and `d`,
|
|
467
|
+
// and those would otherwise stay on for every later stroke on the page - a dash pattern
|
|
468
|
+
// leaking into the next table rule. Existing IR carries no stroke, so nothing moves.
|
|
469
|
+
const paint = node.fill !== undefined ? `B${star}` : "S";
|
|
470
|
+
const color = node.fill !== undefined ? `${node.fill.toPDFColorString()} rg\n` : "";
|
|
471
|
+
const gs = PdfBackend.alphaPrefix(om, node.fill !== undefined ? node.fill.getAlpha() : 1, stroke.color.getAlpha());
|
|
472
|
+
return `q\n${gs}${color}${PdfBackend.strokeState(stroke)}${path}${paint}\nQ\n`;
|
|
341
473
|
}
|
|
342
|
-
const body = `${node.fill.toPDFColorString()} rg\n${path}f\n`;
|
|
474
|
+
const body = `${node.fill.toPDFColorString()} rg\n${path}f${star}\n`;
|
|
343
475
|
const gs = PdfBackend.alphaPrefix(om, node.fill.getAlpha(), 1);
|
|
344
476
|
return gs ? `q\n${gs}${body}Q\n` : body;
|
|
345
477
|
}
|
|
478
|
+
case "clip-path-push": {
|
|
479
|
+
// `W n` sets the clip from the current path without painting it; the q is closed by clip-pop.
|
|
480
|
+
const star = node.fillRule === "evenodd" ? "*" : "";
|
|
481
|
+
return `q\n${PdfBackend.pathOps(node.commands)}W${star} n\n`;
|
|
482
|
+
}
|
|
346
483
|
case "clip-pop":
|
|
347
484
|
return `Q\n`;
|
|
348
485
|
case "transform-push": {
|
|
@@ -14,8 +14,13 @@ export class PDFDocumentRenderer {
|
|
|
14
14
|
// pure (`{ fitted, remainder }`), so the full list of physical pages can be settled up front. Only
|
|
15
15
|
// then is the total page count known, which is what page numbers in a header/footer need.
|
|
16
16
|
const physicalPages = [];
|
|
17
|
+
// Where each sheet sits WITHIN the logical page it came from. Pass A already knows: one logical
|
|
18
|
+
// `Page` element produces one run of sheets, so the run's length is that section's total.
|
|
19
|
+
const section = [];
|
|
17
20
|
for (const page of document.getProps().children) {
|
|
18
|
-
|
|
21
|
+
const sheets = PDFDocumentRenderer.paginateLogicalPage(page, ctx);
|
|
22
|
+
sheets.forEach((_, i) => section.push({ number: i + 1, total: sheets.length }));
|
|
23
|
+
physicalPages.push(...sheets);
|
|
19
24
|
}
|
|
20
25
|
// Pass B - draw them, in order, so the emitted page objects keep their previous numbering. Each page
|
|
21
26
|
// is laid out again with its own PageInfo, which is how a `PageBuilder` anywhere in the tree (header,
|
|
@@ -25,6 +30,8 @@ export class PDFDocumentRenderer {
|
|
|
25
30
|
const pageInfo = {
|
|
26
31
|
pageNumber: index + 1,
|
|
27
32
|
pageCount: physicalPages.length,
|
|
33
|
+
subPageNumber: section[index].number,
|
|
34
|
+
subPageTotalPages: section[index].total,
|
|
28
35
|
pageSize: resolvePageSize(PDFDocumentRenderer.configOf(physical, ctx)),
|
|
29
36
|
};
|
|
30
37
|
pageNumbers.push(await PDFDocumentRenderer.renderPhysicalPage(physical, objectManager, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PDFDocumentRenderer } from "./pdf-document-renderer.js";
|
|
2
2
|
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
3
|
-
import { DefaultTextStyleElement, ExpandedElement, ImageElement, LineElement, PaddingElement, TextElement, } from "../elements/index.js";
|
|
3
|
+
import { DefaultTextStyleElement, ExpandedElement, ImageElement, LineElement, CanvasElement, PaddingElement, SvgElement, TextElement, } from "../elements/index.js";
|
|
4
4
|
import { TextRenderer } from "./text-renderer.js";
|
|
5
5
|
import { ContainerElement } from "../elements/container-element.js";
|
|
6
6
|
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
@@ -13,6 +13,8 @@ import { PaddingRenderer } from "./padding-renderer.js";
|
|
|
13
13
|
import { DefaultTextStyleRenderer } from "./default-text-style-renderer.js";
|
|
14
14
|
import { ImageRenderer } from "./image-renderer.js";
|
|
15
15
|
import { LineRenderer } from "./line-renderer.js";
|
|
16
|
+
import { SvgRenderer } from "./svg-renderer.js";
|
|
17
|
+
import { CanvasRenderer } from "./canvas-renderer.js";
|
|
16
18
|
import { RepeatingHeaderElement } from "../elements/layout/repeating-header-element.js";
|
|
17
19
|
import { RepeatingHeaderRenderer } from "./repeating-header-renderer.js";
|
|
18
20
|
import { DeferredElement } from "../elements/layout/deferred-element.js";
|
|
@@ -67,6 +69,8 @@ export class PDFRenderer {
|
|
|
67
69
|
RendererRegistry.register(DefaultTextStyleElement, DefaultTextStyleRenderer.render);
|
|
68
70
|
RendererRegistry.register(ImageElement, ImageRenderer.render);
|
|
69
71
|
RendererRegistry.register(LineElement, LineRenderer.render);
|
|
72
|
+
RendererRegistry.register(SvgElement, SvgRenderer.render);
|
|
73
|
+
RendererRegistry.register(CanvasElement, CanvasRenderer.render);
|
|
70
74
|
RendererRegistry.register(RepeatingHeaderElement, RepeatingHeaderRenderer.render);
|
|
71
75
|
RendererRegistry.register(DeferredElement, DeferredRenderer.render);
|
|
72
76
|
RendererRegistry.register(PositionedElement, PositionedRenderer.render);
|
|
@@ -5,6 +5,6 @@ export class PositionedRenderer {
|
|
|
5
5
|
// after the frame's flow content, so it paints on top.
|
|
6
6
|
const { child } = positionedElement.getProps();
|
|
7
7
|
const renderer = RendererRegistry.getRenderer(child);
|
|
8
|
-
return renderer
|
|
8
|
+
return renderer(child, objectManager);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
import { isGradientInput, resolveGradient } from "../api/gradient.js";
|
|
2
3
|
export class RectangleRenderer {
|
|
3
4
|
static async render(rectangleElement, objectManager) {
|
|
4
5
|
const { x, y, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, overflow, } = rectangleElement.getProps();
|
|
5
6
|
const h = height;
|
|
6
7
|
const nodes = [];
|
|
8
|
+
// A gradient is written box-relative (an angle and stops); this is the first place the box is
|
|
9
|
+
// known, so it is resolved into the absolute page coordinates the IR wants.
|
|
10
|
+
const fill = backgroundColor !== undefined && isGradientInput(backgroundColor)
|
|
11
|
+
? resolveGradient(backgroundColor, x, y, width, h)
|
|
12
|
+
: backgroundColor;
|
|
7
13
|
if (sideBorders) {
|
|
8
14
|
// Per-side borders: a fill-only box (no stroke), then a line per present side
|
|
9
15
|
// (sharp corners). This is what lets cells draw grid lines.
|
|
10
16
|
if (backgroundColor) {
|
|
11
|
-
nodes.push({ type: "rect", x, y, width, height: h, strokeWidth: 0, fill
|
|
17
|
+
nodes.push({ type: "rect", x, y, width, height: h, strokeWidth: 0, fill });
|
|
12
18
|
}
|
|
13
19
|
nodes.push(...RectangleRenderer.sideLines(x, y, width, h, borderWidth, sideBorders));
|
|
14
20
|
}
|
|
@@ -23,7 +29,7 @@ export class RectangleRenderer {
|
|
|
23
29
|
height: h,
|
|
24
30
|
stroke: color,
|
|
25
31
|
strokeWidth: borderWidth,
|
|
26
|
-
...(
|
|
32
|
+
...(fill ? { fill } : {}),
|
|
27
33
|
...(radius ? { radius } : {}),
|
|
28
34
|
};
|
|
29
35
|
nodes.push(node);
|
|
@@ -38,9 +44,7 @@ export class RectangleRenderer {
|
|
|
38
44
|
if (children)
|
|
39
45
|
for (const child of children) {
|
|
40
46
|
const renderer = RendererRegistry.getRenderer(child);
|
|
41
|
-
|
|
42
|
-
nodes.push(...(await renderer(child, objectManager)));
|
|
43
|
-
}
|
|
47
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
44
48
|
}
|
|
45
49
|
if (clip)
|
|
46
50
|
nodes.push({ type: "clip-pop" });
|
|
@@ -5,8 +5,7 @@ export class RepeatingHeaderRenderer {
|
|
|
5
5
|
const nodes = [];
|
|
6
6
|
for (const child of [header, body]) {
|
|
7
7
|
const renderer = RendererRegistry.getRenderer(child);
|
|
8
|
-
|
|
9
|
-
nodes.push(...(await renderer(child, objectManager)));
|
|
8
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
10
9
|
}
|
|
11
10
|
return nodes;
|
|
12
11
|
}
|
|
@@ -22,7 +22,7 @@ export class RotatedRenderer {
|
|
|
22
22
|
cy - sin * cx - cos * cy,
|
|
23
23
|
];
|
|
24
24
|
const renderer = RendererRegistry.getRenderer(child);
|
|
25
|
-
const childNodes =
|
|
25
|
+
const childNodes = await renderer(child, objectManager);
|
|
26
26
|
// Wrap the child's drawing between a push (apply the matrix) and a pop (restore).
|
|
27
27
|
return [{ type: "transform-push", matrix }, ...childNodes, { type: "transform-pop" }];
|
|
28
28
|
}
|
|
@@ -6,9 +6,7 @@ export class RowRenderer {
|
|
|
6
6
|
if (children)
|
|
7
7
|
for (const child of children) {
|
|
8
8
|
const renderer = RendererRegistry.getRenderer(child);
|
|
9
|
-
|
|
10
|
-
nodes.push(...(await renderer(child, objectManager)));
|
|
11
|
-
}
|
|
9
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
12
10
|
}
|
|
13
11
|
return nodes;
|
|
14
12
|
}
|
|
@@ -8,7 +8,7 @@ export class StructGroupRenderer {
|
|
|
8
8
|
const { role, child } = group.getProps();
|
|
9
9
|
const renderChild = async () => {
|
|
10
10
|
const renderer = RendererRegistry.getRenderer(child);
|
|
11
|
-
return renderer
|
|
11
|
+
return renderer(child, objectManager);
|
|
12
12
|
};
|
|
13
13
|
if (!objectManager.struct.enabled)
|
|
14
14
|
return renderChild();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { SvgElement } from "../elements/svg-element.js";
|
|
3
|
+
import type { IRNode } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* Turns the drawing into display-list nodes inside the box layout gave it. All the work is in
|
|
6
|
+
* `svg/`; this is only the seam, which is why an SVG needs no new IR primitive - the vector layer
|
|
7
|
+
* that colour emoji already draw through carries it.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SvgRenderer {
|
|
10
|
+
static render(element: SvgElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { svgToIr } from "../svg/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Turns the drawing into display-list nodes inside the box layout gave it. All the work is in
|
|
4
|
+
* `svg/`; this is only the seam, which is why an SVG needs no new IR primitive - the vector layer
|
|
5
|
+
* that colour emoji already draw through carries it.
|
|
6
|
+
*/
|
|
7
|
+
export class SvgRenderer {
|
|
8
|
+
static async render(element, objectManager) {
|
|
9
|
+
const { x, y, width, height, source, alt } = element.getProps();
|
|
10
|
+
const nodes = svgToIr(source, {
|
|
11
|
+
x: x ?? 0,
|
|
12
|
+
y: y ?? 0,
|
|
13
|
+
width: width ?? 0,
|
|
14
|
+
height: height ?? 0,
|
|
15
|
+
});
|
|
16
|
+
// Accessible tagging: a drawing WITH alt text is one Figure - every path of it belongs to the
|
|
17
|
+
// same struct element, which is what `openElement` keyed on `structId` gives us. Without alt it
|
|
18
|
+
// stays untagged and the backend treats it as decoration (an Artifact).
|
|
19
|
+
if (!alt || !objectManager.struct.enabled)
|
|
20
|
+
return nodes;
|
|
21
|
+
const tag = {
|
|
22
|
+
role: "Figure",
|
|
23
|
+
key: objectManager.struct.openElement(element.structId, "Figure", { alt }),
|
|
24
|
+
};
|
|
25
|
+
return nodes.map((node) => (node.type === "path" ? { ...node, tag } : node));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -2,9 +2,9 @@ import { TextElement, TextSegment } from "../elements/text-element.js";
|
|
|
2
2
|
import { FontStyle, PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
3
|
import type { FontMetrics } from "../utils/font-metrics.js";
|
|
4
4
|
import { IRNode } from "../ir/display-list.js";
|
|
5
|
-
import { TextOverflow } from "../text/line-breaker.js";
|
|
5
|
+
import { type LineOptions, TextOverflow } from "../text/line-breaker.js";
|
|
6
6
|
export declare class TextRenderer {
|
|
7
|
-
static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number, maxLines?: number, overflow?: TextOverflow, lineHeight?: number, letterSpacing?: number): number;
|
|
7
|
+
static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number, maxLines?: number, overflow?: TextOverflow, lineHeight?: number, letterSpacing?: number, lineOptions?: LineOptions): number;
|
|
8
8
|
static render(textElement: TextElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
9
9
|
private static _expandColorGlyphs;
|
|
10
10
|
private static _glyphToPath;
|