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
|
@@ -109,24 +109,23 @@ const renderElement = async (element, opts, config) => {
|
|
|
109
109
|
canvas = await renderer.render(root);
|
|
110
110
|
perfMonitor.end('render');
|
|
111
111
|
}
|
|
112
|
-
perfMonitor.start('cleanup');
|
|
113
|
-
if (opts.removeContainer ?? true) {
|
|
114
|
-
if (!document_cloner_1.DocumentCloner.destroy(container)) {
|
|
115
|
-
context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
perfMonitor.end('cleanup');
|
|
119
|
-
perfMonitor.end('total');
|
|
120
|
-
context.logger.debug(`Finished rendering`);
|
|
121
|
-
if (performanceMonitoring) {
|
|
122
|
-
perfMonitor.logSummary();
|
|
123
|
-
}
|
|
124
|
-
return canvas;
|
|
125
112
|
}
|
|
126
113
|
finally {
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
perfMonitor.start('cleanup');
|
|
115
|
+
try {
|
|
116
|
+
if (root)
|
|
117
|
+
root.restoreTree();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (opts.removeContainer ?? true)
|
|
121
|
+
document_cloner_1.DocumentCloner.destroy(container);
|
|
122
|
+
perfMonitor.end('cleanup');
|
|
129
123
|
}
|
|
130
124
|
}
|
|
125
|
+
perfMonitor.end('total');
|
|
126
|
+
context.logger.debug(`Finished rendering`);
|
|
127
|
+
if (performanceMonitoring)
|
|
128
|
+
perfMonitor.logSummary();
|
|
129
|
+
return canvas;
|
|
131
130
|
};
|
|
132
131
|
exports.renderElement = renderElement;
|
|
@@ -19,7 +19,7 @@ exports.filter = {
|
|
|
19
19
|
// Canvas API supports the same CSS filter string format
|
|
20
20
|
switch (fn.name) {
|
|
21
21
|
case 'blur':
|
|
22
|
-
parts.push(`blur(${renderedArgs}
|
|
22
|
+
parts.push(`blur(${renderedArgs})`);
|
|
23
23
|
break;
|
|
24
24
|
case 'brightness':
|
|
25
25
|
case 'contrast':
|
|
@@ -33,7 +33,7 @@ exports.filter = {
|
|
|
33
33
|
parts.push(`grayscale(${renderedArgs})`);
|
|
34
34
|
break;
|
|
35
35
|
case 'hue-rotate':
|
|
36
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
36
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
37
37
|
break;
|
|
38
38
|
case 'drop-shadow':
|
|
39
39
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -54,9 +54,19 @@ const renderFilterArgs = (values) => {
|
|
|
54
54
|
const parts = [];
|
|
55
55
|
for (const v of values) {
|
|
56
56
|
if (v.type === 31 /* TokenType.WHITESPACE_TOKEN */) {
|
|
57
|
+
parts.push(' ');
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
|
-
if (v.type ===
|
|
60
|
+
if (v.type === 18 /* TokenType.FUNCTION */) {
|
|
61
|
+
parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
|
|
62
|
+
}
|
|
63
|
+
else if (v.type === 4 /* TokenType.COMMA_TOKEN */) {
|
|
64
|
+
parts.push(',');
|
|
65
|
+
}
|
|
66
|
+
else if (v.type === 6 /* TokenType.DELIM_TOKEN */) {
|
|
67
|
+
parts.push(v.value);
|
|
68
|
+
}
|
|
69
|
+
else if (v.type === 15 /* TokenType.DIMENSION_TOKEN */) {
|
|
60
70
|
parts.push(`${v.number}${v.unit}`);
|
|
61
71
|
}
|
|
62
72
|
else if (v.type === 17 /* TokenType.NUMBER_TOKEN */) {
|
|
@@ -72,5 +82,5 @@ const renderFilterArgs = (values) => {
|
|
|
72
82
|
parts.push(`#${v.value}`);
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
return parts.join('
|
|
85
|
+
return parts.join('').trim();
|
|
76
86
|
};
|
|
@@ -106,6 +106,7 @@ exports.parseTree = parseTree;
|
|
|
106
106
|
const createsRealStackingContext = (node, container, root) => {
|
|
107
107
|
return (container.styles.isPositionedWithZIndex() ||
|
|
108
108
|
container.styles.opacity < 1 ||
|
|
109
|
+
Boolean(container.styles.filter) ||
|
|
109
110
|
container.styles.isTransformed() ||
|
|
110
111
|
((0, node_type_guards_1.isBodyElement)(node) && root.styles.isTransparent()));
|
|
111
112
|
};
|
|
@@ -7,6 +7,12 @@ const path_1 = require("../path");
|
|
|
7
7
|
const bound_curves_1 = require("../bound-curves");
|
|
8
8
|
const background_1 = require("../background");
|
|
9
9
|
const box_sizing_1 = require("../box-sizing");
|
|
10
|
+
const effects_1 = require("../effects");
|
|
11
|
+
const mix_blend_mode_1 = require("../../css/property-descriptors/mix-blend-mode");
|
|
12
|
+
const filter_surface_1 = require("./filter-surface");
|
|
13
|
+
const surface_bounds_1 = require("./surface-bounds");
|
|
14
|
+
const bounds_1 = require("../../css/layout/bounds");
|
|
15
|
+
const bitwise_1 = require("../../core/bitwise");
|
|
10
16
|
const text_renderer_1 = require("./text-renderer");
|
|
11
17
|
const background_renderer_1 = require("./background-renderer");
|
|
12
18
|
const border_renderer_1 = require("./border-renderer");
|
|
@@ -20,12 +26,19 @@ const writing_mode_1 = require("../../css/property-descriptors/writing-mode");
|
|
|
20
26
|
const font_utils_1 = require("./font-utils");
|
|
21
27
|
const text_1 = require("../../css/layout/text");
|
|
22
28
|
class CanvasRenderer {
|
|
23
|
-
constructor(context, options) {
|
|
29
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
30
|
+
this.surfaceRoot = surfaceRoot;
|
|
31
|
+
this.surfaceBudget = surfaceBudget;
|
|
32
|
+
this.legacySubtree = false;
|
|
24
33
|
this.context = context;
|
|
25
34
|
this.options = options;
|
|
26
35
|
this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
|
|
27
36
|
const ctx = this.canvas.getContext('2d');
|
|
28
37
|
if (!ctx) {
|
|
38
|
+
if (!options.canvas)
|
|
39
|
+
(0, filter_surface_1.releaseSurface)(this.canvas);
|
|
40
|
+
if (surfaceRoot)
|
|
41
|
+
throw new filter_surface_1.FilterSurfaceError('Filter surface canvas is unavailable');
|
|
29
42
|
throw new Error('Failed to get 2D rendering context from canvas');
|
|
30
43
|
}
|
|
31
44
|
this.ctx = ctx;
|
|
@@ -71,9 +84,101 @@ class CanvasRenderer {
|
|
|
71
84
|
async renderStack(stack) {
|
|
72
85
|
const styles = stack.element.container.styles;
|
|
73
86
|
if (styles.isVisible()) {
|
|
87
|
+
const filter = (0, filter_surface_1.parseSimpleFilter)(styles.filter);
|
|
88
|
+
if (stack.element !== this.surfaceRoot &&
|
|
89
|
+
!this.legacySubtree &&
|
|
90
|
+
filter &&
|
|
91
|
+
(styles.filter || styles.opacity < 1) &&
|
|
92
|
+
this.canComposite(stack.element)) {
|
|
93
|
+
if (!(await this.renderCompositedStack(stack, filter))) {
|
|
94
|
+
// A rejected optional surface must preserve the previous subtree path.
|
|
95
|
+
this.legacySubtree = true;
|
|
96
|
+
try {
|
|
97
|
+
await this.renderStackContent(stack);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
this.legacySubtree = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
74
105
|
await this.renderStackContent(stack);
|
|
75
106
|
}
|
|
76
107
|
}
|
|
108
|
+
// Draft scope: transformed, blended and clip-path subtrees keep the old path.
|
|
109
|
+
// The surface root owns its filter/opacity; ancestors are applied at composition.
|
|
110
|
+
canComposite(paint) {
|
|
111
|
+
const supported = (container) => {
|
|
112
|
+
const styles = container.styles;
|
|
113
|
+
return (!styles.isTransformed() &&
|
|
114
|
+
styles.zoom === 1 &&
|
|
115
|
+
styles.mixBlendMode === mix_blend_mode_1.MIX_BLEND_MODE.NORMAL &&
|
|
116
|
+
styles.clipPath.type === 0 /* CLIP_PATH_TYPE.NONE */ &&
|
|
117
|
+
!(0, bitwise_1.contains)(styles.display, 2048 /* DISPLAY.LIST_ITEM */) &&
|
|
118
|
+
(0, filter_surface_1.parseSimpleFilter)(styles.filter) !== null);
|
|
119
|
+
};
|
|
120
|
+
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
121
|
+
let ancestor = paint.parent;
|
|
122
|
+
while (ancestor && ancestor !== this.surfaceRoot) {
|
|
123
|
+
if (!supported(ancestor.container))
|
|
124
|
+
return false;
|
|
125
|
+
ancestor = ancestor.parent;
|
|
126
|
+
}
|
|
127
|
+
return subtree(paint.container);
|
|
128
|
+
}
|
|
129
|
+
async renderCompositedStack(stack, filter) {
|
|
130
|
+
const signal = this.options.signal;
|
|
131
|
+
if (signal?.aborted)
|
|
132
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
133
|
+
const bounds = (0, surface_bounds_1.cropSurface)((0, surface_bounds_1.surfaceBounds)(stack.element.container), new bounds_1.Bounds(this.options.x, this.options.y, this.options.width, this.options.height), (0, filter_surface_1.filterOutset)(filter), this.options.scale);
|
|
134
|
+
if (!bounds.width || !bounds.height)
|
|
135
|
+
return true;
|
|
136
|
+
const reserved = (0, surface_bounds_1.reserveSurface)(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
137
|
+
if (!reserved) {
|
|
138
|
+
this.context.logger.info('Filter surface budget exceeded; using the existing renderer');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const options = {
|
|
142
|
+
...this.options,
|
|
143
|
+
canvas: undefined,
|
|
144
|
+
backgroundColor: null,
|
|
145
|
+
x: bounds.left,
|
|
146
|
+
y: bounds.top,
|
|
147
|
+
width: bounds.width,
|
|
148
|
+
height: bounds.height
|
|
149
|
+
};
|
|
150
|
+
let source;
|
|
151
|
+
let filtered;
|
|
152
|
+
try {
|
|
153
|
+
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
154
|
+
await source.renderStackContent(stack);
|
|
155
|
+
source.effectsRenderer.applyEffects([]);
|
|
156
|
+
filtered = await (0, filter_surface_1.renderFilterSurface)(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
|
|
157
|
+
if (signal?.aborted)
|
|
158
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
159
|
+
const own = stack.element.effects;
|
|
160
|
+
const effects = stack.element
|
|
161
|
+
.getEffects(4 /* EffectTarget.CONTENT */, this.surfaceRoot)
|
|
162
|
+
.filter((effect) => !own.includes(effect) ||
|
|
163
|
+
(!(0, effects_1.isFilterEffect)(effect) && !(0, effects_1.isOpacityEffect)(effect) && !(0, effects_1.isClipEffect)(effect)));
|
|
164
|
+
this.effectsRenderer.applyEffects(effects);
|
|
165
|
+
this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (!(error instanceof filter_surface_1.FilterSurfaceError))
|
|
170
|
+
throw error;
|
|
171
|
+
this.context.logger.info(`${error.message}; using the existing renderer`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (source)
|
|
176
|
+
(0, filter_surface_1.releaseSurface)(source.canvas);
|
|
177
|
+
if (filtered)
|
|
178
|
+
(0, filter_surface_1.releaseSurface)(filtered);
|
|
179
|
+
this.surfaceBudget.pixels -= reserved;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
77
182
|
async renderNode(paint) {
|
|
78
183
|
if (paint.container.debugRender) {
|
|
79
184
|
debugger;
|
|
@@ -103,7 +208,7 @@ class CanvasRenderer {
|
|
|
103
208
|
}
|
|
104
209
|
}
|
|
105
210
|
async renderNodeContent(paint) {
|
|
106
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT
|
|
211
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */, this.surfaceRoot));
|
|
107
212
|
const container = paint.container;
|
|
108
213
|
const curves = paint.curves;
|
|
109
214
|
const styles = container.styles;
|
|
@@ -207,8 +312,61 @@ class CanvasRenderer {
|
|
|
207
312
|
formatPath(paths) {
|
|
208
313
|
(0, canvas_path_1.formatCanvasPath)(this.ctx, paths);
|
|
209
314
|
}
|
|
315
|
+
async renderSurfaceBoxShadow(paint, shadow) {
|
|
316
|
+
// Rasterize a visible silhouette, then blur it. Linux WebKit can drop the
|
|
317
|
+
// interior of Canvas shadows whose solid mask is outside a cropped canvas.
|
|
318
|
+
const spread = shadow.spread.number;
|
|
319
|
+
const blur = shadow.blur.number / 2;
|
|
320
|
+
const margin = Math.ceil(3 * blur) + 1;
|
|
321
|
+
const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
322
|
+
if (box.width <= 0 || box.height <= 0)
|
|
323
|
+
return;
|
|
324
|
+
const scale = this.options.scale;
|
|
325
|
+
const bounds = (0, surface_bounds_1.cropSurface)(box.add(-margin, -margin, 2 * margin, 2 * margin), new bounds_1.Bounds(this.options.x - shadow.offsetX.number, this.options.y - shadow.offsetY.number, this.options.width, this.options.height), margin, scale);
|
|
326
|
+
if (!bounds.width || !bounds.height)
|
|
327
|
+
return;
|
|
328
|
+
const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
|
|
329
|
+
const reserved = (0, surface_bounds_1.reserveSurface)(this.surfaceBudget, width, height);
|
|
330
|
+
if (!reserved)
|
|
331
|
+
throw new filter_surface_1.FilterSurfaceError('Box shadow surface budget exceeded');
|
|
332
|
+
let source;
|
|
333
|
+
let filtered;
|
|
334
|
+
try {
|
|
335
|
+
source = document.createElement('canvas');
|
|
336
|
+
source.width = width;
|
|
337
|
+
source.height = height;
|
|
338
|
+
const ctx = source.getContext('2d');
|
|
339
|
+
if (!ctx)
|
|
340
|
+
throw new filter_surface_1.FilterSurfaceError('Box shadow surface canvas is unavailable');
|
|
341
|
+
ctx.scale(scale, scale);
|
|
342
|
+
ctx.translate(-bounds.left, -bounds.top);
|
|
343
|
+
(0, canvas_path_1.createCanvasPath)(ctx, (0, path_1.transformPath)((0, bound_curves_1.calculateBorderBoxPath)(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
|
|
344
|
+
ctx.fillStyle = (0, color_utilities_1.asString)(shadow.color);
|
|
345
|
+
ctx.fill();
|
|
346
|
+
filtered = await (0, filter_surface_1.renderFilterSurface)(source, { blur }, 1, scale, this.options.signal);
|
|
347
|
+
this.ctx.save();
|
|
348
|
+
try {
|
|
349
|
+
this.ctx.beginPath();
|
|
350
|
+
this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
351
|
+
(0, canvas_path_1.formatCanvasPath)(this.ctx, (0, bound_curves_1.calculateBorderBoxPath)(paint.curves));
|
|
352
|
+
this.ctx.closePath();
|
|
353
|
+
this.ctx.clip('evenodd');
|
|
354
|
+
this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
this.ctx.restore();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
if (source)
|
|
362
|
+
(0, filter_surface_1.releaseSurface)(source);
|
|
363
|
+
if (filtered)
|
|
364
|
+
(0, filter_surface_1.releaseSurface)(filtered);
|
|
365
|
+
this.surfaceBudget.pixels -= reserved;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
210
368
|
async renderNodeBackgroundAndBorders(paint) {
|
|
211
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS
|
|
369
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */, this.surfaceRoot));
|
|
212
370
|
const styles = paint.container.styles;
|
|
213
371
|
const hasBackground = !(0, color_utilities_1.isTransparent)(styles.backgroundColor) || styles.backgroundImage.length;
|
|
214
372
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -236,10 +394,11 @@ class CanvasRenderer {
|
|
|
236
394
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
237
395
|
this.ctx.restore();
|
|
238
396
|
}
|
|
239
|
-
styles.boxShadow
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
397
|
+
for (const shadow of styles.boxShadow.slice(0).reverse()) {
|
|
398
|
+
if (this.surfaceRoot && !shadow.inset) {
|
|
399
|
+
await this.renderSurfaceBoxShadow(paint, shadow);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
243
402
|
this.ctx.save();
|
|
244
403
|
const borderBoxArea = (0, bound_curves_1.calculateBorderBoxPath)(paint.curves);
|
|
245
404
|
const maskOffset = shadow.inset ? 0 : constants_1.SHADOW_MASK_OFFSET;
|
|
@@ -254,14 +413,17 @@ class CanvasRenderer {
|
|
|
254
413
|
this.ctx.clip();
|
|
255
414
|
this.path(shadowPaintingArea);
|
|
256
415
|
}
|
|
257
|
-
|
|
258
|
-
|
|
416
|
+
// Canvas shadow metrics ignore the transform. Surface sources
|
|
417
|
+
// need capture-pixel offsets, including the displaced mask.
|
|
418
|
+
const shadowScale = this.surfaceRoot ? this.options.scale : 1;
|
|
419
|
+
this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
|
|
420
|
+
this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
|
|
259
421
|
this.ctx.shadowColor = (0, color_utilities_1.asString)(shadow.color);
|
|
260
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
422
|
+
this.ctx.shadowBlur = shadow.blur.number * shadowScale;
|
|
261
423
|
this.ctx.fillStyle = shadow.inset ? (0, color_utilities_1.asString)(shadow.color) : 'rgba(0,0,0,1)';
|
|
262
424
|
this.ctx.fill();
|
|
263
425
|
this.ctx.restore();
|
|
264
|
-
}
|
|
426
|
+
}
|
|
265
427
|
}
|
|
266
428
|
// Render border-image if present (replaces traditional borders per CSS spec)
|
|
267
429
|
if (styles.borderImageSource) {
|
|
@@ -335,14 +497,23 @@ class CanvasRenderer {
|
|
|
335
497
|
}
|
|
336
498
|
}
|
|
337
499
|
async render(element) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
500
|
+
try {
|
|
501
|
+
if (this.options.backgroundColor) {
|
|
502
|
+
this.ctx.fillStyle = (0, color_utilities_1.asString)(this.options.backgroundColor);
|
|
503
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
504
|
+
}
|
|
505
|
+
const stack = (0, stacking_context_1.parseStackingContexts)(element);
|
|
506
|
+
await this.renderStack(stack);
|
|
507
|
+
this.effectsRenderer.applyEffects([]);
|
|
508
|
+
return this.canvas;
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
// Failed captures have no returned canvas. Release only our backing
|
|
512
|
+
// store; a canvas supplied by the caller remains caller-owned.
|
|
513
|
+
if (!this.options.canvas)
|
|
514
|
+
(0, filter_surface_1.releaseSurface)(this.canvas);
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
346
517
|
}
|
|
347
518
|
/**
|
|
348
519
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.releaseSurface = exports.FilterSurfaceError = void 0;
|
|
4
|
+
exports.parseSimpleFilter = parseSimpleFilter;
|
|
5
|
+
exports.filterOutset = filterOutset;
|
|
6
|
+
exports.supportsNativeFilters = supportsNativeFilters;
|
|
7
|
+
exports.renderFilterSurface = renderFilterSurface;
|
|
8
|
+
exports.renderSvgFilterSurface = renderSvgFilterSurface;
|
|
9
|
+
const effects_1 = require("../effects");
|
|
10
|
+
const abort_helper_1 = require("../../core/abort-helper");
|
|
11
|
+
// Only errors in the optional surface path are eligible for legacy fallback.
|
|
12
|
+
class FilterSurfaceError extends Error {
|
|
13
|
+
}
|
|
14
|
+
exports.FilterSurfaceError = FilterSurfaceError;
|
|
15
|
+
const releaseSurface = (canvas) => {
|
|
16
|
+
canvas.width = canvas.height = 0;
|
|
17
|
+
};
|
|
18
|
+
exports.releaseSurface = releaseSurface;
|
|
19
|
+
// This first renderer integration handles computed blur followed by one shadow.
|
|
20
|
+
// Unsupported chains retain the existing renderer until general filter support lands.
|
|
21
|
+
function parseSimpleFilter(value) {
|
|
22
|
+
if (!value || value === 'none')
|
|
23
|
+
return { blur: 0 };
|
|
24
|
+
const effect = new effects_1.FilterEffect(value);
|
|
25
|
+
const shadows = value.match(/drop-shadow\(/g) || [];
|
|
26
|
+
if (shadows.length > 1 || (shadows.length === 1 && !effect.shadow))
|
|
27
|
+
return null;
|
|
28
|
+
const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
|
|
29
|
+
if (effect.safeFilterString && !blur)
|
|
30
|
+
return null;
|
|
31
|
+
if (blur && shadows.length && value.indexOf('blur(') > value.indexOf('drop-shadow('))
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
blur: blur ? Number(blur[1]) : 0,
|
|
35
|
+
shadow: effect.shadow
|
|
36
|
+
? {
|
|
37
|
+
x: effect.shadow.offsetX,
|
|
38
|
+
y: effect.shadow.offsetY,
|
|
39
|
+
blur: effect.shadow.blur,
|
|
40
|
+
color: effect.shadow.color
|
|
41
|
+
}
|
|
42
|
+
: undefined
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function filterOutset(filter) {
|
|
46
|
+
return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) +
|
|
47
|
+
Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
|
|
48
|
+
}
|
|
49
|
+
function canvasLike(source) {
|
|
50
|
+
const canvas = document.createElement('canvas');
|
|
51
|
+
canvas.width = source.width;
|
|
52
|
+
canvas.height = source.height;
|
|
53
|
+
return canvas;
|
|
54
|
+
}
|
|
55
|
+
const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
|
|
56
|
+
const finish = (error) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
signal?.removeEventListener('abort', abort);
|
|
59
|
+
if (error)
|
|
60
|
+
reject(error);
|
|
61
|
+
else
|
|
62
|
+
resolve();
|
|
63
|
+
};
|
|
64
|
+
const abort = () => finish(new DOMException('The operation was aborted.', 'AbortError'));
|
|
65
|
+
const timer = setTimeout(() => finish(new FilterSurfaceError('Filter surface image decode timed out')), 10000);
|
|
66
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
67
|
+
if (signal?.aborted)
|
|
68
|
+
abort();
|
|
69
|
+
else
|
|
70
|
+
image.decode().then(() => finish(), () => finish(new FilterSurfaceError('Filter surface image decode failed')));
|
|
71
|
+
});
|
|
72
|
+
// Cache only a capability result, never raster storage. Separate Documents may use
|
|
73
|
+
// different Canvas implementations (including embedded webviews and test realms).
|
|
74
|
+
const canvasFilterSupport = new WeakMap();
|
|
75
|
+
function supportsNativeFilters(owner = document) {
|
|
76
|
+
const cached = canvasFilterSupport.get(owner);
|
|
77
|
+
if (cached !== undefined)
|
|
78
|
+
return cached;
|
|
79
|
+
const source = owner.createElement('canvas');
|
|
80
|
+
const target = owner.createElement('canvas');
|
|
81
|
+
let supported = false;
|
|
82
|
+
try {
|
|
83
|
+
source.width = source.height = target.width = target.height = 32;
|
|
84
|
+
const input = source.getContext('2d');
|
|
85
|
+
const context = target.getContext('2d');
|
|
86
|
+
// Check before assignment: on older WebKit assigning filter creates an
|
|
87
|
+
// expando property, not a working implementation.
|
|
88
|
+
if (input && context && 'filter' in context) {
|
|
89
|
+
input.fillStyle = '#fff';
|
|
90
|
+
input.fillRect(8, 8, 8, 8);
|
|
91
|
+
context.filter = 'blur(2px)';
|
|
92
|
+
context.drawImage(source, 0, 0);
|
|
93
|
+
const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
|
|
94
|
+
context.clearRect(0, 0, 32, 32);
|
|
95
|
+
context.filter = 'drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))';
|
|
96
|
+
context.drawImage(source, 0, 0);
|
|
97
|
+
const shadow = context.getImageData(24, 12, 1, 1).data;
|
|
98
|
+
supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Readback can itself be unavailable. Be conservative and retain SVG.
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
(0, exports.releaseSurface)(source);
|
|
106
|
+
(0, exports.releaseSurface)(target);
|
|
107
|
+
}
|
|
108
|
+
canvasFilterSupport.set(owner, supported);
|
|
109
|
+
return supported;
|
|
110
|
+
}
|
|
111
|
+
function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
112
|
+
let filtered;
|
|
113
|
+
let output;
|
|
114
|
+
let complete = false;
|
|
115
|
+
try {
|
|
116
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
117
|
+
filtered = canvasLike(source);
|
|
118
|
+
const context = filtered.getContext('2d');
|
|
119
|
+
if (!context || !('filter' in context))
|
|
120
|
+
return null;
|
|
121
|
+
const functions = [];
|
|
122
|
+
if (blur)
|
|
123
|
+
functions.push(`blur(${blur * scale}px)`);
|
|
124
|
+
if (shadow)
|
|
125
|
+
functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
|
|
126
|
+
context.filter = functions.join(' ');
|
|
127
|
+
if (context.filter === 'none')
|
|
128
|
+
return null;
|
|
129
|
+
// Canvas applies globalAlpha before filtering. Filter the complete layer
|
|
130
|
+
// at alpha 1, then composite opacity separately, including its shadow.
|
|
131
|
+
context.drawImage(source, 0, 0);
|
|
132
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
133
|
+
output = canvasLike(source);
|
|
134
|
+
const composite = output.getContext('2d');
|
|
135
|
+
if (!composite)
|
|
136
|
+
return null;
|
|
137
|
+
composite.globalAlpha = opacity;
|
|
138
|
+
composite.drawImage(filtered, 0, 0);
|
|
139
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
140
|
+
complete = true;
|
|
141
|
+
return output;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// A native allocation/draw failure may still succeed through SVG, but
|
|
145
|
+
// cancellation must never silently degrade to a successful fallback.
|
|
146
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
if (filtered)
|
|
151
|
+
(0, exports.releaseSurface)(filtered);
|
|
152
|
+
if (output && !complete)
|
|
153
|
+
(0, exports.releaseSurface)(output);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Backend selection is internal; the public html2canvas options are unchanged.
|
|
157
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
158
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
159
|
+
if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
|
|
160
|
+
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
161
|
+
if (output)
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
165
|
+
}
|
|
166
|
+
// Explicit SVG entry is internal and allows independent fallback benchmarks/tests.
|
|
167
|
+
// No foreignObject or external image is used.
|
|
168
|
+
async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
169
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
170
|
+
if (!blur && !shadow) {
|
|
171
|
+
const output = canvasLike(source);
|
|
172
|
+
const context = output.getContext('2d');
|
|
173
|
+
if (!context) {
|
|
174
|
+
(0, exports.releaseSurface)(output);
|
|
175
|
+
throw new FilterSurfaceError('Filter surface canvas is unavailable');
|
|
176
|
+
}
|
|
177
|
+
context.globalAlpha = opacity;
|
|
178
|
+
try {
|
|
179
|
+
context.drawImage(source, 0, 0);
|
|
180
|
+
return output;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
(0, exports.releaseSurface)(output);
|
|
184
|
+
throw new FilterSurfaceError('Filter surface image cannot be drawn');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
188
|
+
const element = (tag, attributes = {}) => {
|
|
189
|
+
const node = document.createElementNS(namespace, tag);
|
|
190
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
191
|
+
return node;
|
|
192
|
+
};
|
|
193
|
+
const svg = element('svg', {
|
|
194
|
+
width: source.width,
|
|
195
|
+
height: source.height,
|
|
196
|
+
viewBox: `0 0 ${source.width} ${source.height}`
|
|
197
|
+
});
|
|
198
|
+
const defs = element('defs');
|
|
199
|
+
const filter = element('filter', {
|
|
200
|
+
id: 'composited-filter',
|
|
201
|
+
filterUnits: 'userSpaceOnUse',
|
|
202
|
+
x: 0,
|
|
203
|
+
y: 0,
|
|
204
|
+
width: source.width,
|
|
205
|
+
height: source.height,
|
|
206
|
+
'color-interpolation-filters': 'sRGB'
|
|
207
|
+
});
|
|
208
|
+
if (blur) {
|
|
209
|
+
filter.appendChild(element('feGaussianBlur', { stdDeviation: blur * scale, result: 'blurred-source' }));
|
|
210
|
+
}
|
|
211
|
+
if (shadow) {
|
|
212
|
+
// Explicit primitives avoid the narrower feDropShadow blur observed in WebKit.
|
|
213
|
+
filter.appendChild(element('feGaussianBlur', {
|
|
214
|
+
in: blur ? 'blurred-source' : 'SourceAlpha',
|
|
215
|
+
stdDeviation: shadow.blur * scale
|
|
216
|
+
}));
|
|
217
|
+
filter.appendChild(element('feOffset', { dx: shadow.x * scale, dy: shadow.y * scale, result: 'offset-shadow' }));
|
|
218
|
+
filter.appendChild(element('feFlood', { 'flood-color': shadow.color }));
|
|
219
|
+
filter.appendChild(element('feComposite', { in2: 'offset-shadow', operator: 'in' }));
|
|
220
|
+
const merge = element('feMerge');
|
|
221
|
+
merge.appendChild(element('feMergeNode'));
|
|
222
|
+
merge.appendChild(element('feMergeNode', { in: blur ? 'blurred-source' : 'SourceGraphic' }));
|
|
223
|
+
filter.appendChild(merge);
|
|
224
|
+
}
|
|
225
|
+
defs.appendChild(filter);
|
|
226
|
+
svg.appendChild(defs);
|
|
227
|
+
let sourceUrl;
|
|
228
|
+
try {
|
|
229
|
+
sourceUrl = source.toDataURL('image/png');
|
|
230
|
+
if (sourceUrl === 'data:,')
|
|
231
|
+
throw new Error('Canvas size is unsupported');
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// allowTaint may intentionally produce an unreadable canvas. Keep that
|
|
235
|
+
// API behavior by letting the caller render the subtree on the old path.
|
|
236
|
+
throw new FilterSurfaceError('Filter surface pixels cannot be serialized');
|
|
237
|
+
}
|
|
238
|
+
svg.appendChild(element('image', {
|
|
239
|
+
width: source.width,
|
|
240
|
+
height: source.height,
|
|
241
|
+
href: sourceUrl,
|
|
242
|
+
filter: 'url(#composited-filter)'
|
|
243
|
+
}));
|
|
244
|
+
const image = new Image();
|
|
245
|
+
let output;
|
|
246
|
+
try {
|
|
247
|
+
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
248
|
+
await decodeSurface(image, signal);
|
|
249
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
250
|
+
output = canvasLike(source);
|
|
251
|
+
const context = output.getContext('2d');
|
|
252
|
+
if (!context)
|
|
253
|
+
throw new FilterSurfaceError('Filter surface canvas is unavailable');
|
|
254
|
+
context.globalAlpha = opacity;
|
|
255
|
+
context.drawImage(image, 0, 0);
|
|
256
|
+
return output;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (output)
|
|
260
|
+
(0, exports.releaseSurface)(output);
|
|
261
|
+
if (signal?.aborted)
|
|
262
|
+
(0, abort_helper_1.throwIfAborted)(signal);
|
|
263
|
+
throw error instanceof FilterSurfaceError
|
|
264
|
+
? error
|
|
265
|
+
: new FilterSurfaceError('Filter surface image cannot be drawn');
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
image.removeAttribute('src');
|
|
269
|
+
}
|
|
270
|
+
}
|