html2canvas-pro 2.4.3 → 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.
- package/dist/html2canvas-pro.cjs +451 -70
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +451 -70
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +451 -70
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +5 -5
- package/dist/lib/core/render-element.js +14 -15
- package/dist/lib/css/property-descriptors/filter.js +14 -4
- package/dist/lib/dom/node-parser.js +1 -0
- package/dist/lib/render/canvas/canvas-renderer.js +190 -19
- package/dist/lib/render/canvas/filter-surface.js +270 -0
- package/dist/lib/render/canvas/surface-bounds.js +64 -0
- package/dist/lib/render/canvas/text-renderer.js +13 -19
- package/dist/lib/render/stacking-context.js +6 -5
- package/dist/types/render/canvas/canvas-renderer.d.ts +8 -1
- package/dist/types/render/canvas/filter-surface.d.ts +17 -0
- package/dist/types/render/canvas/surface-bounds.d.ts +10 -0
- package/dist/types/render/canvas/text-renderer.d.ts +10 -8
- package/dist/types/render/stacking-context.d.ts +1 -1
- package/package.json +4 -2
|
@@ -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
|
-
*
|
|
31
|
-
*
|
|
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
|
|
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
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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.
|
|
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",
|