@jasy/pdf 1.0.0-beta.3 → 1.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/color.d.ts +1 -24
- package/dist/api/color.js +2 -278
- package/dist/api/content.d.ts +10 -5
- package/dist/api/content.js +29 -13
- package/dist/api/descriptor.js +30 -3
- package/dist/api/flow.d.ts +55 -0
- package/dist/api/flow.js +71 -0
- package/dist/api/font-url.d.ts +1 -2
- package/dist/api/font-url.js +8 -74
- package/dist/api/forms.d.ts +3 -0
- package/dist/api/forms.js +3 -0
- package/dist/api/gradient.d.ts +1 -53
- package/dist/api/gradient.js +2 -97
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.js +3 -0
- package/dist/api/layout.d.ts +6 -0
- package/dist/api/layout.js +9 -3
- package/dist/api/page-builder.d.ts +4 -0
- package/dist/api/page-builder.js +4 -0
- package/dist/api/structure.d.ts +10 -2
- package/dist/api/structure.js +9 -6
- package/dist/api/table.d.ts +5 -3
- package/dist/api/table.js +101 -22
- package/dist/api/text.d.ts +8 -8
- package/dist/api/text.js +6 -0
- package/dist/canvas/painter.d.ts +2 -2
- package/dist/canvas/painter.js +2 -2
- package/dist/common/color-input.d.ts +24 -0
- package/dist/common/color-input.js +278 -0
- package/dist/common/diagnostics.d.ts +61 -0
- package/dist/common/diagnostics.js +77 -0
- package/dist/common/gradient.d.ts +53 -0
- package/dist/common/gradient.js +97 -0
- package/dist/edit/acroform-reader.js +7 -7
- package/dist/edit/appearance.js +3 -3
- package/dist/edit/attachments.d.ts +52 -0
- package/dist/edit/attachments.js +178 -0
- package/dist/edit/document.d.ts +38 -8
- package/dist/edit/document.js +208 -51
- package/dist/edit/fill.js +8 -23
- package/dist/edit/filters.d.ts +15 -0
- package/dist/edit/filters.js +157 -0
- package/dist/edit/index.d.ts +4 -1
- package/dist/edit/index.js +3 -1
- package/dist/edit/objects.d.ts +3 -4
- package/dist/edit/objects.js +5 -18
- package/dist/elements/canvas-element.d.ts +3 -0
- package/dist/elements/canvas-element.js +14 -3
- package/dist/elements/container-element.d.ts +8 -1
- package/dist/elements/container-element.js +49 -17
- package/dist/elements/forms/checkbox-element.d.ts +2 -0
- package/dist/elements/forms/checkbox-element.js +12 -3
- package/dist/elements/forms/choice-element.d.ts +6 -1
- package/dist/elements/forms/choice-element.js +17 -6
- package/dist/elements/forms/field-font.d.ts +8 -0
- package/dist/elements/forms/field-font.js +5 -0
- package/dist/elements/forms/push-button-element.d.ts +6 -1
- package/dist/elements/forms/push-button-element.js +16 -4
- package/dist/elements/forms/radio-element.d.ts +2 -0
- package/dist/elements/forms/radio-element.js +12 -3
- package/dist/elements/forms/signature-element.d.ts +6 -1
- package/dist/elements/forms/signature-element.js +16 -4
- package/dist/elements/forms/text-field-element.d.ts +6 -1
- package/dist/elements/forms/text-field-element.js +16 -4
- package/dist/elements/image-element.d.ts +70 -21
- package/dist/elements/image-element.js +173 -52
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.js +3 -0
- package/dist/elements/layout/anchor-element.d.ts +8 -10
- package/dist/elements/layout/anchor-element.js +10 -18
- package/dist/elements/layout/bookmark-element.d.ts +9 -11
- package/dist/elements/layout/bookmark-element.js +11 -20
- package/dist/elements/layout/column-break-element.d.ts +17 -0
- package/dist/elements/layout/column-break-element.js +26 -0
- package/dist/elements/layout/default-text-style-element.d.ts +11 -13
- package/dist/elements/layout/default-text-style-element.js +14 -31
- package/dist/elements/layout/deferred-element.d.ts +20 -4
- package/dist/elements/layout/deferred-element.js +41 -9
- package/dist/elements/layout/expanded-element.d.ts +11 -0
- package/dist/elements/layout/expanded-element.js +53 -18
- package/dist/elements/layout/flow-element.d.ts +109 -0
- package/dist/elements/layout/flow-element.js +317 -0
- package/dist/elements/layout/keep-together-element.d.ts +8 -18
- package/dist/elements/layout/keep-together-element.js +33 -45
- package/dist/elements/layout/link-element.d.ts +8 -13
- package/dist/elements/layout/link-element.js +9 -19
- package/dist/elements/layout/padding-element.d.ts +18 -1
- package/dist/elements/layout/padding-element.js +63 -28
- package/dist/elements/layout/page-break-element.d.ts +4 -1
- package/dist/elements/layout/page-break-element.js +6 -1
- package/dist/elements/layout/page-builder-element.d.ts +5 -0
- package/dist/elements/layout/page-builder-element.js +13 -2
- package/dist/elements/layout/positioned-element.d.ts +23 -4
- package/dist/elements/layout/positioned-element.js +42 -12
- package/dist/elements/layout/proxy-element.d.ts +47 -0
- package/dist/elements/layout/proxy-element.js +106 -0
- package/dist/elements/layout/region-element.d.ts +53 -0
- package/dist/elements/layout/region-element.js +108 -0
- package/dist/elements/layout/repeating-header-element.d.ts +9 -0
- package/dist/elements/layout/repeating-header-element.js +55 -13
- package/dist/elements/layout/rotated-box-element.d.ts +6 -0
- package/dist/elements/layout/rotated-box-element.js +27 -14
- package/dist/elements/layout/rotated-element.d.ts +9 -0
- package/dist/elements/layout/rotated-element.js +33 -2
- package/dist/elements/layout/struct-group.d.ts +9 -17
- package/dist/elements/layout/struct-group.js +10 -35
- package/dist/elements/line-element.d.ts +1 -0
- package/dist/elements/line-element.js +6 -0
- package/dist/elements/page-element.d.ts +10 -3
- package/dist/elements/page-element.js +39 -31
- package/dist/elements/pdf-document-element.d.ts +3 -0
- package/dist/elements/pdf-document-element.js +7 -0
- package/dist/elements/pdf-element.d.ts +105 -16
- package/dist/elements/pdf-element.js +118 -19
- package/dist/elements/rectangle-element.d.ts +22 -2
- package/dist/elements/rectangle-element.js +119 -55
- package/dist/elements/row-element.d.ts +17 -12
- package/dist/elements/row-element.js +89 -9
- package/dist/elements/svg-element.d.ts +30 -6
- package/dist/elements/svg-element.js +91 -26
- package/dist/elements/text-element.d.ts +48 -13
- package/dist/elements/text-element.js +161 -116
- package/dist/forms/acroform.d.ts +9 -1
- package/dist/forms/acroform.js +83 -34
- package/dist/forms/appearance.d.ts +17 -6
- package/dist/forms/appearance.js +18 -16
- package/dist/ir/display-list.d.ts +18 -3
- package/dist/layout/box-constraints.d.ts +17 -0
- package/dist/layout/box-constraints.js +31 -0
- package/dist/layout/collect-images.d.ts +6 -4
- package/dist/layout/collect-images.js +14 -8
- package/dist/layout/flex-item.d.ts +11 -0
- package/dist/layout/flex-item.js +51 -0
- package/dist/layout/fragmentation.d.ts +23 -9
- package/dist/layout/fragmentation.js +62 -19
- package/dist/layout/layout-cache.d.ts +53 -0
- package/dist/layout/layout-cache.js +110 -0
- package/dist/platform/browser-fs.d.ts +1 -2
- package/dist/platform/browser-fs.js +3 -6
- package/dist/platform/node-fs.d.ts +0 -1
- package/dist/platform/node-fs.js +0 -4
- package/dist/renderer/anchor-renderer.d.ts +2 -1
- package/dist/renderer/anchor-renderer.js +5 -4
- package/dist/renderer/bookmark-renderer.d.ts +2 -1
- package/dist/renderer/bookmark-renderer.js +5 -4
- package/dist/renderer/canvas-renderer.d.ts +2 -1
- package/dist/renderer/canvas-renderer.js +5 -2
- package/dist/renderer/column-break-renderer.d.ts +8 -0
- package/dist/renderer/column-break-renderer.js +15 -0
- package/dist/renderer/container-renderer.d.ts +2 -1
- package/dist/renderer/container-renderer.js +6 -5
- package/dist/renderer/default-text-style-renderer.d.ts +2 -1
- package/dist/renderer/default-text-style-renderer.js +2 -3
- package/dist/renderer/deferred-renderer.d.ts +2 -1
- package/dist/renderer/deferred-renderer.js +2 -3
- package/dist/renderer/emoji-image.js +3 -1
- package/dist/renderer/expanded-renderer.d.ts +2 -1
- package/dist/renderer/expanded-renderer.js +2 -3
- package/dist/renderer/flow-renderer.d.ts +16 -0
- package/dist/renderer/flow-renderer.js +21 -0
- package/dist/renderer/form-field-renderer.d.ts +2 -1
- package/dist/renderer/form-field-renderer.js +16 -3
- package/dist/renderer/image-renderer.d.ts +2 -1
- package/dist/renderer/image-renderer.js +33 -53
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.d.ts +2 -1
- package/dist/renderer/keep-together-renderer.js +2 -3
- package/dist/renderer/line-renderer.d.ts +2 -1
- package/dist/renderer/line-renderer.js +7 -2
- package/dist/renderer/link-renderer.d.ts +2 -1
- package/dist/renderer/link-renderer.js +36 -5
- package/dist/renderer/padding-renderer.d.ts +2 -1
- package/dist/renderer/padding-renderer.js +5 -5
- package/dist/renderer/page-break-renderer.d.ts +6 -5
- package/dist/renderer/page-break-renderer.js +14 -7
- package/dist/renderer/page-builder-renderer.d.ts +2 -1
- package/dist/renderer/page-builder-renderer.js +2 -3
- package/dist/renderer/page-renderer.js +30 -10
- package/dist/renderer/pdf-backend.d.ts +4 -3
- package/dist/renderer/pdf-backend.js +14 -7
- package/dist/renderer/pdf-document-renderer.d.ts +1 -8
- package/dist/renderer/pdf-document-renderer.js +17 -17
- package/dist/renderer/pdf-renderer.js +20 -11
- package/dist/renderer/positioned-renderer.d.ts +18 -1
- package/dist/renderer/positioned-renderer.js +43 -6
- package/dist/renderer/rectangle-renderer.d.ts +2 -1
- package/dist/renderer/rectangle-renderer.js +18 -8
- package/dist/renderer/repeating-header-renderer.d.ts +2 -1
- package/dist/renderer/repeating-header-renderer.js +6 -5
- package/dist/renderer/rotated-renderer.d.ts +2 -1
- package/dist/renderer/rotated-renderer.js +9 -5
- package/dist/renderer/row-renderer.d.ts +2 -1
- package/dist/renderer/row-renderer.js +6 -5
- package/dist/renderer/struct-group-renderer.d.ts +2 -1
- package/dist/renderer/struct-group-renderer.js +2 -3
- package/dist/renderer/svg-renderer.d.ts +2 -1
- package/dist/renderer/svg-renderer.js +8 -2
- package/dist/renderer/text-renderer.d.ts +4 -6
- package/dist/renderer/text-renderer.js +114 -127
- package/dist/svg/gradients.js +5 -0
- package/dist/svg/index.d.ts +1 -1
- package/dist/svg/index.js +36 -6
- package/dist/svg/style.d.ts +2 -0
- package/dist/svg/style.js +12 -1
- package/dist/text/advance.d.ts +4 -0
- package/dist/text/advance.js +17 -4
- package/dist/text/bidi.d.ts +0 -2
- package/dist/text/bidi.js +0 -10
- package/dist/text/glyph-coverage.d.ts +1 -1
- package/dist/text/glyph-coverage.js +7 -3
- package/dist/text/hyphen-breaks.d.ts +17 -0
- package/dist/text/hyphen-breaks.js +54 -0
- package/dist/text/line-breaker.d.ts +55 -29
- package/dist/text/line-breaker.js +453 -188
- package/dist/text/line-metrics.d.ts +8 -3
- package/dist/text/line-metrics.js +11 -2
- package/dist/text/pdf-text-string.d.ts +21 -0
- package/dist/text/pdf-text-string.js +114 -0
- package/dist/text/text-decoration.d.ts +5 -5
- package/dist/text/text-decoration.js +5 -5
- package/dist/text/word-splitting.d.ts +15 -2
- package/dist/text/word-splitting.js +45 -13
- package/dist/utils/afm-parser.d.ts +2 -0
- package/dist/utils/afm-parser.js +10 -1
- package/dist/utils/dest-registry.d.ts +1 -1
- package/dist/utils/dest-registry.js +3 -2
- package/dist/utils/fetch-bytes.d.ts +19 -0
- package/dist/utils/fetch-bytes.js +79 -0
- package/dist/utils/flex-layout.d.ts +16 -14
- package/dist/utils/flex-layout.js +427 -217
- package/dist/utils/font-metrics.d.ts +4 -3
- package/dist/utils/image-helper.d.ts +59 -11
- package/dist/utils/image-helper.js +209 -81
- package/dist/utils/pdf-object-manager.d.ts +17 -6
- package/dist/utils/pdf-object-manager.js +70 -20
- package/dist/utils/renderer-registry.d.ts +49 -2
- package/dist/utils/renderer-registry.js +40 -0
- package/dist/utils/struct-tree.d.ts +24 -0
- package/dist/utils/struct-tree.js +61 -9
- package/dist/utils/ttf-parser.d.ts +4 -0
- package/dist/utils/ttf-parser.js +30 -6
- package/dist/validators/element-validator.js +2 -1
- package/package.json +1 -1
- package/dist/elements/layout/sized-container-element.d.ts +0 -14
- package/dist/elements/layout/sized-container-element.js +0 -32
|
@@ -3,42 +3,30 @@ import { HorizontalAlignment } from "../elements/pdf-element.js";
|
|
|
3
3
|
import { VERTICAL_TEXT_SHIFT } from "../elements/text-element.js";
|
|
4
4
|
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
5
5
|
import { emojiImageNode } from "./emoji-image.js";
|
|
6
|
-
import {
|
|
7
|
-
import { lineBoxForSegmentLine
|
|
6
|
+
import { MAX_SPACE_SHRINK, breakSegmentsIntoLines, lineGaps, runStyleOf, } from "../text/line-breaker.js";
|
|
7
|
+
import { lineBoxForSegmentLine } from "../text/line-metrics.js";
|
|
8
8
|
import { runAdvance } from "../text/advance.js";
|
|
9
|
-
import {
|
|
9
|
+
import { visualRunsOf } from "../text/bidi.js";
|
|
10
10
|
import { skipInkSegments, strikethroughStroke, underlineStroke, } from "../text/text-decoration.js";
|
|
11
11
|
import { quadToCubic } from "../vector/path.js";
|
|
12
|
+
/** What a link says when read aloud (its `/Contents`): its pieces, a space where a line ended. */
|
|
13
|
+
function spokenText(pieces) {
|
|
14
|
+
let out = "";
|
|
15
|
+
pieces.forEach((piece, i) => {
|
|
16
|
+
if (i > 0 && piece.y !== pieces[i - 1].y)
|
|
17
|
+
out += " ";
|
|
18
|
+
out += piece.text;
|
|
19
|
+
});
|
|
20
|
+
return out.replace(/\s+/g, " ").trim();
|
|
21
|
+
}
|
|
12
22
|
export class TextRenderer {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return lines.length * box.height;
|
|
21
|
-
}
|
|
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
|
-
};
|
|
33
|
-
const lines = breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow);
|
|
34
|
-
return lines.reduce((total, line) => total + lineBoxForSegmentLine(line, defaults, objectManager, lineHeight).height, 0);
|
|
35
|
-
}
|
|
36
|
-
static async render(textElement, objectManager) {
|
|
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();
|
|
38
|
-
// Component -> display list. Wrapping and positioning stay here; the backend
|
|
39
|
-
// turns each run into BT/Tf/Td/Tj/ET. The wrapping algorithm is unchanged from
|
|
40
|
-
// the original renderer - unifying it into the engine is Phase 3.
|
|
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);
|
|
23
|
+
static async render(textElement, objectManager, scope) {
|
|
24
|
+
const { x, y, width, color, spans, textAlignment, maxLines, overflow, lineHeight, underline, strikethrough, skipInk, direction, textIndent, role, breaking, } = textElement.getProps();
|
|
25
|
+
// Component -> display list: the lines come from the one breaker, positioning stays here; the
|
|
26
|
+
// backend turns each run into BT/Tf/Td/Tj/ET.
|
|
27
|
+
const { runs, links, decorations, linkGroups } = TextRenderer._buildRuns(spans, breaking, objectManager, width ?? Number.NaN, textAlignment, color,
|
|
28
|
+
// Every run, decoration and link rect derives from this one absolute origin.
|
|
29
|
+
scope.origin.x + (x ?? 0), scope.origin.y + (y ?? 0), maxLines, overflow, lineHeight, underline, strikethrough, skipInk, direction, textIndent);
|
|
42
30
|
// Accessible tagging: this whole text block is one structure element (a paragraph P, or a heading
|
|
43
31
|
// from `role`); each run's text lands under it. Emoji sub-nodes produced below stay untagged (drawn
|
|
44
32
|
// as artifacts). Keyed by structId so a block split across pages stays one element.
|
|
@@ -47,6 +35,25 @@ export class TextRenderer {
|
|
|
47
35
|
const key = objectManager.struct.openElement(textElement.structId, pdfRole);
|
|
48
36
|
for (const run of runs)
|
|
49
37
|
run.tag = { role: pdfRole, key };
|
|
38
|
+
// An inline link is a /Link element inside the paragraph: it holds its own text and its
|
|
39
|
+
// annotations, which describe themselves by that text (PDF/UA 7.18.5). One cut by a page break
|
|
40
|
+
// stays ONE element: the next piece of the paragraph continues the link its last one ended in.
|
|
41
|
+
const struct = objectManager.struct;
|
|
42
|
+
const carried = struct.takeCarriedLink(textElement.structId);
|
|
43
|
+
linkGroups.forEach((group, i) => {
|
|
44
|
+
const continues = i === 0 && group.runs[0] === runs[0] && carried?.target === group.target;
|
|
45
|
+
const linkKey = continues ? carried.key : struct.addElement("Link", key);
|
|
46
|
+
for (const run of group.runs)
|
|
47
|
+
run.tag = { role: "Link", key: linkKey };
|
|
48
|
+
const first = group.links[0];
|
|
49
|
+
const alt = spokenText(group.pieces) || (first.href ?? first.dest ?? "");
|
|
50
|
+
for (const link of group.links)
|
|
51
|
+
Object.assign(link, { structKey: linkKey, alt });
|
|
52
|
+
const last = group.runs[group.runs.length - 1];
|
|
53
|
+
if (i === linkGroups.length - 1 && last === runs[runs.length - 1]) {
|
|
54
|
+
struct.carryLink(textElement.structId, group.target, linkKey);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
50
57
|
}
|
|
51
58
|
// Color fonts (COLR/CPAL): expand each run's emoji into filled vector layers (or fetched images
|
|
52
59
|
// for an image emoji source); plain runs and monochrome fonts pass straight through unchanged.
|
|
@@ -224,11 +231,17 @@ export class TextRenderer {
|
|
|
224
231
|
// Lay the content out into absolutely-positioned text runs. Glyph positions match
|
|
225
232
|
// the previous hand-written operators exactly (verified by pixel-identical render);
|
|
226
233
|
// only the output form changed from PDF strings to `TextRun`s.
|
|
227
|
-
static _buildRuns(content,
|
|
234
|
+
static _buildRuns(content,
|
|
235
|
+
// How the text is broken - the element's own settings, the ones it was measured with.
|
|
236
|
+
breaking, objectManager, maxWidth, textAlignment, color, initialX, yPosition, maxLines, overflow, lineHeight, underline = false, strikethrough = false, skipInk = false, direction = "ltr", textIndent = 0) {
|
|
228
237
|
const runs = [];
|
|
229
238
|
// /Link annotation rects for any `href` spans, collected as we place segments. Empty for the
|
|
230
239
|
// common (no-href) case, so plain text keeps producing exactly the runs it did before.
|
|
231
240
|
const links = [];
|
|
241
|
+
// The same links grouped as the reader meets them: consecutive runs of one target are ONE link,
|
|
242
|
+
// across a line break too, with the text they cover (for the structure tree).
|
|
243
|
+
const linkGroups = [];
|
|
244
|
+
let openLink;
|
|
232
245
|
// Underline / strikethrough strokes, one per decorated run (so a wrapped paragraph gets one
|
|
233
246
|
// per line, and a decorated span only spans its own glyphs).
|
|
234
247
|
const decorations = [];
|
|
@@ -256,44 +269,49 @@ export class TextRenderer {
|
|
|
256
269
|
* Spaces are stretched by moving the pen, not by the `Tw` operator: `Tw` applies only to the
|
|
257
270
|
* single byte 32, so an embedded (Identity-H) font would ignore it entirely.
|
|
258
271
|
*/
|
|
259
|
-
const justifyExtra = (
|
|
272
|
+
const justifyExtra = (segments, naturalWidth, isLast, indent = 0) => {
|
|
260
273
|
if (align !== HorizontalAlignment.justify)
|
|
261
274
|
return 0;
|
|
262
|
-
const gaps
|
|
275
|
+
const { gaps, width: gapWidth } = lineGaps(segments, breaking, objectManager);
|
|
263
276
|
if (gaps === 0)
|
|
264
277
|
return 0;
|
|
265
278
|
const slack = maxWidth - indent - naturalWidth;
|
|
266
279
|
// Negative slack is real now: the breaker keeps a word when the line can be SQUEEZED to hold
|
|
267
280
|
// 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
|
|
269
|
-
|
|
270
|
-
const extra = Math.max(slack / gaps, -
|
|
281
|
+
// used - the gaps' widths, summed - which is what stops a line it never promised from being
|
|
282
|
+
// crushed.
|
|
283
|
+
const extra = Math.max(slack / gaps, (-gapWidth * MAX_SPACE_SHRINK) / gaps);
|
|
271
284
|
// The last line is never STRETCHED - the rule print and CSS follow. It may still be SQUEEZED:
|
|
272
285
|
// the breaker was allowed to pack it that way, and drawing it loose would push it out of its box.
|
|
273
286
|
return isLast ? Math.min(0, extra) : extra;
|
|
274
287
|
};
|
|
275
288
|
/**
|
|
276
|
-
* Push one drawn piece and return the advance it took. When the
|
|
277
|
-
* extra `extra` at every space, which means one run per word - the
|
|
278
|
-
* embedded font, whose spaces `Tw` cannot reach.
|
|
289
|
+
* Push one drawn piece and return the advance it took. When the spaces get more room (word-spacing,
|
|
290
|
+
* justification) the pen jumps an extra `extra` at every space, which means one run per word - the
|
|
291
|
+
* only way that works for an embedded font, whose spaces `Tw` cannot reach.
|
|
279
292
|
*
|
|
280
|
-
* A SHAPED piece is
|
|
281
|
-
*
|
|
293
|
+
* A SHAPED piece is split there too, each word shaped on its own (`shape`): no slice of its glyph
|
|
294
|
+
* list is correct, but neither a ligature nor Arabic joining crosses a space - and a word plus a
|
|
295
|
+
* space is what the breaker measured.
|
|
282
296
|
*/
|
|
283
|
-
const emitPiece = (piece, x, extra, style, runFont, spacing) => {
|
|
297
|
+
const emitPiece = (piece, shape, rtl, x, extra, style, runFont, spacing) => {
|
|
284
298
|
const push = (at, text, glyphs) => {
|
|
285
299
|
runs.push({ type: "text", x: at, text, ...(glyphs ? { glyphs } : {}), ...style });
|
|
286
300
|
};
|
|
287
|
-
if (extra === 0 ||
|
|
301
|
+
if (extra === 0 || !piece.text.includes(" ")) {
|
|
288
302
|
push(x, piece.text, piece.glyphs);
|
|
289
303
|
return runAdvance(objectManager, piece.text, runFont, spacing);
|
|
290
304
|
}
|
|
291
305
|
const spaceWidth = runAdvance(objectManager, " ", runFont, spacing);
|
|
306
|
+
// A shaped piece holds its LOGICAL text: drawn right to left, its words come in reverse.
|
|
292
307
|
const words = piece.text.split(" ");
|
|
308
|
+
if (piece.glyphs && rtl)
|
|
309
|
+
words.reverse();
|
|
293
310
|
let pen = x;
|
|
294
311
|
words.forEach((word, i) => {
|
|
295
312
|
if (word !== "") {
|
|
296
|
-
|
|
313
|
+
const drawn = piece.glyphs ? shape(word) : { text: word };
|
|
314
|
+
push(pen, drawn.text, drawn.glyphs);
|
|
297
315
|
pen += runAdvance(objectManager, word, runFont, spacing);
|
|
298
316
|
}
|
|
299
317
|
if (i < words.length - 1)
|
|
@@ -303,7 +321,7 @@ export class TextRenderer {
|
|
|
303
321
|
};
|
|
304
322
|
// The decoration strokes for one drawn run, at the geometry ITS OWN font declares. A mixed-size
|
|
305
323
|
// line therefore gets a thicker line under the bigger span, which is what a browser does too.
|
|
306
|
-
const decorate = (text, runX, runWidth, baselineY, family, style, size, runColor, wantsUnderline, wantsStrikethrough, runLetterSpacing) => {
|
|
324
|
+
const decorate = (text, runX, runWidth, baselineY, family, style, size, runColor, wantsUnderline, wantsStrikethrough, runLetterSpacing, runSkipInk) => {
|
|
307
325
|
if ((!wantsUnderline && !wantsStrikethrough) || runWidth <= 0)
|
|
308
326
|
return;
|
|
309
327
|
const metrics = objectManager.getFontDecoration(family, style);
|
|
@@ -320,7 +338,7 @@ export class TextRenderer {
|
|
|
320
338
|
};
|
|
321
339
|
if (wantsUnderline) {
|
|
322
340
|
const stroke = underlineStroke(metrics, size, baselineY);
|
|
323
|
-
for (const [from, to] of underlineSegments(text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing)) {
|
|
341
|
+
for (const [from, to] of underlineSegments(text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing, runSkipInk)) {
|
|
324
342
|
push(from, to, stroke);
|
|
325
343
|
}
|
|
326
344
|
}
|
|
@@ -332,8 +350,8 @@ export class TextRenderer {
|
|
|
332
350
|
};
|
|
333
351
|
// The x-ranges the underline actually draws. Without `skipInk` that is the whole run; with it,
|
|
334
352
|
// the descenders are cut out of it (which needs the real outlines, i.e. an embedded font).
|
|
335
|
-
const underlineSegments = (text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing) => {
|
|
336
|
-
if (!
|
|
353
|
+
const underlineSegments = (text, runWidth, baselineY, size, family, style, stroke, runLetterSpacing, runSkipInk) => {
|
|
354
|
+
if (!runSkipInk)
|
|
337
355
|
return [[0, runWidth]];
|
|
338
356
|
if (!objectManager.isCustomFont(family, style)) {
|
|
339
357
|
throw new Error(`skipInk needs an embedded font, but "${family}" is a standard-14 font whose glyph outlines ` +
|
|
@@ -352,98 +370,63 @@ export class TextRenderer {
|
|
|
352
370
|
* GLYPHS for an rtl run. A font that does not shape keeps the old path exactly: draw the visual
|
|
353
371
|
* text, no glyph list, byte-identical output.
|
|
354
372
|
*/
|
|
355
|
-
const shapedPiece = (part, family, style) => {
|
|
373
|
+
const shapedPiece = (part, family, style, runLigatures) => {
|
|
356
374
|
// The run's OWN setting, not the default: measuring asked with it (`runAdvance` -> RunFont), and
|
|
357
375
|
// 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,
|
|
376
|
+
const shaped = objectManager.shapeText(part.logical, family, style, runLigatures ? undefined : []);
|
|
359
377
|
if (!shaped)
|
|
360
378
|
return { text: part.text };
|
|
361
379
|
const ordered = part.rtl ? [...shaped].reverse() : shaped;
|
|
362
380
|
return { text: part.logical, glyphs: ordered.map((g) => g.glyph) };
|
|
363
381
|
};
|
|
364
|
-
//
|
|
365
|
-
// `letterSpacing` per code point (the `Tc` operator), from the one shared `advance.ts` primitive.
|
|
366
|
-
const font = { fontFamily, fontSize, fontStyle, ligatures };
|
|
367
|
-
// --- Plain string: one run per wrapped line. ---
|
|
368
|
-
if (typeof content === "string") {
|
|
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
|
-
});
|
|
379
|
-
// yPosition is the top of the text box (top-left). The line box seats its own baseline; lines
|
|
380
|
-
// then stack by that box's height. Same numbers as calculateTextHeight, by construction.
|
|
381
|
-
const box = lineBoxForString(objectManager, fontFamily, fontStyle, fontSize, lineHeight);
|
|
382
|
-
lines.forEach((line, index) => {
|
|
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);
|
|
387
|
-
const baseline = yPosition + box.baseline + box.height * index;
|
|
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
|
-
}
|
|
405
|
-
});
|
|
406
|
-
return { runs, links, decorations };
|
|
407
|
-
}
|
|
408
|
-
// --- Segments: break into lines (shared breaker), then emit one run per segment.
|
|
382
|
+
// Break into lines (the one breaker), then emit one run per segment.
|
|
409
383
|
// Segment 0 starts at the aligned line origin; each following segment is offset by
|
|
410
384
|
// the previous segment's kerning-free advance. Each line drops by its OWN leading
|
|
411
385
|
// (tallest font on that line), so mixed-font lines space correctly and the drawn
|
|
412
386
|
// height matches the measured height.
|
|
413
|
-
const pushLine = (line, lineY, isLast) => {
|
|
414
|
-
|
|
415
|
-
|
|
387
|
+
const pushLine = (line, lineY, isLast, indent) => {
|
|
388
|
+
// CSS `text-indent`: the FIRST line starts in, with that much less room.
|
|
389
|
+
let x = initialX + indent + alignmentOffset(line.width, indent);
|
|
390
|
+
// Word-spacing is a per-gap advance just like the justification slack, so it rides the same
|
|
391
|
+
// mechanism: `Tw` would reach only the single byte 32 and miss every embedded font.
|
|
392
|
+
const justify = justifyExtra(line.segments, line.width, isLast, indent);
|
|
416
393
|
// Across span boundaries, so a Hebrew span is ordered against its neighbours rather than inside
|
|
417
394
|
// itself. Each run keeps the segment it came from, and with it that span's style.
|
|
418
395
|
const ordered = visualRunsOf(line.segments.map((segment) => ({ text: segment.content, source: segment })), direction);
|
|
419
396
|
ordered.forEach((part) => {
|
|
397
|
+
// A blank line keeps its height (its box is sized from the piece), but there is nothing to draw.
|
|
398
|
+
if (part.text === "")
|
|
399
|
+
return;
|
|
420
400
|
const segment = part.source;
|
|
421
|
-
const
|
|
422
|
-
const size =
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const piece = shapedPiece(part, family, style);
|
|
401
|
+
const run = runStyleOf(segment, breaking);
|
|
402
|
+
const { fontFamily: family, fontSize: size, fontStyle: style } = run.font;
|
|
403
|
+
const segLetterSpacing = run.letterSpacing;
|
|
404
|
+
const runLigatures = run.font.ligatures ?? true;
|
|
405
|
+
const piece = shapedPiece(part, family, style, runLigatures);
|
|
426
406
|
const runText = piece.text;
|
|
427
407
|
const runColor = segment.fontColor || color;
|
|
428
408
|
// `vertical-align`: this run alone leaves the line's baseline. Its own decoration and link box
|
|
429
409
|
// ride along, since both are placed from the same y.
|
|
430
410
|
const shift = VERTICAL_TEXT_SHIFT[segment.verticalAlign ?? "baseline"] * size;
|
|
431
411
|
const runY = lineY - shift;
|
|
432
|
-
const
|
|
412
|
+
const firstRun = runs.length;
|
|
413
|
+
const drawn = emitPiece(piece, (word) => shapedPiece({ text: word, logical: word, rtl: part.rtl }, family, style, runLigatures), part.rtl, x, run.wordSpacing + justify, {
|
|
433
414
|
y: runY,
|
|
434
415
|
fontFamily: family,
|
|
435
416
|
fontStyle: style,
|
|
436
417
|
fontSize: size,
|
|
437
418
|
color: runColor,
|
|
438
419
|
...(segLetterSpacing ? { letterSpacing: segLetterSpacing } : {}),
|
|
439
|
-
},
|
|
440
|
-
|
|
420
|
+
},
|
|
421
|
+
// `ligatures` is part of the font: the advances must be the glyphs that are drawn.
|
|
422
|
+
run.font, segLetterSpacing);
|
|
423
|
+
decorate(runText, x, drawn, runY, family, style, size, runColor, segment.underline ?? underline, segment.strikethrough ?? strikethrough, segLetterSpacing, segment.skipInk ?? skipInk);
|
|
441
424
|
// An href/to span becomes a /Link annotation over exactly this run's glyph box: from its own
|
|
442
425
|
// ascent above the baseline down to its own descender. A span wrapped across lines yields
|
|
443
426
|
// one rect per line.
|
|
444
427
|
if (segment.href !== undefined || segment.dest !== undefined) {
|
|
445
428
|
const v = objectManager.getFontVerticals(family, style);
|
|
446
|
-
|
|
429
|
+
const link = {
|
|
447
430
|
type: "link",
|
|
448
431
|
x,
|
|
449
432
|
y: runY - v.ascent * size,
|
|
@@ -451,34 +434,38 @@ export class TextRenderer {
|
|
|
451
434
|
height: (v.ascent + v.descent) * size,
|
|
452
435
|
href: segment.href,
|
|
453
436
|
dest: segment.dest,
|
|
454
|
-
}
|
|
437
|
+
};
|
|
438
|
+
links.push(link);
|
|
439
|
+
const target = segment.href !== undefined ? `href:${segment.href}` : `dest:${segment.dest}`;
|
|
440
|
+
if (openLink?.target !== target) {
|
|
441
|
+
openLink = { target, group: { target, links: [], runs: [], pieces: [] } };
|
|
442
|
+
linkGroups.push(openLink.group);
|
|
443
|
+
}
|
|
444
|
+
openLink.group.links.push(link);
|
|
445
|
+
openLink.group.runs.push(...runs.slice(firstRun));
|
|
446
|
+
openLink.group.pieces.push({ text: runText, y: runY });
|
|
447
|
+
}
|
|
448
|
+
else if (runText.trim() !== "") {
|
|
449
|
+
openLink = undefined;
|
|
455
450
|
}
|
|
456
451
|
x += drawn;
|
|
457
452
|
});
|
|
458
453
|
};
|
|
459
454
|
// yPosition is the top of the text box (top-left). Each line seats its own baseline inside its
|
|
460
455
|
// own box (tallest ascent / deepest descent ON THAT LINE), then the next line starts below it.
|
|
461
|
-
// The seam flips the whole thing to PDF space.
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
const defaults = {
|
|
465
|
-
fontFamily,
|
|
466
|
-
fontSize,
|
|
467
|
-
fontStyle,
|
|
468
|
-
letterSpacing,
|
|
469
|
-
splitting: { breakWord, hyphenate },
|
|
470
|
-
ligatures,
|
|
471
|
-
};
|
|
456
|
+
// The seam flips the whole thing to PDF space. The lines are broken with the element's own
|
|
457
|
+
// settings - the ones `textHeight` measured with - so measured equals drawn by construction.
|
|
458
|
+
const defaults = breaking;
|
|
472
459
|
let top = yPosition;
|
|
473
|
-
|
|
474
|
-
const segmentLines = [
|
|
475
|
-
...breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow),
|
|
476
|
-
];
|
|
460
|
+
const segmentLines = breakSegmentsIntoLines(content, defaults, maxWidth, objectManager, maxLines, overflow);
|
|
477
461
|
segmentLines.forEach((line, index) => {
|
|
478
462
|
const box = lineBoxForSegmentLine(line, defaults, objectManager, lineHeight);
|
|
479
|
-
|
|
463
|
+
// Only a line that WRAPPED is stretched: the end of a paragraph and the line before a `\n` keep
|
|
464
|
+
// their natural spacing (CSS). The last line of a piece continued on the next page wrapped.
|
|
465
|
+
const ends = line.end === "hard" || line.end === "end";
|
|
466
|
+
pushLine(line, top + box.baseline, ends, index === 0 ? textIndent : 0);
|
|
480
467
|
top += box.height;
|
|
481
468
|
});
|
|
482
|
-
return { runs, links, decorations };
|
|
469
|
+
return { runs, links, decorations, linkGroups };
|
|
483
470
|
}
|
|
484
471
|
}
|
package/dist/svg/gradients.js
CHANGED
|
@@ -135,6 +135,11 @@ export function resolveSvgGradient(def, defs, commands) {
|
|
|
135
135
|
];
|
|
136
136
|
const scale = Math.abs(matrix[0]);
|
|
137
137
|
const extend = SPREAD[attributes["spreadMethod"] ?? "pad"] ?? "pad";
|
|
138
|
+
if (extend !== "pad") {
|
|
139
|
+
// A PDF shading only clamps its end colours; repeating or mirroring would need a tiling pattern.
|
|
140
|
+
// Drawn as `pad` it is a different picture - so it is named.
|
|
141
|
+
throw new SvgUnsupportedError(`spreadMethod="${extend}" on a gradient`, "Only pad (the default) is drawn. Expand the repeats into stops in your editor.");
|
|
142
|
+
}
|
|
138
143
|
if (def.kind === "linear") {
|
|
139
144
|
// Both anchors go through the transform, so a rotation or a shear lands correctly by itself.
|
|
140
145
|
const [x0, y0] = point(px(num(attributes["x1"], 0)), py(num(attributes["y1"], 0)));
|
package/dist/svg/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface SvgTarget {
|
|
|
7
7
|
width: number;
|
|
8
8
|
height: number;
|
|
9
9
|
}
|
|
10
|
-
/** The intrinsic size of an SVG: its `width`/`height`, else its `viewBox
|
|
10
|
+
/** The intrinsic size of an SVG in points: its `width`/`height`, else its `viewBox`, else 300 x 150 px. */
|
|
11
11
|
export interface SvgSize {
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
package/dist/svg/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseXml } from "./xml.js";
|
|
|
2
2
|
import { SvgParseError, SvgUnsupportedError } from "./errors.js";
|
|
3
3
|
import { IDENTITY, isIdentity, multiply, parseTransform } from "./transform.js";
|
|
4
4
|
import { shapeOf } from "./shapes.js";
|
|
5
|
+
import { length } from "./units.js";
|
|
5
6
|
import { transformCommands } from "../vector/path.js";
|
|
6
7
|
import { declarationsFor, parseStylesheet } from "./css.js";
|
|
7
8
|
import { ROOT_STYLE, isPaintRef, cascadedReader, paintAlpha, resolveStyle, } from "./style.js";
|
|
@@ -72,6 +73,21 @@ function stylesheetOf(node, out = [], depth = 0) {
|
|
|
72
73
|
const elements = (node) => node.type === "element"
|
|
73
74
|
? node.children.filter((child) => child.type === "element")
|
|
74
75
|
: [];
|
|
76
|
+
/** SVG's user unit is the CSS px: 1/96 inch, so 0.75pt - the resolution an image without one gets too. */
|
|
77
|
+
const PT_PER_USER_UNIT = 72 / 96;
|
|
78
|
+
/** The size of a replaced element that declares none (CSS). */
|
|
79
|
+
const DEFAULT_VIEWPORT = { width: 300, height: 150 };
|
|
80
|
+
/**
|
|
81
|
+
* A root `width`/`height` in user units, or `undefined` when it gives none. A percentage sizes against a
|
|
82
|
+
* container, which a drawing on its own does not have - a browser treats it as not given, and so do we.
|
|
83
|
+
*/
|
|
84
|
+
function rootLength(value, name) {
|
|
85
|
+
const text = value?.trim() ?? "";
|
|
86
|
+
if (text === "" || text.endsWith("%"))
|
|
87
|
+
return undefined;
|
|
88
|
+
const n = length(text, `the root <svg> ${name}`, NaN);
|
|
89
|
+
return n > 0 ? n : undefined;
|
|
90
|
+
}
|
|
75
91
|
/**
|
|
76
92
|
* Reads the root `<svg>` and returns the matrix that maps its user space onto `target`, plus the
|
|
77
93
|
* intrinsic size. The `viewBox` scales UNIFORMLY and centres - SVG's default `preserveAspectRatio`
|
|
@@ -85,11 +101,16 @@ function rootMapping(root, target) {
|
|
|
85
101
|
.split(/[\s,]+/)
|
|
86
102
|
.map(Number);
|
|
87
103
|
const hasBox = box.length === 4 && box.every((n) => Number.isFinite(n)) && box[2] > 0 && box[3] > 0;
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
// The viewport in user units. One side given and a viewBox: the other follows its ratio, as in a
|
|
105
|
+
// browser. Nothing to go on: CSS's 300 x 150 for a replaced element without a size.
|
|
106
|
+
const w = rootLength(attributes["width"], "width");
|
|
107
|
+
const h = rootLength(attributes["height"], "height");
|
|
108
|
+
const ratio = hasBox ? box[2] / box[3] : undefined;
|
|
109
|
+
const userWidth = w ?? (h !== undefined && ratio ? h * ratio : hasBox ? box[2] : DEFAULT_VIEWPORT.width);
|
|
110
|
+
const userHeight = h ?? (w !== undefined && ratio ? w / ratio : hasBox ? box[3] : DEFAULT_VIEWPORT.height);
|
|
90
111
|
const size = {
|
|
91
|
-
width:
|
|
92
|
-
height:
|
|
112
|
+
width: userWidth * PT_PER_USER_UNIT,
|
|
113
|
+
height: userHeight * PT_PER_USER_UNIT,
|
|
93
114
|
};
|
|
94
115
|
// We always scale uniformly and centre, which is SVG's default "xMidYMid meet". Any other value
|
|
95
116
|
// moves or crops the drawing, so it is named rather than quietly ignored.
|
|
@@ -97,7 +118,7 @@ function rootMapping(root, target) {
|
|
|
97
118
|
if (par !== "" && !/^xMidYMid(\s+meet)?$/.test(par)) {
|
|
98
119
|
throw new SvgUnsupportedError(`preserveAspectRatio="${par}"`, 'Only the default "xMidYMid meet" is honoured. Size the drawing through width/height instead.');
|
|
99
120
|
}
|
|
100
|
-
const [vx, vy, vw, vh] = hasBox ? box : [0, 0,
|
|
121
|
+
const [vx, vy, vw, vh] = hasBox ? box : [0, 0, userWidth, userHeight];
|
|
101
122
|
const scale = Math.min(target.width / vw, target.height / vh);
|
|
102
123
|
const dx = target.x + (target.width - vw * scale) / 2;
|
|
103
124
|
const dy = target.y + (target.height - vh * scale) / 2;
|
|
@@ -112,9 +133,14 @@ function rootMapping(root, target) {
|
|
|
112
133
|
* it draws something the file did not describe: a drop-shadow logo as a hard silhouette. `clip-path`
|
|
113
134
|
* used to be listed here; it is resolved now, which is the honest version of the same rule.
|
|
114
135
|
*/
|
|
136
|
+
const MARKER_HINT = "Markers (arrowheads, dots on a line) are not drawn yet; expand them into shapes in your editor.";
|
|
115
137
|
const REFUSED_ATTRIBUTES = {
|
|
116
138
|
mask: "Masks are pixel operations; flatten the artwork in your editor instead.",
|
|
117
139
|
filter: "Filters are pixel operations; rasterise that part to a PNG instead.",
|
|
140
|
+
marker: MARKER_HINT,
|
|
141
|
+
"marker-start": MARKER_HINT,
|
|
142
|
+
"marker-mid": MARKER_HINT,
|
|
143
|
+
"marker-end": MARKER_HINT,
|
|
118
144
|
};
|
|
119
145
|
function refuseAttributes(tagName, read) {
|
|
120
146
|
for (const [name, hint] of Object.entries(REFUSED_ATTRIBUTES)) {
|
|
@@ -210,6 +236,10 @@ function walk(node, parentStyle, rules, clips, gradients, out, depth = 0) {
|
|
|
210
236
|
// Everything below reads through the cascade, not the raw attributes: `filter="url(#f)"` and
|
|
211
237
|
// `style="filter:url(#f)"` are the same instruction, and a `<style>` rule can set either.
|
|
212
238
|
const read = cascadedReader(attributes, css);
|
|
239
|
+
// `display: none` takes the element out with everything inside it - nothing of it is drawn, so
|
|
240
|
+
// nothing of it can be unsupported either.
|
|
241
|
+
if (read("display")?.trim() === "none")
|
|
242
|
+
return;
|
|
213
243
|
refuseAttributes(tagName, read);
|
|
214
244
|
const style = resolveStyle(parentStyle, attributes, css);
|
|
215
245
|
const transform = attributes["transform"]
|
|
@@ -248,7 +278,7 @@ function walk(node, parentStyle, rules, clips, gradients, out, depth = 0) {
|
|
|
248
278
|
}
|
|
249
279
|
// A shape with neither fill nor stroke, or with no geometry, paints nothing - and an empty Path
|
|
250
280
|
// node would only cost bytes.
|
|
251
|
-
if (commands.length > 0 && (style.fill || style.stroke)) {
|
|
281
|
+
if (commands.length > 0 && style.visible && (style.fill || style.stroke)) {
|
|
252
282
|
// A gradient is resolved HERE too, and for the same reason: in its default units its anchors
|
|
253
283
|
// are fractions of the SHAPE's bounding box, which only exists at this point.
|
|
254
284
|
const shapeAlpha = style.fillOpacity * style.opacity;
|
package/dist/svg/style.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface SvgStyle {
|
|
|
30
30
|
fillOpacity: number;
|
|
31
31
|
strokeOpacity: number;
|
|
32
32
|
opacity: number;
|
|
33
|
+
/** `visibility` (inherits): a hidden shape paints nothing, but a child may set `visible` again. */
|
|
34
|
+
visible: boolean;
|
|
33
35
|
}
|
|
34
36
|
/** The colour a shape actually paints with: its raw colour, times the opacities that reached it. */
|
|
35
37
|
export declare function paintAlpha(color: Color, opacity: number): Color;
|
package/dist/svg/style.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Color } from "../common/color.js";
|
|
2
|
-
import { toColor } from "../
|
|
2
|
+
import { toColor } from "../common/color-input.js";
|
|
3
3
|
import { SvgParseError, SvgUnsupportedError } from "./errors.js";
|
|
4
4
|
import { length, ratio } from "./units.js";
|
|
5
5
|
export const isPaintRef = (paint) => paint !== undefined && !(paint instanceof Color);
|
|
@@ -18,6 +18,7 @@ export const ROOT_STYLE = {
|
|
|
18
18
|
fillOpacity: 1,
|
|
19
19
|
strokeOpacity: 1,
|
|
20
20
|
opacity: 1,
|
|
21
|
+
visible: true,
|
|
21
22
|
};
|
|
22
23
|
/** Splits a `style=""` bag into declarations. It wins over both other sources, as in CSS. */
|
|
23
24
|
function styleBag(value) {
|
|
@@ -140,5 +141,15 @@ export function resolveStyle(parent, attributes, css = {}) {
|
|
|
140
141
|
fillOpacity: ratio(read("fill-opacity"), parent.fillOpacity),
|
|
141
142
|
strokeOpacity: ratio(read("stroke-opacity"), parent.strokeOpacity),
|
|
142
143
|
opacity,
|
|
144
|
+
visible: visibility(read("visibility"), parent.visible),
|
|
143
145
|
};
|
|
144
146
|
}
|
|
147
|
+
/** `hidden` and `collapse` hide, `visible` shows, anything else (`inherit`, unset) keeps the parent's. */
|
|
148
|
+
function visibility(value, parent) {
|
|
149
|
+
const v = value?.trim();
|
|
150
|
+
if (v === "hidden" || v === "collapse")
|
|
151
|
+
return false;
|
|
152
|
+
if (v === "visible")
|
|
153
|
+
return true;
|
|
154
|
+
return parent;
|
|
155
|
+
}
|
package/dist/text/advance.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface RunFont {
|
|
|
32
32
|
/** Number of Unicode code points (not UTF-16 units): an astral char is ONE, so it takes one
|
|
33
33
|
* letter-spacing, not two. Matches how `getStringWidth` iterates. */
|
|
34
34
|
export declare function codePointCount(text: string): number;
|
|
35
|
+
/** U+00AD: "a line may break here, with a hyphen". Invisible wherever it does not. */
|
|
36
|
+
export declare const SOFT_HYPHEN = "\u00AD";
|
|
37
|
+
/** `text` as it is drawn: without its soft hyphens. */
|
|
38
|
+
export declare function withoutSoftHyphens(text: string): string;
|
|
35
39
|
/** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
|
|
36
40
|
* plus one `letterSpacing` per code point. */
|
|
37
41
|
export declare function runAdvance(metrics: FontMetrics, text: string, font: RunFont, letterSpacing?: number): number;
|
package/dist/text/advance.js
CHANGED
|
@@ -96,6 +96,11 @@ function baseAdvance(metrics, text, font) {
|
|
|
96
96
|
}
|
|
97
97
|
/** The measurement itself, once a miss has found the map it belongs in. */
|
|
98
98
|
function measureAndStore(metrics, text, font, byText) {
|
|
99
|
+
// A soft hyphen is never drawn where it stands (a break there draws a real hyphen instead), so it has
|
|
100
|
+
// no width - taken out BEFORE measuring, so the kerning pair across it is the one drawn. Here, on a
|
|
101
|
+
// cache miss, and not on every call: the stored width is the drawn one, keyed by the source text.
|
|
102
|
+
const key = text;
|
|
103
|
+
text = withoutSoftHyphens(text);
|
|
99
104
|
let advance = metrics.getStringWidth(text, font.fontFamily, font.fontSize, font.fontStyle, font.ligatures);
|
|
100
105
|
if (metrics.kerningEnabled) {
|
|
101
106
|
let units = 0;
|
|
@@ -103,17 +108,25 @@ function measureAndStore(metrics, text, font, byText) {
|
|
|
103
108
|
units += k;
|
|
104
109
|
advance += (units / 1000) * font.fontSize; // kern units are em/1000
|
|
105
110
|
}
|
|
106
|
-
byText.set(
|
|
111
|
+
byText.set(key, advance);
|
|
107
112
|
return advance;
|
|
108
113
|
}
|
|
114
|
+
/** U+00AD: "a line may break here, with a hyphen". Invisible wherever it does not. */
|
|
115
|
+
export const SOFT_HYPHEN = "\u00AD";
|
|
116
|
+
/** `text` as it is drawn: without its soft hyphens. */
|
|
117
|
+
export function withoutSoftHyphens(text) {
|
|
118
|
+
return text.includes(SOFT_HYPHEN) ? text.split(SOFT_HYPHEN).join("") : text;
|
|
119
|
+
}
|
|
109
120
|
/** The advance of `text` in points: the plain glyph widths, plus kerning (if the document has it on),
|
|
110
121
|
* plus one `letterSpacing` per code point. */
|
|
111
122
|
export function runAdvance(metrics, text, font, letterSpacing = 0) {
|
|
112
123
|
let advance = baseAdvance(metrics, text, font);
|
|
113
124
|
if (letterSpacing !== 0) {
|
|
114
|
-
// Per DRAWN glyph, which is what `Tc` applies to; only a ligature makes that differ.
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
// Per DRAWN glyph, which is what `Tc` applies to; only a ligature makes that differ. A soft hyphen is
|
|
126
|
+
// not drawn.
|
|
127
|
+
const drawn = withoutSoftHyphens(text);
|
|
128
|
+
const glyphs = metrics.shapedGlyphCount?.(drawn, font.fontFamily, font.fontStyle, font.ligatures) ??
|
|
129
|
+
codePointCount(drawn);
|
|
117
130
|
advance += glyphs * letterSpacing;
|
|
118
131
|
}
|
|
119
132
|
return advance;
|
package/dist/text/bidi.d.ts
CHANGED
|
@@ -29,5 +29,3 @@ export declare function visualRunsOf<T>(pieces: {
|
|
|
29
29
|
text: string;
|
|
30
30
|
source: T;
|
|
31
31
|
}[], base?: Direction): AttributedRun<T>[];
|
|
32
|
-
/** The same for one plain string. An empty string still yields one run, or a blank line would vanish. */
|
|
33
|
-
export declare const visualRuns: (text: string, base?: Direction) => VisualRun[];
|
package/dist/text/bidi.js
CHANGED
|
@@ -82,13 +82,3 @@ export function visualRunsOf(pieces, base = "ltr") {
|
|
|
82
82
|
flush();
|
|
83
83
|
return runs;
|
|
84
84
|
}
|
|
85
|
-
/** The same for one plain string. An empty string still yields one run, or a blank line would vanish. */
|
|
86
|
-
export const visualRuns = (text, base = "ltr") => {
|
|
87
|
-
if (text === "")
|
|
88
|
-
return [{ text, logical: text, rtl: false }];
|
|
89
|
-
return visualRunsOf([{ text, source: null }], base).map(({ text: t, logical, rtl }) => ({
|
|
90
|
-
text: t,
|
|
91
|
-
logical,
|
|
92
|
-
rtl,
|
|
93
|
-
}));
|
|
94
|
-
};
|
|
@@ -10,6 +10,6 @@ export interface CoverageResult {
|
|
|
10
10
|
* Replaces or removes what `fontFamily` cannot draw. Returns the input unchanged (and no allocation
|
|
11
11
|
* beyond the scan) when everything is drawable, which is the overwhelmingly common case.
|
|
12
12
|
*/
|
|
13
|
-
export declare function coverText(text: string, fontFamily: string, fontStyle: FontStyle, metrics: FontMetrics): CoverageResult;
|
|
13
|
+
export declare function coverText(text: string, fontFamily: string, fontStyle: FontStyle, metrics: FontMetrics, emoji?: boolean): CoverageResult;
|
|
14
14
|
/** `U+2713` - how a dropped code point is named to the caller. */
|
|
15
15
|
export declare function formatCodePoint(cp: number): string;
|