html2canvas-pro 2.4.2 → 2.4.4

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.
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_SURFACE_SIDE = exports.MAX_SURFACE_PIXELS = void 0;
4
+ exports.reserveSurface = reserveSurface;
5
+ exports.surfaceBounds = surfaceBounds;
6
+ exports.cropSurface = cropSurface;
7
+ const bounds_1 = require("../../css/layout/bounds");
8
+ const length_percentage_1 = require("../../css/types/length-percentage");
9
+ const filter_surface_1 = require("./filter-surface");
10
+ // Budget backing stores across all active nested surfaces in one capture.
11
+ // Reserve source, filtered output and decoded SVG/PNG rasters conservatively.
12
+ // Browser encoder/decoder overhead and the caller's output canvas are separate.
13
+ exports.MAX_SURFACE_PIXELS = 16 * 1024 * 1024;
14
+ exports.MAX_SURFACE_SIDE = 8192;
15
+ function reserveSurface(budget, width, height) {
16
+ const pixels = width * height * 4;
17
+ if (!Number.isFinite(pixels) ||
18
+ width <= 0 ||
19
+ height <= 0 ||
20
+ width > exports.MAX_SURFACE_SIDE ||
21
+ height > exports.MAX_SURFACE_SIDE ||
22
+ budget.pixels + pixels > exports.MAX_SURFACE_PIXELS)
23
+ return 0;
24
+ budget.pixels += pixels;
25
+ return pixels;
26
+ }
27
+ const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
28
+ const union = (a, b) => new bounds_1.Bounds(Math.min(a.left, b.left), Math.min(a.top, b.top), Math.max(a.left + a.width, b.left + b.width) - Math.min(a.left, b.left), Math.max(a.top + a.height, b.top + b.height) - Math.min(a.top, b.top));
29
+ // Conservative paint bounds for the untransformed subtree supported by this path.
30
+ // Include overflow descendants, text ink padding, box/text shadows and nested filters.
31
+ function surfaceBounds(container) {
32
+ const styles = container.styles;
33
+ let bounds = container.bounds;
34
+ for (const shadow of styles.boxShadow) {
35
+ if (!shadow.inset) {
36
+ const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
37
+ bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
38
+ }
39
+ }
40
+ for (const text of container.textNodes) {
41
+ for (const part of text.textBounds) {
42
+ const ink = expand(part.bounds, 2 * (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
43
+ bounds = union(bounds, ink);
44
+ for (const shadow of styles.textShadow) {
45
+ bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
46
+ }
47
+ }
48
+ }
49
+ for (const child of container.elements) {
50
+ if (child.styles.isVisible())
51
+ bounds = union(bounds, surfaceBounds(child));
52
+ }
53
+ const filter = (0, filter_surface_1.parseSimpleFilter)(styles.filter);
54
+ return expand(bounds, filter ? (0, filter_surface_1.filterOutset)(filter) : 0);
55
+ }
56
+ function cropSurface(bounds, viewport, margin, scale) {
57
+ // Pixels just outside the destination still contribute through blur/shadow.
58
+ const area = expand(viewport, margin);
59
+ const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
60
+ const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
61
+ const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
62
+ const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
63
+ return new bounds_1.Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
64
+ }
@@ -27,8 +27,8 @@ const iOSBrokenFonts = ['-apple-system', 'system-ui'];
27
27
  const TEXT_OVERFLOW_EPSILON = 1;
28
28
  /**
29
29
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
30
- * CJK characters use the ideographic baseline in browsers, which differs
31
- * from the alphabetic baseline used for Latin script.
30
+ * Vertical writing modes use it to keep CJK glyphs upright and on the
31
+ * ideographic baseline while other scripts are rotated.
32
32
  *
33
33
  * Covers:
34
34
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -128,15 +128,17 @@ class TextRenderer {
128
128
  return w;
129
129
  }
130
130
  /**
131
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
132
- * per-script baseline for each character.
131
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
133
132
  *
134
133
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
135
- * character. This helper centralises two fixes applied during that iteration:
136
- * 1. Add `letterSpacing` to each character's advance width (was previously
137
- * omitted, causing characters to render without any spacing).
138
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
139
- * position matches how browsers lay them out in the DOM.
134
+ * character, adding `letterSpacing` to each character's advance width.
135
+ *
136
+ * In horizontal text every glyph keeps the alphabetic baseline, CJK included:
137
+ * inline layout aligns a whole line on its alphabetic baseline regardless of
138
+ * script, and `baseline` was measured for that. Drawing CJK graphemes at the
139
+ * same y with `textBaseline = 'ideographic'` lifts them above the Latin text
140
+ * next to them, and away from where the `letterSpacing === 0` whole-string
141
+ * path draws them.
140
142
  *
141
143
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
142
144
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -152,15 +154,7 @@ class TextRenderer {
152
154
  const fontString = this.ctx.font;
153
155
  let left = text.bounds.left;
154
156
  for (const letter of letters) {
155
- if ((0, exports.hasCJKCharacters)(letter)) {
156
- const savedBaseline = this.ctx.textBaseline;
157
- this.ctx.textBaseline = 'ideographic';
158
- renderFn(letter, left, y);
159
- this.ctx.textBaseline = savedBaseline;
160
- }
161
- else {
162
- renderFn(letter, left, y);
163
- }
157
+ renderFn(letter, left, y);
164
158
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
165
159
  }
166
160
  }
@@ -213,7 +207,7 @@ class TextRenderer {
213
207
  /**
214
208
  * Render text with letter-spacing applied (fill pass).
215
209
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
216
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
210
+ * grapheme is drawn individually so spacing is applied correctly.
217
211
  */
218
212
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0 /* WRITING_MODE.HORIZONTAL_TB */) {
219
213
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -83,12 +83,13 @@ class ElementPaint {
83
83
  this.effects.push(new effects_1.TransformEffect(offsetX, offsetY, zoomMatrix));
84
84
  }
85
85
  }
86
- getEffects(target) {
86
+ getEffects(target, surfaceRoot) {
87
87
  let inFlow = [2 /* POSITION.ABSOLUTE */, 3 /* POSITION.FIXED */].indexOf(this.container.styles.position) === -1;
88
- let parent = this.parent;
89
- const effects = this.effects.slice(0);
88
+ let parent = this === surfaceRoot ? null : this.parent;
89
+ const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || (!(0, effects_1.isFilterEffect)(effect) && !(0, effects_1.isOpacityEffect)(effect)));
90
+ const effects = sourceEffects(this);
90
91
  while (parent) {
91
- const croplessEffects = parent.effects.filter((effect) => !(0, effects_1.isClipEffect)(effect));
92
+ const croplessEffects = sourceEffects(parent).filter((effect) => !(0, effects_1.isClipEffect)(effect));
92
93
  if (inFlow || parent.container.styles.position !== 0 /* POSITION.STATIC */ || !parent.parent) {
93
94
  inFlow = [2 /* POSITION.ABSOLUTE */, 3 /* POSITION.FIXED */].indexOf(parent.container.styles.position) === -1;
94
95
  if ((0, exports.hasOverflowClip)(parent.container.styles)) {
@@ -103,7 +104,7 @@ class ElementPaint {
103
104
  else {
104
105
  effects.unshift(...croplessEffects);
105
106
  }
106
- parent = parent.parent;
107
+ parent = parent === surfaceRoot ? null : parent.parent;
107
108
  }
108
109
  return effects.filter((effect) => (0, bitwise_1.contains)(effect.target, target));
109
110
  }
@@ -4,6 +4,7 @@ import { ElementContainer } from '../../dom/element-container';
4
4
  import { Path } from '../path';
5
5
  import { BoundCurves } from '../bound-curves';
6
6
  import { ReplacedElementContainer } from '../../dom/replaced-elements';
7
+ import { SurfaceBudget } from './surface-bounds';
7
8
  import { Context } from '../../core/context';
8
9
  export type RenderConfigurations = RenderOptions & {
9
10
  backgroundColor: Color | null;
@@ -33,6 +34,8 @@ export interface RenderOptions {
33
34
  height: number;
34
35
  }
35
36
  export declare class CanvasRenderer {
37
+ private readonly surfaceRoot?;
38
+ private readonly surfaceBudget;
36
39
  canvas: HTMLCanvasElement;
37
40
  ctx: CanvasRenderingContext2D;
38
41
  private readonly context;
@@ -42,8 +45,11 @@ export declare class CanvasRenderer {
42
45
  private readonly borderImageRenderer;
43
46
  private readonly effectsRenderer;
44
47
  private readonly textRenderer;
45
- constructor(context: Context, options: RenderConfigurations);
48
+ private legacySubtree;
49
+ constructor(context: Context, options: RenderConfigurations, surfaceRoot?: ElementPaint | undefined, surfaceBudget?: SurfaceBudget);
46
50
  renderStack(stack: StackingContext): Promise<void>;
51
+ private canComposite;
52
+ private renderCompositedStack;
47
53
  renderNode(paint: ElementPaint): Promise<void>;
48
54
  /**
49
55
  * Helper method to render text with paint order support
@@ -55,6 +61,7 @@ export declare class CanvasRenderer {
55
61
  mask(paths: Path[]): void;
56
62
  path(paths: Path[]): void;
57
63
  formatPath(paths: Path[]): void;
64
+ private renderSurfaceBoxShadow;
58
65
  renderNodeBackgroundAndBorders(paint: ElementPaint): Promise<void>;
59
66
  render(element: ElementContainer): Promise<HTMLCanvasElement>;
60
67
  /**
@@ -0,0 +1,17 @@
1
+ export declare class FilterSurfaceError extends Error {
2
+ }
3
+ export declare const releaseSurface: (canvas: HTMLCanvasElement) => void;
4
+ export interface SimpleFilter {
5
+ blur: number;
6
+ shadow?: {
7
+ x: number;
8
+ y: number;
9
+ blur: number;
10
+ color: string;
11
+ };
12
+ }
13
+ export declare function parseSimpleFilter(value: string | null): SimpleFilter | null;
14
+ export declare function filterOutset(filter: SimpleFilter): number;
15
+ export declare function supportsNativeFilters(owner?: Document): boolean;
16
+ export declare function renderFilterSurface(source: HTMLCanvasElement, filter: SimpleFilter, opacity: number, scale: number, signal?: AbortSignal): Promise<HTMLCanvasElement>;
17
+ export declare function renderSvgFilterSurface(source: HTMLCanvasElement, { blur, shadow }: SimpleFilter, opacity: number, scale: number, signal?: AbortSignal): Promise<HTMLCanvasElement>;
@@ -0,0 +1,10 @@
1
+ import { Bounds } from '../../css/layout/bounds';
2
+ import { ElementContainer } from '../../dom/element-container';
3
+ export declare const MAX_SURFACE_PIXELS: number;
4
+ export declare const MAX_SURFACE_SIDE = 8192;
5
+ export interface SurfaceBudget {
6
+ pixels: number;
7
+ }
8
+ export declare function reserveSurface(budget: SurfaceBudget, width: number, height: number): number;
9
+ export declare function surfaceBounds(container: ElementContainer): Bounds;
10
+ export declare function cropSurface(bounds: Bounds, viewport: Bounds, margin: number, scale: number): Bounds;
@@ -49,15 +49,17 @@ export declare class TextRenderer {
49
49
  */
50
50
  private cachedMeasureText;
51
51
  /**
52
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
53
- * per-script baseline for each character.
52
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
54
53
  *
55
54
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
56
- * character. This helper centralises two fixes applied during that iteration:
57
- * 1. Add `letterSpacing` to each character's advance width (was previously
58
- * omitted, causing characters to render without any spacing).
59
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
60
- * position matches how browsers lay them out in the DOM.
55
+ * character, adding `letterSpacing` to each character's advance width.
56
+ *
57
+ * In horizontal text every glyph keeps the alphabetic baseline, CJK included:
58
+ * inline layout aligns a whole line on its alphabetic baseline regardless of
59
+ * script, and `baseline` was measured for that. Drawing CJK graphemes at the
60
+ * same y with `textBaseline = 'ideographic'` lifts them above the Latin text
61
+ * next to them, and away from where the `letterSpacing === 0` whole-string
62
+ * path draws them.
61
63
  *
62
64
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
63
65
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -68,7 +70,7 @@ export declare class TextRenderer {
68
70
  /**
69
71
  * Render text with letter-spacing applied (fill pass).
70
72
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
71
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
73
+ * grapheme is drawn individually so spacing is applied correctly.
72
74
  */
73
75
  renderTextWithLetterSpacing(text: TextBounds, letterSpacing: number, baseline: number, writingMode?: WRITING_MODE): void;
74
76
  private canRenderWholeText;
@@ -21,7 +21,7 @@ export declare class ElementPaint {
21
21
  readonly curves: BoundCurves;
22
22
  listValue?: string;
23
23
  constructor(container: ElementContainer, parent: ElementPaint | null);
24
- getEffects(target: EffectTarget): IElementEffect[];
24
+ getEffects(target: EffectTarget, surfaceRoot?: ElementPaint): IElementEffect[];
25
25
  }
26
26
  /** @internal – exported for testing only. */
27
27
  export declare const hasOverflowClip: (styles: ElementContainer["styles"]) => boolean;
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "default": "./dist/html2canvas-pro.esm.js"
20
20
  }
21
21
  },
22
- "version": "2.4.2",
22
+ "version": "2.4.4",
23
23
  "author": {
24
24
  "name": "yorickshan",
25
25
  "email": "yorickshan@gmail.com",
@@ -67,6 +67,7 @@
67
67
  "express": "^5.2.1",
68
68
  "filenamify-url": "4.0.0",
69
69
  "glob": "^13.0.0",
70
+ "html2canvas-pro-baseline": "npm:html2canvas-pro@2.4.3",
70
71
  "husky": "^9.1.7",
71
72
  "jsdom": "^29.1.1",
72
73
  "karma": "^6.3.2",
@@ -82,6 +83,7 @@
82
83
  "node-simctl": "^8.1.2",
83
84
  "pixelmatch": "^7.2.0",
84
85
  "platform": "^1.3.6",
86
+ "playwright": "1.60.0",
85
87
  "pngjs": "^7.0.0",
86
88
  "prettier": "^3.3.3",
87
89
  "puppeteer": "^24.34.0",
@@ -110,7 +112,7 @@
110
112
  },
111
113
  "scripts": {
112
114
  "prebuild": "rimraf dist/ && rimraf build/ && mkdir -p dist && mkdir -p build",
113
- "build": "tsc --module commonjs -p tsconfig.build.json && rolldown -c rolldown.config.ts && pnpm build:create-reftest-list && rolldown -c tests/rolldown.testrunner.config.ts",
115
+ "build": "tsc --module commonjs -p tsconfig.build.json && rolldown -c rolldown.config.ts && pnpm build:create-reftest-list && rolldown -c tests/rolldown.testrunner.config.ts && node scripts/copy-filter-baseline.mjs",
114
116
  "build:testrunner": "rolldown -c tests/rolldown.testrunner.config.ts",
115
117
  "build:reftest-result-list": "tsx scripts/create-reftest-result-list.ts",
116
118
  "build:create-reftest-list": "tsx scripts/create-reftest-list.ts tests/reftests/ignore.txt build/reftests.js",