@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
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
import { Color } from "../common/color.js";
|
|
2
2
|
import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
3
|
+
import { VERTICAL_TEXT_SHIFT } from "../elements/text-element.js";
|
|
3
4
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
4
5
|
import { emojiImageNode } from "./emoji-image.js";
|
|
5
|
-
import { wrapStringIntoLines, breakSegmentsIntoLines, } from "../text/line-breaker.js";
|
|
6
|
+
import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, } from "../text/line-breaker.js";
|
|
6
7
|
import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
|
|
7
8
|
import { runAdvance } from "../text/advance.js";
|
|
9
|
+
import { visualRuns, visualRunsOf } from "../text/bidi.js";
|
|
8
10
|
import { skipInkSegments, strikethroughStroke, underlineStroke, } from "../text/text-decoration.js";
|
|
11
|
+
import { quadToCubic } from "../vector/path.js";
|
|
9
12
|
export class TextRenderer {
|
|
10
13
|
// Measuring only needs metrics, not the full object manager. (The render pass below
|
|
11
14
|
// still receives the manager because it also registers fonts/images.)
|
|
12
|
-
static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, maxLines, overflow, lineHeight, letterSpacing = 0) {
|
|
15
|
+
static calculateTextHeight(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, maxLines, overflow, lineHeight, letterSpacing = 0, lineOptions = {}) {
|
|
13
16
|
// Plain string: every wrapped line gets the same box.
|
|
14
17
|
if (typeof content === "string") {
|
|
15
|
-
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing);
|
|
18
|
+
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing, lineOptions);
|
|
16
19
|
const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
|
|
17
20
|
return lines.length * box.height;
|
|
18
21
|
}
|
|
19
|
-
// Segments: each line's box comes from the fonts actually on it.
|
|
20
|
-
|
|
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
|
+
};
|
|
21
33
|
const lines = breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow);
|
|
22
34
|
return lines.reduce((total, line) => total + lineBoxForSegmentLine(line, defaults, objectManager, lineHeight).height, 0);
|
|
23
35
|
}
|
|
24
36
|
static async render(textElement, objectManager) {
|
|
25
|
-
const { x, y, width, fontSize, color, content, fontFamily, fontStyle, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, 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();
|
|
26
38
|
// Component -> display list. Wrapping and positioning stay here; the backend
|
|
27
39
|
// turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
|
|
28
40
|
// the original renderer - unifying it into the engine is Phase 3.
|
|
29
|
-
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);
|
|
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);
|
|
30
42
|
// Accessible tagging: this whole text block is one structure element (a paragraph P, or a heading
|
|
31
43
|
// from `role`); each run's text lands under it. Emoji sub-nodes produced below stay untagged (drawn
|
|
32
44
|
// as artifacts). Keyed by structId so a block split across pages stays one element.
|
|
@@ -61,6 +73,13 @@ export class TextRenderer {
|
|
|
61
73
|
const imageSource = om.getEmojiImageSource();
|
|
62
74
|
if (!own && !fallback && !imageSource)
|
|
63
75
|
return [run];
|
|
76
|
+
// A SHAPED run cannot be split here: the sub-runs are cut by code point, while `glyphs` is a
|
|
77
|
+
// drawn-order list in which a ligature is one glyph for several code points - so no sub-run could
|
|
78
|
+
// be given the right slice, and inheriting the whole list would draw every glyph again per piece.
|
|
79
|
+
// Passed through whole instead: an emoji inside Arabic then comes from the text font rather than
|
|
80
|
+
// the emoji source (`todo.md`).
|
|
81
|
+
if (run.glyphs)
|
|
82
|
+
return [run];
|
|
64
83
|
const nodes = [];
|
|
65
84
|
let cursorX = run.x; // absolute x of the next glyph
|
|
66
85
|
let pending = ""; // run of consecutive non-color characters
|
|
@@ -152,19 +171,9 @@ export class TextRenderer {
|
|
|
152
171
|
out.push({ op: "l", x: curX, y: curY });
|
|
153
172
|
}
|
|
154
173
|
else if (cmd.type === "Q") {
|
|
155
|
-
const ctrlX = mapX(cmd.cx, cmd.cy);
|
|
156
|
-
const ctrlY = mapY(cmd.cx, cmd.cy);
|
|
157
174
|
const endX = mapX(cmd.x, cmd.y);
|
|
158
175
|
const endY = mapY(cmd.x, cmd.y);
|
|
159
|
-
out.push(
|
|
160
|
-
op: "c",
|
|
161
|
-
x1: curX + (2 / 3) * (ctrlX - curX),
|
|
162
|
-
y1: curY + (2 / 3) * (ctrlY - curY),
|
|
163
|
-
x2: endX + (2 / 3) * (ctrlX - endX),
|
|
164
|
-
y2: endY + (2 / 3) * (ctrlY - endY),
|
|
165
|
-
x: endX,
|
|
166
|
-
y: endY,
|
|
167
|
-
});
|
|
176
|
+
out.push(quadToCubic(curX, curY, mapX(cmd.cx, cmd.cy), mapY(cmd.cx, cmd.cy), endX, endY));
|
|
168
177
|
curX = endX;
|
|
169
178
|
curY = endY;
|
|
170
179
|
}
|
|
@@ -215,7 +224,7 @@ export class TextRenderer {
|
|
|
215
224
|
// Lay the content out into absolutely-positioned text runs. Glyph positions match
|
|
216
225
|
// the previous hand-written operators exactly (verified by pixel-identical render);
|
|
217
226
|
// only the output form changed from PDF strings to `TextRun`s.
|
|
218
|
-
static _buildRuns(content, fontSize, fontFamily, fontStyle, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight, underline = false, strikethrough = false, skipInk = false, letterSpacing = 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) {
|
|
219
228
|
const runs = [];
|
|
220
229
|
// /Link annotation rects for any `href` spans, collected as we place segments. Empty for the
|
|
221
230
|
// common (no-href) case, so plain text keeps producing exactly the runs it did before.
|
|
@@ -224,13 +233,74 @@ export class TextRenderer {
|
|
|
224
233
|
// per line, and a decorated span only spans its own glyphs).
|
|
225
234
|
const decorations = [];
|
|
226
235
|
// Horizontal offset of a line of the given width under the current alignment.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
// `start` is the one alignment that depends on the direction, resolved at the single place
|
|
237
|
+
// alignment is consumed.
|
|
238
|
+
const align = textAlignment === HorizontalAlignment.start
|
|
239
|
+
? direction === "rtl"
|
|
240
|
+
? HorizontalAlignment.right
|
|
241
|
+
: HorizontalAlignment.left
|
|
242
|
+
: textAlignment;
|
|
243
|
+
const alignmentOffset = (lineWidth, indent = 0) => {
|
|
244
|
+
const room = maxWidth - indent;
|
|
245
|
+
if (align === HorizontalAlignment.center)
|
|
246
|
+
return (room - lineWidth) / 2;
|
|
247
|
+
if (align === HorizontalAlignment.right)
|
|
248
|
+
return room - lineWidth;
|
|
232
249
|
return 0;
|
|
233
250
|
};
|
|
251
|
+
/**
|
|
252
|
+
* Justification: how much extra to put in EACH space of a line so it reaches the full width.
|
|
253
|
+
* The LAST line of a paragraph keeps its natural spacing, as it does in CSS and in print - a
|
|
254
|
+
* justified last line is the classic sign of a broken text engine.
|
|
255
|
+
*
|
|
256
|
+
* Spaces are stretched by moving the pen, not by the `Tw` operator: `Tw` applies only to the
|
|
257
|
+
* single byte 32, so an embedded (Identity-H) font would ignore it entirely.
|
|
258
|
+
*/
|
|
259
|
+
const justifyExtra = (line, naturalWidth, isLast, indent = 0) => {
|
|
260
|
+
if (align !== HorizontalAlignment.justify)
|
|
261
|
+
return 0;
|
|
262
|
+
const gaps = [...line].filter((c) => c === " ").length;
|
|
263
|
+
if (gaps === 0)
|
|
264
|
+
return 0;
|
|
265
|
+
const slack = maxWidth - indent - naturalWidth;
|
|
266
|
+
// Negative slack is real now: the breaker keeps a word when the line can be SQUEEZED to hold
|
|
267
|
+
// it, so justification has to deliver that squeeze. The floor is the same limit the breaker
|
|
268
|
+
// used, which is what stops a line it never promised from being crushed.
|
|
269
|
+
const spaceWidth = runAdvance(objectManager, " ", { fontFamily, fontSize, fontStyle }, letterSpacing);
|
|
270
|
+
const extra = Math.max(slack / gaps, -spaceWidth * MAX_SPACE_SHRINK);
|
|
271
|
+
// The last line is never STRETCHED - the rule print and CSS follow. It may still be SQUEEZED:
|
|
272
|
+
// the breaker was allowed to pack it that way, and drawing it loose would push it out of its box.
|
|
273
|
+
return isLast ? Math.min(0, extra) : extra;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Push one drawn piece and return the advance it took. When the line is justified the pen jumps an
|
|
277
|
+
* extra `extra` at every space, which means one run per word - the only way that works for an
|
|
278
|
+
* embedded font, whose spaces `Tw` cannot reach.
|
|
279
|
+
*
|
|
280
|
+
* A SHAPED piece is never split: its glyph list is in drawn order with ligatures merged, so no
|
|
281
|
+
* slice of it is correct. Such a piece keeps its natural spacing.
|
|
282
|
+
*/
|
|
283
|
+
const emitPiece = (piece, x, extra, style, runFont, spacing) => {
|
|
284
|
+
const push = (at, text, glyphs) => {
|
|
285
|
+
runs.push({ type: "text", x: at, text, ...(glyphs ? { glyphs } : {}), ...style });
|
|
286
|
+
};
|
|
287
|
+
if (extra === 0 || piece.glyphs || !piece.text.includes(" ")) {
|
|
288
|
+
push(x, piece.text, piece.glyphs);
|
|
289
|
+
return runAdvance(objectManager, piece.text, runFont, spacing);
|
|
290
|
+
}
|
|
291
|
+
const spaceWidth = runAdvance(objectManager, " ", runFont, spacing);
|
|
292
|
+
const words = piece.text.split(" ");
|
|
293
|
+
let pen = x;
|
|
294
|
+
words.forEach((word, i) => {
|
|
295
|
+
if (word !== "") {
|
|
296
|
+
push(pen, word);
|
|
297
|
+
pen += runAdvance(objectManager, word, runFont, spacing);
|
|
298
|
+
}
|
|
299
|
+
if (i < words.length - 1)
|
|
300
|
+
pen += spaceWidth + extra;
|
|
301
|
+
});
|
|
302
|
+
return pen - x;
|
|
303
|
+
};
|
|
234
304
|
// The decoration strokes for one drawn run, at the geometry ITS OWN font declares. A mixed-size
|
|
235
305
|
// line therefore gets a thicker line under the bigger span, which is what a browser does too.
|
|
236
306
|
const decorate = (text, runX, runWidth, baselineY, family, style, size, runColor, wantsUnderline, wantsStrikethrough, runLetterSpacing) => {
|
|
@@ -276,31 +346,62 @@ export class TextRenderer {
|
|
|
276
346
|
const inkSpans = objectManager.getInkSpansInBand(text, size, family, style, -below + half, -below - half, runLetterSpacing);
|
|
277
347
|
return skipInkSegments(runWidth, inkSpans, stroke.thickness);
|
|
278
348
|
};
|
|
349
|
+
/**
|
|
350
|
+
* One drawn piece of a line. SHAPING must see the run's LOGICAL text - a letter's form comes from
|
|
351
|
+
* its neighbours, and a reversed run has those swapped - so we shape that and then reverse the
|
|
352
|
+
* GLYPHS for an rtl run. A font that does not shape keeps the old path exactly: draw the visual
|
|
353
|
+
* text, no glyph list, byte-identical output.
|
|
354
|
+
*/
|
|
355
|
+
const shapedPiece = (part, family, style) => {
|
|
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 : []);
|
|
359
|
+
if (!shaped)
|
|
360
|
+
return { text: part.text };
|
|
361
|
+
const ordered = part.rtl ? [...shaped].reverse() : shaped;
|
|
362
|
+
return { text: part.logical, glyphs: ordered.map((g) => g.glyph) };
|
|
363
|
+
};
|
|
279
364
|
// The advance the viewer will use: plain glyph widths (a `Tj` never kerns) plus one
|
|
280
365
|
// `letterSpacing` per code point (the `Tc` operator), from the one shared `advance.ts` primitive.
|
|
281
|
-
const font = { fontFamily, fontSize, fontStyle };
|
|
366
|
+
const font = { fontFamily, fontSize, fontStyle, ligatures };
|
|
282
367
|
// --- Plain string: one run per wrapped line. ---
|
|
283
368
|
if (typeof content === "string") {
|
|
284
|
-
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing
|
|
369
|
+
const lines = wrapStringIntoLines(content, fontFamily, fontSize, fontStyle, maxWidth, objectManager, maxLines, overflow, letterSpacing,
|
|
370
|
+
// The SAME knobs the measure pass used (`TextElement.lineOptions`), or a line would be
|
|
371
|
+
// measured one width and drawn another.
|
|
372
|
+
{
|
|
373
|
+
wordSpacing,
|
|
374
|
+
indent: textIndent,
|
|
375
|
+
shrink: align === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
376
|
+
splitting: { breakWord, hyphenate },
|
|
377
|
+
ligatures,
|
|
378
|
+
});
|
|
285
379
|
// yPosition is the top of the text box (top-left). The line box seats its own baseline; lines
|
|
286
380
|
// then stack by that box's height. Same numbers as calculateTextHeight, by construction.
|
|
287
381
|
const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
|
|
288
382
|
lines.forEach((line, index) => {
|
|
289
|
-
const lineWidth =
|
|
290
|
-
|
|
383
|
+
const lineWidth = singleLineWidth(line, font, objectManager, letterSpacing, wordSpacing);
|
|
384
|
+
// CSS `text-indent`: the FIRST line starts in, and had that much less room to fill.
|
|
385
|
+
const indent = index === 0 ? textIndent : 0;
|
|
386
|
+
let x = initialX + indent + alignmentOffset(lineWidth, indent);
|
|
291
387
|
const baseline = yPosition + box.baseline + box.height * index;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
fontFamily,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
388
|
+
// Latin-only text in an `ltr` paragraph comes back as one unchanged piece.
|
|
389
|
+
// Word-spacing is a per-gap advance just like the justification slack, so it rides the same
|
|
390
|
+
// mechanism: `Tw` would reach only the single byte 32 and miss every embedded font.
|
|
391
|
+
const extra = wordSpacing + justifyExtra(line, lineWidth, index === lines.length - 1, indent);
|
|
392
|
+
for (const part of visualRuns(line, direction)) {
|
|
393
|
+
const piece = shapedPiece(part, fontFamily, fontStyle);
|
|
394
|
+
const partWidth = emitPiece(piece, x, extra, {
|
|
395
|
+
y: baseline,
|
|
396
|
+
fontFamily,
|
|
397
|
+
fontStyle,
|
|
398
|
+
fontSize,
|
|
399
|
+
color,
|
|
400
|
+
...(letterSpacing ? { letterSpacing } : {}),
|
|
401
|
+
}, font, letterSpacing);
|
|
402
|
+
decorate(piece.text, x, partWidth, baseline, fontFamily, fontStyle, fontSize, color, underline, strikethrough, letterSpacing);
|
|
403
|
+
x += partWidth;
|
|
404
|
+
}
|
|
304
405
|
});
|
|
305
406
|
return { runs, links, decorations };
|
|
306
407
|
}
|
|
@@ -309,27 +410,34 @@ export class TextRenderer {
|
|
|
309
410
|
// the previous segment's kerning-free advance. Each line drops by its OWN leading
|
|
310
411
|
// (tallest font on that line), so mixed-font lines space correctly and the drawn
|
|
311
412
|
// height matches the measured height.
|
|
312
|
-
const pushLine = (line, lineY) => {
|
|
413
|
+
const pushLine = (line, lineY, isLast) => {
|
|
313
414
|
let x = initialX + alignmentOffset(line.width);
|
|
314
|
-
line.segments.
|
|
415
|
+
const extra = justifyExtra(line.segments.map((seg) => seg.content).join(""), line.width, isLast);
|
|
416
|
+
// Across span boundaries, so a Hebrew span is ordered against its neighbours rather than inside
|
|
417
|
+
// itself. Each run keeps the segment it came from, and with it that span's style.
|
|
418
|
+
const ordered = visualRunsOf(line.segments.map((segment) => ({ text: segment.content, source: segment })), direction);
|
|
419
|
+
ordered.forEach((part) => {
|
|
420
|
+
const segment = part.source;
|
|
315
421
|
const family = segment.fontFamily || fontFamily;
|
|
316
422
|
const size = segment.fontSize || fontSize;
|
|
317
423
|
const style = segment.fontStyle || fontStyle;
|
|
318
424
|
const segLetterSpacing = segment.letterSpacing ?? letterSpacing;
|
|
319
|
-
const
|
|
425
|
+
const piece = shapedPiece(part, family, style);
|
|
426
|
+
const runText = piece.text;
|
|
320
427
|
const runColor = segment.fontColor || color;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
428
|
+
// `vertical-align`: this run alone leaves the line's baseline. Its own decoration and link box
|
|
429
|
+
// ride along, since both are placed from the same y.
|
|
430
|
+
const shift = VERTICAL_TEXT_SHIFT[segment.verticalAlign ?? "baseline"] * size;
|
|
431
|
+
const runY = lineY - shift;
|
|
432
|
+
const drawn = emitPiece(piece, x, extra, {
|
|
433
|
+
y: runY,
|
|
326
434
|
fontFamily: family,
|
|
327
435
|
fontStyle: style,
|
|
328
436
|
fontSize: size,
|
|
329
437
|
color: runColor,
|
|
330
438
|
...(segLetterSpacing ? { letterSpacing: segLetterSpacing } : {}),
|
|
331
|
-
});
|
|
332
|
-
decorate(
|
|
439
|
+
}, { fontFamily: family, fontSize: size, fontStyle: style }, segLetterSpacing);
|
|
440
|
+
decorate(runText, x, drawn, runY, family, style, size, runColor, segment.underline ?? underline, segment.strikethrough ?? strikethrough, segLetterSpacing);
|
|
333
441
|
// An href/to span becomes a /Link annotation over exactly this run's glyph box: from its own
|
|
334
442
|
// ascent above the baseline down to its own descender. A span wrapped across lines yields
|
|
335
443
|
// one rect per line.
|
|
@@ -338,14 +446,14 @@ export class TextRenderer {
|
|
|
338
446
|
links.push({
|
|
339
447
|
type: "link",
|
|
340
448
|
x,
|
|
341
|
-
y:
|
|
342
|
-
width:
|
|
449
|
+
y: runY - v.ascent * size,
|
|
450
|
+
width: drawn,
|
|
343
451
|
height: (v.ascent + v.descent) * size,
|
|
344
452
|
href: segment.href,
|
|
345
453
|
dest: segment.dest,
|
|
346
454
|
});
|
|
347
455
|
}
|
|
348
|
-
x +=
|
|
456
|
+
x += drawn;
|
|
349
457
|
});
|
|
350
458
|
};
|
|
351
459
|
// yPosition is the top of the text box (top-left). Each line seats its own baseline inside its
|
|
@@ -353,13 +461,24 @@ export class TextRenderer {
|
|
|
353
461
|
// The seam flips the whole thing to PDF space. `letterSpacing` MUST be in the defaults so a span
|
|
354
462
|
// that does not override it wraps and aligns with the element's spacing - the same defaults the
|
|
355
463
|
// measure path (calculateTextHeight) uses, or segmented spaced text mis-wraps (measured != drawn).
|
|
356
|
-
const defaults = {
|
|
464
|
+
const defaults = {
|
|
465
|
+
fontFamily,
|
|
466
|
+
fontSize,
|
|
467
|
+
fontStyle,
|
|
468
|
+
letterSpacing,
|
|
469
|
+
splitting: { breakWord, hyphenate },
|
|
470
|
+
ligatures,
|
|
471
|
+
};
|
|
357
472
|
let top = yPosition;
|
|
358
|
-
|
|
473
|
+
// Materialised, because justification has to know which line is the LAST one of the paragraph.
|
|
474
|
+
const segmentLines = [
|
|
475
|
+
...breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow),
|
|
476
|
+
];
|
|
477
|
+
segmentLines.forEach((line, index) => {
|
|
359
478
|
const box = lineBoxForSegmentLine(line, defaults, objectManager, lineHeight);
|
|
360
|
-
pushLine(line, top + box.baseline);
|
|
479
|
+
pushLine(line, top + box.baseline, index === segmentLines.length - 1);
|
|
361
480
|
top += box.height;
|
|
362
|
-
}
|
|
481
|
+
});
|
|
363
482
|
return { runs, links, decorations };
|
|
364
483
|
}
|
|
365
484
|
}
|
|
@@ -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>;
|
package/dist/svg/css.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { SvgUnsupportedError } from "./errors.js";
|
|
2
|
+
const COMMENTS = /\/\*[\s\S]*?\*\//g;
|
|
3
|
+
/** One compound selector, e.g. `rect.st0#a`. Anything structural is refused, not approximated. */
|
|
4
|
+
function parseSelector(selector, order) {
|
|
5
|
+
const text = selector.trim();
|
|
6
|
+
if (/[\s>+~]/.test(text)) {
|
|
7
|
+
throw new SvgUnsupportedError(`the CSS selector "${text}" (it combines elements)`, "Only flat selectors are resolved. Re-export with presentation attributes instead.");
|
|
8
|
+
}
|
|
9
|
+
if (text.includes(":") || text.includes("[")) {
|
|
10
|
+
throw new SvgUnsupportedError(`the CSS selector "${text}" (pseudo-class or attribute selector)`, "Only a tag, .class and #id are resolved. Re-export with presentation attributes instead.");
|
|
11
|
+
}
|
|
12
|
+
const classes = [...text.matchAll(/\.([\w-]+)/g)].map((m) => m[1]);
|
|
13
|
+
const id = /#([\w-]+)/.exec(text)?.[1];
|
|
14
|
+
const tag = /^[\w-]+/.exec(text)?.[0];
|
|
15
|
+
// The usual a-b-c weights, flattened: an id beats any number of classes, a class beats a tag.
|
|
16
|
+
const weight = (id ? 10000 : 0) + classes.length * 100 + (tag ? 1 : 0);
|
|
17
|
+
return { tag, id, classes, weight, order };
|
|
18
|
+
}
|
|
19
|
+
function parseDeclarations(body) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const declaration of body.split(";")) {
|
|
22
|
+
const colon = declaration.indexOf(":");
|
|
23
|
+
if (colon === -1)
|
|
24
|
+
continue;
|
|
25
|
+
const property = declaration.slice(0, colon).trim();
|
|
26
|
+
// `!important` would need a second cascade level; exporters do not emit it, so it is refused
|
|
27
|
+
// rather than silently treated as a normal declaration.
|
|
28
|
+
const value = declaration.slice(colon + 1).trim();
|
|
29
|
+
if (!property)
|
|
30
|
+
continue;
|
|
31
|
+
if (/!\s*important$/i.test(value)) {
|
|
32
|
+
throw new SvgUnsupportedError(`"!important" in a <style> block`, "Remove it, or re-export with presentation attributes.");
|
|
33
|
+
}
|
|
34
|
+
out[property] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parses every `<style>` block's text into rules, in source order.
|
|
40
|
+
*
|
|
41
|
+
* The brace matching is done by hand rather than with one regex: `@media print { .a { … } }` nests,
|
|
42
|
+
* and a regex that skips to the first balanced-looking block silently applies the INNER rule while
|
|
43
|
+
* never seeing the at-rule that guards it.
|
|
44
|
+
*/
|
|
45
|
+
export function parseStylesheet(css) {
|
|
46
|
+
const text = css.replace(COMMENTS, "");
|
|
47
|
+
const rules = [];
|
|
48
|
+
let order = 0;
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < text.length) {
|
|
51
|
+
const open = text.indexOf("{", i);
|
|
52
|
+
if (open === -1)
|
|
53
|
+
break;
|
|
54
|
+
// A statement at-rule (`@import "a";`) ends at its semicolon. Taking everything up to the next
|
|
55
|
+
// brace as one prelude would swallow the rule that follows it.
|
|
56
|
+
const head = text.slice(i, open);
|
|
57
|
+
const prelude = head.slice(head.lastIndexOf(";") + 1).trim();
|
|
58
|
+
let depth = 1;
|
|
59
|
+
let j = open + 1;
|
|
60
|
+
while (j < text.length && depth > 0) {
|
|
61
|
+
if (text[j] === "{")
|
|
62
|
+
depth++;
|
|
63
|
+
else if (text[j] === "}")
|
|
64
|
+
depth--;
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
if (prelude.startsWith("@")) {
|
|
68
|
+
// An at-rule is DROPPED, not reported - which is what CSS itself does with one it cannot
|
|
69
|
+
// evaluate, and what the corpus demanded: 68 of 10,819 real files carry a `@media
|
|
70
|
+
// (prefers-color-scheme)` or a `@keyframes` that can never apply to a static page, and
|
|
71
|
+
// refusing them meant those files did not render at all.
|
|
72
|
+
// The exception is a query that always holds for print, whose rules DO apply.
|
|
73
|
+
const [at, ...rest] = prelude.split(/\s+/);
|
|
74
|
+
// A media list applies if ANY of its components does - `@media print, screen` is for print too.
|
|
75
|
+
const applies = at === "@media" &&
|
|
76
|
+
rest
|
|
77
|
+
.join(" ")
|
|
78
|
+
.split(",")
|
|
79
|
+
.some((q) => ["print", "all", ""].includes(q.trim()));
|
|
80
|
+
if (applies) {
|
|
81
|
+
// Its body is a nested stylesheet; parse it in place so the rules keep their source order.
|
|
82
|
+
for (const rule of parseStylesheet(text.slice(open + 1, j - 1))) {
|
|
83
|
+
rules.push({ ...rule, order: order++ });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
i = j;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const declarations = parseDeclarations(text.slice(open + 1, j - 1));
|
|
90
|
+
for (const selector of prelude.split(",")) {
|
|
91
|
+
if (selector.trim() === "")
|
|
92
|
+
continue;
|
|
93
|
+
rules.push({ ...parseSelector(selector, order++), declarations });
|
|
94
|
+
}
|
|
95
|
+
i = j;
|
|
96
|
+
}
|
|
97
|
+
return rules;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The declarations that apply to one element, weakest first - so a caller can just assign them in
|
|
101
|
+
* order and let the last one win, as the cascade does.
|
|
102
|
+
*/
|
|
103
|
+
export function declarationsFor(rules, tagName, attributes) {
|
|
104
|
+
if (rules.length === 0)
|
|
105
|
+
return {};
|
|
106
|
+
const classes = (attributes.class ?? "").trim().split(/\s+/).filter(Boolean);
|
|
107
|
+
const matched = rules.filter((rule) => (rule.tag === undefined || rule.tag === tagName) &&
|
|
108
|
+
(rule.id === undefined || rule.id === attributes.id) &&
|
|
109
|
+
rule.classes.every((name) => classes.includes(name)));
|
|
110
|
+
matched.sort((a, b) => a.weight - b.weight || a.order - b.order);
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const rule of matched)
|
|
113
|
+
Object.assign(out, rule.declarations);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG is a document format, not a graphics format: it carries CSS, text layout, filters, masks and
|
|
3
|
+
* animation. Filters and masks are PIXEL operations, so no vector backend can express them - which is
|
|
4
|
+
* why every complete SVG renderer owns a rasterizer, and why every PDF library ships a subset instead.
|
|
5
|
+
*
|
|
6
|
+
* We ship a subset too. The difference is what happens at its edge: an element we cannot draw is a
|
|
7
|
+
* NAMED ERROR, never a silent skip. Two reasons, and the second is the important one.
|
|
8
|
+
*
|
|
9
|
+
* 1. A silently dropped element leaves a logo that renders and is quietly wrong - the fill missing,
|
|
10
|
+
* the icon half there - and nobody finds out. (react-pdf skips, and its `<style>` handling loses
|
|
11
|
+
* every fill of an Illustrator export made with the default "Internal CSS" option.)
|
|
12
|
+
* 2. It keeps every later stage a MINOR. Supporting something new turns an error into a picture, which
|
|
13
|
+
* can never break a document that worked. Going the other way - from silently skipping to drawing -
|
|
14
|
+
* would change existing output.
|
|
15
|
+
*/
|
|
16
|
+
/** An element or attribute value we deliberately do not support yet. */
|
|
17
|
+
export declare class SvgUnsupportedError extends Error {
|
|
18
|
+
readonly feature: string;
|
|
19
|
+
constructor(feature: string, hint: string);
|
|
20
|
+
}
|
|
21
|
+
/** The file is not SVG we can read at all - malformed, or with no root `<svg>`. */
|
|
22
|
+
export declare class SvgParseError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG is a document format, not a graphics format: it carries CSS, text layout, filters, masks and
|
|
3
|
+
* animation. Filters and masks are PIXEL operations, so no vector backend can express them - which is
|
|
4
|
+
* why every complete SVG renderer owns a rasterizer, and why every PDF library ships a subset instead.
|
|
5
|
+
*
|
|
6
|
+
* We ship a subset too. The difference is what happens at its edge: an element we cannot draw is a
|
|
7
|
+
* NAMED ERROR, never a silent skip. Two reasons, and the second is the important one.
|
|
8
|
+
*
|
|
9
|
+
* 1. A silently dropped element leaves a logo that renders and is quietly wrong - the fill missing,
|
|
10
|
+
* the icon half there - and nobody finds out. (react-pdf skips, and its `<style>` handling loses
|
|
11
|
+
* every fill of an Illustrator export made with the default "Internal CSS" option.)
|
|
12
|
+
* 2. It keeps every later stage a MINOR. Supporting something new turns an error into a picture, which
|
|
13
|
+
* can never break a document that worked. Going the other way - from silently skipping to drawing -
|
|
14
|
+
* would change existing output.
|
|
15
|
+
*/
|
|
16
|
+
/** An element or attribute value we deliberately do not support yet. */
|
|
17
|
+
export class SvgUnsupportedError extends Error {
|
|
18
|
+
constructor(feature, hint) {
|
|
19
|
+
super(`@jasy/pdf: ${feature} is not supported yet. ${hint}`);
|
|
20
|
+
this.feature = feature;
|
|
21
|
+
this.name = "SvgUnsupportedError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The file is not SVG we can read at all - malformed, or with no root `<svg>`. */
|
|
25
|
+
export class SvgParseError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(`@jasy/pdf: ${message}`);
|
|
28
|
+
this.name = "SvgParseError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Gradient, GradientStop, PathCommand } from "../ir/display-list.js";
|
|
2
|
+
import { type Attributes } from "./style.js";
|
|
3
|
+
import type { XmlNode } from "./xml.js";
|
|
4
|
+
/**
|
|
5
|
+
* `<linearGradient>` and `<radialGradient>`.
|
|
6
|
+
*
|
|
7
|
+
* The backend has painted shadings since colour emoji, and `Box({ bg: linearGradient(...) })` uses
|
|
8
|
+
* the same node - so this file only has to turn a `url(#id)` reference into that node. Two things
|
|
9
|
+
* make it more than a lookup:
|
|
10
|
+
*
|
|
11
|
+
* - **`gradientUnits` defaults to `objectBoundingBox`**, i.e. the coordinates are fractions of the
|
|
12
|
+
* SHAPE's bounding box, not of the page. So a gradient cannot be resolved where it is defined,
|
|
13
|
+
* only where it is used - which is why this takes the shape's commands.
|
|
14
|
+
* - **`href` chains.** Illustrator emits one gradient with the stops and a second that references it
|
|
15
|
+
* and only moves the coordinates. Missing that leaves a shape with no stops at all.
|
|
16
|
+
*/
|
|
17
|
+
export interface GradientDef {
|
|
18
|
+
kind: "linear" | "radial";
|
|
19
|
+
attributes: Attributes;
|
|
20
|
+
stops: GradientStop[];
|
|
21
|
+
/** The `stop-opacity` of each stop, in the same order. */
|
|
22
|
+
opacities: number[];
|
|
23
|
+
/** The id this definition inherits from (`href` / `xlink:href`), without the `#`. */
|
|
24
|
+
href?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Every gradient definition in the document, by id - they are referenced, not drawn in place. */
|
|
27
|
+
export declare function gradientsOf(node: XmlNode, into?: Map<string, GradientDef>, depth?: number): Map<string, GradientDef>;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves a reference into an IR gradient positioned on `commands`.
|
|
30
|
+
*
|
|
31
|
+
* The anchors are mapped into the shape's own space, then through `gradientTransform` - so they end
|
|
32
|
+
* up in the SAME coordinates as the path, which is what the backend and the Y-flip both assume.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveSvgGradient(def: GradientDef, defs: ReadonlyMap<string, GradientDef>, commands: readonly PathCommand[]): Gradient;
|