html2canvas-pro 2.2.4 → 2.3.0
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/html2canvas-pro.cjs +11218 -0
- package/dist/html2canvas-pro.cjs.map +1 -0
- package/dist/html2canvas-pro.esm.js +2601 -2379
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +2601 -2379
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +4 -5
- package/dist/lib/core/abort-helper.js +20 -0
- package/dist/lib/core/background-parser.js +41 -0
- package/dist/lib/core/cache-storage.js +63 -7
- package/dist/lib/core/config-assembler.js +88 -0
- package/dist/lib/core/lru-map.js +66 -0
- package/dist/lib/core/render-element.js +24 -103
- package/dist/lib/css/property-descriptors/background-position.js +1 -11
- package/dist/lib/css/property-descriptors/background-size.js +3 -1
- package/dist/lib/css/syntax/token-constants.js +214 -0
- package/dist/lib/css/syntax/token-singletons.js +36 -0
- package/dist/lib/css/syntax/token-types.js +10 -0
- package/dist/lib/css/syntax/tokenizer.js +175 -307
- package/dist/lib/css/types/length-percentage.js +63 -3
- package/dist/lib/dom/document-cloner.js +23 -22
- package/dist/lib/dom/node-parser.js +1 -21
- package/dist/lib/dom/slot-cloner.js +8 -8
- package/dist/lib/render/background.js +4 -1
- package/dist/lib/render/canvas/background-renderer.js +27 -39
- package/dist/lib/render/canvas/canvas-renderer.js +2 -2
- package/dist/lib/render/canvas/effects-renderer.js +94 -32
- package/dist/lib/render/canvas/text-renderer.js +68 -12
- package/dist/lib/render/stacking-context.js +12 -6
- package/dist/types/core/abort-helper.d.ts +12 -0
- package/dist/types/core/background-parser.d.ts +16 -0
- package/dist/types/core/cache-storage.d.ts +25 -0
- package/dist/types/core/config-assembler.d.ts +28 -0
- package/dist/types/core/lru-map.d.ts +37 -0
- package/dist/types/css/syntax/token-constants.d.ts +74 -0
- package/dist/types/css/syntax/token-singletons.d.ts +22 -0
- package/dist/types/css/syntax/token-types.d.ts +72 -0
- package/dist/types/css/syntax/tokenizer.d.ts +6 -73
- package/dist/types/css/types/length-percentage.d.ts +42 -1
- package/dist/types/dom/document-cloner.d.ts +5 -0
- package/dist/types/dom/node-parser.d.ts +0 -2
- package/dist/types/render/canvas/background-renderer.d.ts +0 -10
- package/dist/types/render/canvas/effects-renderer.d.ts +42 -17
- package/dist/types/render/canvas/text-renderer.d.ts +12 -0
- package/dist/types/render/stacking-context.d.ts +26 -1
- package/package.json +5 -7
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - Transform effects (matrix transformations)
|
|
8
8
|
* - Clip effects (overflow / border-radius clipping via Path[])
|
|
9
9
|
* - Clip-path effects (CSS clip-path shapes: inset, circle, ellipse, polygon, path)
|
|
10
|
+
* - Blend effects (mix-blend-mode)
|
|
11
|
+
* - Filter effects (CSS filter functions)
|
|
10
12
|
*/
|
|
11
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
14
|
exports.EffectsRenderer = void 0;
|
|
@@ -16,61 +18,104 @@ const effects_1 = require("../effects");
|
|
|
16
18
|
*
|
|
17
19
|
* Manages rendering effects stack including opacity, transforms, and clipping.
|
|
18
20
|
* Extracted from CanvasRenderer to improve code organization and maintainability.
|
|
21
|
+
*
|
|
22
|
+
* ## Save/restore optimisation
|
|
23
|
+
*
|
|
24
|
+
* Canvas `save()` / `restore()` snapshot and restore the entire canvas state
|
|
25
|
+
* (transform matrix, clip region, compositing mode, filter, shadow, …) which
|
|
26
|
+
* is relatively expensive. The old implementation called `save()` / `restore()`
|
|
27
|
+
* for every single effect unconditionally.
|
|
28
|
+
*
|
|
29
|
+
* This implementation does a **batch pre-scan** before applying effects:
|
|
30
|
+
*
|
|
31
|
+
* | Effect type | Modifies | Reversible? | Needs save? |
|
|
32
|
+
* |--------------------|----------------------|---------------|-------------|
|
|
33
|
+
* | TransformEffect | transform matrix | ❌ irreversible | **YES** |
|
|
34
|
+
* | ClipEffect | clip region | ❌ cumulative | **YES** |
|
|
35
|
+
* | ClipPathEffect | clip region | ❌ cumulative | **YES** |
|
|
36
|
+
* | OpacityEffect | globalAlpha | ✅ scalar | NO |
|
|
37
|
+
* | BlendEffect | globalCompositeOp | ✅ scalar | NO |
|
|
38
|
+
* | FilterEffect | filter + shadow | ✅ string+vec | NO |
|
|
39
|
+
*
|
|
40
|
+
* When the batch contains *only* lightweight effects (opacity / blend / filter)
|
|
41
|
+
* we skip `save()` entirely and manually reset properties on pop.
|
|
42
|
+
*
|
|
43
|
+
* When the batch contains at least one heavyweight effect (transform / clip /
|
|
44
|
+
* clip-path) we call `save()` **once** before the first heavyweight effect and
|
|
45
|
+
* `restore()` **once** when that effect is popped.
|
|
19
46
|
*/
|
|
20
47
|
class EffectsRenderer {
|
|
21
48
|
constructor(deps, pathCallback) {
|
|
22
49
|
this.activeEffects = [];
|
|
50
|
+
/** Whether a canvas state save was performed for the current batch. */
|
|
51
|
+
this.didSave = false;
|
|
52
|
+
/** Index (0-based, from start of activeEffects array) of the last
|
|
53
|
+
* heavyweight effect in the batch — the one whose pop triggers
|
|
54
|
+
* restore(). Set to -1 when no save was performed. */
|
|
55
|
+
this.saveAtDepth = -1;
|
|
23
56
|
this.ctx = deps.ctx;
|
|
24
57
|
this.pathCallback = pathCallback;
|
|
25
58
|
}
|
|
26
59
|
/**
|
|
27
|
-
* Apply multiple effects
|
|
28
|
-
* Clears existing effects and applies new ones
|
|
29
|
-
*
|
|
30
|
-
* @param effects - Array of effects to apply
|
|
60
|
+
* Apply multiple effects.
|
|
61
|
+
* Clears existing effects and applies new ones.
|
|
31
62
|
*/
|
|
32
63
|
applyEffects(effects) {
|
|
33
|
-
//
|
|
64
|
+
// ── 1. Pop all previously active effects ──────────────────────
|
|
34
65
|
while (this.activeEffects.length) {
|
|
35
66
|
this.popEffect();
|
|
36
67
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
68
|
+
// ── 2. Determine whether to save canvas state ─────────────────
|
|
69
|
+
this.didSave = false;
|
|
70
|
+
this.saveAtDepth = -1;
|
|
71
|
+
const isHeavy = (e) => (0, effects_1.isTransformEffect)(e) || (0, effects_1.isClipEffect)(e) || (0, effects_1.isClipPathEffect)(e);
|
|
72
|
+
// Pre-compute the index of the *last* heavyweight effect so pop
|
|
73
|
+
// can reliably match it regardless of how many heavyweight effects
|
|
74
|
+
// are in the batch (the save was issued before the first, but the
|
|
75
|
+
// restore must happen when the LAST heavyweight is popped).
|
|
76
|
+
const lastHeavyIdx = effects.reduceRight((found, e, idx) => (found !== -1 ? found : isHeavy(e) ? idx : -1), -1);
|
|
77
|
+
// ── 3. Apply each effect ──────────────────────────────────────
|
|
78
|
+
for (let i = 0; i < effects.length; i++) {
|
|
79
|
+
const effect = effects[i];
|
|
80
|
+
if (isHeavy(effect)) {
|
|
81
|
+
if (!this.didSave) {
|
|
82
|
+
this.ctx.save();
|
|
83
|
+
this.didSave = true;
|
|
84
|
+
}
|
|
85
|
+
if (i === lastHeavyIdx) {
|
|
86
|
+
this.saveAtDepth = this.activeEffects.length;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.applyEffect(effect);
|
|
90
|
+
}
|
|
39
91
|
}
|
|
40
92
|
/**
|
|
41
|
-
* Apply a single effect
|
|
42
|
-
*
|
|
43
|
-
* @param effect - Effect to apply
|
|
93
|
+
* Apply a single effect (called internally by applyEffects).
|
|
44
94
|
*/
|
|
45
95
|
applyEffect(effect) {
|
|
46
|
-
|
|
47
|
-
if ((0, effects_1.isOpacityEffect)(effect)) {
|
|
48
|
-
// Opacity: multiply into the current global alpha for nested transparency.
|
|
49
|
-
this.ctx.globalAlpha = effect.opacity;
|
|
50
|
-
}
|
|
51
|
-
else if ((0, effects_1.isTransformEffect)(effect)) {
|
|
52
|
-
// Transform: translate to origin, apply matrix, translate back.
|
|
96
|
+
if ((0, effects_1.isTransformEffect)(effect)) {
|
|
53
97
|
this.ctx.translate(effect.offsetX, effect.offsetY);
|
|
54
98
|
this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
|
|
55
99
|
this.ctx.translate(-effect.offsetX, -effect.offsetY);
|
|
56
100
|
}
|
|
57
101
|
else if ((0, effects_1.isClipEffect)(effect)) {
|
|
58
|
-
// Clip (overflow / border-radius): build path via callback then clip.
|
|
59
102
|
this.pathCallback.path(effect.path);
|
|
60
103
|
this.ctx.clip();
|
|
61
104
|
}
|
|
62
105
|
else if ((0, effects_1.isClipPathEffect)(effect)) {
|
|
63
|
-
// Clip-path: delegate shape drawing (beginPath … clip()) to the effect.
|
|
64
106
|
effect.applyClip(this.ctx);
|
|
65
107
|
}
|
|
108
|
+
else if ((0, effects_1.isOpacityEffect)(effect)) {
|
|
109
|
+
// Multiply into current globalAlpha so nested opacity works
|
|
110
|
+
// without save/restore overhead.
|
|
111
|
+
this.ctx.globalAlpha *= effect.opacity;
|
|
112
|
+
}
|
|
66
113
|
else if ((0, effects_1.isBlendEffect)(effect)) {
|
|
67
114
|
this.ctx.globalCompositeOperation = effect.compositeOperation;
|
|
68
115
|
}
|
|
69
116
|
else if ((0, effects_1.isFilterEffect)(effect)) {
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// because ctx.filter="drop-shadow(...)" taints the canvas
|
|
73
|
-
// even for same-origin content (Chrome, Firefox).
|
|
117
|
+
// drop-shadow() is rendered via ctx.shadow* to avoid canvas
|
|
118
|
+
// taint; remaining filters go through ctx.filter.
|
|
74
119
|
this.ctx.filter = effect.safeFilterString || 'none';
|
|
75
120
|
if (effect.shadow) {
|
|
76
121
|
this.ctx.shadowOffsetX = effect.shadow.offsetX;
|
|
@@ -82,25 +127,42 @@ class EffectsRenderer {
|
|
|
82
127
|
this.activeEffects.push(effect);
|
|
83
128
|
}
|
|
84
129
|
/**
|
|
85
|
-
* Remove the most recent effect
|
|
86
|
-
* Restores
|
|
130
|
+
* Remove the most recent effect.
|
|
131
|
+
* Restores canvas state if needed.
|
|
87
132
|
*/
|
|
88
133
|
popEffect() {
|
|
134
|
+
if (this.activeEffects.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
// If the effect being popped is the one that triggered the save,
|
|
137
|
+
// restore the canvas state now.
|
|
138
|
+
if (this.didSave && this.activeEffects.length - 1 === this.saveAtDepth) {
|
|
139
|
+
this.ctx.restore();
|
|
140
|
+
this.didSave = false;
|
|
141
|
+
this.saveAtDepth = -1;
|
|
142
|
+
}
|
|
89
143
|
this.activeEffects.pop();
|
|
90
|
-
|
|
144
|
+
// If the batch had ONLY lightweight effects (no save was performed),
|
|
145
|
+
// reset properties to their defaults. We only do this when all effects
|
|
146
|
+
// have been popped (activeEffects is empty), so properties don't
|
|
147
|
+
// flip mid-batch.
|
|
148
|
+
if (!this.didSave && this.activeEffects.length === 0) {
|
|
149
|
+
this.ctx.globalAlpha = 1;
|
|
150
|
+
this.ctx.globalCompositeOperation = 'source-over';
|
|
151
|
+
this.ctx.filter = 'none';
|
|
152
|
+
this.ctx.shadowOffsetX = 0;
|
|
153
|
+
this.ctx.shadowOffsetY = 0;
|
|
154
|
+
this.ctx.shadowBlur = 0;
|
|
155
|
+
this.ctx.shadowColor = 'rgba(0, 0, 0, 0)';
|
|
156
|
+
}
|
|
91
157
|
}
|
|
92
158
|
/**
|
|
93
|
-
* Get the current number of active effects
|
|
94
|
-
*
|
|
95
|
-
* @returns Number of active effects
|
|
159
|
+
* Get the current number of active effects.
|
|
96
160
|
*/
|
|
97
161
|
getActiveEffectCount() {
|
|
98
162
|
return this.activeEffects.length;
|
|
99
163
|
}
|
|
100
164
|
/**
|
|
101
|
-
* Check if there are any active effects
|
|
102
|
-
*
|
|
103
|
-
* @returns True if there are active effects
|
|
165
|
+
* Check if there are any active effects.
|
|
104
166
|
*/
|
|
105
167
|
hasActiveEffects() {
|
|
106
168
|
return this.activeEffects.length > 0;
|
|
@@ -93,10 +93,35 @@ const getTextStrokeLineJoin = () => {
|
|
|
93
93
|
*/
|
|
94
94
|
class TextRenderer {
|
|
95
95
|
constructor(deps) {
|
|
96
|
+
/**
|
|
97
|
+
* Per-render glyph width cache. Keyed by glyph + font string,
|
|
98
|
+
* avoids redundant `measureText` calls when letter-spacing is non-zero.
|
|
99
|
+
* Cleared at the start of each renderTextNode call.
|
|
100
|
+
*/
|
|
101
|
+
this.glyphWidthCache = null;
|
|
96
102
|
this.ctx = deps.ctx;
|
|
97
103
|
this.options = deps.options;
|
|
98
104
|
this.decorationRenderer = new text_decoration_renderer_1.TextDecorationRenderer(deps.ctx);
|
|
99
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns the cached width for a glyph+font pair, measuring via
|
|
108
|
+
* measureText if not yet cached. Only used when letterSpacing !== 0
|
|
109
|
+
* (the `canRenderWholeText` fast path handles the zero-spacing case).
|
|
110
|
+
*/
|
|
111
|
+
cachedMeasureText(glyph, fontString) {
|
|
112
|
+
let cache = this.glyphWidthCache;
|
|
113
|
+
if (!cache) {
|
|
114
|
+
cache = new Map();
|
|
115
|
+
this.glyphWidthCache = cache;
|
|
116
|
+
}
|
|
117
|
+
const key = glyph + fontString;
|
|
118
|
+
let w = cache.get(key);
|
|
119
|
+
if (w === undefined) {
|
|
120
|
+
w = this.ctx.measureText(glyph).width;
|
|
121
|
+
cache.set(key, w);
|
|
122
|
+
}
|
|
123
|
+
return w;
|
|
124
|
+
}
|
|
100
125
|
/**
|
|
101
126
|
* Iterate grapheme clusters one-by-one, applying correct letter-spacing and
|
|
102
127
|
* per-script baseline for each character.
|
|
@@ -119,6 +144,7 @@ class TextRenderer {
|
|
|
119
144
|
}
|
|
120
145
|
const letters = (0, text_1.segmentGraphemes)(text.text);
|
|
121
146
|
const y = text.bounds.top + baseline;
|
|
147
|
+
const fontString = this.ctx.font;
|
|
122
148
|
let left = text.bounds.left;
|
|
123
149
|
for (const letter of letters) {
|
|
124
150
|
if ((0, exports.hasCJKCharacters)(letter)) {
|
|
@@ -130,29 +156,53 @@ class TextRenderer {
|
|
|
130
156
|
else {
|
|
131
157
|
renderFn(letter, left, y);
|
|
132
158
|
}
|
|
133
|
-
left += this.
|
|
159
|
+
left += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
iterateVerticalGlyphs(text, letterSpacing, baseline, writingMode, renderFn) {
|
|
137
163
|
const letters = (0, text_1.segmentGraphemes)(text.text);
|
|
164
|
+
const fontString = this.ctx.font;
|
|
165
|
+
const rotationAngle = writingMode === 4 /* WRITING_MODE.SIDEWAYS_LR */ ? -Math.PI / 2 : Math.PI / 2;
|
|
138
166
|
let top = text.bounds.top;
|
|
139
|
-
for (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
renderFn(letter, 0, 0);
|
|
145
|
-
this.ctx.restore();
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
167
|
+
for (let i = 0; i < letters.length;) {
|
|
168
|
+
const letter = letters[i];
|
|
169
|
+
const isSideways = (0, writing_mode_1.isSidewaysWritingMode)(writingMode) || (!(0, exports.hasCJKCharacters)(letter) && letter.trim().length > 0);
|
|
170
|
+
if (!isSideways) {
|
|
171
|
+
// CJK glyph in vertical writing mode — render upright
|
|
148
172
|
const savedBaseline = this.ctx.textBaseline;
|
|
149
173
|
if ((0, exports.hasCJKCharacters)(letter)) {
|
|
150
174
|
this.ctx.textBaseline = 'ideographic';
|
|
151
175
|
}
|
|
152
176
|
renderFn(letter, text.bounds.left, top + baseline);
|
|
153
177
|
this.ctx.textBaseline = savedBaseline;
|
|
178
|
+
top += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// ── Batch consecutive sideways glyphs into one save/restore ──
|
|
183
|
+
const runStart = i;
|
|
184
|
+
while (i < letters.length) {
|
|
185
|
+
const ch = letters[i];
|
|
186
|
+
if (!(0, writing_mode_1.isSidewaysWritingMode)(writingMode) && !(0, exports.hasCJKCharacters)(ch) && ch.trim().length === 0)
|
|
187
|
+
break;
|
|
188
|
+
if ((0, writing_mode_1.isSidewaysWritingMode)(writingMode) || (!(0, exports.hasCJKCharacters)(ch) && ch.trim().length > 0)) {
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Render entire sideways run in one save/restore
|
|
196
|
+
this.ctx.save();
|
|
197
|
+
this.ctx.translate(text.bounds.left + baseline, top);
|
|
198
|
+
this.ctx.rotate(rotationAngle);
|
|
199
|
+
let runOffset = 0;
|
|
200
|
+
for (let j = runStart; j < i; j++) {
|
|
201
|
+
renderFn(letters[j], 0, runOffset);
|
|
202
|
+
runOffset += this.cachedMeasureText(letters[j], fontString) + letterSpacing;
|
|
154
203
|
}
|
|
155
|
-
|
|
204
|
+
this.ctx.restore();
|
|
205
|
+
top += runOffset;
|
|
156
206
|
}
|
|
157
207
|
}
|
|
158
208
|
/**
|
|
@@ -253,6 +303,8 @@ class TextRenderer {
|
|
|
253
303
|
// kerning and ligatures when rendering multiple glyphs together, so
|
|
254
304
|
// measuring them as one string is more precise than summing individual widths.
|
|
255
305
|
// Binary search reduces measurements from O(n) to O(log n).
|
|
306
|
+
// NOTE: can't use cachedMeasureText here — the binary search measures
|
|
307
|
+
// concatenated substrings, and kerning/ligatures depend on the full string.
|
|
256
308
|
const fits = (n) => this.ctx.measureText(graphemes.slice(0, n).join('')).width + ellipsisWidth <= maxWidth;
|
|
257
309
|
let lo = 0;
|
|
258
310
|
let hi = graphemes.length;
|
|
@@ -268,10 +320,11 @@ class TextRenderer {
|
|
|
268
320
|
return graphemes.slice(0, lo).join('') + ellipsis;
|
|
269
321
|
}
|
|
270
322
|
else {
|
|
323
|
+
const fontString = this.ctx.font;
|
|
271
324
|
let width = ellipsisWidth;
|
|
272
325
|
const result = [];
|
|
273
326
|
for (const letter of graphemes) {
|
|
274
|
-
const glyphWidth = this.
|
|
327
|
+
const glyphWidth = this.cachedMeasureText(letter, fontString);
|
|
275
328
|
// Check against glyph width only (no trailing spacing): letter-spacing
|
|
276
329
|
// is applied *between* characters, not after the final glyph. Using
|
|
277
330
|
// `glyphWidth + letterSpacing` would incorrectly discard letters that
|
|
@@ -383,6 +436,9 @@ class TextRenderer {
|
|
|
383
436
|
return true;
|
|
384
437
|
}
|
|
385
438
|
async renderTextNode(text, styles, containerBounds) {
|
|
439
|
+
// Reset glyph width cache at the start of each text node render —
|
|
440
|
+
// the font may change between nodes.
|
|
441
|
+
this.glyphWidthCache = null;
|
|
386
442
|
this.ctx.font = this.createFontStyle(styles)[0];
|
|
387
443
|
this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
|
|
388
444
|
this.ctx.textAlign = 'left';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseStackingContexts = exports.ElementPaint = exports.StackingContext = void 0;
|
|
3
|
+
exports.parseStackingContexts = exports.buildClipPathEffect = exports.resolveAxisRadius = exports.hasOverflowClip = exports.ElementPaint = exports.StackingContext = void 0;
|
|
4
4
|
const bitwise_1 = require("../core/bitwise");
|
|
5
5
|
const bound_curves_1 = require("./bound-curves");
|
|
6
6
|
const effects_1 = require("./effects");
|
|
@@ -51,7 +51,7 @@ class ElementPaint {
|
|
|
51
51
|
const matrix = this.container.styles.transform;
|
|
52
52
|
this.effects.push(new effects_1.TransformEffect(offsetX, offsetY, matrix));
|
|
53
53
|
}
|
|
54
|
-
if (hasOverflowClip(this.container.styles)) {
|
|
54
|
+
if ((0, exports.hasOverflowClip)(this.container.styles)) {
|
|
55
55
|
const borderBox = (0, bound_curves_1.calculateBorderBoxPath)(this.curves);
|
|
56
56
|
const paddingBox = (0, bound_curves_1.calculatePaddingBoxPath)(this.curves);
|
|
57
57
|
if ((0, path_1.equalPath)(borderBox, paddingBox)) {
|
|
@@ -63,7 +63,7 @@ class ElementPaint {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
if (this.container.styles.clipPath.type !== 0 /* CLIP_PATH_TYPE.NONE */) {
|
|
66
|
-
const clipPathEffect = buildClipPathEffect(this.container.styles.clipPath, this.container.bounds);
|
|
66
|
+
const clipPathEffect = (0, exports.buildClipPathEffect)(this.container.styles.clipPath, this.container.bounds);
|
|
67
67
|
if (clipPathEffect) {
|
|
68
68
|
this.effects.push(clipPathEffect);
|
|
69
69
|
}
|
|
@@ -91,7 +91,7 @@ class ElementPaint {
|
|
|
91
91
|
const croplessEffects = parent.effects.filter((effect) => !(0, effects_1.isClipEffect)(effect));
|
|
92
92
|
if (inFlow || parent.container.styles.position !== 0 /* POSITION.STATIC */ || !parent.parent) {
|
|
93
93
|
inFlow = [2 /* POSITION.ABSOLUTE */, 3 /* POSITION.FIXED */].indexOf(parent.container.styles.position) === -1;
|
|
94
|
-
if (hasOverflowClip(parent.container.styles)) {
|
|
94
|
+
if ((0, exports.hasOverflowClip)(parent.container.styles)) {
|
|
95
95
|
const borderBox = (0, bound_curves_1.calculateBorderBoxPath)(parent.curves);
|
|
96
96
|
const paddingBox = (0, bound_curves_1.calculatePaddingBoxPath)(parent.curves);
|
|
97
97
|
if (!(0, path_1.equalPath)(borderBox, paddingBox)) {
|
|
@@ -109,7 +109,9 @@ class ElementPaint {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
exports.ElementPaint = ElementPaint;
|
|
112
|
+
/** @internal – exported for testing only. */
|
|
112
113
|
const hasOverflowClip = (styles) => styles.overflowX !== 0 /* OVERFLOW.VISIBLE */ || styles.overflowY !== 0 /* OVERFLOW.VISIBLE */;
|
|
114
|
+
exports.hasOverflowClip = hasOverflowClip;
|
|
113
115
|
/**
|
|
114
116
|
* Resolve a `closest-side` or `farthest-side` shape-radius keyword to pixels
|
|
115
117
|
* for a single axis. Used by both `circle()` (per-axis) and `ellipse()`.
|
|
@@ -119,6 +121,7 @@ const hasOverflowClip = (styles) => styles.overflowX !== 0 /* OVERFLOW.VISIBLE *
|
|
|
119
121
|
* @param start - Absolute start of the reference box on this axis.
|
|
120
122
|
* @param end - Absolute end of the reference box on this axis.
|
|
121
123
|
* @param dimRef - Reference dimension for resolving a length-percentage value.
|
|
124
|
+
* @internal – exported for testing only.
|
|
122
125
|
*/
|
|
123
126
|
const resolveAxisRadius = (r, center, start, end, dimRef) => {
|
|
124
127
|
if (r === 'closest-side')
|
|
@@ -127,12 +130,14 @@ const resolveAxisRadius = (r, center, start, end, dimRef) => {
|
|
|
127
130
|
return Math.max(center - start, end - center);
|
|
128
131
|
return (0, length_percentage_1.getAbsoluteValue)(r, dimRef);
|
|
129
132
|
};
|
|
133
|
+
exports.resolveAxisRadius = resolveAxisRadius;
|
|
130
134
|
/**
|
|
131
135
|
* Convert a parsed ClipPathValue + element bounds into a ClipPathEffect whose
|
|
132
136
|
* `applyClip` callback draws the clip shape directly onto the canvas context.
|
|
133
137
|
*
|
|
134
138
|
* All coordinates are computed in page-absolute space at construction time so
|
|
135
139
|
* the callback itself is allocation-free and executes synchronously.
|
|
140
|
+
* @internal – exported for testing only.
|
|
136
141
|
*/
|
|
137
142
|
const buildClipPathEffect = (clipPath, bounds) => {
|
|
138
143
|
const { left: bLeft, top: bTop, width: bWidth, height: bHeight } = bounds;
|
|
@@ -174,8 +179,8 @@ const buildClipPathEffect = (clipPath, bounds) => {
|
|
|
174
179
|
case 3 /* CLIP_PATH_TYPE.ELLIPSE */: {
|
|
175
180
|
const cx = bLeft + (0, length_percentage_1.getAbsoluteValue)(clipPath.cx, bWidth);
|
|
176
181
|
const cy = bTop + (0, length_percentage_1.getAbsoluteValue)(clipPath.cy, bHeight);
|
|
177
|
-
const rx = resolveAxisRadius(clipPath.rx, cx, bLeft, bLeft + bWidth, bWidth);
|
|
178
|
-
const ry = resolveAxisRadius(clipPath.ry, cy, bTop, bTop + bHeight, bHeight);
|
|
182
|
+
const rx = (0, exports.resolveAxisRadius)(clipPath.rx, cx, bLeft, bLeft + bWidth, bWidth);
|
|
183
|
+
const ry = (0, exports.resolveAxisRadius)(clipPath.ry, cy, bTop, bTop + bHeight, bHeight);
|
|
179
184
|
return new effects_1.ClipPathEffect((ctx) => {
|
|
180
185
|
ctx.beginPath();
|
|
181
186
|
ctx.ellipse(cx, cy, Math.max(0, rx), Math.max(0, ry), 0, 0, Math.PI * 2);
|
|
@@ -239,6 +244,7 @@ const buildClipPathEffect = (clipPath, bounds) => {
|
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
246
|
};
|
|
247
|
+
exports.buildClipPathEffect = buildClipPathEffect;
|
|
242
248
|
const parseStackTree = (parent, stackingContext, realStackingContext, listItems) => {
|
|
243
249
|
parent.container.elements.forEach((child) => {
|
|
244
250
|
const treatAsRealStackingContext = child.createsRealStackingContext;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AbortSignal helper utilities.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
6
|
+
* Used at checkpoints throughout the rendering pipeline to support
|
|
7
|
+
* cancellable renders.
|
|
8
|
+
*
|
|
9
|
+
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
10
|
+
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
11
|
+
*/
|
|
12
|
+
export declare const throwIfAborted: (signal?: AbortSignal) => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Color } from '../css/types/color';
|
|
2
|
+
import type { Context } from './context';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the background colour for the rendered canvas, following CSS
|
|
5
|
+
* background propagation rules:
|
|
6
|
+
*
|
|
7
|
+
* 1. If the target element is `<html>`, inherit the first opaque ancestor
|
|
8
|
+
* (doc → body → fallback).
|
|
9
|
+
* 2. Otherwise use the user-supplied backgroundColor, or opaque white.
|
|
10
|
+
*
|
|
11
|
+
* @param context - Current rendering context.
|
|
12
|
+
* @param element - The root element being rendered.
|
|
13
|
+
* @param backgroundColorOverride - User-supplied override (string | null).
|
|
14
|
+
* @returns A resolved colour value suitable for filling the canvas.
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseBackgroundColor: (context: Context, element: HTMLElement, backgroundColorOverride?: string | null) => Color;
|
|
@@ -13,8 +13,33 @@ export declare class Cache {
|
|
|
13
13
|
private readonly _cache;
|
|
14
14
|
private readonly maxSize;
|
|
15
15
|
private readonly _pendingOperations;
|
|
16
|
+
/** When true, addImage() collects URLs without starting loads. */
|
|
17
|
+
private _deferMode;
|
|
18
|
+
/** URLs collected during defer mode, pending batch preload. */
|
|
19
|
+
private _collectedUrls;
|
|
16
20
|
constructor(context: Context, _options: ResourceOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Enter defer mode: subsequent addImage() calls only collect URLs.
|
|
23
|
+
* Call preloadAll() to exit defer mode and batch-load all URLs.
|
|
24
|
+
*/
|
|
25
|
+
startDefer(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Exit defer mode and load all collected URLs in parallel, respecting
|
|
28
|
+
* an optional concurrency cap to avoid overwhelming the browser's network
|
|
29
|
+
* stack (default: 10 concurrent loads).
|
|
30
|
+
* After this call returns, all images are either loaded (cache hit) or
|
|
31
|
+
* failed (logged). Subsequent cache.match() calls return immediately.
|
|
32
|
+
*
|
|
33
|
+
* @param concurrency - Max concurrent image loads (1–100, default 10).
|
|
34
|
+
*/
|
|
35
|
+
preloadAll(concurrency?: number): Promise<void>;
|
|
17
36
|
addImage(src: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Shared helper: enqueues loading for a single URL with pending-operation
|
|
39
|
+
* deduplication and error swallowing. Used by both addImage() (normal mode)
|
|
40
|
+
* and preloadAll() (batch mode).
|
|
41
|
+
*/
|
|
42
|
+
private _addImageWithPending;
|
|
18
43
|
private _addImageInternal;
|
|
19
44
|
private withTimeout;
|
|
20
45
|
match(src: string): Promise<HTMLImageElement | HTMLCanvasElement | undefined> | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Options } from '../options';
|
|
2
|
+
import type { Html2CanvasConfig } from '../config';
|
|
3
|
+
import type { ResourceOptions } from './cache-storage';
|
|
4
|
+
import type { ContextOptions } from './context';
|
|
5
|
+
import type { CloneConfigurations } from '../dom/document-cloner';
|
|
6
|
+
import type { RenderConfigurations } from '../render/canvas/canvas-renderer';
|
|
7
|
+
/**
|
|
8
|
+
* Coerce known numeric options from string (or other) values to actual numbers.
|
|
9
|
+
* Mutates the opts object in place — this is intentionally a normalisation
|
|
10
|
+
* (not pure) step that runs once at the beginning of renderElement.
|
|
11
|
+
*/
|
|
12
|
+
export declare const coerceNumberOptions: (opts: Partial<Options>) => void;
|
|
13
|
+
/** Assemble resource loading options from user-provided Options. */
|
|
14
|
+
export declare const assembleResourceOptions: (opts: Partial<Options>) => ResourceOptions;
|
|
15
|
+
/** Assemble context (logging + cache) options, extending resource options. */
|
|
16
|
+
export declare const assembleContextOptions: (opts: Partial<Options>, config: Html2CanvasConfig, resourceOptions: ResourceOptions) => ContextOptions;
|
|
17
|
+
/** Assemble window / viewport options. */
|
|
18
|
+
export interface AssembledWindowOptions {
|
|
19
|
+
windowWidth: number;
|
|
20
|
+
windowHeight: number;
|
|
21
|
+
scrollX: number;
|
|
22
|
+
scrollY: number;
|
|
23
|
+
}
|
|
24
|
+
export declare const assembleWindowOptions: (opts: Partial<Options>, defaultView: Window) => AssembledWindowOptions;
|
|
25
|
+
/** Assemble DOM cloning options. */
|
|
26
|
+
export declare const assembleCloneOptions: (opts: Partial<Options>, config: Html2CanvasConfig, foreignObjectRendering: boolean) => CloneConfigurations;
|
|
27
|
+
/** Assemble canvas rendering options. */
|
|
28
|
+
export declare const assembleRenderOptions: (opts: Partial<Options>, backgroundColor: number | null, left: number, top: number, width: number, height: number, devicePixelRatio: number) => RenderConfigurations;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic LRU (Least-Recently-Used) wrapper over a native `Map`.
|
|
3
|
+
*
|
|
4
|
+
* Both the CSS parse cache and the background pattern cache implement the
|
|
5
|
+
* same LRU eviction pattern using Map's insertion-order guarantee. This
|
|
6
|
+
* tiny utility centralises that logic so it can be reused without
|
|
7
|
+
* duplication.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* const cache = new LRUMap<string, CanvasPattern>(50);
|
|
12
|
+
* const entry = cache.get(key); // promotes if found, returns undefined otherwise
|
|
13
|
+
* cache.set(key, value); // evicts oldest entry when at capacity
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare class LRUMap<K, V> {
|
|
17
|
+
private readonly maxSize;
|
|
18
|
+
private readonly _map;
|
|
19
|
+
constructor(maxSize: number);
|
|
20
|
+
/**
|
|
21
|
+
* Get a value by key.
|
|
22
|
+
* On cache hit the entry is promoted to the end of the Map (most-recently-used).
|
|
23
|
+
* Returns `undefined` on miss.
|
|
24
|
+
*/
|
|
25
|
+
get(key: K): V | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Insert or update a key-value pair.
|
|
28
|
+
* If the key already exists it is moved to MRU position. If the map is
|
|
29
|
+
* at capacity the least-recently-used entry (oldest insertion order) is
|
|
30
|
+
* evicted before inserting.
|
|
31
|
+
*/
|
|
32
|
+
set(key: K, value: V): void;
|
|
33
|
+
/** Current number of entries. */
|
|
34
|
+
get size(): number;
|
|
35
|
+
/** Remove all entries. */
|
|
36
|
+
clear(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
declare const LINE_FEED = 10;
|
|
2
|
+
declare const SOLIDUS = 47;
|
|
3
|
+
declare const REVERSE_SOLIDUS = 92;
|
|
4
|
+
declare const CHARACTER_TABULATION = 9;
|
|
5
|
+
declare const SPACE = 32;
|
|
6
|
+
declare const QUOTATION_MARK = 34;
|
|
7
|
+
declare const EQUALS_SIGN = 61;
|
|
8
|
+
declare const NUMBER_SIGN = 35;
|
|
9
|
+
declare const DOLLAR_SIGN = 36;
|
|
10
|
+
declare const PERCENTAGE_SIGN = 37;
|
|
11
|
+
declare const APOSTROPHE = 39;
|
|
12
|
+
declare const LEFT_PARENTHESIS = 40;
|
|
13
|
+
declare const RIGHT_PARENTHESIS = 41;
|
|
14
|
+
declare const LOW_LINE = 95;
|
|
15
|
+
declare const HYPHEN_MINUS = 45;
|
|
16
|
+
declare const EXCLAMATION_MARK = 33;
|
|
17
|
+
declare const LESS_THAN_SIGN = 60;
|
|
18
|
+
declare const GREATER_THAN_SIGN = 62;
|
|
19
|
+
declare const COMMERCIAL_AT = 64;
|
|
20
|
+
declare const LEFT_SQUARE_BRACKET = 91;
|
|
21
|
+
declare const RIGHT_SQUARE_BRACKET = 93;
|
|
22
|
+
declare const CIRCUMFLEX_ACCENT = 61;
|
|
23
|
+
declare const LEFT_CURLY_BRACKET = 123;
|
|
24
|
+
declare const QUESTION_MARK = 63;
|
|
25
|
+
declare const RIGHT_CURLY_BRACKET = 125;
|
|
26
|
+
declare const VERTICAL_LINE = 124;
|
|
27
|
+
declare const TILDE = 126;
|
|
28
|
+
declare const CONTROL = 128;
|
|
29
|
+
declare const REPLACEMENT_CHARACTER = 65533;
|
|
30
|
+
declare const ASTERISK = 42;
|
|
31
|
+
declare const PLUS_SIGN = 43;
|
|
32
|
+
declare const COMMA = 44;
|
|
33
|
+
declare const COLON = 58;
|
|
34
|
+
declare const SEMICOLON = 59;
|
|
35
|
+
declare const FULL_STOP = 46;
|
|
36
|
+
declare const NULL = 0;
|
|
37
|
+
declare const BACKSPACE = 8;
|
|
38
|
+
declare const LINE_TABULATION = 11;
|
|
39
|
+
declare const SHIFT_OUT = 14;
|
|
40
|
+
declare const INFORMATION_SEPARATOR_ONE = 31;
|
|
41
|
+
declare const DELETE = 127;
|
|
42
|
+
declare const EOF = -1;
|
|
43
|
+
declare const ZERO = 48;
|
|
44
|
+
declare const a = 97;
|
|
45
|
+
declare const e = 101;
|
|
46
|
+
declare const f = 102;
|
|
47
|
+
declare const u = 117;
|
|
48
|
+
declare const z = 122;
|
|
49
|
+
declare const A = 65;
|
|
50
|
+
declare const E = 69;
|
|
51
|
+
declare const F = 70;
|
|
52
|
+
declare const U = 85;
|
|
53
|
+
declare const Z = 90;
|
|
54
|
+
/** Re-exported code-point constants used by the Tokenizer class internally. */
|
|
55
|
+
export { LINE_FEED, SOLIDUS, REVERSE_SOLIDUS, CHARACTER_TABULATION, SPACE, QUOTATION_MARK, EQUALS_SIGN, NUMBER_SIGN, DOLLAR_SIGN, PERCENTAGE_SIGN, APOSTROPHE, LEFT_PARENTHESIS, RIGHT_PARENTHESIS, LOW_LINE, HYPHEN_MINUS, EXCLAMATION_MARK, LESS_THAN_SIGN, GREATER_THAN_SIGN, COMMERCIAL_AT, LEFT_SQUARE_BRACKET, RIGHT_SQUARE_BRACKET, CIRCUMFLEX_ACCENT, LEFT_CURLY_BRACKET, QUESTION_MARK, RIGHT_CURLY_BRACKET, VERTICAL_LINE, TILDE, CONTROL, REPLACEMENT_CHARACTER, ASTERISK, PLUS_SIGN, COMMA, COLON, SEMICOLON, FULL_STOP, NULL, BACKSPACE, LINE_TABULATION, SHIFT_OUT, INFORMATION_SEPARATOR_ONE, DELETE, EOF, ZERO, a, e, f, u, z, A, E, F, U, Z };
|
|
56
|
+
declare const isDigit: (codePoint: number) => boolean;
|
|
57
|
+
declare const isSurrogateCodePoint: (codePoint: number) => boolean;
|
|
58
|
+
declare const isHex: (codePoint: number) => boolean;
|
|
59
|
+
declare const isLowerCaseLetter: (codePoint: number) => boolean;
|
|
60
|
+
declare const isUpperCaseLetter: (codePoint: number) => boolean;
|
|
61
|
+
declare const isLetter: (codePoint: number) => boolean;
|
|
62
|
+
declare const isNonASCIICodePoint: (codePoint: number) => boolean;
|
|
63
|
+
declare const isWhiteSpace: (codePoint: number) => boolean;
|
|
64
|
+
declare const isNameStartCodePoint: (codePoint: number) => boolean;
|
|
65
|
+
declare const isNameCodePoint: (codePoint: number) => boolean;
|
|
66
|
+
declare const isNonPrintableCodePoint: (codePoint: number) => boolean;
|
|
67
|
+
declare const isValidEscape: (c1: number, c2: number) => boolean;
|
|
68
|
+
declare const isIdentifierStart: (c1: number, c2: number, c3: number) => boolean;
|
|
69
|
+
declare const isNumberStart: (c1: number, c2: number, c3: number) => boolean;
|
|
70
|
+
/** Re-exported character-classification predicates. */
|
|
71
|
+
export { isDigit, isSurrogateCodePoint, isHex, isLowerCaseLetter, isUpperCaseLetter, isLetter, isNonASCIICodePoint, isWhiteSpace, isNameStartCodePoint, isNameCodePoint, isNonPrintableCodePoint, isValidEscape, isIdentifierStart, isNumberStart };
|
|
72
|
+
declare const stringToNumber: (codePoints: number[]) => number;
|
|
73
|
+
/** Re-exported helper: converts an array of code points to a JS number. */
|
|
74
|
+
export { stringToNumber };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Token } from './token-types';
|
|
2
|
+
export declare const LEFT_PARENTHESIS_TOKEN: Token;
|
|
3
|
+
export declare const RIGHT_PARENTHESIS_TOKEN: Token;
|
|
4
|
+
export declare const COMMA_TOKEN: Token;
|
|
5
|
+
export declare const SUFFIX_MATCH_TOKEN: Token;
|
|
6
|
+
export declare const PREFIX_MATCH_TOKEN: Token;
|
|
7
|
+
export declare const COLUMN_TOKEN: Token;
|
|
8
|
+
export declare const DASH_MATCH_TOKEN: Token;
|
|
9
|
+
export declare const INCLUDE_MATCH_TOKEN: Token;
|
|
10
|
+
export declare const LEFT_CURLY_BRACKET_TOKEN: Token;
|
|
11
|
+
export declare const RIGHT_CURLY_BRACKET_TOKEN: Token;
|
|
12
|
+
export declare const SUBSTRING_MATCH_TOKEN: Token;
|
|
13
|
+
export declare const BAD_URL_TOKEN: Token;
|
|
14
|
+
export declare const BAD_STRING_TOKEN: Token;
|
|
15
|
+
export declare const CDO_TOKEN: Token;
|
|
16
|
+
export declare const CDC_TOKEN: Token;
|
|
17
|
+
export declare const COLON_TOKEN: Token;
|
|
18
|
+
export declare const SEMICOLON_TOKEN: Token;
|
|
19
|
+
export declare const LEFT_SQUARE_BRACKET_TOKEN: Token;
|
|
20
|
+
export declare const RIGHT_SQUARE_BRACKET_TOKEN: Token;
|
|
21
|
+
export declare const WHITESPACE_TOKEN: Token;
|
|
22
|
+
export declare const EOF_TOKEN: Token;
|