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
@@ -3,18 +3,71 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getAbsoluteValue = exports.getAbsoluteValueForTuple = exports.HUNDRED_PERCENT = exports.FIFTY_PERCENT = exports.ZERO_LENGTH = exports.parseLengthPercentageTuple = exports.evaluateCalcToLengthPercentage = exports.isCalcFunction = exports.isLengthPercentage = void 0;
6
+ exports.getAbsoluteValue = exports.getAbsoluteValueForTuple = exports.HUNDRED_PERCENT = exports.FIFTY_PERCENT = exports.ZERO_LENGTH = exports.parseLengthPercentageTuple = exports.evaluateCalcToLengthPercentage = exports.parseOptionalCalcOrLength = exports.parseCalcForLengthPercentage = exports.isCalcFunction = exports.isCalcWithPercentage = exports.isLengthPercentage = void 0;
7
7
  const tokenizer_1 = require("../syntax/tokenizer");
8
8
  const parser_1 = require("../syntax/parser");
9
9
  const length_1 = require("./length");
10
10
  const safe_eval_1 = __importDefault(require("./safe-eval"));
11
- const isLengthPercentage = (token) => token.type === 16 /* TokenType.PERCENTAGE_TOKEN */ || (0, length_1.isLength)(token);
11
+ const isLengthPercentage = (token) => token.type === 16 /* TokenType.PERCENTAGE_TOKEN */ || (0, length_1.isLength)(token) || (0, exports.isCalcWithPercentage)(token);
12
12
  exports.isLengthPercentage = isLengthPercentage;
13
+ /**
14
+ * Check if a token is a CalcWithPercentage deferred token.
15
+ * Accepts CSSValue (structural duck-type check) for use in type guards.
16
+ */
17
+ const isCalcWithPercentage = (token) => token.type === 17 /* TokenType.NUMBER_TOKEN */ && token._calcPercentage !== undefined;
18
+ exports.isCalcWithPercentage = isCalcWithPercentage;
13
19
  /**
14
20
  * Check if a token is a calc() function
15
21
  */
16
22
  const isCalcFunction = (token) => token.type === 18 /* TokenType.FUNCTION */ && token.name === 'calc';
17
23
  exports.isCalcFunction = isCalcFunction;
24
+ /**
25
+ * Parse a calc() token and produce a LengthPercentage.
26
+ *
27
+ * Uses two-point evaluation to extract the percentage coefficient and pixel
28
+ * offset from a calc() expression so it can be resolved later at render time
29
+ * when the container size is known.
30
+ *
31
+ * evaluate(0) → pixelOffset
32
+ * evaluate(100) → percentage + pixelOffset → percentage = evaluate(100) - pixelOffset
33
+ *
34
+ * If the expression has no percentage component, returns a plain NUMBER_TOKEN
35
+ * for backwards compatibility.
36
+ */
37
+ const parseCalcForLengthPercentage = (calcToken) => {
38
+ const withZero = (0, exports.evaluateCalcToLengthPercentage)(calcToken, 0);
39
+ if (!withZero)
40
+ return null;
41
+ const withHundred = (0, exports.evaluateCalcToLengthPercentage)(calcToken, 100);
42
+ if (!withHundred)
43
+ return null;
44
+ const pixelOffset = withZero.number;
45
+ const percentage = withHundred.number - pixelOffset;
46
+ // If there's no percentage component, return a plain NUMBER_TOKEN
47
+ if (percentage === 0) {
48
+ return withZero;
49
+ }
50
+ return {
51
+ type: 17 /* TokenType.NUMBER_TOKEN */,
52
+ number: pixelOffset,
53
+ flags: tokenizer_1.FLAG_INTEGER,
54
+ _calcPercentage: percentage,
55
+ _calcPixelOffset: pixelOffset
56
+ };
57
+ };
58
+ exports.parseCalcForLengthPercentage = parseCalcForLengthPercentage;
59
+ /**
60
+ * Convenience helper for parsing an optional calc() or length-percentage value.
61
+ * Used by property descriptors that accept both calc() and regular length-percentage tokens.
62
+ * Returns null if the value is neither a calc() function nor a length-percentage token.
63
+ */
64
+ const parseOptionalCalcOrLength = (value) => {
65
+ if ((0, exports.isCalcFunction)(value)) {
66
+ return (0, exports.parseCalcForLengthPercentage)(value);
67
+ }
68
+ return (0, exports.isLengthPercentage)(value) ? value : null;
69
+ };
70
+ exports.parseOptionalCalcOrLength = parseOptionalCalcOrLength;
18
71
  /**
19
72
  * Evaluate a calc() expression and convert to LengthPercentage token
20
73
  * Supports basic arithmetic: +, -, *, /
@@ -119,6 +172,9 @@ const getAbsoluteValueForTuple = (tuple, width, height) => {
119
172
  };
120
173
  exports.getAbsoluteValueForTuple = getAbsoluteValueForTuple;
121
174
  const getAbsoluteValue = (token, parent) => {
175
+ if ((0, exports.isCalcWithPercentage)(token)) {
176
+ return (token._calcPercentage / 100) * parent + token._calcPixelOffset;
177
+ }
122
178
  if (token.type === 16 /* TokenType.PERCENTAGE_TOKEN */) {
123
179
  return (token.number / 100) * parent;
124
180
  }
