@jasy/pdf 1.0.0-alpha.9 → 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 +27 -9
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +40 -1
- package/dist/api/content.js +83 -0
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +3 -2
- package/dist/api/font-url.js +3 -2
- package/dist/api/structure.d.ts +7 -0
- package/dist/api/structure.js +38 -3
- package/dist/api/text.d.ts +23 -2
- package/dist/api/text.js +9 -0
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +2 -0
- package/dist/elements/container-element.js +6 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +2 -0
- package/dist/elements/layout/expanded-element.js +4 -0
- 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 +5 -0
- package/dist/elements/layout/padding-element.js +8 -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 +19 -3
- package/dist/elements/pdf-element.js +22 -4
- package/dist/elements/rectangle-element.d.ts +2 -0
- package/dist/elements/rectangle-element.js +6 -0
- package/dist/elements/row-element.d.ts +2 -0
- package/dist/elements/row-element.js +6 -0
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +33 -1
- package/dist/elements/text-element.js +80 -4
- package/dist/ir/display-list.d.ts +44 -5
- 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/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 +15 -4
- package/dist/renderer/pdf-backend.js +125 -35
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +1 -3
- 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.js +30 -19
- 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 +4 -3
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +8 -0
- package/dist/text/line-breaker.js +146 -70
- package/dist/text/shape.d.ts +3 -3
- package/dist/text/shape.js +34 -10
- package/dist/text/text-style.d.ts +13 -0
- package/dist/text/text-style.js +6 -0
- 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.js +67 -18
- package/dist/utils/font-metrics.d.ts +9 -3
- package/dist/utils/pdf-object-manager.d.ts +25 -5
- package/dist/utils/pdf-object-manager.js +82 -18
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +5 -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRounded } from "../ir/display-list.js";
|
|
1
|
+
import { isRounded, } from "../ir/display-list.js";
|
|
2
2
|
import { Color } from "../common/color.js";
|
|
3
3
|
/**
|
|
4
4
|
* PDF backend - turns display-list primitives into content-stream operators.
|
|
@@ -46,6 +46,19 @@ export class PdfBackend {
|
|
|
46
46
|
}
|
|
47
47
|
return flipped;
|
|
48
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
|
+
};
|
|
49
62
|
case "clip-push":
|
|
50
63
|
// Flip the clip rect around its bottom edge, like a rect.
|
|
51
64
|
return { ...node, y: pageHeight - node.y - node.height };
|
|
@@ -63,7 +76,7 @@ export class PdfBackend {
|
|
|
63
76
|
});
|
|
64
77
|
// A gradient fill carries its own page-space anchor points; flip their y too (radii and
|
|
65
78
|
// x stay). A solid Color fill has no geometry.
|
|
66
|
-
const fill = node.fill instanceof Color
|
|
79
|
+
const fill = node.fill === undefined || node.fill instanceof Color
|
|
67
80
|
? node.fill
|
|
68
81
|
: { ...node.fill, y0: pageHeight - node.fill.y0, y1: pageHeight - node.fill.y1 };
|
|
69
82
|
return { ...node, commands, fill };
|
|
@@ -140,6 +153,7 @@ export class PdfBackend {
|
|
|
140
153
|
// catalog entries instead of content-stream operators.
|
|
141
154
|
if (!struct ||
|
|
142
155
|
node.type === "clip-push" ||
|
|
156
|
+
node.type === "clip-path-push" ||
|
|
143
157
|
node.type === "clip-pop" ||
|
|
144
158
|
node.type === "transform-push" ||
|
|
145
159
|
node.type === "transform-pop" ||
|
|
@@ -164,25 +178,72 @@ export class PdfBackend {
|
|
|
164
178
|
}
|
|
165
179
|
/**
|
|
166
180
|
* Builds the `[ ... ]` operand of a `TJ` from a run and its per-gap kern adjustments (em/1000, the
|
|
167
|
-
* sign the font declares
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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`.
|
|
170
186
|
*/
|
|
171
|
-
static kernedArray(
|
|
172
|
-
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
|
+
}
|
|
173
195
|
let out = "";
|
|
174
|
-
let chunk =
|
|
196
|
+
let chunk = units.length > 0 ? [units[0]] : [];
|
|
175
197
|
for (let i = 0; i < kerns.length; i++) {
|
|
176
198
|
if (kerns[i] === 0) {
|
|
177
|
-
chunk
|
|
199
|
+
chunk.push(units[i + 1]);
|
|
178
200
|
}
|
|
179
201
|
else {
|
|
180
202
|
out += encode(chunk) + ` ${-kerns[i]} `;
|
|
181
|
-
chunk =
|
|
203
|
+
chunk = [units[i + 1]];
|
|
182
204
|
}
|
|
183
205
|
}
|
|
184
206
|
return `[${out}${encode(chunk)}]`;
|
|
185
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
|
+
}
|
|
186
247
|
/**
|
|
187
248
|
* Returns the `/GSx gs` operator that selects a transparency state, or `""` when both
|
|
188
249
|
* alphas are fully opaque. Opaque draws emit nothing here, so output stays
|
|
@@ -273,7 +334,8 @@ export class PdfBackend {
|
|
|
273
334
|
const gradient = node.fill !== undefined && !(node.fill instanceof Color) ? node.fill : undefined;
|
|
274
335
|
if (gradient) {
|
|
275
336
|
const shading = om.registerShading(gradient);
|
|
276
|
-
|
|
337
|
+
const gsFill = PdfBackend.alphaPrefix(om, gradient.alpha ?? 1, 1);
|
|
338
|
+
let out = `q\n${gsFill}${path} W n\n/${shading} sh\nQ\n`;
|
|
277
339
|
if (doStroke) {
|
|
278
340
|
const gsStroke = PdfBackend.alphaPrefix(om, 1, node.stroke.getAlpha());
|
|
279
341
|
const stroke = `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n` + `${path} S\n`;
|
|
@@ -308,7 +370,7 @@ export class PdfBackend {
|
|
|
308
370
|
? om.getCustomFontResource(node.fontFamily, node.fontStyle)
|
|
309
371
|
: om.registerFont(node.fontFamily, node.fontStyle);
|
|
310
372
|
const encode = (t) => isCustom
|
|
311
|
-
? `<${om.encodeCustomText(node.fontFamily, t, node.fontStyle)}>`
|
|
373
|
+
? `<${om.encodeCustomText(node.fontFamily, t, node.fontStyle, false)}>`
|
|
312
374
|
: `(${PdfBackend.escapePdfString(t)})`;
|
|
313
375
|
// A shaped run brings its own glyphs, already in drawing order - they cannot be derived from
|
|
314
376
|
// the text here.
|
|
@@ -318,13 +380,24 @@ export class PdfBackend {
|
|
|
318
380
|
// Kerning (if the document has it on): a `TJ` array with the font's per-pair adjustments
|
|
319
381
|
// between glyph chunks, measured into the layout by the SAME numbers (runAdvance). A plain
|
|
320
382
|
// `Tj` when the run has no kerning, byte-identical.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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`
|
|
326
399
|
: kerns.some((k) => k !== 0)
|
|
327
|
-
? `${PdfBackend.kernedArray(node.text, kerns, encode)} TJ`
|
|
400
|
+
? `${PdfBackend.kernedArray([...node.text], kerns, (chunk) => encode(chunk.join("")))} TJ`
|
|
328
401
|
: `${encode(node.text)} Tj`;
|
|
329
402
|
// letterSpacing is the `Tc` operator: extra advance after EVERY glyph (an embedded
|
|
330
403
|
// Identity-H font too - that is `Tw`, word spacing, which only touches single-byte code 32).
|
|
@@ -364,32 +437,49 @@ export class PdfBackend {
|
|
|
364
437
|
return `q\n${path}\nW n\n`;
|
|
365
438
|
}
|
|
366
439
|
case "path": {
|
|
367
|
-
// A
|
|
368
|
-
//
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
else if (c.op === "l")
|
|
375
|
-
path += `${f(c.x)} ${f(c.y)} l\n`;
|
|
376
|
-
else if (c.op === "c")
|
|
377
|
-
path += `${f(c.x1)} ${f(c.y1)} ${f(c.x2)} ${f(c.y2)} ${f(c.x)} ${f(c.y)} c\n`;
|
|
378
|
-
else
|
|
379
|
-
path += `h\n`;
|
|
380
|
-
}
|
|
381
|
-
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)) {
|
|
382
447
|
// Gradient: clip to the path (W n keeps it as the clip without painting it), then paint
|
|
383
448
|
// the registered shading across that clip. The q/Q isolates the clip. Note: per-stop alpha
|
|
384
449
|
// is not represented (a DeviceRGB shading has no alpha channel - that would need a soft-mask
|
|
385
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.
|
|
386
453
|
const shading = om.registerShading(node.fill);
|
|
387
|
-
|
|
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`;
|
|
388
473
|
}
|
|
389
|
-
const body = `${node.fill.toPDFColorString()} rg\n${path}f\n`;
|
|
474
|
+
const body = `${node.fill.toPDFColorString()} rg\n${path}f${star}\n`;
|
|
390
475
|
const gs = PdfBackend.alphaPrefix(om, node.fill.getAlpha(), 1);
|
|
391
476
|
return gs ? `q\n${gs}${body}Q\n` : body;
|
|
392
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
|
+
}
|
|
393
483
|
case "clip-pop":
|
|
394
484
|
return `Q\n`;
|
|
395
485
|
case "transform-push": {
|
|
@@ -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
|
}
|
|
@@ -44,9 +44,7 @@ export class RectangleRenderer {
|
|
|
44
44
|
if (children)
|
|
45
45
|
for (const child of children) {
|
|
46
46
|
const renderer = RendererRegistry.getRenderer(child);
|
|
47
|
-
|
|
48
|
-
nodes.push(...(await renderer(child, objectManager)));
|
|
49
|
-
}
|
|
47
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
50
48
|
}
|
|
51
49
|
if (clip)
|
|
52
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
|
+
}
|
|
@@ -8,6 +8,7 @@ import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js
|
|
|
8
8
|
import { runAdvance } from "../text/advance.js";
|
|
9
9
|
import { visualRuns, visualRunsOf } from "../text/bidi.js";
|
|
10
10
|
import { skipInkSegments, strikethroughStroke, underlineStroke, } from "../text/text-decoration.js";
|
|
11
|
+
import { quadToCubic } from "../vector/path.js";
|
|
11
12
|
export class TextRenderer {
|
|
12
13
|
// Measuring only needs metrics, not the full object manager. (The render pass below
|
|
13
14
|
// still receives the manager because it also registers fonts/images.)
|
|
@@ -18,17 +19,26 @@ export class TextRenderer {
|
|
|
18
19
|
const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
|
|
19
20
|
return lines.length * box.height;
|
|
20
21
|
}
|
|
21
|
-
// Segments: each line's box comes from the fonts actually on it.
|
|
22
|
-
|
|
22
|
+
// Segments: each line's box comes from the fonts actually on it. `splitting` has to travel with
|
|
23
|
+
// them for the same reason `letterSpacing` does: it changes where a line ENDS, so leaving it out
|
|
24
|
+
// measures a paragraph at one line count and draws it at another.
|
|
25
|
+
const defaults = {
|
|
26
|
+
fontFamily,
|
|
27
|
+
fontSize,
|
|
28
|
+
fontStyle,
|
|
29
|
+
letterSpacing,
|
|
30
|
+
splitting: lineOptions.splitting,
|
|
31
|
+
ligatures: lineOptions.ligatures,
|
|
32
|
+
};
|
|
23
33
|
const lines = breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow);
|
|
24
34
|
return lines.reduce((total, line) => total + lineBoxForSegmentLine(line, defaults, objectManager, lineHeight).height, 0);
|
|
25
35
|
}
|
|
26
36
|
static async render(textElement, objectManager) {
|
|
27
|
-
const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent, role, } = textElement.getProps();
|
|
37
|
+
const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, } = textElement.getProps();
|
|
28
38
|
// Component -> display list. Wrapping and positioning stay here; the backend
|
|
29
39
|
// turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
|
|
30
40
|
// the original renderer - unifying it into the engine is Phase 3.
|
|
31
|
-
const { runs, links, decorations } = TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width ?? Number.NaN, textAlignment, color, x, y, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent);
|
|
41
|
+
const { runs, links, decorations } = TextRenderer._buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, width ?? Number.NaN, textAlignment, color, x, y, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, wordSpacing, textIndent, breakWord, hyphenate, ligatures);
|
|
32
42
|
// Accessible tagging: this whole text block is one structure element (a paragraph P, or a heading
|
|
33
43
|
// from `role`); each run's text lands under it. Emoji sub-nodes produced below stay untagged (drawn
|
|
34
44
|
// as artifacts). Keyed by structId so a block split across pages stays one element.
|
|
@@ -161,19 +171,9 @@ export class TextRenderer {
|
|
|
161
171
|
out.push({ op: "l", x: curX, y: curY });
|
|
162
172
|
}
|
|
163
173
|
else if (cmd.type === "Q") {
|
|
164
|
-
const ctrlX = mapX(cmd.cx, cmd.cy);
|
|
165
|
-
const ctrlY = mapY(cmd.cx, cmd.cy);
|
|
166
174
|
const endX = mapX(cmd.x, cmd.y);
|
|
167
175
|
const endY = mapY(cmd.x, cmd.y);
|
|
168
|
-
out.push(
|
|
169
|
-
op: "c",
|
|
170
|
-
x1: curX + (2 / 3) * (ctrlX - curX),
|
|
171
|
-
y1: curY + (2 / 3) * (ctrlY - curY),
|
|
172
|
-
x2: endX + (2 / 3) * (ctrlX - endX),
|
|
173
|
-
y2: endY + (2 / 3) * (ctrlY - endY),
|
|
174
|
-
x: endX,
|
|
175
|
-
y: endY,
|
|
176
|
-
});
|
|
176
|
+
out.push(quadToCubic(curX, curY, mapX(cmd.cx, cmd.cy), mapY(cmd.cx, cmd.cy), endX, endY));
|
|
177
177
|
curX = endX;
|
|
178
178
|
curY = endY;
|
|
179
179
|
}
|
|
@@ -224,7 +224,7 @@ export class TextRenderer {
|
|
|
224
224
|
// Lay the content out into absolutely-positioned text runs. Glyph positions match
|
|
225
225
|
// the previous hand-written operators exactly (verified by pixel-identical render);
|
|
226
226
|
// only the output form changed from PDF strings to `TextRun`s.
|
|
227
|
-
static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight, underline = false, strikethrough = false, skipInk = false, letterSpacing = 0, direction = "ltr", wordSpacing = 0, textIndent = 0) {
|
|
227
|
+
static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight, underline = false, strikethrough = false, skipInk = false, letterSpacing = 0, direction = "ltr", wordSpacing = 0, textIndent = 0, breakWord = false, hyphenate, ligatures = true) {
|
|
228
228
|
const runs = [];
|
|
229
229
|
// /Link annotation rects for any `href` spans, collected as we place segments. Empty for the
|
|
230
230
|
// common (no-href) case, so plain text keeps producing exactly the runs it did before.
|
|
@@ -353,7 +353,9 @@ export class TextRenderer {
|
|
|
353
353
|
* text, no glyph list, byte-identical output.
|
|
354
354
|
*/
|
|
355
355
|
const shapedPiece = (part, family, style) => {
|
|
356
|
-
|
|
356
|
+
// The run's OWN setting, not the default: measuring asked with it (`runAdvance` -> RunFont), and
|
|
357
|
+
// drawing has to ask the same question or the line is measured one width and drawn another.
|
|
358
|
+
const shaped = objectManager.shapeText(part.logical, family, style, ligatures ? undefined : []);
|
|
357
359
|
if (!shaped)
|
|
358
360
|
return { text: part.text };
|
|
359
361
|
const ordered = part.rtl ? [...shaped].reverse() : shaped;
|
|
@@ -361,7 +363,7 @@ export class TextRenderer {
|
|
|
361
363
|
};
|
|
362
364
|
// The advance the viewer will use: plain glyph widths (a `Tj` never kerns) plus one
|
|
363
365
|
// `letterSpacing` per code point (the `Tc` operator), from the one shared `advance.ts` primitive.
|
|
364
|
-
const font = { fontFamily, fontSize, fontStyle };
|
|
366
|
+
const font = { fontFamily, fontSize, fontStyle, ligatures };
|
|
365
367
|
// --- Plain string: one run per wrapped line. ---
|
|
366
368
|
if (typeof content === "string") {
|
|
367
369
|
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing,
|
|
@@ -371,6 +373,8 @@ export class TextRenderer {
|
|
|
371
373
|
wordSpacing,
|
|
372
374
|
indent: textIndent,
|
|
373
375
|
shrink: align === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
376
|
+
splitting: { breakWord, hyphenate },
|
|
377
|
+
ligatures,
|
|
374
378
|
});
|
|
375
379
|
// yPosition is the top of the text box (top-left). The line box seats its own baseline; lines
|
|
376
380
|
// then stack by that box's height. Same numbers as calculateTextHeight, by construction.
|
|
@@ -457,7 +461,14 @@ export class TextRenderer {
|
|
|
457
461
|
// The seam flips the whole thing to PDF space. `letterSpacing` MUST be in the defaults so a span
|
|
458
462
|
// that does not override it wraps and aligns with the element's spacing - the same defaults the
|
|
459
463
|
// measure path (calculateTextHeight) uses, or segmented spaced text mis-wraps (measured != drawn).
|
|
460
|
-
const defaults = {
|
|
464
|
+
const defaults = {
|
|
465
|
+
fontFamily,
|
|
466
|
+
fontSize,
|
|
467
|
+
fontStyle,
|
|
468
|
+
letterSpacing,
|
|
469
|
+
splitting: { breakWord, hyphenate },
|
|
470
|
+
ligatures,
|
|
471
|
+
};
|
|
461
472
|
let top = yPosition;
|
|
462
473
|
// Materialised, because justification has to know which line is the LAST one of the paragraph.
|
|
463
474
|
const segmentLines = [
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `<style>` block - the third way an SVG carries style, beside presentation attributes and the
|
|
3
|
+
* `style=""` bag.
|
|
4
|
+
*
|
|
5
|
+
* It exists here because it is the case that BREAKS in practice: Illustrator's default export option
|
|
6
|
+
* is "Internal CSS", which puts every fill into a class and leaves the shapes with nothing but
|
|
7
|
+
* `class="st0"`. Dropping the block therefore does not lose a detail - it makes the whole logo black.
|
|
8
|
+
* (react-pdf drops it, with a warning; we dropped it silently until this module existed.)
|
|
9
|
+
*
|
|
10
|
+
* Only FLAT selectors are resolved - a tag, `.class`, `#id`, and compounds of those - because that is
|
|
11
|
+
* what exporters emit. A combinator or a pseudo-class is reported rather than quietly mismatched: a
|
|
12
|
+
* selector that silently fails to match is the same black logo again, one level down.
|
|
13
|
+
*/
|
|
14
|
+
export interface CssRule {
|
|
15
|
+
tag?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
classes: string[];
|
|
18
|
+
/** CSS specificity as one number, and the source order that breaks ties. */
|
|
19
|
+
weight: number;
|
|
20
|
+
order: number;
|
|
21
|
+
declarations: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parses every `<style>` block's text into rules, in source order.
|
|
25
|
+
*
|
|
26
|
+
* The brace matching is done by hand rather than with one regex: `@media print { .a { … } }` nests,
|
|
27
|
+
* and a regex that skips to the first balanced-looking block silently applies the INNER rule while
|
|
28
|
+
* never seeing the at-rule that guards it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseStylesheet(css: string): CssRule[];
|
|
31
|
+
/**
|
|
32
|
+
* The declarations that apply to one element, weakest first - so a caller can just assign them in
|
|
33
|
+
* order and let the last one win, as the cascade does.
|
|
34
|
+
*/
|
|
35
|
+
export declare function declarationsFor(rules: readonly CssRule[], tagName: string, attributes: {
|
|
36
|
+
class?: string;
|
|
37
|
+
id?: string;
|
|
38
|
+
}): Record<string, string>;
|