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.
- package/dist/html2canvas-pro.cjs +463 -72
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +463 -72
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +463 -72
- 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/css/property-descriptors/font-family.js +16 -1
- 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
package/dist/html2canvas-pro.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.4.
|
|
2
|
+
* html2canvas-pro 2.4.4 <https://yorickshan.github.io/html2canvas-pro/>
|
|
3
3
|
* Copyright (c) 2024-present yorickshan and html2canvas-pro contributors
|
|
4
4
|
* Released under MIT License
|
|
5
5
|
*/
|
|
@@ -5331,10 +5331,15 @@
|
|
|
5331
5331
|
parse: (_context, tokens) => {
|
|
5332
5332
|
const accumulator = [];
|
|
5333
5333
|
const results = [];
|
|
5334
|
+
const quoted = [];
|
|
5335
|
+
let isQuoted = false;
|
|
5334
5336
|
tokens.forEach((token) => {
|
|
5335
5337
|
switch (token.type) {
|
|
5336
5338
|
case 20:
|
|
5339
|
+
accumulator.push(token.value);
|
|
5340
|
+
break;
|
|
5337
5341
|
case 0:
|
|
5342
|
+
isQuoted = true;
|
|
5338
5343
|
accumulator.push(token.value);
|
|
5339
5344
|
break;
|
|
5340
5345
|
case 17:
|
|
@@ -5342,12 +5347,17 @@
|
|
|
5342
5347
|
break;
|
|
5343
5348
|
case 4:
|
|
5344
5349
|
results.push(accumulator.join(" "));
|
|
5350
|
+
quoted.push(isQuoted);
|
|
5351
|
+
isQuoted = false;
|
|
5345
5352
|
accumulator.length = 0;
|
|
5346
5353
|
break;
|
|
5347
5354
|
}
|
|
5348
5355
|
});
|
|
5349
|
-
if (accumulator.length)
|
|
5350
|
-
|
|
5356
|
+
if (accumulator.length) {
|
|
5357
|
+
results.push(accumulator.join(" "));
|
|
5358
|
+
quoted.push(isQuoted);
|
|
5359
|
+
}
|
|
5360
|
+
return results.map((result, index) => quoted[index] || result.indexOf(" ") !== -1 ? `'${result.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ").replace(/\f/g, "\\c ")}'` : result);
|
|
5351
5361
|
}
|
|
5352
5362
|
};
|
|
5353
5363
|
//#endregion
|
|
@@ -5710,7 +5720,7 @@
|
|
|
5710
5720
|
const renderedArgs = renderFilterArgs(fn.values);
|
|
5711
5721
|
switch (fn.name) {
|
|
5712
5722
|
case "blur":
|
|
5713
|
-
parts.push(`blur(${renderedArgs}
|
|
5723
|
+
parts.push(`blur(${renderedArgs})`);
|
|
5714
5724
|
break;
|
|
5715
5725
|
case "brightness":
|
|
5716
5726
|
case "contrast":
|
|
@@ -5724,7 +5734,7 @@
|
|
|
5724
5734
|
parts.push(`grayscale(${renderedArgs})`);
|
|
5725
5735
|
break;
|
|
5726
5736
|
case "hue-rotate":
|
|
5727
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
5737
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
5728
5738
|
break;
|
|
5729
5739
|
case "drop-shadow":
|
|
5730
5740
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -5742,14 +5752,20 @@
|
|
|
5742
5752
|
const renderFilterArgs = (values) => {
|
|
5743
5753
|
const parts = [];
|
|
5744
5754
|
for (const v of values) {
|
|
5745
|
-
if (v.type === 31)
|
|
5746
|
-
|
|
5755
|
+
if (v.type === 31) {
|
|
5756
|
+
parts.push(" ");
|
|
5757
|
+
continue;
|
|
5758
|
+
}
|
|
5759
|
+
if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
|
|
5760
|
+
else if (v.type === 4) parts.push(",");
|
|
5761
|
+
else if (v.type === 6) parts.push(v.value);
|
|
5762
|
+
else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
|
|
5747
5763
|
else if (v.type === 17) parts.push(`${v.number}`);
|
|
5748
5764
|
else if (v.type === 16) parts.push(`${v.number}%`);
|
|
5749
5765
|
else if (v.type === 20) parts.push(v.value);
|
|
5750
5766
|
else if (v.type === 5) parts.push(`#${v.value}`);
|
|
5751
5767
|
}
|
|
5752
|
-
return parts.join("
|
|
5768
|
+
return parts.join("").trim();
|
|
5753
5769
|
};
|
|
5754
5770
|
//#endregion
|
|
5755
5771
|
//#region src/css/property-descriptors/font-variant-ligatures.ts
|
|
@@ -8299,7 +8315,7 @@
|
|
|
8299
8315
|
return container;
|
|
8300
8316
|
};
|
|
8301
8317
|
const createsRealStackingContext = (node, container, root) => {
|
|
8302
|
-
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8318
|
+
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8303
8319
|
};
|
|
8304
8320
|
const createsStackingContext = (styles) => {
|
|
8305
8321
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
@@ -8736,12 +8752,13 @@
|
|
|
8736
8752
|
this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
|
|
8737
8753
|
}
|
|
8738
8754
|
}
|
|
8739
|
-
getEffects(target) {
|
|
8755
|
+
getEffects(target, surfaceRoot) {
|
|
8740
8756
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
8741
|
-
let parent = this.parent;
|
|
8742
|
-
const
|
|
8757
|
+
let parent = this === surfaceRoot ? null : this.parent;
|
|
8758
|
+
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
|
|
8759
|
+
const effects = sourceEffects(this);
|
|
8743
8760
|
while (parent) {
|
|
8744
|
-
const croplessEffects = parent.
|
|
8761
|
+
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
8745
8762
|
if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
|
|
8746
8763
|
inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
|
|
8747
8764
|
if (hasOverflowClip(parent.container.styles)) {
|
|
@@ -8751,7 +8768,7 @@
|
|
|
8751
8768
|
}
|
|
8752
8769
|
effects.unshift(...croplessEffects);
|
|
8753
8770
|
} else effects.unshift(...croplessEffects);
|
|
8754
|
-
parent = parent.parent;
|
|
8771
|
+
parent = parent === surfaceRoot ? null : parent.parent;
|
|
8755
8772
|
}
|
|
8756
8773
|
return effects.filter((effect) => contains(effect.target, target));
|
|
8757
8774
|
}
|
|
@@ -9032,6 +9049,268 @@
|
|
|
9032
9049
|
}
|
|
9033
9050
|
};
|
|
9034
9051
|
//#endregion
|
|
9052
|
+
//#region src/core/abort-helper.ts
|
|
9053
|
+
/**
|
|
9054
|
+
* AbortSignal helper utilities.
|
|
9055
|
+
*/
|
|
9056
|
+
/**
|
|
9057
|
+
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
9058
|
+
* Used at checkpoints throughout the rendering pipeline to support
|
|
9059
|
+
* cancellable renders.
|
|
9060
|
+
*
|
|
9061
|
+
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
9062
|
+
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
9063
|
+
*/
|
|
9064
|
+
const throwIfAborted = (signal) => {
|
|
9065
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
9066
|
+
};
|
|
9067
|
+
//#endregion
|
|
9068
|
+
//#region src/render/canvas/filter-surface.ts
|
|
9069
|
+
var FilterSurfaceError = class extends Error {};
|
|
9070
|
+
const releaseSurface = (canvas) => {
|
|
9071
|
+
canvas.width = canvas.height = 0;
|
|
9072
|
+
};
|
|
9073
|
+
function parseSimpleFilter(value) {
|
|
9074
|
+
if (!value || value === "none") return { blur: 0 };
|
|
9075
|
+
const effect = new FilterEffect(value);
|
|
9076
|
+
const shadows = value.match(/drop-shadow\(/g) || [];
|
|
9077
|
+
if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
|
|
9078
|
+
const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
|
|
9079
|
+
if (effect.safeFilterString && !blur) return null;
|
|
9080
|
+
if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
|
|
9081
|
+
return {
|
|
9082
|
+
blur: blur ? Number(blur[1]) : 0,
|
|
9083
|
+
shadow: effect.shadow ? {
|
|
9084
|
+
x: effect.shadow.offsetX,
|
|
9085
|
+
y: effect.shadow.offsetY,
|
|
9086
|
+
blur: effect.shadow.blur,
|
|
9087
|
+
color: effect.shadow.color
|
|
9088
|
+
} : void 0
|
|
9089
|
+
};
|
|
9090
|
+
}
|
|
9091
|
+
function filterOutset(filter) {
|
|
9092
|
+
return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
|
|
9093
|
+
}
|
|
9094
|
+
function canvasLike(source) {
|
|
9095
|
+
const canvas = document.createElement("canvas");
|
|
9096
|
+
canvas.width = source.width;
|
|
9097
|
+
canvas.height = source.height;
|
|
9098
|
+
return canvas;
|
|
9099
|
+
}
|
|
9100
|
+
const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
|
|
9101
|
+
const finish = (error) => {
|
|
9102
|
+
clearTimeout(timer);
|
|
9103
|
+
signal?.removeEventListener("abort", abort);
|
|
9104
|
+
if (error) reject(error);
|
|
9105
|
+
else resolve();
|
|
9106
|
+
};
|
|
9107
|
+
const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
|
|
9108
|
+
const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
|
|
9109
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9110
|
+
if (signal?.aborted) abort();
|
|
9111
|
+
else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
|
|
9112
|
+
});
|
|
9113
|
+
const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
|
|
9114
|
+
function supportsNativeFilters(owner = document) {
|
|
9115
|
+
const cached = canvasFilterSupport.get(owner);
|
|
9116
|
+
if (cached !== void 0) return cached;
|
|
9117
|
+
const source = owner.createElement("canvas");
|
|
9118
|
+
const target = owner.createElement("canvas");
|
|
9119
|
+
let supported = false;
|
|
9120
|
+
try {
|
|
9121
|
+
source.width = source.height = target.width = target.height = 32;
|
|
9122
|
+
const input = source.getContext("2d");
|
|
9123
|
+
const context = target.getContext("2d");
|
|
9124
|
+
if (input && context && "filter" in context) {
|
|
9125
|
+
input.fillStyle = "#fff";
|
|
9126
|
+
input.fillRect(8, 8, 8, 8);
|
|
9127
|
+
context.filter = "blur(2px)";
|
|
9128
|
+
context.drawImage(source, 0, 0);
|
|
9129
|
+
const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
|
|
9130
|
+
context.clearRect(0, 0, 32, 32);
|
|
9131
|
+
context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
|
|
9132
|
+
context.drawImage(source, 0, 0);
|
|
9133
|
+
const shadow = context.getImageData(24, 12, 1, 1).data;
|
|
9134
|
+
supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
|
|
9135
|
+
}
|
|
9136
|
+
} catch {} finally {
|
|
9137
|
+
releaseSurface(source);
|
|
9138
|
+
releaseSurface(target);
|
|
9139
|
+
}
|
|
9140
|
+
canvasFilterSupport.set(owner, supported);
|
|
9141
|
+
return supported;
|
|
9142
|
+
}
|
|
9143
|
+
function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9144
|
+
let filtered;
|
|
9145
|
+
let output;
|
|
9146
|
+
let complete = false;
|
|
9147
|
+
try {
|
|
9148
|
+
throwIfAborted(signal);
|
|
9149
|
+
filtered = canvasLike(source);
|
|
9150
|
+
const context = filtered.getContext("2d");
|
|
9151
|
+
if (!context || !("filter" in context)) return null;
|
|
9152
|
+
const functions = [];
|
|
9153
|
+
if (blur) functions.push(`blur(${blur * scale}px)`);
|
|
9154
|
+
if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
|
|
9155
|
+
context.filter = functions.join(" ");
|
|
9156
|
+
if (context.filter === "none") return null;
|
|
9157
|
+
context.drawImage(source, 0, 0);
|
|
9158
|
+
throwIfAborted(signal);
|
|
9159
|
+
output = canvasLike(source);
|
|
9160
|
+
const composite = output.getContext("2d");
|
|
9161
|
+
if (!composite) return null;
|
|
9162
|
+
composite.globalAlpha = opacity;
|
|
9163
|
+
composite.drawImage(filtered, 0, 0);
|
|
9164
|
+
throwIfAborted(signal);
|
|
9165
|
+
complete = true;
|
|
9166
|
+
return output;
|
|
9167
|
+
} catch {
|
|
9168
|
+
throwIfAborted(signal);
|
|
9169
|
+
return null;
|
|
9170
|
+
} finally {
|
|
9171
|
+
if (filtered) releaseSurface(filtered);
|
|
9172
|
+
if (output && !complete) releaseSurface(output);
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
9175
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
9176
|
+
throwIfAborted(signal);
|
|
9177
|
+
if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
|
|
9178
|
+
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
9179
|
+
if (output) return output;
|
|
9180
|
+
}
|
|
9181
|
+
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
9182
|
+
}
|
|
9183
|
+
async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9184
|
+
throwIfAborted(signal);
|
|
9185
|
+
if (!blur && !shadow) {
|
|
9186
|
+
const output = canvasLike(source);
|
|
9187
|
+
const context = output.getContext("2d");
|
|
9188
|
+
if (!context) {
|
|
9189
|
+
releaseSurface(output);
|
|
9190
|
+
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9191
|
+
}
|
|
9192
|
+
context.globalAlpha = opacity;
|
|
9193
|
+
try {
|
|
9194
|
+
context.drawImage(source, 0, 0);
|
|
9195
|
+
return output;
|
|
9196
|
+
} catch {
|
|
9197
|
+
releaseSurface(output);
|
|
9198
|
+
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9199
|
+
}
|
|
9200
|
+
}
|
|
9201
|
+
const namespace = "http://www.w3.org/2000/svg";
|
|
9202
|
+
const element = (tag, attributes = {}) => {
|
|
9203
|
+
const node = document.createElementNS(namespace, tag);
|
|
9204
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
9205
|
+
return node;
|
|
9206
|
+
};
|
|
9207
|
+
const svg = element("svg", {
|
|
9208
|
+
width: source.width,
|
|
9209
|
+
height: source.height,
|
|
9210
|
+
viewBox: `0 0 ${source.width} ${source.height}`
|
|
9211
|
+
});
|
|
9212
|
+
const defs = element("defs");
|
|
9213
|
+
const filter = element("filter", {
|
|
9214
|
+
id: "composited-filter",
|
|
9215
|
+
filterUnits: "userSpaceOnUse",
|
|
9216
|
+
x: 0,
|
|
9217
|
+
y: 0,
|
|
9218
|
+
width: source.width,
|
|
9219
|
+
height: source.height,
|
|
9220
|
+
"color-interpolation-filters": "sRGB"
|
|
9221
|
+
});
|
|
9222
|
+
if (blur) filter.appendChild(element("feGaussianBlur", {
|
|
9223
|
+
stdDeviation: blur * scale,
|
|
9224
|
+
result: "blurred-source"
|
|
9225
|
+
}));
|
|
9226
|
+
if (shadow) {
|
|
9227
|
+
filter.appendChild(element("feGaussianBlur", {
|
|
9228
|
+
in: blur ? "blurred-source" : "SourceAlpha",
|
|
9229
|
+
stdDeviation: shadow.blur * scale
|
|
9230
|
+
}));
|
|
9231
|
+
filter.appendChild(element("feOffset", {
|
|
9232
|
+
dx: shadow.x * scale,
|
|
9233
|
+
dy: shadow.y * scale,
|
|
9234
|
+
result: "offset-shadow"
|
|
9235
|
+
}));
|
|
9236
|
+
filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
|
|
9237
|
+
filter.appendChild(element("feComposite", {
|
|
9238
|
+
in2: "offset-shadow",
|
|
9239
|
+
operator: "in"
|
|
9240
|
+
}));
|
|
9241
|
+
const merge = element("feMerge");
|
|
9242
|
+
merge.appendChild(element("feMergeNode"));
|
|
9243
|
+
merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
|
|
9244
|
+
filter.appendChild(merge);
|
|
9245
|
+
}
|
|
9246
|
+
defs.appendChild(filter);
|
|
9247
|
+
svg.appendChild(defs);
|
|
9248
|
+
let sourceUrl;
|
|
9249
|
+
try {
|
|
9250
|
+
sourceUrl = source.toDataURL("image/png");
|
|
9251
|
+
if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
|
|
9252
|
+
} catch {
|
|
9253
|
+
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
9254
|
+
}
|
|
9255
|
+
svg.appendChild(element("image", {
|
|
9256
|
+
width: source.width,
|
|
9257
|
+
height: source.height,
|
|
9258
|
+
href: sourceUrl,
|
|
9259
|
+
filter: "url(#composited-filter)"
|
|
9260
|
+
}));
|
|
9261
|
+
const image = new Image();
|
|
9262
|
+
let output;
|
|
9263
|
+
try {
|
|
9264
|
+
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
9265
|
+
await decodeSurface(image, signal);
|
|
9266
|
+
throwIfAborted(signal);
|
|
9267
|
+
output = canvasLike(source);
|
|
9268
|
+
const context = output.getContext("2d");
|
|
9269
|
+
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9270
|
+
context.globalAlpha = opacity;
|
|
9271
|
+
context.drawImage(image, 0, 0);
|
|
9272
|
+
return output;
|
|
9273
|
+
} catch (error) {
|
|
9274
|
+
if (output) releaseSurface(output);
|
|
9275
|
+
if (signal?.aborted) throwIfAborted(signal);
|
|
9276
|
+
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9277
|
+
} finally {
|
|
9278
|
+
image.removeAttribute("src");
|
|
9279
|
+
}
|
|
9280
|
+
}
|
|
9281
|
+
function reserveSurface(budget, width, height) {
|
|
9282
|
+
const pixels = width * height * 4;
|
|
9283
|
+
if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
|
|
9284
|
+
budget.pixels += pixels;
|
|
9285
|
+
return pixels;
|
|
9286
|
+
}
|
|
9287
|
+
const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
|
|
9288
|
+
const union = (a, b) => new 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));
|
|
9289
|
+
function surfaceBounds(container) {
|
|
9290
|
+
const styles = container.styles;
|
|
9291
|
+
let bounds = container.bounds;
|
|
9292
|
+
for (const shadow of styles.boxShadow) if (!shadow.inset) {
|
|
9293
|
+
const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
|
|
9294
|
+
bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
|
|
9295
|
+
}
|
|
9296
|
+
for (const text of container.textNodes) for (const part of text.textBounds) {
|
|
9297
|
+
const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
|
|
9298
|
+
bounds = union(bounds, ink);
|
|
9299
|
+
for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
|
|
9300
|
+
}
|
|
9301
|
+
for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
|
|
9302
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
9303
|
+
return expand(bounds, filter ? filterOutset(filter) : 0);
|
|
9304
|
+
}
|
|
9305
|
+
function cropSurface(bounds, viewport, margin, scale) {
|
|
9306
|
+
const area = expand(viewport, margin);
|
|
9307
|
+
const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
|
|
9308
|
+
const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
|
|
9309
|
+
const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
|
|
9310
|
+
const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
|
|
9311
|
+
return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
|
|
9312
|
+
}
|
|
9313
|
+
//#endregion
|
|
9035
9314
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9036
9315
|
var TextDecorationRenderer = class {
|
|
9037
9316
|
constructor(ctx) {
|
|
@@ -9161,8 +9440,8 @@
|
|
|
9161
9440
|
const TEXT_OVERFLOW_EPSILON = 1;
|
|
9162
9441
|
/**
|
|
9163
9442
|
* Detect CJK (Chinese, Japanese, Korean) characters in a string.
|
|
9164
|
-
*
|
|
9165
|
-
*
|
|
9443
|
+
* Vertical writing modes use it to keep CJK glyphs upright and on the
|
|
9444
|
+
* ideographic baseline while other scripts are rotated.
|
|
9166
9445
|
*
|
|
9167
9446
|
* Covers:
|
|
9168
9447
|
* U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
|
|
@@ -9232,15 +9511,17 @@
|
|
|
9232
9511
|
return w;
|
|
9233
9512
|
}
|
|
9234
9513
|
/**
|
|
9235
|
-
* Iterate grapheme clusters one-by-one, applying correct letter-spacing
|
|
9236
|
-
* per-script baseline for each character.
|
|
9514
|
+
* Iterate grapheme clusters one-by-one, applying correct letter-spacing.
|
|
9237
9515
|
*
|
|
9238
9516
|
* Issue #73: When letter-spacing is non-zero, text must be rendered character by
|
|
9239
|
-
* character
|
|
9240
|
-
*
|
|
9241
|
-
*
|
|
9242
|
-
*
|
|
9243
|
-
*
|
|
9517
|
+
* character, adding `letterSpacing` to each character's advance width.
|
|
9518
|
+
*
|
|
9519
|
+
* In horizontal text every glyph keeps the alphabetic baseline, CJK included:
|
|
9520
|
+
* inline layout aligns a whole line on its alphabetic baseline regardless of
|
|
9521
|
+
* script, and `baseline` was measured for that. Drawing CJK graphemes at the
|
|
9522
|
+
* same y with `textBaseline = 'ideographic'` lifts them above the Latin text
|
|
9523
|
+
* next to them, and away from where the `letterSpacing === 0` whole-string
|
|
9524
|
+
* path draws them.
|
|
9244
9525
|
*
|
|
9245
9526
|
* The `renderFn` callback receives (letter, x, y) and performs the actual draw
|
|
9246
9527
|
* call (fillText or strokeText), allowing fill and stroke paths to share one
|
|
@@ -9256,12 +9537,7 @@
|
|
|
9256
9537
|
const fontString = this.ctx.font;
|
|
9257
9538
|
let left = text.bounds.left;
|
|
9258
9539
|
for (const letter of letters) {
|
|
9259
|
-
|
|
9260
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
9261
|
-
this.ctx.textBaseline = "ideographic";
|
|
9262
|
-
renderFn(letter, left, y);
|
|
9263
|
-
this.ctx.textBaseline = savedBaseline;
|
|
9264
|
-
} else renderFn(letter, left, y);
|
|
9540
|
+
renderFn(letter, left, y);
|
|
9265
9541
|
left += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
9266
9542
|
}
|
|
9267
9543
|
}
|
|
@@ -9303,7 +9579,7 @@
|
|
|
9303
9579
|
/**
|
|
9304
9580
|
* Render text with letter-spacing applied (fill pass).
|
|
9305
9581
|
* When letterSpacing is 0 the whole string is drawn in one call; otherwise each
|
|
9306
|
-
* grapheme is drawn individually so spacing
|
|
9582
|
+
* grapheme is drawn individually so spacing is applied correctly.
|
|
9307
9583
|
*/
|
|
9308
9584
|
renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
|
|
9309
9585
|
this.renderFillText(text, letterSpacing, baseline, writingMode);
|
|
@@ -10538,12 +10814,19 @@
|
|
|
10538
10814
|
//#endregion
|
|
10539
10815
|
//#region src/render/canvas/canvas-renderer.ts
|
|
10540
10816
|
var CanvasRenderer = class CanvasRenderer {
|
|
10541
|
-
constructor(context, options) {
|
|
10817
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
10818
|
+
this.surfaceRoot = surfaceRoot;
|
|
10819
|
+
this.surfaceBudget = surfaceBudget;
|
|
10820
|
+
this.legacySubtree = false;
|
|
10542
10821
|
this.context = context;
|
|
10543
10822
|
this.options = options;
|
|
10544
10823
|
this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
|
|
10545
10824
|
const ctx = this.canvas.getContext("2d");
|
|
10546
|
-
if (!ctx)
|
|
10825
|
+
if (!ctx) {
|
|
10826
|
+
if (!options.canvas) releaseSurface(this.canvas);
|
|
10827
|
+
if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10828
|
+
throw new Error("Failed to get 2D rendering context from canvas");
|
|
10829
|
+
}
|
|
10547
10830
|
this.ctx = ctx;
|
|
10548
10831
|
if (!options.canvas) {
|
|
10549
10832
|
this.canvas.width = Math.floor(options.width * options.scale);
|
|
@@ -10579,7 +10862,77 @@
|
|
|
10579
10862
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
10580
10863
|
}
|
|
10581
10864
|
async renderStack(stack) {
|
|
10582
|
-
|
|
10865
|
+
const styles = stack.element.container.styles;
|
|
10866
|
+
if (styles.isVisible()) {
|
|
10867
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
10868
|
+
if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
|
|
10869
|
+
if (!await this.renderCompositedStack(stack, filter)) {
|
|
10870
|
+
this.legacySubtree = true;
|
|
10871
|
+
try {
|
|
10872
|
+
await this.renderStackContent(stack);
|
|
10873
|
+
} finally {
|
|
10874
|
+
this.legacySubtree = false;
|
|
10875
|
+
}
|
|
10876
|
+
}
|
|
10877
|
+
return;
|
|
10878
|
+
}
|
|
10879
|
+
await this.renderStackContent(stack);
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
canComposite(paint) {
|
|
10883
|
+
const supported = (container) => {
|
|
10884
|
+
const styles = container.styles;
|
|
10885
|
+
return !styles.isTransformed() && styles.zoom === 1 && styles.mixBlendMode === MIX_BLEND_MODE$1.NORMAL && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
|
|
10886
|
+
};
|
|
10887
|
+
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
10888
|
+
let ancestor = paint.parent;
|
|
10889
|
+
while (ancestor && ancestor !== this.surfaceRoot) {
|
|
10890
|
+
if (!supported(ancestor.container)) return false;
|
|
10891
|
+
ancestor = ancestor.parent;
|
|
10892
|
+
}
|
|
10893
|
+
return subtree(paint.container);
|
|
10894
|
+
}
|
|
10895
|
+
async renderCompositedStack(stack, filter) {
|
|
10896
|
+
const signal = this.options.signal;
|
|
10897
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10898
|
+
const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filterOutset(filter), this.options.scale);
|
|
10899
|
+
if (!bounds.width || !bounds.height) return true;
|
|
10900
|
+
const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
10901
|
+
if (!reserved) {
|
|
10902
|
+
this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
|
|
10903
|
+
return false;
|
|
10904
|
+
}
|
|
10905
|
+
const options = {
|
|
10906
|
+
...this.options,
|
|
10907
|
+
canvas: void 0,
|
|
10908
|
+
backgroundColor: null,
|
|
10909
|
+
x: bounds.left,
|
|
10910
|
+
y: bounds.top,
|
|
10911
|
+
width: bounds.width,
|
|
10912
|
+
height: bounds.height
|
|
10913
|
+
};
|
|
10914
|
+
let source;
|
|
10915
|
+
let filtered;
|
|
10916
|
+
try {
|
|
10917
|
+
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
10918
|
+
await source.renderStackContent(stack);
|
|
10919
|
+
source.effectsRenderer.applyEffects([]);
|
|
10920
|
+
filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
|
|
10921
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10922
|
+
const own = stack.element.effects;
|
|
10923
|
+
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
|
|
10924
|
+
this.effectsRenderer.applyEffects(effects);
|
|
10925
|
+
this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
|
|
10926
|
+
return true;
|
|
10927
|
+
} catch (error) {
|
|
10928
|
+
if (!(error instanceof FilterSurfaceError)) throw error;
|
|
10929
|
+
this.context.logger.info(`${error.message}; using the existing renderer`);
|
|
10930
|
+
return false;
|
|
10931
|
+
} finally {
|
|
10932
|
+
if (source) releaseSurface(source.canvas);
|
|
10933
|
+
if (filtered) releaseSurface(filtered);
|
|
10934
|
+
this.surfaceBudget.pixels -= reserved;
|
|
10935
|
+
}
|
|
10583
10936
|
}
|
|
10584
10937
|
async renderNode(paint) {
|
|
10585
10938
|
if (paint.container.debugRender) debugger;
|
|
@@ -10607,7 +10960,7 @@
|
|
|
10607
10960
|
}
|
|
10608
10961
|
}
|
|
10609
10962
|
async renderNodeContent(paint) {
|
|
10610
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4));
|
|
10963
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
|
|
10611
10964
|
const container = paint.container;
|
|
10612
10965
|
const curves = paint.curves;
|
|
10613
10966
|
const styles = container.styles;
|
|
@@ -10671,8 +11024,51 @@
|
|
|
10671
11024
|
formatPath(paths) {
|
|
10672
11025
|
formatCanvasPath(this.ctx, paths);
|
|
10673
11026
|
}
|
|
11027
|
+
async renderSurfaceBoxShadow(paint, shadow) {
|
|
11028
|
+
const spread = shadow.spread.number;
|
|
11029
|
+
const blur = shadow.blur.number / 2;
|
|
11030
|
+
const margin = Math.ceil(3 * blur) + 1;
|
|
11031
|
+
const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
11032
|
+
if (box.width <= 0 || box.height <= 0) return;
|
|
11033
|
+
const scale = this.options.scale;
|
|
11034
|
+
const bounds = cropSurface(box.add(-margin, -margin, 2 * margin, 2 * margin), new Bounds(this.options.x - shadow.offsetX.number, this.options.y - shadow.offsetY.number, this.options.width, this.options.height), margin, scale);
|
|
11035
|
+
if (!bounds.width || !bounds.height) return;
|
|
11036
|
+
const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
|
|
11037
|
+
const reserved = reserveSurface(this.surfaceBudget, width, height);
|
|
11038
|
+
if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
|
|
11039
|
+
let source;
|
|
11040
|
+
let filtered;
|
|
11041
|
+
try {
|
|
11042
|
+
source = document.createElement("canvas");
|
|
11043
|
+
source.width = width;
|
|
11044
|
+
source.height = height;
|
|
11045
|
+
const ctx = source.getContext("2d");
|
|
11046
|
+
if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
|
|
11047
|
+
ctx.scale(scale, scale);
|
|
11048
|
+
ctx.translate(-bounds.left, -bounds.top);
|
|
11049
|
+
createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
|
|
11050
|
+
ctx.fillStyle = asString(shadow.color);
|
|
11051
|
+
ctx.fill();
|
|
11052
|
+
filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
|
|
11053
|
+
this.ctx.save();
|
|
11054
|
+
try {
|
|
11055
|
+
this.ctx.beginPath();
|
|
11056
|
+
this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11057
|
+
formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
|
|
11058
|
+
this.ctx.closePath();
|
|
11059
|
+
this.ctx.clip("evenodd");
|
|
11060
|
+
this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
|
|
11061
|
+
} finally {
|
|
11062
|
+
this.ctx.restore();
|
|
11063
|
+
}
|
|
11064
|
+
} finally {
|
|
11065
|
+
if (source) releaseSurface(source);
|
|
11066
|
+
if (filtered) releaseSurface(filtered);
|
|
11067
|
+
this.surfaceBudget.pixels -= reserved;
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
10674
11070
|
async renderNodeBackgroundAndBorders(paint) {
|
|
10675
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
11071
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
|
|
10676
11072
|
const styles = paint.container.styles;
|
|
10677
11073
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10678
11074
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -10713,7 +11109,11 @@
|
|
|
10713
11109
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10714
11110
|
this.ctx.restore();
|
|
10715
11111
|
}
|
|
10716
|
-
styles.boxShadow.slice(0).reverse()
|
|
11112
|
+
for (const shadow of styles.boxShadow.slice(0).reverse()) {
|
|
11113
|
+
if (this.surfaceRoot && !shadow.inset) {
|
|
11114
|
+
await this.renderSurfaceBoxShadow(paint, shadow);
|
|
11115
|
+
continue;
|
|
11116
|
+
}
|
|
10717
11117
|
this.ctx.save();
|
|
10718
11118
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
10719
11119
|
const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
|
|
@@ -10727,14 +11127,15 @@
|
|
|
10727
11127
|
this.ctx.clip();
|
|
10728
11128
|
this.path(shadowPaintingArea);
|
|
10729
11129
|
}
|
|
10730
|
-
this.
|
|
10731
|
-
this.ctx.
|
|
11130
|
+
const shadowScale = this.surfaceRoot ? this.options.scale : 1;
|
|
11131
|
+
this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
|
|
11132
|
+
this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
|
|
10732
11133
|
this.ctx.shadowColor = asString(shadow.color);
|
|
10733
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
11134
|
+
this.ctx.shadowBlur = shadow.blur.number * shadowScale;
|
|
10734
11135
|
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
|
|
10735
11136
|
this.ctx.fill();
|
|
10736
11137
|
this.ctx.restore();
|
|
10737
|
-
}
|
|
11138
|
+
}
|
|
10738
11139
|
}
|
|
10739
11140
|
if (styles.borderImageSource) {
|
|
10740
11141
|
const source = styles.borderImageSource;
|
|
@@ -10790,14 +11191,19 @@
|
|
|
10790
11191
|
}
|
|
10791
11192
|
}
|
|
10792
11193
|
async render(element) {
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
11194
|
+
try {
|
|
11195
|
+
if (this.options.backgroundColor) {
|
|
11196
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
11197
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11198
|
+
}
|
|
11199
|
+
const stack = parseStackingContexts(element);
|
|
11200
|
+
await this.renderStack(stack);
|
|
11201
|
+
this.effectsRenderer.applyEffects([]);
|
|
11202
|
+
return this.canvas;
|
|
11203
|
+
} catch (error) {
|
|
11204
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
11205
|
+
throw error;
|
|
10796
11206
|
}
|
|
10797
|
-
const stack = parseStackingContexts(element);
|
|
10798
|
-
await this.renderStack(stack);
|
|
10799
|
-
this.effectsRenderer.applyEffects([]);
|
|
10800
|
-
return this.canvas;
|
|
10801
11207
|
}
|
|
10802
11208
|
/**
|
|
10803
11209
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
@@ -11213,22 +11619,6 @@
|
|
|
11213
11619
|
}
|
|
11214
11620
|
};
|
|
11215
11621
|
//#endregion
|
|
11216
|
-
//#region src/core/abort-helper.ts
|
|
11217
|
-
/**
|
|
11218
|
-
* AbortSignal helper utilities.
|
|
11219
|
-
*/
|
|
11220
|
-
/**
|
|
11221
|
-
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
11222
|
-
* Used at checkpoints throughout the rendering pipeline to support
|
|
11223
|
-
* cancellable renders.
|
|
11224
|
-
*
|
|
11225
|
-
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
11226
|
-
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
11227
|
-
*/
|
|
11228
|
-
const throwIfAborted = (signal) => {
|
|
11229
|
-
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11230
|
-
};
|
|
11231
|
-
//#endregion
|
|
11232
11622
|
//#region src/core/config-assembler.ts
|
|
11233
11623
|
/**
|
|
11234
11624
|
* Coerce known numeric options from string (or other) values to actual numbers.
|
|
@@ -11397,18 +11787,19 @@
|
|
|
11397
11787
|
canvas = await new CanvasRenderer(context, renderOptions).render(root);
|
|
11398
11788
|
perfMonitor.end("render");
|
|
11399
11789
|
}
|
|
11790
|
+
} finally {
|
|
11400
11791
|
perfMonitor.start("cleanup");
|
|
11401
|
-
|
|
11402
|
-
if (
|
|
11792
|
+
try {
|
|
11793
|
+
if (root) root.restoreTree();
|
|
11794
|
+
} finally {
|
|
11795
|
+
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
11796
|
+
perfMonitor.end("cleanup");
|
|
11403
11797
|
}
|
|
11404
|
-
perfMonitor.end("cleanup");
|
|
11405
|
-
perfMonitor.end("total");
|
|
11406
|
-
context.logger.debug(`Finished rendering`);
|
|
11407
|
-
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11408
|
-
return canvas;
|
|
11409
|
-
} finally {
|
|
11410
|
-
if (root) root.restoreTree();
|
|
11411
11798
|
}
|
|
11799
|
+
perfMonitor.end("total");
|
|
11800
|
+
context.logger.debug(`Finished rendering`);
|
|
11801
|
+
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11802
|
+
return canvas;
|
|
11412
11803
|
};
|
|
11413
11804
|
//#endregion
|
|
11414
11805
|
//#region src/index.ts
|