@@ -126,7 +182,11 @@ const getAbsoluteValue = (token, parent) => {
126
182
  switch (token.unit) {
127
183
  case 'rem':
128
184
  case 'em':
129
- return 16 * token.number; // TODO use correct font-size
185
+ // NOTE: Assumes a 16px root font-size for rem/em units. A fully
186
+ // accurate implementation would require access to the element's
187
+ // computed font-size (em) or the root element's font-size (rem),
188
+ // which are not available in this pure-resolution function.
189
+ return 16 * token.number;
130
190
  case 'px':
131
191
  default:
132
192
  return token.number;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.copyCSSStyles = exports.DocumentCloner = void 0;
4
- const node_parser_1 = require("./node-parser");
3
+ exports.serializeDoctype = exports.copyCSSStyles = exports.DocumentCloner = void 0;
5
4
  const node_type_guards_1 = require("./node-type-guards");
6
5
  const parser_1 = require("../css/syntax/parser");
7
6
  const counter_1 = require("../css/types/functions/counter");
@@ -102,7 +101,7 @@ class DocumentCloner {
102
101
  // rawHTML is always a static, internally-generated string:
103
102
  // serializeDoctype(document.doctype) + '<html></html>'
104
103
  // No user-controlled input — safe for document.write in the sandbox iframe.
105
- const rawHTML = serializeDoctype(document.doctype) + '<html></html>';
104
+ const rawHTML = (0, exports.serializeDoctype)(document.doctype) + '<html></html>';
106
105
  try {
107
106
  const ownerWindow = this.referenceElement.ownerDocument?.defaultView;
108
107
  const trustedTypesFactory = ownerWindow && ownerWindow.trustedTypes;
@@ -149,18 +148,18 @@ class DocumentCloner {
149
148
  if ((0, debugger_1.isDebugging)(node, 2 /* DebuggerType.CLONE */)) {
150
149
  debugger;
151
150
  }
152
- if ((0, node_parser_1.isCanvasElement)(node)) {
151
+ if ((0, node_type_guards_1.isCanvasElement)(node)) {
153
152
  return this.createCanvasClone(node);
154
153
  }
155
- if ((0, node_parser_1.isVideoElement)(node)) {
154
+ if ((0, node_type_guards_1.isVideoElement)(node)) {
156
155
  return this.createVideoClone(node);
157
156
  }
158
- if ((0, node_parser_1.isStyleElement)(node)) {
157
+ if ((0, node_type_guards_1.isStyleElement)(node)) {
159
158
  return this.createStyleClone(node);
160
159
  }
161
160
  const clone = node.cloneNode(false);
162
- if ((0, node_parser_1.isImageElement)(clone)) {
163
- if ((0, node_parser_1.isImageElement)(node) && node.currentSrc && node.currentSrc !== node.src) {
161
+ if ((0, node_type_guards_1.isImageElement)(clone)) {
162
+ if ((0, node_type_guards_1.isImageElement)(node) && node.currentSrc && node.currentSrc !== node.src) {
164
163
  clone.src = node.currentSrc;
165
164
  clone.srcset = '';
166
165
  }
@@ -168,7 +167,7 @@ class DocumentCloner {
168
167
  clone.loading = 'eager';
169
168
  }
170
169
  }
171
- if ((0, node_parser_1.isCustomElement)(clone) && !(0, node_parser_1.isSVGElementNode)(clone)) {
170
+ if ((0, node_type_guards_1.isCustomElement)(clone) && !(0, node_type_guards_1.isSVGElementNode)(clone)) {
172
171
  return this.createCustomElementClone(clone);
173
172
  }
174
173
  return clone;
@@ -295,24 +294,24 @@ class DocumentCloner {
295
294
  this.slotCloner.cloneChildNodes(node, clone, copyStyles);
296
295
  }
297
296
  cloneNode(node, copyStyles) {
298
- if ((0, node_parser_1.isTextNode)(node)) {
297
+ if ((0, node_type_guards_1.isTextNode)(node)) {
299
298
  return document.createTextNode(node.data);
300
299
  }
301
300
  if (!node.ownerDocument) {
302
301
  return node.cloneNode(false);
303
302
  }
304
303
  const window = node.ownerDocument.defaultView;
305
- if (window && (0, node_parser_1.isElementNode)(node) && ((0, node_parser_1.isHTMLElementNode)(node) || (0, node_parser_1.isSVGElementNode)(node))) {
304
+ if (window && (0, node_type_guards_1.isElementNode)(node) && ((0, node_type_guards_1.isHTMLElementNode)(node) || (0, node_type_guards_1.isSVGElementNode)(node))) {
306
305
  const clone = this.createElementClone(node);
307
306
  clone.style.transitionProperty = 'none';
308
307
  const style = window.getComputedStyle(node);
309
308
  // Per CSS spec, replaced elements, void elements, and SVG elements
310
309
  // cannot have ::before / ::after pseudo-elements — skip these queries.
311
310
  const checkPseudoElements = (0, node_type_guards_1.canHavePseudoElements)(node);
312
- if (this.referenceElement === node && (0, node_parser_1.isHTMLElementNode)(clone)) {
311
+ if (this.referenceElement === node && (0, node_type_guards_1.isHTMLElementNode)(clone)) {
313
312
  this.clonedReferenceElement = clone;
314
313
  }
315
- if ((0, node_parser_1.isBodyElement)(clone)) {
314
+ if ((0, node_type_guards_1.isBodyElement)(clone)) {
316
315
  createPseudoHideStyles(clone, this.options.cspNonce);
317
316
  }
318
317
  const counters = this.counters.parse(new index_1.CSSParsedCounterDeclaration(this.context, style));
@@ -328,22 +327,22 @@ class DocumentCloner {
328
327
  clone.appendChild(after);
329
328
  }
330
329
  }
331
- if ((0, node_parser_1.isCustomElement)(node)) {
330
+ if ((0, node_type_guards_1.isCustomElement)(node)) {
332
331
  copyStyles = true;
333
332
  }
334
- if (!(0, node_parser_1.isVideoElement)(node)) {
333
+ if (!(0, node_type_guards_1.isVideoElement)(node)) {
335
334
  this.cloneChildNodes(node, clone, copyStyles);
336
335
  }
337
336
  this.counters.pop(counters);
338
- if ((style && (this.options.copyStyles || (0, node_parser_1.isSVGElementNode)(node)) && !(0, node_parser_1.isIFrameElement)(node)) ||
337
+ if ((style && (this.options.copyStyles || (0, node_type_guards_1.isSVGElementNode)(node)) && !(0, node_type_guards_1.isIFrameElement)(node)) ||
339
338
  copyStyles) {
340
339
  (0, exports.copyCSSStyles)(style, clone);
341
340
  }
342
341
  if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
343
342
  this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
344
343
  }
345
- if (((0, node_parser_1.isTextareaElement)(node) || (0, node_parser_1.isSelectElement)(node)) &&
346
- ((0, node_parser_1.isTextareaElement)(clone) || (0, node_parser_1.isSelectElement)(clone))) {
344
+ if (((0, node_type_guards_1.isTextareaElement)(node) || (0, node_type_guards_1.isSelectElement)(node)) &&
345
+ ((0, node_type_guards_1.isTextareaElement)(clone) || (0, node_type_guards_1.isSelectElement)(clone))) {
347
346
  clone.value = node.value;
348
347
  }
349
348
  return clone;
@@ -404,9 +403,6 @@ class DocumentCloner {
404
403
  anonymousReplacedElement.appendChild(document.createTextNode(text));
405
404
  }
406
405
  }
407
- else {
408
- // console.log('FUNCTION_TOKEN', token);
409
- }
410
406
  }
411
407
  else if (token.type === 20 /* TokenType.IDENT_TOKEN */) {
412
408
  switch (token.value) {
@@ -426,7 +422,7 @@ class DocumentCloner {
426
422
  const newClassName = pseudoElt === PseudoElementType.BEFORE
427
423
  ? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
428
424
  : ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
429
- if ((0, node_parser_1.isSVGElementNode)(clone)) {
425
+ if ((0, node_type_guards_1.isSVGElementNode)(clone)) {
430
426
  clone.className.baseValue += newClassName;
431
427
  }
432
428
  else {
@@ -531,6 +527,10 @@ const copyCSSStyles = (style, target) => {
531
527
  return target;
532
528
  };
533
529
  exports.copyCSSStyles = copyCSSStyles;
530
+ /**
531
+ * Serialise a document type declaration to an HTML string.
532
+ * @internal – exported for testing only, not part of the public API.
533
+ */
534
534
  const serializeDoctype = (doctype) => {
535
535
  let str = '';
536
536
  if (doctype) {
@@ -554,6 +554,7 @@ const serializeDoctype = (doctype) => {
554
554
  }
555
555
  return str;
556
556
  };
557
+ exports.serializeDoctype = serializeDoctype;
557
558
  const restoreOwnerScroll = (ownerDocument, x, y) => {
558
559
  if (ownerDocument &&
559
560
  ownerDocument.defaultView &&
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseTree = exports.isVideoElement = exports.isTextNode = exports.isTextareaElement = exports.isSVGElementNode = exports.isSVGElement = exports.isStyleElement = exports.isSlotElement = exports.isSelectElement = exports.isScriptElement = exports.isOLElement = exports.isLIElement = exports.isInputElement = exports.isImageElement = exports.isIFrameElement = exports.isHTMLElementNode = exports.isHTMLElement = exports.isElementNode = exports.isCustomElement = exports.isCanvasElement = exports.isBodyElement = void 0;
3
+ exports.parseTree = void 0;
4
4
  const element_container_1 = require("./element-container");
5
5
  const text_container_1 = require("./text-container");
6
6
  const image_element_container_1 = require("./replaced-elements/image-element-container");
@@ -13,26 +13,6 @@ const select_element_container_1 = require("./elements/select-element-container"
13
13
  const textarea_element_container_1 = require("./elements/textarea-element-container");
14
14
  const iframe_element_container_1 = require("./replaced-elements/iframe-element-container");
15
15
  const node_type_guards_1 = require("./node-type-guards");
16
- Object.defineProperty(exports, "isBodyElement", { enumerable: true, get: function () { return node_type_guards_1.isBodyElement; } });
17
- Object.defineProperty(exports, "isCanvasElement", { enumerable: true, get: function () { return node_type_guards_1.isCanvasElement; } });
18
- Object.defineProperty(exports, "isCustomElement", { enumerable: true, get: function () { return node_type_guards_1.isCustomElement; } });
19
- Object.defineProperty(exports, "isElementNode", { enumerable: true, get: function () { return node_type_guards_1.isElementNode; } });
20
- Object.defineProperty(exports, "isHTMLElement", { enumerable: true, get: function () { return node_type_guards_1.isHTMLElement; } });
21
- Object.defineProperty(exports, "isHTMLElementNode", { enumerable: true, get: function () { return node_type_guards_1.isHTMLElementNode; } });
22
- Object.defineProperty(exports, "isIFrameElement", { enumerable: true, get: function () { return node_type_guards_1.isIFrameElement; } });
23
- Object.defineProperty(exports, "isImageElement", { enumerable: true, get: function () { return node_type_guards_1.isImageElement; } });
24
- Object.defineProperty(exports, "isInputElement", { enumerable: true, get: function () { return node_type_guards_1.isInputElement; } });
25
- Object.defineProperty(exports, "isLIElement", { enumerable: true, get: function () { return node_type_guards_1.isLIElement; } });
26
- Object.defineProperty(exports, "isOLElement", { enumerable: true, get: function () { return node_type_guards_1.isOLElement; } });
27
- Object.defineProperty(exports, "isScriptElement", { enumerable: true, get: function () { return node_type_guards_1.isScriptElement; } });
28
- Object.defineProperty(exports, "isSelectElement", { enumerable: true, get: function () { return node_type_guards_1.isSelectElement; } });
29
- Object.defineProperty(exports, "isSlotElement", { enumerable: true, get: function () { return node_type_guards_1.isSlotElement; } });
30
- Object.defineProperty(exports, "isStyleElement", { enumerable: true, get: function () { return node_type_guards_1.isStyleElement; } });
31
- Object.defineProperty(exports, "isSVGElement", { enumerable: true, get: function () { return node_type_guards_1.isSVGElement; } });
32
- Object.defineProperty(exports, "isSVGElementNode", { enumerable: true, get: function () { return node_type_guards_1.isSVGElementNode; } });
33
- Object.defineProperty(exports, "isTextareaElement", { enumerable: true, get: function () { return node_type_guards_1.isTextareaElement; } });
34
- Object.defineProperty(exports, "isTextNode", { enumerable: true, get: function () { return node_type_guards_1.isTextNode; } });
35
- Object.defineProperty(exports, "isVideoElement", { enumerable: true, get: function () { return node_type_guards_1.isVideoElement; } });
36
16
  const bitwise_1 = require("../core/bitwise");
37
17
  const LIST_OWNERS = ['OL', 'UL', 'MENU'];
38
18
  const parseNodeTree = (context, node, parent, root) => {
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SlotCloner = exports.IGNORE_ATTRIBUTE = void 0;
4
- const node_parser_1 = require("./node-parser");
4
+ const node_type_guards_1 = require("./node-type-guards");
5
5
  /** Exported for reuse in document-cloner.ts */
6
6
  exports.IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
7
7
  /**
@@ -43,8 +43,8 @@ class SlotCloner {
43
43
  * Filters out: scripts, ignored elements, and optionally styles.
44
44
  */
45
45
  shouldCloneChild(child) {
46
- return (!(0, node_parser_1.isElementNode)(child) ||
47
- (!(0, node_parser_1.isScriptElement)(child) &&
46
+ return (!(0, node_type_guards_1.isElementNode)(child) ||
47
+ (!(0, node_type_guards_1.isScriptElement)(child) &&
48
48
  !child.hasAttribute(exports.IGNORE_ATTRIBUTE) &&
49
49
  (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child))));
50
50
  }
@@ -52,7 +52,7 @@ class SlotCloner {
52
52
  * Check if a style element should be cloned based on copyStyles option.
53
53
  */
54
54
  shouldCloneStyleElement(child) {
55
- return !this.options.copyStyles || !(0, node_parser_1.isElementNode)(child) || !(0, node_parser_1.isStyleElement)(child);
55
+ return !this.options.copyStyles || !(0, node_type_guards_1.isElementNode)(child) || !(0, node_type_guards_1.isStyleElement)(child);
56
56
  }
57
57
  /**
58
58
  * Safely append a cloned child to a target, applying all filtering rules.
@@ -82,7 +82,7 @@ class SlotCloner {
82
82
  * Handle cloning of a slot element, including assigned nodes or fallback content.
83
83
  */
84
84
  cloneSlotElement(slot, targetShadowRoot, copyStyles) {
85
- if (!(0, node_parser_1.isSlotElement)(slot)) {
85
+ if (!(0, node_type_guards_1.isSlotElement)(slot)) {
86
86
  return;
87
87
  }
88
88
  const slotElement = slot;
@@ -113,7 +113,7 @@ class SlotCloner {
113
113
  */
114
114
  cloneShadowDOMChildren(shadowRoot, targetShadowRoot, copyStyles) {
115
115
  for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
116
- if ((0, node_parser_1.isElementNode)(child) && (0, node_parser_1.isSlotElement)(child)) {
116
+ if ((0, node_type_guards_1.isElementNode)(child) && (0, node_type_guards_1.isSlotElement)(child)) {
117
117
  // Handle slot elements specially
118
118
  this.cloneSlotElement(child, targetShadowRoot, copyStyles);
119
119
  }
@@ -135,7 +135,7 @@ class SlotCloner {
135
135
  * Clone slot element as light DOM when shadow root creation failed.
136
136
  */
137
137
  cloneSlotElementAsLightDOM(slot, clone, copyStyles) {
138
- if (!(0, node_parser_1.isSlotElement)(slot)) {
138
+ if (!(0, node_type_guards_1.isSlotElement)(slot)) {
139
139
  return;
140
140
  }
141
141
  const slotElement = slot;
@@ -164,7 +164,7 @@ class SlotCloner {
164
164
  */
165
165
  cloneShadowDOMAsLightDOM(shadowRoot, clone, copyStyles) {
166
166
  for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
167
- if ((0, node_parser_1.isElementNode)(child) && (0, node_parser_1.isSlotElement)(child)) {
167
+ if ((0, node_type_guards_1.isElementNode)(child) && (0, node_type_guards_1.isSlotElement)(child)) {
168
168
  this.cloneSlotElementAsLightDOM(child, clone, copyStyles);
169
169
  }
170
170
  else {
@@ -75,7 +75,10 @@ const calculateBackgroundSize = (size, [intrinsicWidth, intrinsicHeight, intrins
75
75
  if (!hasIntrinsicProportion && !hasIntrinsicDimensions) {
76
76
  return [bounds.width, bounds.height];
77
77
  }
78
- // TODO If the image has no intrinsic dimensions but has intrinsic proportions, it's rendered as if contain had been specified instead.
78
+ // NOTE (known gap): Per the CSS spec, images with intrinsic proportions
79
+ // but no intrinsic dimensions should be rendered as if 'contain' had been
80
+ // specified. This branch is not yet implemented — the image will render
81
+ // at the size of the background positioning area instead.
79
82
  // If the image has only one intrinsic dimension and has intrinsic proportions, it's rendered at the size corresponding to that one dimension.
80
83
  // The other dimension is computed using the specified dimension and the intrinsic proportions.
81
84
  if (hasIntrinsicDimensions && hasIntrinsicProportion) {
@@ -18,6 +18,7 @@ const length_percentage_1 = require("../../css/types/length-percentage");
18
18
  const color_utilities_1 = require("../../css/types/color-utilities");
19
19
  const image_rendering_1 = require("../../css/property-descriptors/image-rendering");
20
20
  const canvas_path_1 = require("./canvas-path");
21
+ const lru_map_1 = require("../../core/lru-map");
21
22
  /**
22
23
  * Background Renderer
23
24
  *
@@ -31,11 +32,8 @@ class BackgroundRenderer {
31
32
  * CanvasPatterns are tied to the rendering context and must not be
32
33
  * shared across different render passes. This cache lives for the
33
34
  * duration of one html2canvas() call.
34
- *
35
- * Also reused for linear-gradient and repeating-linear-gradient
36
- * pattern canvases to avoid redundant offscreen canvas allocation.
37
35
  */
38
- this.patternCache = new Map();
36
+ this.patternCache = new lru_map_1.LRUMap(50);
39
37
  this.ctx = deps.ctx;
40
38
  this.context = deps.context;
41
39
  this.canvas = deps.canvas;
@@ -102,13 +100,22 @@ class BackgroundRenderer {
102
100
  imageHeight,
103
101
  imageWidth / imageHeight
104
102
  ]);
105
- // Cache key: URL + resized dimensions + imageRendering (pattern is dependent on all three)
106
- const cacheKey = `${url}|${Math.round(width)}x${Math.round(height)}|${container.styles.imageRendering}`;
107
- let pattern = this.lruGet(this.patternCache, cacheKey);
103
+ // Determine repetition mode based on CSS background-repeat
104
+ const repeat = (0, background_1.getBackgroundValueForIndex)(container.styles.backgroundRepeat, index);
105
+ const repeatMode = repeat === 1 /* BACKGROUND_REPEAT.NO_REPEAT */
106
+ ? 'no-repeat'
107
+ : repeat === 2 /* BACKGROUND_REPEAT.REPEAT_X */
108
+ ? 'repeat-x'
109
+ : repeat === 3 /* BACKGROUND_REPEAT.REPEAT_Y */
110
+ ? 'repeat-y'
111
+ : 'repeat';
112
+ // Cache key: URL + resized dimensions + imageRendering + repeat mode (pattern is dependent on all four)
113
+ const cacheKey = `${url}|${Math.round(width)}x${Math.round(height)}|${container.styles.imageRendering}|${repeatMode}`;
114
+ let pattern = this.patternCache.get(cacheKey);
108
115
  if (!pattern) {
109
116
  const resized = this.resizeImage(image, width, height, container.styles.imageRendering);
110
- pattern = this.ctx.createPattern(resized, 'repeat');
111
- this.lruSet(this.patternCache, cacheKey, pattern);
117
+ pattern = this.ctx.createPattern(resized, repeatMode);
118
+ this.patternCache.set(cacheKey, pattern);
112
119
  }
113
120
  this.renderRepeat(path, pattern, x, y);
114
121
  }
@@ -143,7 +150,7 @@ class BackgroundRenderer {
143
150
  const ownerDocument = this.canvas.ownerDocument ?? document;
144
151
  // Cache key for this repeating gradient pattern
145
152
  const cacheKey = `rlg|${backgroundImage.angle}|${Math.round(patternLength)}|${JSON.stringify(backgroundImage.stops)}`;
146
- let pattern = this.lruGet(this.patternCache, cacheKey);
153
+ let pattern = this.patternCache.get(cacheKey);
147
154
  if (!pattern) {
148
155
  const canvas = ownerDocument.createElement('canvas');
149
156
  // Create a canvas large enough to hold one full repeating unit
@@ -163,7 +170,7 @@ class BackgroundRenderer {
163
170
  ctx.fillRect(0, 0, canvasSize, canvasSize);
164
171
  if (canvasSize > 0) {
165
172
  pattern = this.ctx.createPattern(canvas, 'repeat');
166
- this.lruSet(this.patternCache, cacheKey, pattern);
173
+ this.patternCache.set(cacheKey, pattern);
167
174
  }
168
175
  }
169
176
  if (pattern) {
@@ -178,7 +185,7 @@ class BackgroundRenderer {
178
185
  const [lineLength, x0, x1, y0, y1] = (0, gradient_1.calculateGradientDirection)(backgroundImage.angle, width, height);
179
186
  // Cache key: angle + dimensions + serialised colour stops
180
187
  const cacheKey = `lg|${backgroundImage.angle}|${Math.round(width)}x${Math.round(height)}|${JSON.stringify(backgroundImage.stops)}`;
181
- let pattern = this.lruGet(this.patternCache, cacheKey);
188
+ let pattern = this.patternCache.get(cacheKey);
182
189
  if (!pattern) {
183
190
  const ownerDocument = this.canvas.ownerDocument ?? document;
184
191
  const canvas = ownerDocument.createElement('canvas');
@@ -194,7 +201,7 @@ class BackgroundRenderer {
194
201
  ctx.fillRect(0, 0, width, height);
195
202
  if (width > 0 && height > 0) {
196
203
  pattern = this.ctx.createPattern(canvas, 'repeat');
197
- this.lruSet(this.patternCache, cacheKey, pattern);
204
+ this.patternCache.set(cacheKey, pattern);
198
205
  }
199
206
  }
200
207
  if (pattern) {
@@ -219,7 +226,7 @@ class BackgroundRenderer {
219
226
  if (rx > 0 && ry > 0) {
220
227
  // Cache key for radial gradient: position + radii + colour stops
221
228
  const cacheKey = `rg|${Math.round(x)}x${Math.round(y)}|${Math.round(rx)}x${Math.round(ry)}|${JSON.stringify(backgroundImage.stops)}`;
222
- let pattern = this.lruGet(this.patternCache, cacheKey);
229
+ let pattern = this.patternCache.get(cacheKey);
223
230
  if (!pattern) {
224
231
  const ownerDocument = this.canvas.ownerDocument ?? document;
225
232
  const size = Math.ceil(Math.max(rx, ry) * 2);
@@ -236,7 +243,7 @@ class BackgroundRenderer {
236
243
  offCtx.scale(1, ry / rx);
237
244
  offCtx.fillRect(0, 0, offRadius * 2, offRadius * 2);
238
245
  pattern = this.ctx.createPattern(offscreen, 'no-repeat');
239
- this.lruSet(this.patternCache, cacheKey, pattern);
246
+ this.patternCache.set(cacheKey, pattern);
240
247
  }
241
248
  }
242
249
  if (pattern) {
@@ -275,10 +282,11 @@ class BackgroundRenderer {
275
282
  * @returns Resized canvas or original image
276
283
  */
277
284
  resizeImage(image, width, height, imageRendering) {
278
- // https://github.com/niklasvh/html2canvas/pull/2911
279
- // if (image.width === width && image.height === height) {
280
- // return image;
281
- // }
285
+ // NOTE: Early-return optimisation disabled per upstream investigation
286
+ // (https://github.com/niklasvh/html2canvas/pull/2911).
287
+ // Returning the original image when dimensions match caused subtle
288
+ // rendering issues — a resized copy through a fresh offscreen canvas
289
+ // is always safer, even when width/height are unchanged.
282
290
  const ownerDocument = this.canvas.ownerDocument ?? document;
283
291
  const canvas = ownerDocument.createElement('canvas');
284
292
  canvas.width = Math.max(1, width);
@@ -315,25 +323,5 @@ class BackgroundRenderer {
315
323
  path(paths) {
316
324
  (0, canvas_path_1.createCanvasPath)(this.ctx, paths);
317
325
  }
318
- /**
319
- * LRU-aware get: returns value and promotes the entry to end of Map.
320
- */
321
- lruGet(cache, key) {
322
- const value = cache.get(key);
323
- if (value !== undefined) {
324
- cache.delete(key);
325
- cache.set(key, value);
326
- }
327
- return value;
328
- }
329
- /** LRU-aware set for CanvasPattern caches. Evicts oldest entry on overflow. */
330
- lruSet(cache, key, value) {
331
- if (cache.size >= BackgroundRenderer.PATTERN_CACHE_MAX) {
332
- const oldestKey = cache.keys().next().value;
333
- cache.delete(oldestKey);
334
- }
335
- cache.set(key, value);
336
- }
337
326
  }
338
327
  exports.BackgroundRenderer = BackgroundRenderer;
339
- BackgroundRenderer.PATTERN_CACHE_MAX = 50;
@@ -16,7 +16,7 @@ const effects_renderer_1 = require("./effects-renderer");
16
16
  const canvas_path_1 = require("./canvas-path");
17
17
  const object_fit_1 = require("../object-fit");
18
18
  const content_renderer_1 = require("./content-renderer");
19
- const MASK_OFFSET = 10000;
19
+ const constants_1 = require("../../core/constants");
20
20
  class CanvasRenderer {
21
21
  constructor(context, options) {
22
22
  this.context = context;
@@ -231,7 +231,7 @@ class CanvasRenderer {
231
231
  .forEach((shadow) => {
232
232
  this.ctx.save();
233
233
  const borderBoxArea = (0, bound_curves_1.calculateBorderBoxPath)(paint.curves);
234
- const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
234
+ const maskOffset = shadow.inset ? 0 : constants_1.SHADOW_MASK_OFFSET;
235
235
  const shadowPaintingArea = (0, path_1.transformPath)(borderBoxArea, -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, (shadow.inset ? 1 : -1) * shadow.spread.number, shadow.spread.number * (shadow.inset ? -2 : 2), shadow.spread.number * (shadow.inset ? -2 : 2));
236
236
  if (shadow.inset) {
237
237
  this.path(borderBoxArea);