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
|
@@ -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
|
*/
|
|
@@ -5713,7 +5713,7 @@ const filter = {
|
|
|
5713
5713
|
const renderedArgs = renderFilterArgs(fn.values);
|
|
5714
5714
|
switch (fn.name) {
|
|
5715
5715
|
case "blur":
|
|
5716
|
-
parts.push(`blur(${renderedArgs}
|
|
5716
|
+
parts.push(`blur(${renderedArgs})`);
|
|
5717
5717
|
break;
|
|
5718
5718
|
case "brightness":
|
|
5719
5719
|
case "contrast":
|
|
@@ -5727,7 +5727,7 @@ const filter = {
|
|
|
5727
5727
|
parts.push(`grayscale(${renderedArgs})`);
|
|
5728
5728
|
break;
|
|
5729
5729
|
case "hue-rotate":
|
|
5730
|
-
parts.push(`hue-rotate(${renderedArgs}
|
|
5730
|
+
parts.push(`hue-rotate(${renderedArgs})`);
|
|
5731
5731
|
break;
|
|
5732
5732
|
case "drop-shadow":
|
|
5733
5733
|
parts.push(`drop-shadow(${renderedArgs})`);
|
|
@@ -5745,14 +5745,20 @@ const filter = {
|
|
|
5745
5745
|
const renderFilterArgs = (values) => {
|
|
5746
5746
|
const parts = [];
|
|
5747
5747
|
for (const v of values) {
|
|
5748
|
-
if (v.type === 31)
|
|
5749
|
-
|
|
5748
|
+
if (v.type === 31) {
|
|
5749
|
+
parts.push(" ");
|
|
5750
|
+
continue;
|
|
5751
|
+
}
|
|
5752
|
+
if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
|
|
5753
|
+
else if (v.type === 4) parts.push(",");
|
|
5754
|
+
else if (v.type === 6) parts.push(v.value);
|
|
5755
|
+
else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
|
|
5750
5756
|
else if (v.type === 17) parts.push(`${v.number}`);
|
|
5751
5757
|
else if (v.type === 16) parts.push(`${v.number}%`);
|
|
5752
5758
|
else if (v.type === 20) parts.push(v.value);
|
|
5753
5759
|
else if (v.type === 5) parts.push(`#${v.value}`);
|
|
5754
5760
|
}
|
|
5755
|
-
return parts.join("
|
|
5761
|
+
return parts.join("").trim();
|
|
5756
5762
|
};
|
|
5757
5763
|
//#endregion
|
|
5758
5764
|
//#region src/css/property-descriptors/font-variant-ligatures.ts
|
|
@@ -8302,7 +8308,7 @@ const parseTree = (context, element) => {
|
|
|
8302
8308
|
return container;
|
|
8303
8309
|
};
|
|
8304
8310
|
const createsRealStackingContext = (node, container, root) => {
|
|
8305
|
-
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8311
|
+
return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
|
|
8306
8312
|
};
|
|
8307
8313
|
const createsStackingContext = (styles) => {
|
|
8308
8314
|
if (styles.isPositioned() || styles.isFloating()) return true;
|
|
@@ -8739,12 +8745,13 @@ var ElementPaint = class {
|
|
|
8739
8745
|
this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
|
|
8740
8746
|
}
|
|
8741
8747
|
}
|
|
8742
|
-
getEffects(target) {
|
|
8748
|
+
getEffects(target, surfaceRoot) {
|
|
8743
8749
|
let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
|
|
8744
|
-
let parent = this.parent;
|
|
8745
|
-
const
|
|
8750
|
+
let parent = this === surfaceRoot ? null : this.parent;
|
|
8751
|
+
const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
|
|
8752
|
+
const effects = sourceEffects(this);
|
|
8746
8753
|
while (parent) {
|
|
8747
|
-
const croplessEffects = parent.
|
|
8754
|
+
const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
|
|
8748
8755
|
if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
|
|
8749
8756
|
inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
|
|
8750
8757
|
if (hasOverflowClip(parent.container.styles)) {
|
|
@@ -8754,7 +8761,7 @@ var ElementPaint = class {
|
|
|
8754
8761
|
}
|
|
8755
8762
|
effects.unshift(...croplessEffects);
|
|
8756
8763
|
} else effects.unshift(...croplessEffects);
|
|
8757
|
-
parent = parent.parent;
|
|
8764
|
+
parent = parent === surfaceRoot ? null : parent.parent;
|
|
8758
8765
|
}
|
|
8759
8766
|
return effects.filter((effect) => contains(effect.target, target));
|
|
8760
8767
|
}
|
|
@@ -9035,6 +9042,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
|
|
|
9035
9042
|
}
|
|
9036
9043
|
};
|
|
9037
9044
|
//#endregion
|
|
9045
|
+
//#region src/core/abort-helper.ts
|
|
9046
|
+
/**
|
|
9047
|
+
* AbortSignal helper utilities.
|
|
9048
|
+
*/
|
|
9049
|
+
/**
|
|
9050
|
+
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
9051
|
+
* Used at checkpoints throughout the rendering pipeline to support
|
|
9052
|
+
* cancellable renders.
|
|
9053
|
+
*
|
|
9054
|
+
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
9055
|
+
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
9056
|
+
*/
|
|
9057
|
+
const throwIfAborted = (signal) => {
|
|
9058
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
9059
|
+
};
|
|
9060
|
+
//#endregion
|
|
9061
|
+
//#region src/render/canvas/filter-surface.ts
|
|
9062
|
+
var FilterSurfaceError = class extends Error {};
|
|
9063
|
+
const releaseSurface = (canvas) => {
|
|
9064
|
+
canvas.width = canvas.height = 0;
|
|
9065
|
+
};
|
|
9066
|
+
function parseSimpleFilter(value) {
|
|
9067
|
+
if (!value || value === "none") return { blur: 0 };
|
|
9068
|
+
const effect = new FilterEffect(value);
|
|
9069
|
+
const shadows = value.match(/drop-shadow\(/g) || [];
|
|
9070
|
+
if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
|
|
9071
|
+
const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
|
|
9072
|
+
if (effect.safeFilterString && !blur) return null;
|
|
9073
|
+
if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
|
|
9074
|
+
return {
|
|
9075
|
+
blur: blur ? Number(blur[1]) : 0,
|
|
9076
|
+
shadow: effect.shadow ? {
|
|
9077
|
+
x: effect.shadow.offsetX,
|
|
9078
|
+
y: effect.shadow.offsetY,
|
|
9079
|
+
blur: effect.shadow.blur,
|
|
9080
|
+
color: effect.shadow.color
|
|
9081
|
+
} : void 0
|
|
9082
|
+
};
|
|
9083
|
+
}
|
|
9084
|
+
function filterOutset(filter) {
|
|
9085
|
+
return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
|
|
9086
|
+
}
|
|
9087
|
+
function canvasLike(source) {
|
|
9088
|
+
const canvas = document.createElement("canvas");
|
|
9089
|
+
canvas.width = source.width;
|
|
9090
|
+
canvas.height = source.height;
|
|
9091
|
+
return canvas;
|
|
9092
|
+
}
|
|
9093
|
+
const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
|
|
9094
|
+
const finish = (error) => {
|
|
9095
|
+
clearTimeout(timer);
|
|
9096
|
+
signal?.removeEventListener("abort", abort);
|
|
9097
|
+
if (error) reject(error);
|
|
9098
|
+
else resolve();
|
|
9099
|
+
};
|
|
9100
|
+
const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
|
|
9101
|
+
const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
|
|
9102
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9103
|
+
if (signal?.aborted) abort();
|
|
9104
|
+
else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
|
|
9105
|
+
});
|
|
9106
|
+
const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
|
|
9107
|
+
function supportsNativeFilters(owner = document) {
|
|
9108
|
+
const cached = canvasFilterSupport.get(owner);
|
|
9109
|
+
if (cached !== void 0) return cached;
|
|
9110
|
+
const source = owner.createElement("canvas");
|
|
9111
|
+
const target = owner.createElement("canvas");
|
|
9112
|
+
let supported = false;
|
|
9113
|
+
try {
|
|
9114
|
+
source.width = source.height = target.width = target.height = 32;
|
|
9115
|
+
const input = source.getContext("2d");
|
|
9116
|
+
const context = target.getContext("2d");
|
|
9117
|
+
if (input && context && "filter" in context) {
|
|
9118
|
+
input.fillStyle = "#fff";
|
|
9119
|
+
input.fillRect(8, 8, 8, 8);
|
|
9120
|
+
context.filter = "blur(2px)";
|
|
9121
|
+
context.drawImage(source, 0, 0);
|
|
9122
|
+
const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
|
|
9123
|
+
context.clearRect(0, 0, 32, 32);
|
|
9124
|
+
context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
|
|
9125
|
+
context.drawImage(source, 0, 0);
|
|
9126
|
+
const shadow = context.getImageData(24, 12, 1, 1).data;
|
|
9127
|
+
supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
|
|
9128
|
+
}
|
|
9129
|
+
} catch {} finally {
|
|
9130
|
+
releaseSurface(source);
|
|
9131
|
+
releaseSurface(target);
|
|
9132
|
+
}
|
|
9133
|
+
canvasFilterSupport.set(owner, supported);
|
|
9134
|
+
return supported;
|
|
9135
|
+
}
|
|
9136
|
+
function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9137
|
+
let filtered;
|
|
9138
|
+
let output;
|
|
9139
|
+
let complete = false;
|
|
9140
|
+
try {
|
|
9141
|
+
throwIfAborted(signal);
|
|
9142
|
+
filtered = canvasLike(source);
|
|
9143
|
+
const context = filtered.getContext("2d");
|
|
9144
|
+
if (!context || !("filter" in context)) return null;
|
|
9145
|
+
const functions = [];
|
|
9146
|
+
if (blur) functions.push(`blur(${blur * scale}px)`);
|
|
9147
|
+
if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
|
|
9148
|
+
context.filter = functions.join(" ");
|
|
9149
|
+
if (context.filter === "none") return null;
|
|
9150
|
+
context.drawImage(source, 0, 0);
|
|
9151
|
+
throwIfAborted(signal);
|
|
9152
|
+
output = canvasLike(source);
|
|
9153
|
+
const composite = output.getContext("2d");
|
|
9154
|
+
if (!composite) return null;
|
|
9155
|
+
composite.globalAlpha = opacity;
|
|
9156
|
+
composite.drawImage(filtered, 0, 0);
|
|
9157
|
+
throwIfAborted(signal);
|
|
9158
|
+
complete = true;
|
|
9159
|
+
return output;
|
|
9160
|
+
} catch {
|
|
9161
|
+
throwIfAborted(signal);
|
|
9162
|
+
return null;
|
|
9163
|
+
} finally {
|
|
9164
|
+
if (filtered) releaseSurface(filtered);
|
|
9165
|
+
if (output && !complete) releaseSurface(output);
|
|
9166
|
+
}
|
|
9167
|
+
}
|
|
9168
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
9169
|
+
throwIfAborted(signal);
|
|
9170
|
+
if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
|
|
9171
|
+
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
9172
|
+
if (output) return output;
|
|
9173
|
+
}
|
|
9174
|
+
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
9175
|
+
}
|
|
9176
|
+
async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
|
|
9177
|
+
throwIfAborted(signal);
|
|
9178
|
+
if (!blur && !shadow) {
|
|
9179
|
+
const output = canvasLike(source);
|
|
9180
|
+
const context = output.getContext("2d");
|
|
9181
|
+
if (!context) {
|
|
9182
|
+
releaseSurface(output);
|
|
9183
|
+
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9184
|
+
}
|
|
9185
|
+
context.globalAlpha = opacity;
|
|
9186
|
+
try {
|
|
9187
|
+
context.drawImage(source, 0, 0);
|
|
9188
|
+
return output;
|
|
9189
|
+
} catch {
|
|
9190
|
+
releaseSurface(output);
|
|
9191
|
+
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9192
|
+
}
|
|
9193
|
+
}
|
|
9194
|
+
const namespace = "http://www.w3.org/2000/svg";
|
|
9195
|
+
const element = (tag, attributes = {}) => {
|
|
9196
|
+
const node = document.createElementNS(namespace, tag);
|
|
9197
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
9198
|
+
return node;
|
|
9199
|
+
};
|
|
9200
|
+
const svg = element("svg", {
|
|
9201
|
+
width: source.width,
|
|
9202
|
+
height: source.height,
|
|
9203
|
+
viewBox: `0 0 ${source.width} ${source.height}`
|
|
9204
|
+
});
|
|
9205
|
+
const defs = element("defs");
|
|
9206
|
+
const filter = element("filter", {
|
|
9207
|
+
id: "composited-filter",
|
|
9208
|
+
filterUnits: "userSpaceOnUse",
|
|
9209
|
+
x: 0,
|
|
9210
|
+
y: 0,
|
|
9211
|
+
width: source.width,
|
|
9212
|
+
height: source.height,
|
|
9213
|
+
"color-interpolation-filters": "sRGB"
|
|
9214
|
+
});
|
|
9215
|
+
if (blur) filter.appendChild(element("feGaussianBlur", {
|
|
9216
|
+
stdDeviation: blur * scale,
|
|
9217
|
+
result: "blurred-source"
|
|
9218
|
+
}));
|
|
9219
|
+
if (shadow) {
|
|
9220
|
+
filter.appendChild(element("feGaussianBlur", {
|
|
9221
|
+
in: blur ? "blurred-source" : "SourceAlpha",
|
|
9222
|
+
stdDeviation: shadow.blur * scale
|
|
9223
|
+
}));
|
|
9224
|
+
filter.appendChild(element("feOffset", {
|
|
9225
|
+
dx: shadow.x * scale,
|
|
9226
|
+
dy: shadow.y * scale,
|
|
9227
|
+
result: "offset-shadow"
|
|
9228
|
+
}));
|
|
9229
|
+
filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
|
|
9230
|
+
filter.appendChild(element("feComposite", {
|
|
9231
|
+
in2: "offset-shadow",
|
|
9232
|
+
operator: "in"
|
|
9233
|
+
}));
|
|
9234
|
+
const merge = element("feMerge");
|
|
9235
|
+
merge.appendChild(element("feMergeNode"));
|
|
9236
|
+
merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
|
|
9237
|
+
filter.appendChild(merge);
|
|
9238
|
+
}
|
|
9239
|
+
defs.appendChild(filter);
|
|
9240
|
+
svg.appendChild(defs);
|
|
9241
|
+
let sourceUrl;
|
|
9242
|
+
try {
|
|
9243
|
+
sourceUrl = source.toDataURL("image/png");
|
|
9244
|
+
if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
|
|
9245
|
+
} catch {
|
|
9246
|
+
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
9247
|
+
}
|
|
9248
|
+
svg.appendChild(element("image", {
|
|
9249
|
+
width: source.width,
|
|
9250
|
+
height: source.height,
|
|
9251
|
+
href: sourceUrl,
|
|
9252
|
+
filter: "url(#composited-filter)"
|
|
9253
|
+
}));
|
|
9254
|
+
const image = new Image();
|
|
9255
|
+
let output;
|
|
9256
|
+
try {
|
|
9257
|
+
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
9258
|
+
await decodeSurface(image, signal);
|
|
9259
|
+
throwIfAborted(signal);
|
|
9260
|
+
output = canvasLike(source);
|
|
9261
|
+
const context = output.getContext("2d");
|
|
9262
|
+
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
9263
|
+
context.globalAlpha = opacity;
|
|
9264
|
+
context.drawImage(image, 0, 0);
|
|
9265
|
+
return output;
|
|
9266
|
+
} catch (error) {
|
|
9267
|
+
if (output) releaseSurface(output);
|
|
9268
|
+
if (signal?.aborted) throwIfAborted(signal);
|
|
9269
|
+
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
9270
|
+
} finally {
|
|
9271
|
+
image.removeAttribute("src");
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
function reserveSurface(budget, width, height) {
|
|
9275
|
+
const pixels = width * height * 4;
|
|
9276
|
+
if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
|
|
9277
|
+
budget.pixels += pixels;
|
|
9278
|
+
return pixels;
|
|
9279
|
+
}
|
|
9280
|
+
const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
|
|
9281
|
+
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));
|
|
9282
|
+
function surfaceBounds(container) {
|
|
9283
|
+
const styles = container.styles;
|
|
9284
|
+
let bounds = container.bounds;
|
|
9285
|
+
for (const shadow of styles.boxShadow) if (!shadow.inset) {
|
|
9286
|
+
const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
|
|
9287
|
+
bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
|
|
9288
|
+
}
|
|
9289
|
+
for (const text of container.textNodes) for (const part of text.textBounds) {
|
|
9290
|
+
const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
|
|
9291
|
+
bounds = union(bounds, ink);
|
|
9292
|
+
for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
|
|
9293
|
+
}
|
|
9294
|
+
for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
|
|
9295
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
9296
|
+
return expand(bounds, filter ? filterOutset(filter) : 0);
|
|
9297
|
+
}
|
|
9298
|
+
function cropSurface(bounds, viewport, margin, scale) {
|
|
9299
|
+
const area = expand(viewport, margin);
|
|
9300
|
+
const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
|
|
9301
|
+
const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
|
|
9302
|
+
const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
|
|
9303
|
+
const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
|
|
9304
|
+
return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
|
|
9305
|
+
}
|
|
9306
|
+
//#endregion
|
|
9038
9307
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9039
9308
|
var TextDecorationRenderer = class {
|
|
9040
9309
|
constructor(ctx) {
|
|
@@ -9164,8 +9433,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
|
|
|
9164
9433
|
const TEXT_OVERFLOW_EPSILON = 1;
|
|
9165
9434
|
/**
|
|
9166
9435
|
* Detect CJK (Chinese, Japanese, Korean) characters in a string.
|
|
9167
|
-
*
|
|
9168
|
-
*
|
|
9436
|
+
* Vertical writing modes use it to keep CJK glyphs upright and on the
|
|
9437
|
+
* ideographic baseline while other scripts are rotated.
|
|
9169
9438
|
*
|
|
9170
9439
|
* Covers:
|
|
9171
9440
|
* U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
|
|
@@ -9235,15 +9504,17 @@ var TextRenderer = class {
|
|
|
9235
9504
|
return w;
|
|
9236
9505
|
}
|
|
9237
9506
|
/**
|
|
9238
|
-
* Iterate grapheme clusters one-by-one, applying correct letter-spacing
|
|
9239
|
-
* per-script baseline for each character.
|
|
9507
|
+
* Iterate grapheme clusters one-by-one, applying correct letter-spacing.
|
|
9240
9508
|
*
|
|
9241
9509
|
* Issue #73: When letter-spacing is non-zero, text must be rendered character by
|
|
9242
|
-
* character
|
|
9243
|
-
*
|
|
9244
|
-
*
|
|
9245
|
-
*
|
|
9246
|
-
*
|
|
9510
|
+
* character, adding `letterSpacing` to each character's advance width.
|
|
9511
|
+
*
|
|
9512
|
+
* In horizontal text every glyph keeps the alphabetic baseline, CJK included:
|
|
9513
|
+
* inline layout aligns a whole line on its alphabetic baseline regardless of
|
|
9514
|
+
* script, and `baseline` was measured for that. Drawing CJK graphemes at the
|
|
9515
|
+
* same y with `textBaseline = 'ideographic'` lifts them above the Latin text
|
|
9516
|
+
* next to them, and away from where the `letterSpacing === 0` whole-string
|
|
9517
|
+
* path draws them.
|
|
9247
9518
|
*
|
|
9248
9519
|
* The `renderFn` callback receives (letter, x, y) and performs the actual draw
|
|
9249
9520
|
* call (fillText or strokeText), allowing fill and stroke paths to share one
|
|
@@ -9259,12 +9530,7 @@ var TextRenderer = class {
|
|
|
9259
9530
|
const fontString = this.ctx.font;
|
|
9260
9531
|
let left = text.bounds.left;
|
|
9261
9532
|
for (const letter of letters) {
|
|
9262
|
-
|
|
9263
|
-
const savedBaseline = this.ctx.textBaseline;
|
|
9264
|
-
this.ctx.textBaseline = "ideographic";
|
|
9265
|
-
renderFn(letter, left, y);
|
|
9266
|
-
this.ctx.textBaseline = savedBaseline;
|
|
9267
|
-
} else renderFn(letter, left, y);
|
|
9533
|
+
renderFn(letter, left, y);
|
|
9268
9534
|
left += this.cachedMeasureText(letter, fontString) + letterSpacing;
|
|
9269
9535
|
}
|
|
9270
9536
|
}
|
|
@@ -9306,7 +9572,7 @@ var TextRenderer = class {
|
|
|
9306
9572
|
/**
|
|
9307
9573
|
* Render text with letter-spacing applied (fill pass).
|
|
9308
9574
|
* When letterSpacing is 0 the whole string is drawn in one call; otherwise each
|
|
9309
|
-
* grapheme is drawn individually so spacing
|
|
9575
|
+
* grapheme is drawn individually so spacing is applied correctly.
|
|
9310
9576
|
*/
|
|
9311
9577
|
renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
|
|
9312
9578
|
this.renderFillText(text, letterSpacing, baseline, writingMode);
|
|
@@ -10541,12 +10807,19 @@ const canvasTextAlign = (textAlign) => {
|
|
|
10541
10807
|
//#endregion
|
|
10542
10808
|
//#region src/render/canvas/canvas-renderer.ts
|
|
10543
10809
|
var CanvasRenderer = class CanvasRenderer {
|
|
10544
|
-
constructor(context, options) {
|
|
10810
|
+
constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
|
|
10811
|
+
this.surfaceRoot = surfaceRoot;
|
|
10812
|
+
this.surfaceBudget = surfaceBudget;
|
|
10813
|
+
this.legacySubtree = false;
|
|
10545
10814
|
this.context = context;
|
|
10546
10815
|
this.options = options;
|
|
10547
10816
|
this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
|
|
10548
10817
|
const ctx = this.canvas.getContext("2d");
|
|
10549
|
-
if (!ctx)
|
|
10818
|
+
if (!ctx) {
|
|
10819
|
+
if (!options.canvas) releaseSurface(this.canvas);
|
|
10820
|
+
if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10821
|
+
throw new Error("Failed to get 2D rendering context from canvas");
|
|
10822
|
+
}
|
|
10550
10823
|
this.ctx = ctx;
|
|
10551
10824
|
if (!options.canvas) {
|
|
10552
10825
|
this.canvas.width = Math.floor(options.width * options.scale);
|
|
@@ -10582,7 +10855,77 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10582
10855
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
10583
10856
|
}
|
|
10584
10857
|
async renderStack(stack) {
|
|
10585
|
-
|
|
10858
|
+
const styles = stack.element.container.styles;
|
|
10859
|
+
if (styles.isVisible()) {
|
|
10860
|
+
const filter = parseSimpleFilter(styles.filter);
|
|
10861
|
+
if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
|
|
10862
|
+
if (!await this.renderCompositedStack(stack, filter)) {
|
|
10863
|
+
this.legacySubtree = true;
|
|
10864
|
+
try {
|
|
10865
|
+
await this.renderStackContent(stack);
|
|
10866
|
+
} finally {
|
|
10867
|
+
this.legacySubtree = false;
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
return;
|
|
10871
|
+
}
|
|
10872
|
+
await this.renderStackContent(stack);
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
canComposite(paint) {
|
|
10876
|
+
const supported = (container) => {
|
|
10877
|
+
const styles = container.styles;
|
|
10878
|
+
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;
|
|
10879
|
+
};
|
|
10880
|
+
const subtree = (container) => supported(container) && container.elements.every(subtree);
|
|
10881
|
+
let ancestor = paint.parent;
|
|
10882
|
+
while (ancestor && ancestor !== this.surfaceRoot) {
|
|
10883
|
+
if (!supported(ancestor.container)) return false;
|
|
10884
|
+
ancestor = ancestor.parent;
|
|
10885
|
+
}
|
|
10886
|
+
return subtree(paint.container);
|
|
10887
|
+
}
|
|
10888
|
+
async renderCompositedStack(stack, filter) {
|
|
10889
|
+
const signal = this.options.signal;
|
|
10890
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10891
|
+
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);
|
|
10892
|
+
if (!bounds.width || !bounds.height) return true;
|
|
10893
|
+
const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
10894
|
+
if (!reserved) {
|
|
10895
|
+
this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
|
|
10896
|
+
return false;
|
|
10897
|
+
}
|
|
10898
|
+
const options = {
|
|
10899
|
+
...this.options,
|
|
10900
|
+
canvas: void 0,
|
|
10901
|
+
backgroundColor: null,
|
|
10902
|
+
x: bounds.left,
|
|
10903
|
+
y: bounds.top,
|
|
10904
|
+
width: bounds.width,
|
|
10905
|
+
height: bounds.height
|
|
10906
|
+
};
|
|
10907
|
+
let source;
|
|
10908
|
+
let filtered;
|
|
10909
|
+
try {
|
|
10910
|
+
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
10911
|
+
await source.renderStackContent(stack);
|
|
10912
|
+
source.effectsRenderer.applyEffects([]);
|
|
10913
|
+
filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
|
|
10914
|
+
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
10915
|
+
const own = stack.element.effects;
|
|
10916
|
+
const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
|
|
10917
|
+
this.effectsRenderer.applyEffects(effects);
|
|
10918
|
+
this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
|
|
10919
|
+
return true;
|
|
10920
|
+
} catch (error) {
|
|
10921
|
+
if (!(error instanceof FilterSurfaceError)) throw error;
|
|
10922
|
+
this.context.logger.info(`${error.message}; using the existing renderer`);
|
|
10923
|
+
return false;
|
|
10924
|
+
} finally {
|
|
10925
|
+
if (source) releaseSurface(source.canvas);
|
|
10926
|
+
if (filtered) releaseSurface(filtered);
|
|
10927
|
+
this.surfaceBudget.pixels -= reserved;
|
|
10928
|
+
}
|
|
10586
10929
|
}
|
|
10587
10930
|
async renderNode(paint) {
|
|
10588
10931
|
if (paint.container.debugRender) debugger;
|
|
@@ -10610,7 +10953,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10610
10953
|
}
|
|
10611
10954
|
}
|
|
10612
10955
|
async renderNodeContent(paint) {
|
|
10613
|
-
this.effectsRenderer.applyEffects(paint.getEffects(4));
|
|
10956
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
|
|
10614
10957
|
const container = paint.container;
|
|
10615
10958
|
const curves = paint.curves;
|
|
10616
10959
|
const styles = container.styles;
|
|
@@ -10674,8 +11017,51 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10674
11017
|
formatPath(paths) {
|
|
10675
11018
|
formatCanvasPath(this.ctx, paths);
|
|
10676
11019
|
}
|
|
11020
|
+
async renderSurfaceBoxShadow(paint, shadow) {
|
|
11021
|
+
const spread = shadow.spread.number;
|
|
11022
|
+
const blur = shadow.blur.number / 2;
|
|
11023
|
+
const margin = Math.ceil(3 * blur) + 1;
|
|
11024
|
+
const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
|
|
11025
|
+
if (box.width <= 0 || box.height <= 0) return;
|
|
11026
|
+
const scale = this.options.scale;
|
|
11027
|
+
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);
|
|
11028
|
+
if (!bounds.width || !bounds.height) return;
|
|
11029
|
+
const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
|
|
11030
|
+
const reserved = reserveSurface(this.surfaceBudget, width, height);
|
|
11031
|
+
if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
|
|
11032
|
+
let source;
|
|
11033
|
+
let filtered;
|
|
11034
|
+
try {
|
|
11035
|
+
source = document.createElement("canvas");
|
|
11036
|
+
source.width = width;
|
|
11037
|
+
source.height = height;
|
|
11038
|
+
const ctx = source.getContext("2d");
|
|
11039
|
+
if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
|
|
11040
|
+
ctx.scale(scale, scale);
|
|
11041
|
+
ctx.translate(-bounds.left, -bounds.top);
|
|
11042
|
+
createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
|
|
11043
|
+
ctx.fillStyle = asString(shadow.color);
|
|
11044
|
+
ctx.fill();
|
|
11045
|
+
filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
|
|
11046
|
+
this.ctx.save();
|
|
11047
|
+
try {
|
|
11048
|
+
this.ctx.beginPath();
|
|
11049
|
+
this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11050
|
+
formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
|
|
11051
|
+
this.ctx.closePath();
|
|
11052
|
+
this.ctx.clip("evenodd");
|
|
11053
|
+
this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
|
|
11054
|
+
} finally {
|
|
11055
|
+
this.ctx.restore();
|
|
11056
|
+
}
|
|
11057
|
+
} finally {
|
|
11058
|
+
if (source) releaseSurface(source);
|
|
11059
|
+
if (filtered) releaseSurface(filtered);
|
|
11060
|
+
this.surfaceBudget.pixels -= reserved;
|
|
11061
|
+
}
|
|
11062
|
+
}
|
|
10677
11063
|
async renderNodeBackgroundAndBorders(paint) {
|
|
10678
|
-
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
11064
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
|
|
10679
11065
|
const styles = paint.container.styles;
|
|
10680
11066
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10681
11067
|
const hasTextClip = hasTextBackgroundClip(styles);
|
|
@@ -10716,7 +11102,11 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10716
11102
|
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10717
11103
|
this.ctx.restore();
|
|
10718
11104
|
}
|
|
10719
|
-
styles.boxShadow.slice(0).reverse()
|
|
11105
|
+
for (const shadow of styles.boxShadow.slice(0).reverse()) {
|
|
11106
|
+
if (this.surfaceRoot && !shadow.inset) {
|
|
11107
|
+
await this.renderSurfaceBoxShadow(paint, shadow);
|
|
11108
|
+
continue;
|
|
11109
|
+
}
|
|
10720
11110
|
this.ctx.save();
|
|
10721
11111
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
10722
11112
|
const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
|
|
@@ -10730,14 +11120,15 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10730
11120
|
this.ctx.clip();
|
|
10731
11121
|
this.path(shadowPaintingArea);
|
|
10732
11122
|
}
|
|
10733
|
-
this.
|
|
10734
|
-
this.ctx.
|
|
11123
|
+
const shadowScale = this.surfaceRoot ? this.options.scale : 1;
|
|
11124
|
+
this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
|
|
11125
|
+
this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
|
|
10735
11126
|
this.ctx.shadowColor = asString(shadow.color);
|
|
10736
|
-
this.ctx.shadowBlur = shadow.blur.number;
|
|
11127
|
+
this.ctx.shadowBlur = shadow.blur.number * shadowScale;
|
|
10737
11128
|
this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
|
|
10738
11129
|
this.ctx.fill();
|
|
10739
11130
|
this.ctx.restore();
|
|
10740
|
-
}
|
|
11131
|
+
}
|
|
10741
11132
|
}
|
|
10742
11133
|
if (styles.borderImageSource) {
|
|
10743
11134
|
const source = styles.borderImageSource;
|
|
@@ -10793,14 +11184,19 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10793
11184
|
}
|
|
10794
11185
|
}
|
|
10795
11186
|
async render(element) {
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
11187
|
+
try {
|
|
11188
|
+
if (this.options.backgroundColor) {
|
|
11189
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
11190
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
11191
|
+
}
|
|
11192
|
+
const stack = parseStackingContexts(element);
|
|
11193
|
+
await this.renderStack(stack);
|
|
11194
|
+
this.effectsRenderer.applyEffects([]);
|
|
11195
|
+
return this.canvas;
|
|
11196
|
+
} catch (error) {
|
|
11197
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
11198
|
+
throw error;
|
|
10799
11199
|
}
|
|
10800
|
-
const stack = parseStackingContexts(element);
|
|
10801
|
-
await this.renderStack(stack);
|
|
10802
|
-
this.effectsRenderer.applyEffects([]);
|
|
10803
|
-
return this.canvas;
|
|
10804
11200
|
}
|
|
10805
11201
|
/**
|
|
10806
11202
|
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
@@ -11216,22 +11612,6 @@ var Context = class Context {
|
|
|
11216
11612
|
}
|
|
11217
11613
|
};
|
|
11218
11614
|
//#endregion
|
|
11219
|
-
//#region src/core/abort-helper.ts
|
|
11220
|
-
/**
|
|
11221
|
-
* AbortSignal helper utilities.
|
|
11222
|
-
*/
|
|
11223
|
-
/**
|
|
11224
|
-
* Throw a DOMException if the given AbortSignal is already aborted.
|
|
11225
|
-
* Used at checkpoints throughout the rendering pipeline to support
|
|
11226
|
-
* cancellable renders.
|
|
11227
|
-
*
|
|
11228
|
-
* @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
|
|
11229
|
-
* @throws {DOMException} With name 'AbortError' if the signal has been aborted.
|
|
11230
|
-
*/
|
|
11231
|
-
const throwIfAborted = (signal) => {
|
|
11232
|
-
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
11233
|
-
};
|
|
11234
|
-
//#endregion
|
|
11235
11615
|
//#region src/core/config-assembler.ts
|
|
11236
11616
|
/**
|
|
11237
11617
|
* Coerce known numeric options from string (or other) values to actual numbers.
|
|
@@ -11400,18 +11780,19 @@ const renderElement = async (element, opts, config) => {
|
|
|
11400
11780
|
canvas = await new CanvasRenderer(context, renderOptions).render(root);
|
|
11401
11781
|
perfMonitor.end("render");
|
|
11402
11782
|
}
|
|
11783
|
+
} finally {
|
|
11403
11784
|
perfMonitor.start("cleanup");
|
|
11404
|
-
|
|
11405
|
-
if (
|
|
11785
|
+
try {
|
|
11786
|
+
if (root) root.restoreTree();
|
|
11787
|
+
} finally {
|
|
11788
|
+
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
11789
|
+
perfMonitor.end("cleanup");
|
|
11406
11790
|
}
|
|
11407
|
-
perfMonitor.end("cleanup");
|
|
11408
|
-
perfMonitor.end("total");
|
|
11409
|
-
context.logger.debug(`Finished rendering`);
|
|
11410
|
-
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11411
|
-
return canvas;
|
|
11412
|
-
} finally {
|
|
11413
|
-
if (root) root.restoreTree();
|
|
11414
11791
|
}
|
|
11792
|
+
perfMonitor.end("total");
|
|
11793
|
+
context.logger.debug(`Finished rendering`);
|
|
11794
|
+
if (performanceMonitoring) perfMonitor.logSummary();
|
|
11795
|
+
return canvas;
|
|
11415
11796
|
};
|
|
11416
11797
|
//#endregion
|
|
11417
11798
|
//#region src/index.ts
|