@jasy/pdf 1.0.0-alpha.9 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -9
- package/dist/api/color.js +69 -0
- package/dist/api/content.d.ts +40 -1
- package/dist/api/content.js +83 -0
- package/dist/api/descriptor.js +6 -1
- package/dist/api/dimension.d.ts +3 -2
- package/dist/api/font-url.js +3 -2
- package/dist/api/structure.d.ts +7 -0
- package/dist/api/structure.js +38 -3
- package/dist/api/text.d.ts +23 -2
- package/dist/api/text.js +9 -0
- package/dist/canvas/painter.d.ts +102 -0
- package/dist/canvas/painter.js +183 -0
- package/dist/elements/canvas-element.d.ts +38 -0
- package/dist/elements/canvas-element.js +47 -0
- package/dist/elements/container-element.d.ts +2 -0
- package/dist/elements/container-element.js +6 -0
- package/dist/elements/index.d.ts +2 -0
- package/dist/elements/index.js +2 -0
- package/dist/elements/layout/expanded-element.d.ts +2 -0
- package/dist/elements/layout/expanded-element.js +4 -0
- package/dist/elements/layout/keep-together-element.d.ts +2 -0
- package/dist/elements/layout/keep-together-element.js +4 -0
- package/dist/elements/layout/padding-element.d.ts +5 -0
- package/dist/elements/layout/padding-element.js +8 -0
- package/dist/elements/layout/struct-group.d.ts +2 -0
- package/dist/elements/layout/struct-group.js +4 -0
- package/dist/elements/pdf-element.d.ts +19 -3
- package/dist/elements/pdf-element.js +22 -4
- package/dist/elements/rectangle-element.d.ts +2 -0
- package/dist/elements/rectangle-element.js +6 -0
- package/dist/elements/row-element.d.ts +2 -0
- package/dist/elements/row-element.js +6 -0
- package/dist/elements/svg-element.d.ts +41 -0
- package/dist/elements/svg-element.js +93 -0
- package/dist/elements/text-element.d.ts +33 -1
- package/dist/elements/text-element.js +80 -4
- package/dist/ir/display-list.d.ts +44 -5
- package/dist/layout/min-intrinsic.d.ts +29 -0
- package/dist/layout/min-intrinsic.js +37 -0
- package/dist/renderer/anchor-renderer.js +1 -1
- package/dist/renderer/bookmark-renderer.js +1 -1
- package/dist/renderer/canvas-renderer.d.ts +14 -0
- package/dist/renderer/canvas-renderer.js +36 -0
- package/dist/renderer/container-renderer.js +1 -3
- package/dist/renderer/default-text-style-renderer.js +1 -1
- package/dist/renderer/deferred-renderer.js +1 -1
- package/dist/renderer/expanded-renderer.js +1 -1
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/keep-together-renderer.js +1 -1
- package/dist/renderer/link-renderer.js +1 -1
- package/dist/renderer/padding-renderer.js +1 -1
- package/dist/renderer/page-builder-renderer.js +1 -1
- package/dist/renderer/page-renderer.js +1 -3
- package/dist/renderer/pdf-backend.d.ts +15 -4
- package/dist/renderer/pdf-backend.js +125 -35
- package/dist/renderer/pdf-renderer.js +5 -1
- package/dist/renderer/positioned-renderer.js +1 -1
- package/dist/renderer/rectangle-renderer.js +1 -3
- package/dist/renderer/repeating-header-renderer.js +1 -2
- package/dist/renderer/rotated-renderer.js +1 -1
- package/dist/renderer/row-renderer.js +1 -3
- package/dist/renderer/struct-group-renderer.js +1 -1
- package/dist/renderer/svg-renderer.d.ts +11 -0
- package/dist/renderer/svg-renderer.js +27 -0
- package/dist/renderer/text-renderer.js +30 -19
- package/dist/svg/css.d.ts +38 -0
- package/dist/svg/css.js +115 -0
- package/dist/svg/errors.d.ts +24 -0
- package/dist/svg/errors.js +30 -0
- package/dist/svg/gradients.d.ts +34 -0
- package/dist/svg/gradients.js +156 -0
- package/dist/svg/index.d.ts +22 -0
- package/dist/svg/index.js +331 -0
- package/dist/svg/shapes.d.ts +6 -0
- package/dist/svg/shapes.js +177 -0
- package/dist/svg/style.d.ts +61 -0
- package/dist/svg/style.js +144 -0
- package/dist/svg/transform.d.ts +13 -0
- package/dist/svg/transform.js +68 -0
- package/dist/svg/units.d.ts +11 -0
- package/dist/svg/units.js +73 -0
- package/dist/svg/xml.d.ts +24 -0
- package/dist/svg/xml.js +158 -0
- package/dist/text/advance.d.ts +3 -0
- package/dist/text/advance.js +51 -8
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +8 -0
- package/dist/text/line-breaker.js +146 -70
- package/dist/text/shape.d.ts +3 -3
- package/dist/text/shape.js +34 -10
- package/dist/text/text-style.d.ts +13 -0
- package/dist/text/text-style.js +6 -0
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/text/word-splitting.d.ts +42 -0
- package/dist/text/word-splitting.js +72 -0
- package/dist/utils/afm-parser.d.ts +0 -1
- package/dist/utils/afm-parser.js +15 -16
- package/dist/utils/flex-layout.js +67 -18
- package/dist/utils/font-metrics.d.ts +12 -3
- package/dist/utils/pdf-object-manager.d.ts +31 -5
- package/dist/utils/pdf-object-manager.js +107 -21
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +22 -1
- package/dist/utils/utf8-to-windows1252-encoder.js +5 -3
- package/dist/utils/woff2.d.ts +6 -0
- package/dist/utils/woff2.js +651 -0
- package/dist/vector/path.d.ts +33 -0
- package/dist/vector/path.js +81 -0
- package/package.json +5 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileBytes } from "../platform/node-fs.js";
|
|
2
|
+
import { SvgParseError, svgSize, svgToIr } from "../svg/index.js";
|
|
3
|
+
import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
|
|
4
|
+
import { SizedPDFElement } from "./pdf-element.js";
|
|
5
|
+
/** Markup is recognised by its first non-space character - anything else is treated as a path. */
|
|
6
|
+
const looksLikeMarkup = (value) => /^\s*[<]/.test(value);
|
|
7
|
+
/**
|
|
8
|
+
* Reads the file, or says what went wrong in the caller's terms. A string that is neither markup nor
|
|
9
|
+
* a readable path would otherwise surface as a bare `ENOENT`, which never mentions that the argument
|
|
10
|
+
* was taken as a PATH in the first place.
|
|
11
|
+
*/
|
|
12
|
+
function readSource(path) {
|
|
13
|
+
try {
|
|
14
|
+
return readFileBytes(path);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const shown = path.length > 80 ? `${path.slice(0, 77)}...` : path;
|
|
18
|
+
throw new SvgParseError(`could not read "${shown}" as an SVG file. It does not start with "<", so it was taken as a ` +
|
|
19
|
+
`path: ${error.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An SVG drawing placed in the layout like an image.
|
|
24
|
+
*
|
|
25
|
+
* Unlike a bitmap it needs no decoder, so its intrinsic size is known SYNCHRONOUSLY, in the
|
|
26
|
+
* constructor - which also means a broken file fails on the line that asked for it rather than deep
|
|
27
|
+
* inside a later render, the same bargain `addFont` makes.
|
|
28
|
+
*/
|
|
29
|
+
export class SvgElement extends SizedPDFElement {
|
|
30
|
+
constructor(params) {
|
|
31
|
+
super({ x: 0, y: 0, width: params.width, height: params.height });
|
|
32
|
+
const { source, alt, widthFactor, heightFactor, ...sizing } = params;
|
|
33
|
+
this.widthFactor = widthFactor;
|
|
34
|
+
this.heightFactor = heightFactor;
|
|
35
|
+
this.source =
|
|
36
|
+
typeof source === "string"
|
|
37
|
+
? looksLikeMarkup(source)
|
|
38
|
+
? source
|
|
39
|
+
: new TextDecoder().decode(readSource(source))
|
|
40
|
+
: new TextDecoder().decode(source);
|
|
41
|
+
// Parsed here on purpose: an unreadable file is an error where it was NAMED, not inside a render
|
|
42
|
+
// three call frames later. The whole document is walked, not just its root, so an element outside
|
|
43
|
+
// the subset surfaces here too - which is what makes the error predictable rather than a lottery
|
|
44
|
+
// between construction and render time. A logo parses in well under a millisecond.
|
|
45
|
+
this.intrinsic = svgSize(this.source);
|
|
46
|
+
svgToIr(this.source, { x: 0, y: 0, width: 1, height: 1 });
|
|
47
|
+
this.alt = alt;
|
|
48
|
+
this.sizing = sizing;
|
|
49
|
+
this.requested = { width: params.width, height: params.height };
|
|
50
|
+
}
|
|
51
|
+
getProps() {
|
|
52
|
+
return {
|
|
53
|
+
x: this.x,
|
|
54
|
+
y: this.y,
|
|
55
|
+
width: this.width,
|
|
56
|
+
height: this.height,
|
|
57
|
+
source: this.source,
|
|
58
|
+
intrinsic: this.intrinsic,
|
|
59
|
+
alt: this.alt,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
calculateLayout(constraints, offset, _ctx) {
|
|
63
|
+
this.x = offset.x;
|
|
64
|
+
this.y = offset.y;
|
|
65
|
+
const specs = extentSpecs({
|
|
66
|
+
...this.sizing,
|
|
67
|
+
width: this.requested.width,
|
|
68
|
+
height: this.requested.height,
|
|
69
|
+
widthFactor: this.widthFactor,
|
|
70
|
+
heightFactor: this.heightFactor,
|
|
71
|
+
});
|
|
72
|
+
const resolved = resolveSize(specs.width, specs.height, this.sizing.aspectRatio, constraints);
|
|
73
|
+
let w = resolved.width;
|
|
74
|
+
let h = resolved.height;
|
|
75
|
+
const bounds = resolved.constraints;
|
|
76
|
+
// One pinned axis derives the other from the drawing's own ratio (CSS `width: 120; height: auto`).
|
|
77
|
+
// A bitmap needs a decode pass before it can do this; an SVG knows it from its viewBox.
|
|
78
|
+
if (this.sizing.aspectRatio === undefined &&
|
|
79
|
+
this.intrinsic.width > 0 &&
|
|
80
|
+
this.intrinsic.height > 0) {
|
|
81
|
+
const ratio = this.intrinsic.width / this.intrinsic.height;
|
|
82
|
+
if (w !== undefined && h === undefined)
|
|
83
|
+
h = bounds.constrainHeight(w / ratio);
|
|
84
|
+
else if (h !== undefined && w === undefined)
|
|
85
|
+
w = bounds.constrainWidth(h * ratio);
|
|
86
|
+
}
|
|
87
|
+
// Nothing pinned: draw at the intrinsic size, as an unsized `<img>` does - never fill the region,
|
|
88
|
+
// which would blow a 24pt icon up to the whole page.
|
|
89
|
+
this.width = w ?? bounds.constrainWidth(this.intrinsic.width);
|
|
90
|
+
this.height = h ?? bounds.constrainHeight(this.intrinsic.height);
|
|
91
|
+
return { width: this.width, height: this.height };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -6,6 +6,7 @@ import { type TextTransform } from "../text/text-style.js";
|
|
|
6
6
|
import { Fragmentable, FragmentResult } from "../layout/fragmentation.js";
|
|
7
7
|
import { TextOverflow } from "../text/line-breaker.js";
|
|
8
8
|
import { HorizontalAlignment, LayoutContext, SizedPDFElement } from "./pdf-element.js";
|
|
9
|
+
import type { Hyphenator } from "../text/word-splitting.js";
|
|
9
10
|
export interface TextSegment {
|
|
10
11
|
content: string;
|
|
11
12
|
fontStyle?: FontStyle;
|
|
@@ -74,6 +75,12 @@ interface TextElementParams {
|
|
|
74
75
|
wordSpacing?: number;
|
|
75
76
|
/** CSS `text-indent`, in points: how far the first line starts in. */
|
|
76
77
|
textIndent?: number;
|
|
78
|
+
/** CSS `overflow-wrap: break-word` - split a word wider than its box, anywhere. */
|
|
79
|
+
breakWord?: boolean;
|
|
80
|
+
/** The font's ligatures; on unless switched off. */
|
|
81
|
+
ligatures?: boolean;
|
|
82
|
+
/** Language-aware splitting; a hyphen is drawn at the break. See text/word-splitting.ts. */
|
|
83
|
+
hyphenate?: Hyphenator;
|
|
77
84
|
/** Accessibility role for the tagged structure tree (heading level or paragraph; default `"p"`). */
|
|
78
85
|
role?: TextRole;
|
|
79
86
|
}
|
|
@@ -93,6 +100,9 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
93
100
|
private readonly rawTextTransform?;
|
|
94
101
|
private readonly rawWordSpacing?;
|
|
95
102
|
private readonly rawTextIndent?;
|
|
103
|
+
private readonly rawBreakWord?;
|
|
104
|
+
private readonly rawLigatures?;
|
|
105
|
+
private readonly rawHyphenate?;
|
|
96
106
|
private fontSize;
|
|
97
107
|
private fontFamily;
|
|
98
108
|
private fontStyle;
|
|
@@ -109,13 +119,16 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
109
119
|
private textTransform;
|
|
110
120
|
private wordSpacing;
|
|
111
121
|
private textIndent;
|
|
122
|
+
private breakWord;
|
|
123
|
+
private ligatures;
|
|
124
|
+
private hyphenate?;
|
|
112
125
|
private content;
|
|
113
126
|
private maxLines?;
|
|
114
127
|
private orphans?;
|
|
115
128
|
private widows?;
|
|
116
129
|
private overflow;
|
|
117
130
|
private readonly role?;
|
|
118
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, role, }: TextElementParams);
|
|
131
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow, lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, }: TextElementParams);
|
|
119
132
|
private resolveStyle;
|
|
120
133
|
/** Justified lines may be squeezed to keep a word; every other alignment gets no slack. Read by the
|
|
121
134
|
* breaker in BOTH passes, or a measured line and a drawn one would disagree about where it ends. */
|
|
@@ -125,6 +138,13 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
125
138
|
* same width in most fonts).
|
|
126
139
|
*/
|
|
127
140
|
private display;
|
|
141
|
+
/**
|
|
142
|
+
* Drops or substitutes whatever the RESOLVED font cannot draw. Runs last, after the fallback stack
|
|
143
|
+
* has had its say, and before anything is measured - a character removed at draw time only would
|
|
144
|
+
* make the measured line and the drawn one disagree. A font that draws everything gets its input
|
|
145
|
+
* back unchanged, so an ordinary document is byte-identical.
|
|
146
|
+
*/
|
|
147
|
+
private covered;
|
|
128
148
|
/** The content with `text-transform` applied, and nothing else. */
|
|
129
149
|
private recased;
|
|
130
150
|
private lineOptions;
|
|
@@ -151,6 +171,15 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
151
171
|
* - enough to tip a borderline string (e.g. "20 Jun 2026", wider than "04 Jul 2026" only because
|
|
152
172
|
* 'n' beats 'l') one bit over its own width, dropping the last word onto a second line. */
|
|
153
173
|
private naturalWidth;
|
|
174
|
+
/**
|
|
175
|
+
* The widest piece of this text that cannot be broken - CSS's `min-content` width. A shrinking flex
|
|
176
|
+
* parent floors the width it hands us here, so a squeezed paragraph stops at its longest word
|
|
177
|
+
* instead of becoming one word per line.
|
|
178
|
+
*
|
|
179
|
+
* With `breakWord` or `hyphenate` on, the text may be cut mid-word, so the unbreakable piece is a
|
|
180
|
+
* single character - the same reasoning CSS applies to `word-break: break-all`.
|
|
181
|
+
*/
|
|
182
|
+
minIntrinsicMain(horizontal: boolean, ctx: LayoutContext): number;
|
|
154
183
|
getProps(): {
|
|
155
184
|
x: number;
|
|
156
185
|
y: number;
|
|
@@ -174,6 +203,9 @@ export declare class TextElement extends SizedPDFElement implements Fragmentable
|
|
|
174
203
|
direction: Direction;
|
|
175
204
|
wordSpacing: number;
|
|
176
205
|
textIndent: number;
|
|
206
|
+
breakWord: boolean;
|
|
207
|
+
ligatures: boolean;
|
|
208
|
+
hyphenate: Hyphenator | undefined;
|
|
177
209
|
role: TextRole | undefined;
|
|
178
210
|
};
|
|
179
211
|
}
|
|
@@ -6,10 +6,13 @@ import { TextRenderer } from "../renderer/text-renderer.js";
|
|
|
6
6
|
import { DEFAULT_TEXT_STYLE } from "../text/text-style.js";
|
|
7
7
|
import { applyTextTransform } from "../text/text-style.js";
|
|
8
8
|
import { splitByFont } from "../text/font-fallback.js";
|
|
9
|
+
import { runAdvance } from "../text/advance.js";
|
|
9
10
|
import { singleLineWidth, MAX_SPACE_SHRINK, wrapStringIntoLines, breakSegmentsIntoLines, segmentLinesToSegments, } from "../text/line-breaker.js";
|
|
10
11
|
import { adjustForOrphansWidows, DEFAULT_ORPHANS, DEFAULT_WIDOWS, } from "../text/orphans-widows.js";
|
|
11
12
|
import { lineBoxForSegmentLine, lineBoxForString } from "../text/line-metrics.js";
|
|
12
13
|
import { HorizontalAlignment, SizedPDFElement } from "./pdf-element.js";
|
|
14
|
+
import { normalizeContent } from "../text/whitespace.js";
|
|
15
|
+
import { coverText } from "../text/glyph-coverage.js";
|
|
13
16
|
/** How far a run sits off the baseline, as a fraction of its font size (up is positive). */
|
|
14
17
|
export const VERTICAL_TEXT_SHIFT = {
|
|
15
18
|
baseline: 0,
|
|
@@ -28,7 +31,7 @@ function finitePoints(value, name) {
|
|
|
28
31
|
return value;
|
|
29
32
|
}
|
|
30
33
|
export class TextElement extends SizedPDFElement {
|
|
31
|
-
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, role, }) {
|
|
34
|
+
constructor({ fontSize, content, fontFamily, fontStyle, color, textAlignment, maxLines, orphans, widows, overflow = "clip", lineHeight, underline, strikethrough, skipInk, letterSpacing, direction, fontFallback, textTransform, wordSpacing, textIndent, breakWord, hyphenate, ligatures, role, }) {
|
|
32
35
|
super({ x: 0, y: 0 });
|
|
33
36
|
this.role = role;
|
|
34
37
|
this.rawFontSize = fontSize;
|
|
@@ -46,7 +49,12 @@ export class TextElement extends SizedPDFElement {
|
|
|
46
49
|
this.rawTextTransform = textTransform;
|
|
47
50
|
this.rawWordSpacing = finitePoints(wordSpacing, "wordSpacing");
|
|
48
51
|
this.rawTextIndent = finitePoints(textIndent, "textIndent");
|
|
49
|
-
this.
|
|
52
|
+
this.rawBreakWord = breakWord;
|
|
53
|
+
this.rawLigatures = ligatures;
|
|
54
|
+
this.rawHyphenate = hyphenate;
|
|
55
|
+
// Control characters have no glyph and would be DRAWN as .notdef boxes; `\n` survives
|
|
56
|
+
// because the breaker treats it as a hard break. See text/whitespace.ts.
|
|
57
|
+
this.content = normalizeContent(content);
|
|
50
58
|
this.maxLines = maxLines;
|
|
51
59
|
this.orphans = orphans;
|
|
52
60
|
this.widows = widows;
|
|
@@ -69,8 +77,10 @@ export class TextElement extends SizedPDFElement {
|
|
|
69
77
|
// A SPAN may bring its own stack, so the element's being empty is not enough to skip the pass.
|
|
70
78
|
const anyStack = this.fontFallback.length > 0 ||
|
|
71
79
|
(typeof cased !== "string" && cased.some((seg) => (seg.fontFallback?.length ?? 0) > 0));
|
|
72
|
-
if (!
|
|
80
|
+
if (!metrics)
|
|
73
81
|
return cased;
|
|
82
|
+
if (!anyStack)
|
|
83
|
+
return this.covered(cased, metrics);
|
|
74
84
|
// Font fallback turns the content into spans - one per family - which every later pass already
|
|
75
85
|
// knows how to handle. Nothing new downstream.
|
|
76
86
|
const pieces = typeof cased === "string" ? [{ content: cased }] : cased;
|
|
@@ -83,7 +93,28 @@ export class TextElement extends SizedPDFElement {
|
|
|
83
93
|
for (const run of runs)
|
|
84
94
|
out.push({ ...seg, content: run.text, fontFamily: run.fontFamily });
|
|
85
95
|
}
|
|
86
|
-
return out;
|
|
96
|
+
return this.covered(out, metrics);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Drops or substitutes whatever the RESOLVED font cannot draw. Runs last, after the fallback stack
|
|
100
|
+
* has had its say, and before anything is measured - a character removed at draw time only would
|
|
101
|
+
* make the measured line and the drawn one disagree. A font that draws everything gets its input
|
|
102
|
+
* back unchanged, so an ordinary document is byte-identical.
|
|
103
|
+
*/
|
|
104
|
+
covered(content, metrics) {
|
|
105
|
+
const report = (dropped) => metrics.reportMissingGlyph?.(dropped);
|
|
106
|
+
if (typeof content === "string") {
|
|
107
|
+
const { text, dropped } = coverText(content, this.fontFamily, this.fontStyle, metrics);
|
|
108
|
+
if (dropped.length > 0)
|
|
109
|
+
report(dropped);
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
return content.map((seg) => {
|
|
113
|
+
const { text, dropped } = coverText(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontStyle ?? this.fontStyle, metrics);
|
|
114
|
+
if (dropped.length > 0)
|
|
115
|
+
report(dropped);
|
|
116
|
+
return text === seg.content ? seg : { ...seg, content: text };
|
|
117
|
+
});
|
|
87
118
|
}
|
|
88
119
|
/** The content with `text-transform` applied, and nothing else. */
|
|
89
120
|
recased() {
|
|
@@ -104,6 +135,8 @@ export class TextElement extends SizedPDFElement {
|
|
|
104
135
|
return {
|
|
105
136
|
wordSpacing: this.wordSpacing,
|
|
106
137
|
indent: this.textIndent,
|
|
138
|
+
splitting: { breakWord: this.breakWord, hyphenate: this.hyphenate },
|
|
139
|
+
ligatures: this.ligatures,
|
|
107
140
|
// Justified lines may be squeezed to keep a word; every other alignment gets no slack.
|
|
108
141
|
shrink: this.textAlignment === HorizontalAlignment.justify ? MAX_SPACE_SHRINK : 0,
|
|
109
142
|
};
|
|
@@ -124,6 +157,9 @@ export class TextElement extends SizedPDFElement {
|
|
|
124
157
|
this.textTransform = this.rawTextTransform ?? ts.textTransform;
|
|
125
158
|
this.wordSpacing = this.rawWordSpacing ?? ts.wordSpacing;
|
|
126
159
|
this.textIndent = this.rawTextIndent ?? ts.textIndent;
|
|
160
|
+
this.breakWord = this.rawBreakWord ?? ts.breakWord;
|
|
161
|
+
this.ligatures = this.rawLigatures ?? ts.ligatures;
|
|
162
|
+
this.hyphenate = this.rawHyphenate ?? ts.hyphenate;
|
|
127
163
|
}
|
|
128
164
|
/**
|
|
129
165
|
* Splits the paragraph at line boxes (Slice 1). The lines that fit in `maxHeight` stay;
|
|
@@ -257,6 +293,43 @@ export class TextElement extends SizedPDFElement {
|
|
|
257
293
|
return content.reduce((sum, seg) => sum +
|
|
258
294
|
oneLine(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing), indent);
|
|
259
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* The widest piece of this text that cannot be broken - CSS's `min-content` width. A shrinking flex
|
|
298
|
+
* parent floors the width it hands us here, so a squeezed paragraph stops at its longest word
|
|
299
|
+
* instead of becoming one word per line.
|
|
300
|
+
*
|
|
301
|
+
* With `breakWord` or `hyphenate` on, the text may be cut mid-word, so the unbreakable piece is a
|
|
302
|
+
* single character - the same reasoning CSS applies to `word-break: break-all`.
|
|
303
|
+
*/
|
|
304
|
+
minIntrinsicMain(horizontal, ctx) {
|
|
305
|
+
// Only the width is a question here: the height of a text is whatever its lines add up to, and a
|
|
306
|
+
// column squeezing it vertically is the paginator's business, not the shrink pass's.
|
|
307
|
+
if (!horizontal)
|
|
308
|
+
return 0;
|
|
309
|
+
this.resolveStyle(ctx);
|
|
310
|
+
const metrics = ctx.metrics;
|
|
311
|
+
const widest = (text, family, size, style, letterSpacing) => {
|
|
312
|
+
// `ligatures` belongs to the FONT: it decides which glyphs are drawn and therefore how wide the
|
|
313
|
+
// piece is. Leaving it out here would measure a floor the drawing never matches.
|
|
314
|
+
const font = {
|
|
315
|
+
fontFamily: family,
|
|
316
|
+
fontSize: size,
|
|
317
|
+
fontStyle: style,
|
|
318
|
+
ligatures: this.ligatures,
|
|
319
|
+
};
|
|
320
|
+
// Split on the same boundaries the breaker uses - a hard break ends a line too.
|
|
321
|
+
const pieces = this.breakWord || this.hyphenate ? [...text] : text.split(/[ \n]/);
|
|
322
|
+
return pieces.reduce((max, piece) => Math.max(max, runAdvance(metrics, piece, font, letterSpacing)), 0);
|
|
323
|
+
};
|
|
324
|
+
const content = this.display(metrics);
|
|
325
|
+
if (typeof content === "string") {
|
|
326
|
+
return widest(content, this.fontFamily, this.fontSize, this.fontStyle, this.letterSpacing);
|
|
327
|
+
}
|
|
328
|
+
// Segments join without a space, so a word may straddle two of them; taking the widest piece per
|
|
329
|
+
// segment can therefore UNDER-estimate. That errs towards squeezing a little too far, which lands
|
|
330
|
+
// back on the old overflow behaviour rather than on something new.
|
|
331
|
+
return content.reduce((max, seg) => Math.max(max, widest(seg.content, seg.fontFamily ?? this.fontFamily, seg.fontSize ?? this.fontSize, seg.fontStyle ?? this.fontStyle, seg.letterSpacing ?? this.letterSpacing)), 0);
|
|
332
|
+
}
|
|
260
333
|
getProps() {
|
|
261
334
|
return {
|
|
262
335
|
x: this.x,
|
|
@@ -281,6 +354,9 @@ export class TextElement extends SizedPDFElement {
|
|
|
281
354
|
direction: this.direction,
|
|
282
355
|
wordSpacing: this.wordSpacing,
|
|
283
356
|
textIndent: this.textIndent,
|
|
357
|
+
breakWord: this.breakWord,
|
|
358
|
+
ligatures: this.ligatures,
|
|
359
|
+
hyphenate: this.hyphenate,
|
|
284
360
|
role: this.role,
|
|
285
361
|
};
|
|
286
362
|
}
|
|
@@ -233,6 +233,9 @@ export interface LinearGradient {
|
|
|
233
233
|
y1: number;
|
|
234
234
|
stops: GradientStop[];
|
|
235
235
|
extend: "pad" | "repeat" | "reflect";
|
|
236
|
+
/** One opacity for the whole shading (0-1). A PDF shading is DeviceRGB and has no alpha channel,
|
|
237
|
+
* so a PER-STOP alpha is not representable - but a uniform one is, as an ExtGState. */
|
|
238
|
+
alpha?: number;
|
|
236
239
|
}
|
|
237
240
|
/** A radial gradient between two circles, in absolute page coordinates. */
|
|
238
241
|
export interface RadialGradient {
|
|
@@ -245,19 +248,55 @@ export interface RadialGradient {
|
|
|
245
248
|
r1: number;
|
|
246
249
|
stops: GradientStop[];
|
|
247
250
|
extend: "pad" | "repeat" | "reflect";
|
|
251
|
+
/** One opacity for the whole shading (0-1). A PDF shading is DeviceRGB and has no alpha channel,
|
|
252
|
+
* so a PER-STOP alpha is not representable - but a uniform one is, as an ExtGState. */
|
|
253
|
+
alpha?: number;
|
|
248
254
|
}
|
|
249
255
|
export type Gradient = LinearGradient | RadialGradient;
|
|
250
256
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
257
|
+
* How a path's outline is stroked. The names are SVG's, the semantics are the PDF operators they map
|
|
258
|
+
* to (`w`, `J`, `j`, `M`, `d`), and the defaults are SVG's initial values - not PDF's, which differ on
|
|
259
|
+
* the miter limit. Opacity rides on the `Color`, as everywhere else in the IR.
|
|
260
|
+
*/
|
|
261
|
+
export interface StrokeStyle {
|
|
262
|
+
color: Color;
|
|
263
|
+
width: number;
|
|
264
|
+
/** `J`. Default `butt`. */
|
|
265
|
+
cap?: "butt" | "round" | "square";
|
|
266
|
+
/** `j`. Default `miter`. */
|
|
267
|
+
join?: "miter" | "round" | "bevel";
|
|
268
|
+
/** `M`. Default 4 (SVG); PDF's own default is 10, so we always emit it when a join can miter. */
|
|
269
|
+
miterLimit?: number;
|
|
270
|
+
/** `d`: dash lengths in points, plus the phase to start at. Absent or empty = solid. */
|
|
271
|
+
dash?: number[];
|
|
272
|
+
dashOffset?: number;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* A vector path - one or more subpaths that share one fill and/or one stroke. `fill` uses the nonzero
|
|
276
|
+
* winding rule unless `fillRule` says otherwise, so a glyph's inner contour cuts a hole by default.
|
|
277
|
+
* A color glyph's layer is one `Path` filled with a solid `Color` (COLR v0) or a `Gradient` (v1); an
|
|
278
|
+
* SVG shape is one `Path` that may also carry a stroke, or carry ONLY a stroke.
|
|
254
279
|
*/
|
|
255
280
|
export interface Path {
|
|
256
281
|
type: "path";
|
|
257
282
|
commands: PathCommand[];
|
|
258
|
-
|
|
283
|
+
/** Absent = not filled (a stroke-only outline). */
|
|
284
|
+
fill?: Color | Gradient;
|
|
285
|
+
/** `evenodd` maps to the `*` operators; absent = nonzero, which is what glyphs need. */
|
|
286
|
+
fillRule?: "nonzero" | "evenodd";
|
|
287
|
+
stroke?: StrokeStyle;
|
|
259
288
|
/** Accessibility structure tag (absent = drawn as an artifact); color-emoji layers stay untagged. */
|
|
260
289
|
tag?: StructTag;
|
|
261
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Clips to an arbitrary path until the matching `clip-pop` - what SVG's `<clipPath>` needs, and what
|
|
293
|
+
* `ClipPush` cannot express, being a rectangle. The commands are in the same space as a `Path`'s.
|
|
294
|
+
*/
|
|
295
|
+
export interface ClipPathPush {
|
|
296
|
+
type: "clip-path-push";
|
|
297
|
+
commands: PathCommand[];
|
|
298
|
+
/** `evenodd` maps to `W*`; absent = nonzero. SVG spells this `clip-rule`. */
|
|
299
|
+
fillRule?: "nonzero" | "evenodd";
|
|
300
|
+
}
|
|
262
301
|
/** The closed set of primitives the PDF backend knows how to draw. */
|
|
263
|
-
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | FormFieldNode | TransformPush | TransformPop;
|
|
302
|
+
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPathPush | ClipPop | Path | Link | Outline | Anchor | FormFieldNode | TransformPush | TransformPop;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `min-content` sizing - the smallest extent a subtree can be squeezed into and still hold together.
|
|
3
|
+
*
|
|
4
|
+
* The shrink pass in `FlexLayoutHelper` floors every child at this, so an overflowing line stops
|
|
5
|
+
* squeezing instead of grinding a paragraph down to one word per line. It is the same rule a browser
|
|
6
|
+
* applies through the automatic `min-width` of a flex item (CSS Flexbox 4.5).
|
|
7
|
+
*
|
|
8
|
+
* Only the leaves know a real number - a `Text` its longest word, everything else nothing in
|
|
9
|
+
* particular. These helpers are how a container folds its children's answers into its own.
|
|
10
|
+
*/
|
|
11
|
+
import type { LayoutContext } from "../elements/pdf-element.js";
|
|
12
|
+
import type { PDFElement } from "../elements/pdf-element.js";
|
|
13
|
+
/**
|
|
14
|
+
* The children's floor, folded along one axis: children laid out ALONG the queried axis have to fit
|
|
15
|
+
* side by side, so their floors add up (plus the gaps between them); across it they merely overlap,
|
|
16
|
+
* so the widest one wins.
|
|
17
|
+
*
|
|
18
|
+
* A wrapping stack adds up nothing: it may put every child on its own line, so it is only as wide as
|
|
19
|
+
* its widest child - which is what CSS says about a multi-line flex container too.
|
|
20
|
+
*/
|
|
21
|
+
export declare function stackMinIntrinsic(children: PDFElement[], gap: number, alongAxis: boolean, horizontal: boolean, ctx: LayoutContext, wraps?: boolean): number;
|
|
22
|
+
/**
|
|
23
|
+
* Folds an element's own declared extent into the floor its content asks for. CSS calls these the
|
|
24
|
+
* "specified size suggestion" and the "content size suggestion" and takes the SMALLER: a box that
|
|
25
|
+
* declares 400pt around content needing 50 may still be squeezed to 50, while one declaring 400
|
|
26
|
+
* around content needing 500 cannot go below 400. An extent given as a fraction is not a suggestion
|
|
27
|
+
* at all - it has no size of its own until the parent is known - so it is ignored here.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withDeclaredExtent(contentFloor: number, declared: number | undefined, explicitMin?: number): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The children's floor, folded along one axis: children laid out ALONG the queried axis have to fit
|
|
3
|
+
* side by side, so their floors add up (plus the gaps between them); across it they merely overlap,
|
|
4
|
+
* so the widest one wins.
|
|
5
|
+
*
|
|
6
|
+
* A wrapping stack adds up nothing: it may put every child on its own line, so it is only as wide as
|
|
7
|
+
* its widest child - which is what CSS says about a multi-line flex container too.
|
|
8
|
+
*/
|
|
9
|
+
export function stackMinIntrinsic(children, gap, alongAxis, horizontal, ctx, wraps = false) {
|
|
10
|
+
const floors = children.map((c) => c.minIntrinsicMain(horizontal, ctx));
|
|
11
|
+
if (floors.length === 0)
|
|
12
|
+
return 0;
|
|
13
|
+
if (!alongAxis || wraps)
|
|
14
|
+
return Math.max(...floors);
|
|
15
|
+
return floors.reduce((sum, f) => sum + f, 0) + gap * (floors.length - 1);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Folds an element's own declared extent into the floor its content asks for. CSS calls these the
|
|
19
|
+
* "specified size suggestion" and the "content size suggestion" and takes the SMALLER: a box that
|
|
20
|
+
* declares 400pt around content needing 50 may still be squeezed to 50, while one declaring 400
|
|
21
|
+
* around content needing 500 cannot go below 400. An extent given as a fraction is not a suggestion
|
|
22
|
+
* at all - it has no size of its own until the parent is known - so it is ignored here.
|
|
23
|
+
*/
|
|
24
|
+
export function withDeclaredExtent(contentFloor, declared, explicitMin) {
|
|
25
|
+
// An explicit `minWidth`/`minHeight` REPLACES the automatic minimum - it does not compete with it.
|
|
26
|
+
// CSS applies the automatic minimum only while `min-width` is `auto`, so a smaller explicit value
|
|
27
|
+
// wins too: `min-width: 0` is the standard way to let a flex item shrink past its longest word, and
|
|
28
|
+
// taking the larger of the two would quietly ignore it. Measured in Chrome on one long word in a
|
|
29
|
+
// 200pt line: 132.08pt with `auto`, 84.64pt with `0`.
|
|
30
|
+
if (explicitMin !== undefined)
|
|
31
|
+
return explicitMin;
|
|
32
|
+
// Without an explicit bound the floor is CSS's content-based minimum: the smaller of what the
|
|
33
|
+
// element declares and what its content actually needs. It matters because the shrink pass must not
|
|
34
|
+
// aim below a size the child will refuse - the share it was supposed to give would never arrive,
|
|
35
|
+
// and the line would stay over its width.
|
|
36
|
+
return declared === undefined ? contentFloor : Math.min(declared, contentFloor);
|
|
37
|
+
}
|
|
@@ -3,7 +3,7 @@ export class AnchorRenderer {
|
|
|
3
3
|
static async render(element, objectManager) {
|
|
4
4
|
const { name, child, y } = element.getProps();
|
|
5
5
|
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
-
const childNodes =
|
|
6
|
+
const childNodes = await renderer(child, objectManager);
|
|
7
7
|
// The child draws normally; an Anchor IR node carries the name + scroll target for the page renderer
|
|
8
8
|
// to register as a named destination (it produces no content-stream ops itself).
|
|
9
9
|
return [...childNodes, { type: "anchor", y, name }];
|
|
@@ -3,7 +3,7 @@ export class BookmarkRenderer {
|
|
|
3
3
|
static async render(element, objectManager) {
|
|
4
4
|
const { title, level, child, y } = element.getProps();
|
|
5
5
|
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
-
const childNodes =
|
|
6
|
+
const childNodes = await renderer(child, objectManager);
|
|
7
7
|
// The child draws normally; an Outline IR node carries the title + nesting + scroll target for the
|
|
8
8
|
// page renderer to collect into the /Outlines tree (it produces no content-stream ops itself).
|
|
9
9
|
return [...childNodes, { type: "outline", y, title, level }];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { CanvasElement } from "../elements/canvas-element.js";
|
|
3
|
+
import type { IRNode } from "../ir/display-list.js";
|
|
4
|
+
/**
|
|
5
|
+
* Runs the caller's paint callback and hands back what it drew.
|
|
6
|
+
*
|
|
7
|
+
* The painter works in CANVAS coordinates (0,0 is the box's own corner), so the whole drawing is
|
|
8
|
+
* wrapped in one translate rather than every point being offset - the same shape `Svg` uses, and it
|
|
9
|
+
* keeps the callback independent of where the box ended up. The clip is the box: a drawing that runs
|
|
10
|
+
* past its own edge would otherwise bleed into the layout beside it.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CanvasRenderer {
|
|
13
|
+
static render(element: CanvasElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CanvasPainter } from "../canvas/painter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Runs the caller's paint callback and hands back what it drew.
|
|
4
|
+
*
|
|
5
|
+
* The painter works in CANVAS coordinates (0,0 is the box's own corner), so the whole drawing is
|
|
6
|
+
* wrapped in one translate rather than every point being offset - the same shape `Svg` uses, and it
|
|
7
|
+
* keeps the callback independent of where the box ended up. The clip is the box: a drawing that runs
|
|
8
|
+
* past its own edge would otherwise bleed into the layout beside it.
|
|
9
|
+
*/
|
|
10
|
+
export class CanvasRenderer {
|
|
11
|
+
static async render(element, objectManager) {
|
|
12
|
+
const { x, y, width, height, paint, alt } = element.getProps();
|
|
13
|
+
// The layout bounds are kept as plain numbers, and the painter and the callback each get their
|
|
14
|
+
// OWN size object. Sharing one meant a callback that wrote to `size.width` moved the clip away
|
|
15
|
+
// from the box layout had reserved - the drawing would then bleed into what sits beside it.
|
|
16
|
+
const w = width ?? 0;
|
|
17
|
+
const h = height ?? 0;
|
|
18
|
+
const painter = new CanvasPainter({ width: w, height: h });
|
|
19
|
+
paint(painter, { width: w, height: h });
|
|
20
|
+
const drawn = painter.drawn();
|
|
21
|
+
const nodes = [
|
|
22
|
+
{ type: "clip-push", x: x ?? 0, y: y ?? 0, width: w, height: h },
|
|
23
|
+
{ type: "transform-push", matrix: [1, 0, 0, 1, x ?? 0, y ?? 0] },
|
|
24
|
+
...drawn,
|
|
25
|
+
{ type: "transform-pop" },
|
|
26
|
+
{ type: "clip-pop" },
|
|
27
|
+
];
|
|
28
|
+
if (!alt || !objectManager.struct.enabled)
|
|
29
|
+
return nodes;
|
|
30
|
+
const tag = {
|
|
31
|
+
role: "Figure",
|
|
32
|
+
key: objectManager.struct.openElement(element.structId, "Figure", { alt }),
|
|
33
|
+
};
|
|
34
|
+
return nodes.map((node) => (node.type === "path" ? { ...node, tag } : node));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -6,9 +6,7 @@ export class ContainerRenderer {
|
|
|
6
6
|
if (children)
|
|
7
7
|
for (const child of children) {
|
|
8
8
|
const renderer = RendererRegistry.getRenderer(child);
|
|
9
|
-
|
|
10
|
-
nodes.push(...(await renderer(child, objectManager)));
|
|
11
|
-
}
|
|
9
|
+
nodes.push(...(await renderer(child, objectManager)));
|
|
12
10
|
}
|
|
13
11
|
return nodes;
|
|
14
12
|
}
|
|
@@ -5,6 +5,6 @@ export class DefaultTextStyleRenderer {
|
|
|
5
5
|
static async render(element, objectManager) {
|
|
6
6
|
const { child } = element.getProps();
|
|
7
7
|
const renderer = RendererRegistry.getRenderer(child);
|
|
8
|
-
return renderer
|
|
8
|
+
return renderer(child, objectManager);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -3,6 +3,6 @@ export class ExpandedRenderer {
|
|
|
3
3
|
static async render(expandedElement, objectManager) {
|
|
4
4
|
const { child } = expandedElement.getProps();
|
|
5
5
|
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
-
return renderer
|
|
6
|
+
return renderer(child, objectManager);
|
|
7
7
|
}
|
|
8
8
|
}
|
package/dist/renderer/index.d.ts
CHANGED
package/dist/renderer/index.js
CHANGED
|
@@ -7,6 +7,6 @@ export class KeepTogetherRenderer {
|
|
|
7
7
|
static async render(group, objectManager) {
|
|
8
8
|
const { child } = group.getProps();
|
|
9
9
|
const renderer = RendererRegistry.getRenderer(child);
|
|
10
|
-
return renderer
|
|
10
|
+
return renderer(child, objectManager);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -3,7 +3,7 @@ export class LinkRenderer {
|
|
|
3
3
|
static async render(element, objectManager) {
|
|
4
4
|
const { href, dest, child, x, y, width, height } = element.getProps();
|
|
5
5
|
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
-
const childNodes =
|
|
6
|
+
const childNodes = await renderer(child, objectManager);
|
|
7
7
|
// The child draws normally; a Link IR node carries the clickable rect + target (external href or
|
|
8
8
|
// internal dest) for the page renderer to emit as a /Link annotation (no content-stream ops itself).
|
|
9
9
|
return [...childNodes, { type: "link", x, y, width, height, href, dest }];
|
|
@@ -3,6 +3,6 @@ export class PaddingRenderer {
|
|
|
3
3
|
static async render(paddingElement, objectManager) {
|
|
4
4
|
const { child } = paddingElement.getProps();
|
|
5
5
|
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
-
return renderer
|
|
6
|
+
return renderer(child, objectManager);
|
|
7
7
|
}
|
|
8
8
|
}
|
|
@@ -21,9 +21,7 @@ export class PageRenderer {
|
|
|
21
21
|
const nodes = [];
|
|
22
22
|
for (const element of renderables) {
|
|
23
23
|
const renderer = RendererRegistry.getRenderer(element);
|
|
24
|
-
|
|
25
|
-
nodes.push(...(await renderer(element, objectManager)));
|
|
26
|
-
}
|
|
24
|
+
nodes.push(...(await renderer(element, objectManager)));
|
|
27
25
|
}
|
|
28
26
|
// Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
|
|
29
27
|
// serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).
|