html2canvas-pro 2.2.3 → 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 (48) 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 +2714 -2388
  4. package/dist/html2canvas-pro.esm.js.map +1 -1
  5. package/dist/html2canvas-pro.js +2714 -2388
  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/effects.js +134 -13
  30. package/dist/lib/render/stacking-context.js +12 -6
  31. package/dist/types/core/abort-helper.d.ts +12 -0
  32. package/dist/types/core/background-parser.d.ts +16 -0
  33. package/dist/types/core/cache-storage.d.ts +25 -0
  34. package/dist/types/core/config-assembler.d.ts +28 -0
  35. package/dist/types/core/lru-map.d.ts +37 -0
  36. package/dist/types/css/syntax/token-constants.d.ts +74 -0
  37. package/dist/types/css/syntax/token-singletons.d.ts +22 -0
  38. package/dist/types/css/syntax/token-types.d.ts +72 -0
  39. package/dist/types/css/syntax/tokenizer.d.ts +6 -73
  40. package/dist/types/css/types/length-percentage.d.ts +42 -1
  41. package/dist/types/dom/document-cloner.d.ts +5 -0
  42. package/dist/types/dom/node-parser.d.ts +0 -2
  43. package/dist/types/render/canvas/background-renderer.d.ts +0 -10
  44. package/dist/types/render/canvas/effects-renderer.d.ts +42 -17
  45. package/dist/types/render/canvas/text-renderer.d.ts +12 -0
  46. package/dist/types/render/effects.d.ts +35 -1
  47. package/dist/types/render/stacking-context.d.ts +26 -1
  48. package/package.json +5 -7
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /**
3
+ * AbortSignal helper utilities.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.throwIfAborted = void 0;
7
+ /**
8
+ * Throw a DOMException if the given AbortSignal is already aborted.
9
+ * Used at checkpoints throughout the rendering pipeline to support
10
+ * cancellable renders.
11
+ *
12
+ * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
13
+ * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
14
+ */
15
+ const throwIfAborted = (signal) => {
16
+ if (signal?.aborted) {
17
+ throw new DOMException('The operation was aborted.', 'AbortError');
18
+ }
19
+ };
20
+ exports.throwIfAborted = throwIfAborted;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseBackgroundColor = void 0;
4
+ const color_1 = require("../css/types/color");
5
+ const color_utilities_1 = require("../css/types/color-utilities");
6
+ /**
7
+ * Resolve the background colour for the rendered canvas, following CSS
8
+ * background propagation rules:
9
+ *
10
+ * 1. If the target element is `<html>`, inherit the first opaque ancestor
11
+ * (doc → body → fallback).
12
+ * 2. Otherwise use the user-supplied backgroundColor, or opaque white.
13
+ *
14
+ * @param context - Current rendering context.
15
+ * @param element - The root element being rendered.
16
+ * @param backgroundColorOverride - User-supplied override (string | null).
17
+ * @returns A resolved colour value suitable for filling the canvas.
18
+ */
19
+ const parseBackgroundColor = (context, element, backgroundColorOverride) => {
20
+ const ownerDocument = element.ownerDocument;
21
+ // http://www.w3.org/TR/css3-background/#special-backgrounds
22
+ const documentBackgroundColor = ownerDocument.documentElement
23
+ ? (0, color_1.parseColor)(context, getComputedStyle(ownerDocument.documentElement).backgroundColor)
24
+ : color_1.COLORS.TRANSPARENT;
25
+ const bodyBackgroundColor = ownerDocument.body
26
+ ? (0, color_1.parseColor)(context, getComputedStyle(ownerDocument.body).backgroundColor)
27
+ : color_1.COLORS.TRANSPARENT;
28
+ const defaultBackgroundColor = typeof backgroundColorOverride === 'string'
29
+ ? (0, color_1.parseColor)(context, backgroundColorOverride)
30
+ : backgroundColorOverride === null
31
+ ? color_1.COLORS.TRANSPARENT
32
+ : 0xffffffff;
33
+ return element === ownerDocument.documentElement
34
+ ? (0, color_utilities_1.isTransparent)(documentBackgroundColor)
35
+ ? (0, color_utilities_1.isTransparent)(bodyBackgroundColor)
36
+ ? defaultBackgroundColor
37
+ : bodyBackgroundColor
38
+ : documentBackgroundColor
39
+ : defaultBackgroundColor;
40
+ };
41
+ exports.parseBackgroundColor = parseBackgroundColor;
@@ -8,6 +8,10 @@ class Cache {
8
8
  this._options = _options;
9
9
  this._cache = new Map();
10
10
  this._pendingOperations = new Map();
11
+ /** When true, addImage() collects URLs without starting loads. */
12
+ this._deferMode = false;
13
+ /** URLs collected during defer mode, pending batch preload. */
14
+ this._collectedUrls = new Set();
11
15
  // Default cache size: 100 items
12
16
  this.maxSize = _options.maxCacheSize ?? 100;
13
17
  if (this.maxSize < 1) {
@@ -18,7 +22,48 @@ class Cache {
18
22
  `Consider using a smaller value (recommended: 100-1000).`);
19
23
  }
20
24
  }
