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.
Files changed (46) hide show
  1. package/dist/html2canvas-pro.cjs +11218 -0
  2. package/dist/html2canvas-pro.cjs.map +1 -0
  3. package/dist/html2canvas-pro.esm.js +2601 -2379
  4. package/dist/html2canvas-pro.esm.js.map +1 -1
  5. package/dist/html2canvas-pro.js +2601 -2379
  6. package/dist/html2canvas-pro.js.map +1 -1
  7. package/dist/html2canvas-pro.min.js +4 -5
  8. package/dist/lib/core/abort-helper.js +20 -0
  9. package/dist/lib/core/background-parser.js +41 -0
  10. package/dist/lib/core/cache-storage.js +63 -7
  11. package/dist/lib/core/config-assembler.js +88 -0
  12. package/dist/lib/core/lru-map.js +66 -0
  13. package/dist/lib/core/render-element.js +24 -103
  14. package/dist/lib/css/property-descriptors/background-position.js +1 -11
  15. package/dist/lib/css/property-descriptors/background-size.js +3 -1
  16. package/dist/lib/css/syntax/token-constants.js +214 -0
  17. package/dist/lib/css/syntax/token-singletons.js +36 -0
  18. package/dist/lib/css/syntax/token-types.js +10 -0
  19. package/dist/lib/css/syntax/tokenizer.js +175 -307
  20. package/dist/lib/css/types/length-percentage.js +63 -3
  21. package/dist/lib/dom/document-cloner.js +23 -22
  22. package/dist/lib/dom/node-parser.js +1 -21
  23. package/dist/lib/dom/slot-cloner.js +8 -8
  24. package/dist/lib/render/background.js +4 -1
  25. package/dist/lib/render/canvas/background-renderer.js +27 -39
  26. package/dist/lib/render/canvas/canvas-renderer.js +2 -2
  27. package/dist/lib/render/canvas/effects-renderer.js +94 -32
  28. package/dist/lib/render/canvas/text-renderer.js +68 -12
  29. package/dist/lib/render/stacking-context.js +12 -6
  30. package/dist/types/core/abort-helper.d.ts +12 -0
  31. package/dist/types/core/background-parser.d.ts +16 -0
  32. package/dist/types/core/cache-storage.d.ts +25 -0
  33. package/dist/types/core/config-assembler.d.ts +28 -0
  34. package/dist/types/core/lru-map.d.ts +37 -0
  35. package/dist/types/css/syntax/token-constants.d.ts +74 -0
  36. package/dist/types/css/syntax/token-singletons.d.ts +22 -0
  37. package/dist/types/css/syntax/token-types.d.ts +72 -0
  38. package/dist/types/css/syntax/tokenizer.d.ts +6 -73
  39. package/dist/types/css/types/length-percentage.d.ts +42 -1
  40. package/dist/types/dom/document-cloner.d.ts +5 -0
  41. package/dist/types/dom/node-parser.d.ts +0 -2
  42. package/dist/types/render/canvas/background-renderer.d.ts +0 -10
  43. package/dist/types/render/canvas/effects-renderer.d.ts +42 -17
  44. package/dist/types/render/canvas/text-renderer.d.ts +12 -0
  45. package/dist/types/render/stacking-context.d.ts +26 -1
  46. 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
- // Clear all existing effects
64
+ // ── 1. Pop all previously active effects ──────────────────────
34
65
  while (this.activeEffects.length) {
35
66
  this.popEffect();
36
67
  }
37
- // Apply new effects
38
- effects.forEach((effect) => this.applyEffect(effect));
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
- this.ctx.save();
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
- // Apply all filters except drop-shadow() via ctx.filter.
71
- // drop-shadow() is rendered separately through ctx.shadow*
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 the canvas state before the effect was applied
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
- this.ctx.restore();
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.ctx.measureText(letter).width + letterSpacing;
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 (const letter of letters) {
140
- if ((0, writing_mode_1.isSidewaysWritingMode)(writingMode) || (!(0, exports.hasCJKCharacters)(letter) && letter.trim().length > 0)) {
141
- this.ctx.save();
142
- this.ctx.translate(text.bounds.left + baseline, top);
143
- this.ctx.rotate(writingMode === 4 /* WRITING_MODE.SIDEWAYS_LR */ ? -Math.PI / 2 : Math.PI / 2);
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
- top += this.ctx.measureText(letter).width + letterSpacing;
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.ctx.measureText(letter).width;
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;