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
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
|
*/
|
|
@@ -5720,7 +5720,7 @@
|
|
|
5720
5720
|
const renderedArgs = renderFilterArgs(fn.values);
|
|
5721
5721
|
switch (fn.name) {
|
|
5722
5722
|
case "blur":
|
|
5723
|
-
parts.push(`blur(${renderedArgs}
|
|
5723
|
+
parts.push(`blur(${renderedArgs})`);
|
|
5724
5724
|
break;
|
|
5725
5725
|
case "brightness":
|
|
5726
5726
|
case "contrast":
|
|
@@ -5734,7 +5734,7 @@
|
|
|
5734
5734
|
parts.push(`grayscale(${renderedArgs})`);
|
|
5735
5735
|
break;
|
|
5736
5736
|
case "hue-rotate":
|
|
5737
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
5737
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
5738
5738
|
break;
|
|
5739
5739
|
case "drop-shadow":
|
|
5740
5740
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -5752,14 +5752,20 @@
|
|
|
5752
5752
|
const renderFilterArgs = (values) => {
|
|
5753
5753
|
const parts = [];
|
|
5754
5754
|
for (const v of values) {
|
|
5755
|
-
if (v.type === 31)
|
|
5756
|
-
|
|
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}`);
|
|
5757
5763
|
else if (v.type === 17) parts.push(`${v.number}`);
|
|
5758
5764
|
else if (v.type === 16) parts.push(`${v.number}%`);
|
|
5759
5765
|
else if (v.type === 20) parts.push(v.value);
|
|
5760
5766
|
else if (v.type === 5) parts.push(`#${v.value}`);
|
|
5761
5767
|
}
|
|
5762
|
-
return parts.join("
|
|
5768
|
+
return parts.join("").trim();
|
|
5763
5769
|
};
|
|
5764
5770
|
//#endregion
|
|
5765
5771
|
//#region src/css/property-descriptors/font-variant-ligatures.ts
|
|
@@ -8309,7 +8315,7 @@
|
|
|
8309
8315
|
return container;
|
|
8310
8316
|
};
|
|
8311
8317
|
const createsRealStackingContext = (node, container, root) => {
|
|
8312
|
-
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();
|
|
8313
8319
|
};
|
|
8314
8320
|
const createsStackingContext = (styles) => {
|
|
8315
8321
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
@@ -8746,12 +8752,13 @@
|
|
|
8746
8752
|
this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
|
|
8747
8753
|
}
|
|
8748
8754
|
}
|
|
8749
|
-
getEffects(target) {
|
|
8755
|
+
getEffects(target, surfaceRoot) {
|
|
8750
8756
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
8751
|
-
let parent = this.parent;
|
|
8752
|
-
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);
|
|
8753
8760
|
while (parent) {
|
|
8754
|
-
const croplessEffects = parent.
|
|
8761
|
+
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
8755
8762
|
if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
|
|
8756
8763
|
inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
|
|
8757
8764
|
if (hasOverflowClip(parent.container.styles)) {
|
|
@@ -8761,7 +8768,7 @@
|
|
|
8761
8768
|
}
|
|
8762
8769
|
effects.unshift(...croplessEffects);
|
|
8763
8770
|
} else effects.unshift(...croplessEffects);
|
|
8764
|
-
parent = parent.parent;
|
|
8771
|
+
parent = parent === surfaceRoot ? null : parent.parent;
|
|
8765
8772
|
}
|
|
8766
8773
|
return effects.filter((effect) => contains(effect.target, target));
|
|
8767
8774
|
}
|
|
@@ -9042,6 +9049,268 @@
|
|
|
9042
9049
|
}
|
|
9043
9050
|
};
|
|
9044
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
|
|
9045
9314
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9046
9315
|
var TextDecorationRenderer = class {
|
|
9047
9316
|
constructor(ctx) {
|
|
@@ -9171,8 +9440,8 @@
|
|
|
9171
9440
|
const TEXT_OVERFLOW_EPSILON = 1;
|
|
9172
9441
|
/**
|
|
9173
9442
|
* Detect CJK (Chinese, Japanese, Korean) characters in a string.
|
|
9174
|
-
*
|
|
9175
|
-
*
|
|
9443
|
+
* Vertical writing modes use it to keep CJK glyphs upright and on the
|
|
9444
|
+
* ideographic baseline while other scripts are rotated.
|
|
9176
9445
|
*
|
|
9177
9446
|
* Covers:
|
|
9178
9447
|
* U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
|
|
@@ -9242,15 +9511,17 @@
|
|
|
9242
9511
|
return w;
|
|
9243
9512
|
}
|
|
9244
9513
|
/**
|
|
9245
|
-
* Iterate grapheme clusters one-by-one, applying correct letter-spacing
|
|
9246
|
-
* per-script baseline for each character.
|
|
9514
|
+
* Iterate grapheme clusters one-by-one, applying correct letter-spacing.
|
|
9247
9515
|
*
|
|
9248
9516
|
* Issue #73: When letter-spacing is non-zero, text must be rendered character by
|
|
9249
|
-
* character
|
|
9250
|
-
*
|
|
9251
|
-
*
|
|
9252
|
-
*
|
|
9253
|
-
*
|
|
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.
|
|
9254
9525
|
*
|
|
9255
9526
|
* The `renderFn` callback receives (letter, x, y) and performs the actual draw
|
|
9256
9527
|
* call (fillText or strokeText), allowing fill and stroke paths to share one
|
|
@@ -9266,12 +9537,7 @@
|
|
|
9266
9537
|
const fontString = this.ctx.font;
|
|
9267
9538
|
let left = text.bounds.left;
|
|
9268
9539
|
for (const letter of letters) {
|
|
9269
|
-
|
|
9270
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
9271
|
-
this.ctx.textBaseline = "ideographic";
|
|
9272
|
-
renderFn(letter, left, y);
|
|
9273
|
-
this.ctx.textBaseline = savedBaseline;
|
|
9274
|
-
} else renderFn(letter, left, y);
|
|
9540
|
+
renderFn(letter, left, y);
|
|
9275
9541
|
left += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
9276
9542
|
}
|
|
9277
9543
|
}
|
|
@@ -9313,7 +9579,7 @@
|
|
|
9313
9579
|
/**
|
|
9314
9580
|
* Render text with letter-spacing applied (fill pass).
|
|
9315
9581
|
* When letterSpacing is 0 the whole string is drawn in one call; otherwise each
|
|
9316
|
-
* grapheme is drawn individually so spacing
|
|
9582
|
+
* grapheme is drawn individually so spacing is applied correctly.
|
|
9317
9583
|
*/
|
|
9318
9584
|
renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
|
|
9319
9585
|
this.renderFillText(text, letterSpacing, baseline, writingMode);
|
|
@@ -10548,12 +10814,19 @@
|
|
|
10548
10814
|
//#endregion
|
|
10549
10815
|
//#region src/render/canvas/canvas-renderer.ts
|
|
10550
10816
|
var CanvasRenderer = class CanvasRenderer {
|
|
10551
|
-
constructor(context, options) {
|
|
10817
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
10818
|
+
this.surfaceRoot = surfaceRoot;
|
|
10819
|
+
this.surfaceBudget = surfaceBudget;
|
|
10820
|
+
this.legacySubtree = false;
|
|
10552
10821
|
this.context = context;
|
|
10553
10822
|
this.options = options;
|
|
10554
10823
|
this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
|
|
10555
10824
|
const ctx = this.canvas.getContext("2d");
|
|
10556
|
-
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
|
+
}
|
|
10557
10830
|
this.ctx = ctx;
|
|
10558
10831
|
if (!options.canvas) {
|
|
10559
10832
|
this.canvas.width = Math.floor(options.width * options.scale);
|
|
@@ -10589,7 +10862,77 @@
|
|
|
10589
10862
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
10590
10863
|
}
|
|
10591
10864
|
async renderStack(stack) {
|
|
10592
|
-
|
|
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
|
+
}
|
|
10593
10936
|
}
|
|
10594
10937
|
async renderNode(paint) {
|
|
10595
10938
|
if (paint.container.debugRender) debugger;
|
|
@@ -10617,7 +10960,7 @@
|
|
|
10617
10960
|
}
|
|
10618
10961
|
}
|
|
10619
10962
|
async renderNodeContent(paint) {
|
|
10620
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4));
|
|
10963
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
|
|
10621
10964
|
const container = paint.container;
|
|
10622
10965
|
const curves = paint.curves;
|
|
10623
10966
|
const styles = container.styles;
|
|
@@ -10681,8 +11024,51 @@
|
|
|
10681
11024
|
formatPath(paths) {
|
|
10682
11025
|
formatCanvasPath(this.ctx, paths);
|
|
10683
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
|
+
}
|
|
10684
11070
|
async renderNodeBackgroundAndBorders(paint) {
|
|
10685
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
11071
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
|
|
10686
11072
|
const styles = paint.container.styles;
|
|
10687
11073
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10688
11074
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -10723,7 +11109,11 @@
|
|
|
10723
11109
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10724
11110
|
this.ctx.restore();
|
|
10725
11111
|
}
|
|
10726
|
-
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
|
+
}
|
|
10727
11117
|
this.ctx.save();
|
|
10728
11118
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
10729
11119
|
const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
|
|
@@ -10737,14 +11127,15 @@
|
|
|
10737
11127
|
this.ctx.clip();
|
|
10738
11128
|
this.path(shadowPaintingArea);
|
|
10739
11129
|
}
|
|
10740
|
-
this.
|
|
10741
|
-
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;
|
|
10742
11133
|
this.ctx.shadowColor = asString(shadow.color);
|
|
10743
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
11134
|
+
this.ctx.shadowBlur = shadow.blur.number * shadowScale;
|
|
10744
11135
|
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
|
|
10745
11136
|
this.ctx.fill();
|
|
10746
11137
|
this.ctx.restore();
|
|
10747
|
-
}
|
|
11138
|
+
}
|
|
10748
11139
|
}
|
|
10749
11140
|
if (styles.borderImageSource) {
|
|
10750
11141
|
const source = styles.borderImageSource;
|
|
@@ -10800,14 +11191,19 @@
|
|
|
10800
11191
|
}
|
|
10801
11192
|
}
|
|
10802
11193
|
async render(element) {
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
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;
|
|
10806
11206
|
}
|
|
10807
|
-
const stack = parseStackingContexts(element);
|
|
10808
|
-
await this.renderStack(stack);
|
|
10809
|
-
this.effectsRenderer.applyEffects([]);
|
|
10810
|
-
return this.canvas;
|
|
10811
11207
|
}
|
|
10812
11208
|
/**
|
|
10813
11209
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
@@ -11223,22 +11619,6 @@
|
|
|
11223
11619
|
}
|
|
11224
11620
|
};
|
|
11225
11621
|
//#endregion
|
|
11226
|
-
//#region src/core/abort-helper.ts
|
|
11227
|
-
/**
|
|
11228
|
-
* AbortSignal helper utilities.
|
|
11229
|
-
*/
|
|
11230
|
-
/**
|
|
11231
|
-
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
11232
|
-
* Used at checkpoints throughout the rendering pipeline to support
|
|
11233
|
-
* cancellable renders.
|
|
11234
|
-
*
|
|
11235
|
-
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
11236
|
-
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
11237
|
-
*/
|
|
11238
|
-
const throwIfAborted = (signal) => {
|
|
11239
|
-
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11240
|
-
};
|
|
11241
|
-
//#endregion
|
|
11242
11622
|
//#region src/core/config-assembler.ts
|
|
11243
11623
|
/**
|
|
11244
11624
|
* Coerce known numeric options from string (or other) values to actual numbers.
|
|
@@ -11407,18 +11787,19 @@
|
|
|
11407
11787
|
canvas = await new CanvasRenderer(context, renderOptions).render(root);
|
|
11408
11788
|
perfMonitor.end("render");
|
|
11409
11789
|
}
|
|
11790
|
+
} finally {
|
|
11410
11791
|
perfMonitor.start("cleanup");
|
|
11411
|
-
|
|
11412
|
-
if (
|
|
11792
|
+
try {
|
|
11793
|
+
if (root) root.restoreTree();
|
|
11794
|
+
} finally {
|
|
11795
|
+
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
11796
|
+
perfMonitor.end("cleanup");
|
|
11413
11797
|
}
|
|
11414
|
-
perfMonitor.end("cleanup");
|
|
11415
|
-
perfMonitor.end("total");
|
|
11416
|
-
context.logger.debug(`Finished rendering`);
|
|
11417
|
-
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11418
|
-
return canvas;
|
|
11419
|
-
} finally {
|
|
11420
|
-
if (root) root.restoreTree();
|
|
11421
11798
|
}
|
|
11799
|
+
perfMonitor.end("total");
|
|
11800
|
+
context.logger.debug(`Finished rendering`);
|
|
11801
|
+
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11802
|
+
return canvas;
|
|
11422
11803
|
};
|
|
11423
11804
|
//#endregion
|
|
11424
11805
|
//#region src/index.ts
|