25
+ /**
26
+ * Enter defer mode: subsequent addImage() calls only collect URLs.
27
+ * Call preloadAll() to exit defer mode and batch-load all URLs.
28
+ */
29
+ startDefer() {
30
+ this._deferMode = true;
31
+ }
32
+ /**
33
+ * Exit defer mode and load all collected URLs in parallel, respecting
34
+ * an optional concurrency cap to avoid overwhelming the browser's network
35
+ * stack (default: 10 concurrent loads).
36
+ * After this call returns, all images are either loaded (cache hit) or
37
+ * failed (logged). Subsequent cache.match() calls return immediately.
38
+ *
39
+ * @param concurrency - Max concurrent image loads (1–100, default 10).
40
+ */
41
+ async preloadAll(concurrency = 10) {
42
+ this._deferMode = false;
43
+ const urls = Array.from(this._collectedUrls);
44
+ this._collectedUrls.clear();
45
+ if (urls.length === 0) {
46
+ return;
47
+ }
48
+ const limit = Math.max(1, Math.min(concurrency, 100));
49
+ this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
50
+ // Load in batches to respect the concurrency cap while maximising
51
+ // parallelism within each batch.
52
+ for (let i = 0; i < urls.length; i += limit) {
53
+ const batch = urls.slice(i, i + limit);
54
+ const operations = batch.map((url) => this._addImageWithPending(url));
55
+ await Promise.allSettled(operations);
56
+ }
57
+ }
21
58
  addImage(src) {
59
+ // ── Defer mode: collect only ──────────────────────────────
60
+ if (this._deferMode) {
61
+ if (!this.has(src) && (isBlobImage(src) || isRenderable(src))) {
62
+ this._collectedUrls.add(src);
63
+ }
64
+ return Promise.resolve();
65
+ }
66
+ // ── Normal mode: immediate load ───────────────────────────
22
67
  // Wait for any pending operations on this key
23
68
  const pending = this._pendingOperations.get(src);
24
69
  if (pending) {
@@ -32,16 +77,27 @@ class Cache {
32
77
  return Promise.resolve();
33
78
  }
34
79
  if (isBlobImage(src) || isRenderable(src)) {
35
- // Create a pending operation to ensure atomicity
36
- const operation = this._addImageInternal(src);
37
- this._pendingOperations.set(src, operation);
38
- operation.finally(() => {
39
- this._pendingOperations.delete(src);
40
- });
41
- return operation;
80
+ return this._addImageWithPending(src);
42
81
  }
43
82
  return Promise.resolve();
44
83
  }
84
+ /**
85
+ * Shared helper: enqueues loading for a single URL with pending-operation
86
+ * deduplication and error swallowing. Used by both addImage() (normal mode)
87
+ * and preloadAll() (batch mode).
88
+ */
89
+ _addImageWithPending(src) {
90
+ const pending = this._pendingOperations.get(src);
91
+ if (pending) {
92
+ return pending;
93
+ }
94
+ const operation = this._addImageInternal(src);
95
+ this._pendingOperations.set(src, operation);
96
+ operation.finally(() => {
97
+ this._pendingOperations.delete(src);
98
+ });
99
+ return operation;
100
+ }
45
101
  async _addImageInternal(src) {
46
102
  const timeoutMs = this._options.imageTimeout ?? 15000;
47
103
  const imageWithTimeout = this.withTimeout(this.loadImage(src), timeoutMs, `Timed out (${timeoutMs}ms) loading image`);
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assembleRenderOptions = exports.assembleCloneOptions = exports.assembleWindowOptions = exports.assembleContextOptions = exports.assembleResourceOptions = exports.coerceNumberOptions = void 0;
4
+ const constants_1 = require("./constants");
5
+ /**
6
+ * Coerce known numeric options from string (or other) values to actual numbers.
7
+ * Mutates the opts object in place — this is intentionally a normalisation
8
+ * (not pure) step that runs once at the beginning of renderElement.
9
+ */
10
+ const coerceNumberOptions = (opts) => {
11
+ const numKeys = [
12
+ 'scale',
13
+ 'width',
14
+ 'height',
15
+ 'imageTimeout',
16
+ 'x',
17
+ 'y',
18
+ 'windowWidth',
19
+ 'windowHeight',
20
+ 'scrollX',
21
+ 'scrollY'
22
+ ];
23
+ numKeys.forEach((key) => {
24
+ const v = opts[key];
25
+ if (v !== undefined && v !== null && typeof v !== 'number') {
26
+ const n = Number(v);
27
+ if (!Number.isNaN(n)) {
28
+ opts[key] = n;
29
+ }
30
+ }
31
+ });
32
+ };
33
+ exports.coerceNumberOptions = coerceNumberOptions;
34
+ /** Assemble resource loading options from user-provided Options. */
35
+ const assembleResourceOptions = (opts) => ({
36
+ allowTaint: opts.allowTaint ?? false,
37
+ imageTimeout: opts.imageTimeout ?? constants_1.DEFAULT_IMAGE_TIMEOUT_MS,
38
+ proxy: opts.proxy,
39
+ useCORS: opts.useCORS ?? false,
40
+ customIsSameOrigin: opts.customIsSameOrigin,
41
+ maxCacheSize: opts.maxCacheSize
42
+ });
43
+ exports.assembleResourceOptions = assembleResourceOptions;
44
+ /** Assemble context (logging + cache) options, extending resource options. */
45
+ const assembleContextOptions = (opts, config, resourceOptions) => ({
46
+ logging: opts.logging ?? true,
47
+ cache: opts.cache ?? config.cache,
48
+ ...resourceOptions
49
+ });
50
+ exports.assembleContextOptions = assembleContextOptions;
51
+ const DEFAULT_WINDOW_WIDTH = 800;
52
+ const DEFAULT_WINDOW_HEIGHT = 600;
53
+ const DEFAULT_SCROLL = 0;
54
+ const assembleWindowOptions = (opts, defaultView) => {
55
+ const win = defaultView;
56
+ return {
57
+ windowWidth: opts.windowWidth ?? win.innerWidth ?? DEFAULT_WINDOW_WIDTH,
58
+ windowHeight: opts.windowHeight ?? win.innerHeight ?? DEFAULT_WINDOW_HEIGHT,
59
+ scrollX: opts.scrollX ?? win.pageXOffset ?? DEFAULT_SCROLL,
60
+ scrollY: opts.scrollY ?? win.pageYOffset ?? DEFAULT_SCROLL
61
+ };
62
+ };
63
+ exports.assembleWindowOptions = assembleWindowOptions;
64
+ /** Assemble DOM cloning options. */
65
+ const assembleCloneOptions = (opts, config, foreignObjectRendering) => ({
66
+ allowTaint: opts.allowTaint ?? false,
67
+ onclone: opts.onclone,
68
+ ignoreElements: opts.ignoreElements,
69
+ iframeContainer: opts.iframeContainer,
70
+ inlineImages: foreignObjectRendering,
71
+ copyStyles: foreignObjectRendering,
72
+ cspNonce: opts.cspNonce ?? config.cspNonce
73
+ });
74
+ exports.assembleCloneOptions = assembleCloneOptions;
75
+ /** Assemble canvas rendering options. */
76
+ const assembleRenderOptions = (opts, backgroundColor, left, top, width, height, devicePixelRatio) => ({
77
+ canvas: opts.canvas,
78
+ backgroundColor,
79
+ signal: opts.signal,
80
+ scale: opts.scale ?? devicePixelRatio ?? 1,
81
+ x: (opts.x ?? 0) + left,
82
+ y: (opts.y ?? 0) + top,
83
+ width: opts.width ?? Math.ceil(width),
84
+ height: opts.height ?? Math.ceil(height),
85
+ imageSmoothing: opts.imageSmoothing,
86
+ imageSmoothingQuality: opts.imageSmoothingQuality
87
+ });
88
+ exports.assembleRenderOptions = assembleRenderOptions;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LRUMap = void 0;
4
+ /**
5
+ * Generic LRU (Least-Recently-Used) wrapper over a native `Map`.
6
+ *
7
+ * Both the CSS parse cache and the background pattern cache implement the
8
+ * same LRU eviction pattern using Map's insertion-order guarantee. This
9
+ * tiny utility centralises that logic so it can be reused without
10
+ * duplication.
11
+ *
12
+ * Usage:
13
+ * ```ts
14
+ * const cache = new LRUMap<string, CanvasPattern>(50);
15
+ * const entry = cache.get(key); // promotes if found, returns undefined otherwise
16
+ * cache.set(key, value); // evicts oldest entry when at capacity
17
+ * ```
18
+ */
19
+ class LRUMap {
20
+ constructor(maxSize) {
21
+ this.maxSize = maxSize;
22
+ this._map = new Map();
23
+ }
24
+ /**
25
+ * Get a value by key.
26
+ * On cache hit the entry is promoted to the end of the Map (most-recently-used).
27
+ * Returns `undefined` on miss.
28
+ */
29
+ get(key) {
30
+ if (!this._map.has(key)) {
31
+ return undefined;
32
+ }
33
+ // Delete-then-set promotes the entry to MRU position.
34
+ const value = this._map.get(key);
35
+ this._map.delete(key);
36
+ this._map.set(key, value);
37
+ return value;
38
+ }
39
+ /**
40
+ * Insert or update a key-value pair.
41
+ * If the key already exists it is moved to MRU position. If the map is
42
+ * at capacity the least-recently-used entry (oldest insertion order) is
43
+ * evicted before inserting.
44
+ */
45
+ set(key, value) {
46
+ if (this._map.has(key)) {
47
+ this._map.delete(key);
48
+ }
49
+ else if (this._map.size >= this.maxSize) {
50
+ const oldestKey = this._map.keys().next().value;
51
+ if (oldestKey !== undefined) {
52
+ this._map.delete(oldestKey);
53
+ }
54
+ }
55
+ this._map.set(key, value);
56
+ }
57
+ /** Current number of entries. */
58
+ get size() {
59
+ return this._map.size;
60
+ }
61
+ /** Remove all entries. */
62
+ clear() {
63
+ this._map.clear();
64
+ }
65
+ }
66
+ exports.LRUMap = LRUMap;
@@ -3,39 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderElement = void 0;
4
4
  const bounds_1 = require("../css/layout/bounds");
5
5
  const color_1 = require("../css/types/color");
6
- const color_utilities_1 = require("../css/types/color-utilities");
7
6
  const document_cloner_1 = require("../dom/document-cloner");
7
+ const node_type_guards_1 = require("../dom/node-type-guards");
8
8
  const node_parser_1 = require("../dom/node-parser");
9
9
  const canvas_renderer_1 = require("../render/canvas/canvas-renderer");
10
10
  const foreignobject_renderer_1 = require("../render/canvas/foreignobject-renderer");
11
11
  const context_1 = require("./context");
12
12
  const validator_1 = require("./validator");
13
13
  const performance_monitor_1 = require("./performance-monitor");
14
- const coerceNumberOptions = (opts) => {
15
- const numKeys = [
16
- 'scale',
17
- 'width',
18
- 'height',
19
- 'imageTimeout',
20
- 'x',
21
- 'y',
22
- 'windowWidth',
23
- 'windowHeight',
24
- 'scrollX',
25
- 'scrollY'
26
- ];
27
- numKeys.forEach((key) => {
28
- const v = opts[key];
29
- if (v !== undefined && v !== null && typeof v !== 'number') {
30
- const n = Number(v);
31
- if (!Number.isNaN(n)) {
32
- opts[key] = n;
33
- }
34
- }
35
- });
36
- };
14
+ const abort_helper_1 = require("./abort-helper");
15
+ const config_assembler_1 = require("./config-assembler");
16
+ const background_parser_1 = require("./background-parser");
37
17
  const renderElement = async (element, opts, config) => {
38
- coerceNumberOptions(opts);
18
+ (0, config_assembler_1.coerceNumberOptions)(opts);
39
19
  // Input validation (unless explicitly skipped)
40
20
  if (!opts.skipValidation) {
41
21
  const validator = opts.validator || (0, validator_1.createDefaultValidator)();
@@ -59,29 +39,9 @@ const renderElement = async (element, opts, config) => {
59
39
  if (!defaultView) {
60
40
  throw new Error(`Document is not attached to a Window`);
61
41
  }
62
- const resourceOptions = {
63
- allowTaint: opts.allowTaint ?? false,
64
- imageTimeout: opts.imageTimeout ?? 15000,
65
- proxy: opts.proxy,
66
- useCORS: opts.useCORS ?? false,
67
- customIsSameOrigin: opts.customIsSameOrigin,
68
- maxCacheSize: opts.maxCacheSize
69
- };
70
- const contextOptions = {
71
- logging: opts.logging ?? true,
72
- cache: opts.cache ?? config.cache,
73
- ...resourceOptions
74
- };
75
- const DEFAULT_WINDOW_WIDTH = 800;
76
- const DEFAULT_WINDOW_HEIGHT = 600;
77
- const DEFAULT_SCROLL = 0;
78
- const win = defaultView;
79
- const windowOptions = {
80
- windowWidth: opts.windowWidth ?? win.innerWidth ?? DEFAULT_WINDOW_WIDTH,
81
- windowHeight: opts.windowHeight ?? win.innerHeight ?? DEFAULT_WINDOW_HEIGHT,
82
- scrollX: opts.scrollX ?? win.pageXOffset ?? DEFAULT_SCROLL,
83
- scrollY: opts.scrollY ?? win.pageYOffset ?? DEFAULT_SCROLL
84
- };
42
+ const resourceOptions = (0, config_assembler_1.assembleResourceOptions)(opts);
43
+ const contextOptions = (0, config_assembler_1.assembleContextOptions)(opts, config, resourceOptions);
44
+ const windowOptions = (0, config_assembler_1.assembleWindowOptions)(opts, defaultView);
85
45
  const windowBounds = new bounds_1.Bounds(windowOptions.scrollX, windowOptions.scrollY, windowOptions.windowWidth, windowOptions.windowHeight);
86
46
  const context = new context_1.Context(contextOptions, windowBounds, config);
87
47
  const performanceMonitoring = opts.enablePerformanceMonitoring ?? opts.logging ?? false;
@@ -91,19 +51,9 @@ const renderElement = async (element, opts, config) => {
91
51
  height: windowOptions.windowHeight
92
52
  });
93
53
  const signal = opts.signal;
94
- if (signal?.aborted) {
95
- throw new DOMException('The operation was aborted.', 'AbortError');
96
- }
54
+ (0, abort_helper_1.throwIfAborted)(signal);
97
55
  const foreignObjectRendering = opts.foreignObjectRendering ?? false;
98
- const cloneOptions = {
99
- allowTaint: opts.allowTaint ?? false,
100
- onclone: opts.onclone,
101
- ignoreElements: opts.ignoreElements,
102
- iframeContainer: opts.iframeContainer,
103
- inlineImages: foreignObjectRendering,
104
- copyStyles: foreignObjectRendering,
105
- cspNonce: opts.cspNonce ?? config.cspNonce
106
- };
56
+ const cloneOptions = (0, config_assembler_1.assembleCloneOptions)(opts, config, foreignObjectRendering);
107
57
  context.logger.debug(`Starting document clone with size ${windowBounds.width}x${windowBounds.height} scrolled to ${-windowBounds.left},${-windowBounds.top}`);
108
58
  perfMonitor.start('clone');
109
59
  const documentCloner = new document_cloner_1.DocumentCloner(context, element, cloneOptions);
@@ -117,24 +67,13 @@ const renderElement = async (element, opts, config) => {
117
67
  if (opts.removeContainer ?? true) {
118
68
  document_cloner_1.DocumentCloner.destroy(container);
119
69
  }
120
- throw new DOMException('The operation was aborted.', 'AbortError');
70
+ (0, abort_helper_1.throwIfAborted)(signal);
121
71
  }
122
- const { width, height, left, top } = (0, node_parser_1.isBodyElement)(clonedElement) || (0, node_parser_1.isHTMLElement)(clonedElement)
72
+ const { width, height, left, top } = (0, node_type_guards_1.isBodyElement)(clonedElement) || (0, node_type_guards_1.isHTMLElement)(clonedElement)
123
73
  ? (0, bounds_1.parseDocumentSize)(clonedElement.ownerDocument)
124
74
  : (0, bounds_1.parseBounds)(context, clonedElement);
125
- const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
126
- const renderOptions = {
127
- canvas: opts.canvas,
128
- backgroundColor,
129
- signal,
130
- scale: opts.scale ?? defaultView.devicePixelRatio ?? 1,
131
- x: (opts.x ?? 0) + left,
132
- y: (opts.y ?? 0) + top,
133
- width: opts.width ?? Math.ceil(width),
134
- height: opts.height ?? Math.ceil(height),
135
- imageSmoothing: opts.imageSmoothing,
136
- imageSmoothingQuality: opts.imageSmoothingQuality
137
- };
75
+ const backgroundColor = (0, background_parser_1.parseBackgroundColor)(context, clonedElement, opts.backgroundColor);
76
+ const renderOptions = (0, config_assembler_1.assembleRenderOptions)(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
138
77
  let canvas;
139
78
  let root;
140
79
  try {
@@ -149,19 +88,23 @@ const renderElement = async (element, opts, config) => {
149
88
  context.logger.debug(`Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`);
150
89
  context.logger.debug(`Starting DOM parsing`);
151
90
  perfMonitor.start('parse');
152
- if (signal?.aborted) {
153
- throw new DOMException('The operation was aborted.', 'AbortError');
154
- }
91
+ (0, abort_helper_1.throwIfAborted)(signal);
92
+ // Enter defer mode: collect all image URLs during parse without
93
+ // starting individual loads. This avoids serialised loading in
94
+ // DOM-traversal order.
95
+ context.cache.startDefer();
155
96
  root = (0, node_parser_1.parseTree)(context, clonedElement);
156
97
  perfMonitor.end('parse');
98
+ // Batch-preload all collected images in parallel before rendering.
99
+ perfMonitor.start('preload');
100
+ await context.cache.preloadAll();
101
+ perfMonitor.end('preload');
157
102
  if (backgroundColor === root.styles.backgroundColor) {
158
103
  root.styles.backgroundColor = color_1.COLORS.TRANSPARENT;
159
104
  }
160
105
  context.logger.debug(`Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`);
161
106
  perfMonitor.start('render');
162
- if (signal?.aborted) {
163
- throw new DOMException('The operation was aborted.', 'AbortError');
164
- }
107
+ (0, abort_helper_1.throwIfAborted)(signal);
165
108
  const renderer = new canvas_renderer_1.CanvasRenderer(context, renderOptions);
166
109
  canvas = await renderer.render(root);
167
110
  perfMonitor.end('render');
@@ -187,25 +130,3 @@ const renderElement = async (element, opts, config) => {
187
130
  }
188
131
  };
189
132
  exports.renderElement = renderElement;
190
- const parseBackgroundColor = (context, element, backgroundColorOverride) => {
191
- const ownerDocument = element.ownerDocument;
192
- // http://www.w3.org/TR/css3-background/#special-backgrounds
193
- const documentBackgroundColor = ownerDocument.documentElement
194
- ? (0, color_1.parseColor)(context, getComputedStyle(ownerDocument.documentElement).backgroundColor)
195
- : color_1.COLORS.TRANSPARENT;
196
- const bodyBackgroundColor = ownerDocument.body
197
- ? (0, color_1.parseColor)(context, getComputedStyle(ownerDocument.body).backgroundColor)
198
- : color_1.COLORS.TRANSPARENT;
199
- const defaultBackgroundColor = typeof backgroundColorOverride === 'string'
200
- ? (0, color_1.parseColor)(context, backgroundColorOverride)
201
- : backgroundColorOverride === null
202
- ? color_1.COLORS.TRANSPARENT
203
- : 0xffffffff;
204
- return element === ownerDocument.documentElement
205
- ? (0, color_utilities_1.isTransparent)(documentBackgroundColor)
206
- ? (0, color_utilities_1.isTransparent)(bodyBackgroundColor)
207
- ? defaultBackgroundColor
208
- : bodyBackgroundColor
209
- : documentBackgroundColor
210
- : defaultBackgroundColor;
211
- };
@@ -12,17 +12,7 @@ exports.backgroundPosition = {
12
12
  return (0, parser_1.parseFunctionArgs)(tokens)
13
13
  .map((values) => {
14
14
  // Convert calc() to length-percentage tokens, keep other length-percentage as is
15
- return values
16
- .map((value) => {
17
- if ((0, length_percentage_1.isCalcFunction)(value)) {
18
- // For calc() at parse time, we can't know the container size
19
- // So we evaluate with 0 context which will work for px-only calc()
20
- // Percentage-based calc() will need special handling
21
- return (0, length_percentage_1.evaluateCalcToLengthPercentage)(value, 0);
22
- }
23
- return (0, length_percentage_1.isLengthPercentage)(value) ? value : null;
24
- })
25
- .filter((v) => v !== null);
15
+ return values.map(length_percentage_1.parseOptionalCalcOrLength).filter((v) => v !== null);
26
16
  })
27
17
  .map(length_percentage_1.parseLengthPercentageTuple);
28
18
  }
@@ -15,7 +15,9 @@ exports.backgroundSize = {
15
15
  prefix: false,
16
16
  type: 1 /* PropertyDescriptorParsingType.LIST */,
17
17
  parse: (_context, tokens) => {
18
- return (0, parser_1.parseFunctionArgs)(tokens).map((values) => values.filter(isBackgroundSizeInfoToken));
18
+ return (0, parser_1.parseFunctionArgs)(tokens).map((values) => values
19
+ .map((value) => (isBackgroundSizeInfoToken(value) ? value : (0, length_percentage_1.parseOptionalCalcOrLength)(value)))
20
+ .filter((v) => v !== null));
19
21
  }
20
22
  };
21
23
  const isBackgroundSizeInfoToken = (value) => (0, parser_1.isIdentToken)(value) || (0, length_percentage_1.isLengthPercentage)(value);