modern-screenshot
Version:
Quickly generate image from DOM node using HTML5 canvas and SVG
1,544 lines (1,515 loc) • 53.5 kB
JavaScript
function changeJpegDpi(uint8Array, dpi) {
uint8Array[13] = 1;
uint8Array[14] = dpi >> 8;
uint8Array[15] = dpi & 255;
uint8Array[16] = dpi >> 8;
uint8Array[17] = dpi & 255;
return uint8Array;
}
const _P = "p".charCodeAt(0);
const _H = "H".charCodeAt(0);
const _Y = "Y".charCodeAt(0);
const _S = "s".charCodeAt(0);
let pngDataTable;
function createPngDataTable() {
const crcTable = new Int32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
}
crcTable[n] = c;
}
return crcTable;
}
function calcCrc(uint8Array) {
let c = -1;
if (!pngDataTable)
pngDataTable = createPngDataTable();
for (let n = 0; n < uint8Array.length; n++) {
c = pngDataTable[(c ^ uint8Array[n]) & 255] ^ c >>> 8;
}
return c ^ -1;
}
function searchStartOfPhys(uint8Array) {
const length = uint8Array.length - 1;
for (let i = length; i >= 4; i--) {
if (uint8Array[i - 4] === 9 && uint8Array[i - 3] === _P && uint8Array[i - 2] === _H && uint8Array[i - 1] === _Y && uint8Array[i] === _S) {
return i - 3;
}
}
return 0;
}
function changePngDpi(uint8Array, dpi, overwritepHYs = false) {
const physChunk = new Uint8Array(13);
dpi *= 39.3701;
physChunk[0] = _P;
physChunk[1] = _H;
physChunk[2] = _Y;
physChunk[3] = _S;
physChunk[4] = dpi >>> 24;
physChunk[5] = dpi >>> 16;
physChunk[6] = dpi >>> 8;
physChunk[7] = dpi & 255;
physChunk[8] = physChunk[4];
physChunk[9] = physChunk[5];
physChunk[10] = physChunk[6];
physChunk[11] = physChunk[7];
physChunk[12] = 1;
const crc = calcCrc(physChunk);
const crcChunk = new Uint8Array(4);
crcChunk[0] = crc >>> 24;
crcChunk[1] = crc >>> 16;
crcChunk[2] = crc >>> 8;
crcChunk[3] = crc & 255;
if (overwritepHYs) {
const startingIndex = searchStartOfPhys(uint8Array);
uint8Array.set(physChunk, startingIndex);
uint8Array.set(crcChunk, startingIndex + 13);
return uint8Array;
} else {
const chunkLength = new Uint8Array(4);
chunkLength[0] = 0;
chunkLength[1] = 0;
chunkLength[2] = 0;
chunkLength[3] = 9;
const finalHeader = new Uint8Array(54);
finalHeader.set(uint8Array, 0);
finalHeader.set(chunkLength, 33);
finalHeader.set(physChunk, 37);
finalHeader.set(crcChunk, 50);
return finalHeader;
}
}
const b64PhysSignature1 = "AAlwSFlz";
const b64PhysSignature2 = "AAAJcEhZ";
const b64PhysSignature3 = "AAAACXBI";
function detectPhysChunkFromDataUrl(dataUrl) {
let b64index = dataUrl.indexOf(b64PhysSignature1);
if (b64index === -1) {
b64index = dataUrl.indexOf(b64PhysSignature2);
}
if (b64index === -1) {
b64index = dataUrl.indexOf(b64PhysSignature3);
}
return b64index;
}
const PREFIX = "[modern-screenshot]";
const IN_BROWSER = typeof window !== "undefined";
const SUPPORT_WEB_WORKER = IN_BROWSER && "Worker" in window;
const SUPPORT_ATOB = IN_BROWSER && "atob" in window;
const SUPPORT_BTOA = IN_BROWSER && "btoa" in window;
const USER_AGENT = IN_BROWSER ? window.navigator?.userAgent : "";
const IN_CHROME = USER_AGENT.includes("Chrome");
const IN_SAFARI = USER_AGENT.includes("AppleWebKit") && !IN_CHROME;
const IN_FIREFOX = USER_AGENT.includes("Firefox");
const isContext = (value) => value && "__CONTEXT__" in value;
const isCssFontFaceRule = (rule) => rule.constructor.name === "CSSFontFaceRule";
const isCSSImportRule = (rule) => rule.constructor.name === "CSSImportRule";
const isElementNode = (node) => node.nodeType === 1;
const isSVGElementNode = (node) => typeof node.className === "object";
const isSVGImageElementNode = (node) => node.tagName === "image";
const isSVGUseElementNode = (node) => node.tagName === "use";
const isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== "undefined" && !isSVGElementNode(node);
const isCommentNode = (node) => node.nodeType === 8;
const isTextNode = (node) => node.nodeType === 3;
const isImageElement = (node) => node.tagName === "IMG";
const isVideoElement = (node) => node.tagName === "VIDEO";
const isCanvasElement = (node) => node.tagName === "CANVAS";
const isTextareaElement = (node) => node.tagName === "TEXTAREA";
const isInputElement = (node) => node.tagName === "INPUT";
const isStyleElement = (node) => node.tagName === "STYLE";
const isScriptElement = (node) => node.tagName === "SCRIPT";
const isSelectElement = (node) => node.tagName === "SELECT";
const isSlotElement = (node) => node.tagName === "SLOT";
const isIFrameElement = (node) => node.tagName === "IFRAME";
const consoleWarn = (...args) => console.warn(PREFIX, ...args);
function supportWebp(ownerDocument) {
const canvas = ownerDocument?.createElement?.("canvas");
if (canvas) {
canvas.height = canvas.width = 1;
}
return Boolean(canvas) && "toDataURL" in canvas && Boolean(canvas.toDataURL("image/webp").includes("image/webp"));
}
const isDataUrl = (url) => url.startsWith("data:");
function resolveUrl(url, baseUrl) {
if (url.match(/^[a-z]+:\/\//i))
return url;
if (IN_BROWSER && url.match(/^\/\//))
return window.location.protocol + url;
if (url.match(/^[a-z]+:/i))
return url;
if (!IN_BROWSER)
return url;
const doc = getDocument().implementation.createHTMLDocument();
const base = doc.createElement("base");
const a = doc.createElement("a");
doc.head.appendChild(base);
doc.body.appendChild(a);
if (baseUrl)
base.href = baseUrl;
a.href = url;
return a.href;
}
function getDocument(target) {
return (target && isElementNode(target) ? target?.ownerDocument : target) ?? window.document;
}
const XMLNS = "http://www.w3.org/2000/svg";
function createSvg(width, height, ownerDocument) {
const svg = getDocument(ownerDocument).createElementNS(XMLNS, "svg");
svg.setAttributeNS(null, "width", width.toString());
svg.setAttributeNS(null, "height", height.toString());
svg.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
return svg;
}
function svgToDataUrl(svg, removeControlCharacter) {
let xhtml = new XMLSerializer().serializeToString(svg);
if (removeControlCharacter) {
xhtml = xhtml.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu, "");
}
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xhtml)}`;
}
async function canvasToBlob(canvas, type = "image/png", quality = 1) {
try {
return await new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error("Blob is null"));
}
}, type, quality);
});
} catch (error) {
if (SUPPORT_ATOB) {
return dataUrlToBlob(canvas.toDataURL(type, quality));
}
throw error;
}
}
function dataUrlToBlob(dataUrl) {
const [header, base64] = dataUrl.split(",");
const type = header.match(/data:(.+);/)?.[1] ?? void 0;
const decoded = window.atob(base64);
const length = decoded.length;
const buffer = new Uint8Array(length);
for (let i = 0; i < length; i += 1) {
buffer[i] = decoded.charCodeAt(i);
}
return new Blob([buffer], { type });
}
function readBlob(blob, type) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.onabort = () => reject(new Error(`Failed read blob to ${type}`));
if (type === "dataUrl") {
reader.readAsDataURL(blob);
} else if (type === "arrayBuffer") {
reader.readAsArrayBuffer(blob);
}
});
}
const blobToDataUrl = (blob) => readBlob(blob, "dataUrl");
const blobToArrayBuffer = (blob) => readBlob(blob, "arrayBuffer");
function createImage(url, ownerDocument) {
const img = getDocument(ownerDocument).createElement("img");
img.decoding = "sync";
img.loading = "eager";
img.src = url;
return img;
}
function loadMedia(media, options) {
return new Promise((resolve) => {
const { timeout, ownerDocument, onError: userOnError, onWarn } = options ?? {};
const node = typeof media === "string" ? createImage(media, getDocument(ownerDocument)) : media;
let timer = null;
let removeEventListeners = null;
function onResolve() {
resolve(node);
timer && clearTimeout(timer);
removeEventListeners?.();
}
if (timeout) {
timer = setTimeout(onResolve, timeout);
}
if (isVideoElement(node)) {
const currentSrc = node.currentSrc || node.src;
if (!currentSrc) {
if (node.poster) {
return loadMedia(node.poster, options).then(resolve);
}
return onResolve();
}
if (node.readyState >= 2) {
return onResolve();
}
const onLoadeddata = onResolve;
const onError = (error) => {
onWarn?.(
"Failed video load",
currentSrc,
error
);
userOnError?.(error);
onResolve();
};
removeEventListeners = () => {
node.removeEventListener("loadeddata", onLoadeddata);
node.removeEventListener("error", onError);
};
node.addEventListener("loadeddata", onLoadeddata, { once: true });
node.addEventListener("error", onError, { once: true });
} else {
const currentSrc = isSVGImageElementNode(node) ? node.href.baseVal : node.currentSrc || node.src;
if (!currentSrc) {
return onResolve();
}
const onLoad = async () => {
if (isImageElement(node) && "decode" in node) {
try {
await node.decode();
} catch (error) {
onWarn?.(
"Failed to decode image, trying to render anyway",
node.dataset.originalSrc || currentSrc,
error
);
}
}
onResolve();
};
const onError = (error) => {
onWarn?.(
"Failed image load",
node.dataset.originalSrc || currentSrc,
error
);
onResolve();
};
if (isImageElement(node) && node.complete) {
return onLoad();
}
removeEventListeners = () => {
node.removeEventListener("load", onLoad);
node.removeEventListener("error", onError);
};
node.addEventListener("load", onLoad, { once: true });
node.addEventListener("error", onError, { once: true });
}
});
}
async function waitUntilLoad(node, options) {
if (isHTMLElementNode(node)) {
if (isImageElement(node) || isVideoElement(node)) {
await loadMedia(node, options);
} else {
await Promise.all(
["img", "video"].flatMap((selectors) => {
return Array.from(node.querySelectorAll(selectors)).map((el) => loadMedia(el, options));
})
);
}
}
}
const uuid = /* @__PURE__ */ function uuid2() {
let counter = 0;
const random = () => `0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4);
return () => {
counter += 1;
return `u${random()}${counter}`;
};
}();
function splitFontFamily(fontFamily) {
return fontFamily?.split(",").map((val) => val.trim().replace(/"|'/g, "").toLowerCase()).filter(Boolean);
}
let uid = 0;
function createLogger(debug) {
const prefix = `${PREFIX}[#${uid}]`;
uid++;
return {
// eslint-disable-next-line no-console
time: (label) => debug && console.time(`${prefix} ${label}`),
// eslint-disable-next-line no-console
timeEnd: (label) => debug && console.timeEnd(`${prefix} ${label}`),
warn: (...args) => debug && consoleWarn(...args)
};
}
function getDefaultRequestInit(bypassingCache) {
return {
cache: bypassingCache ? "no-cache" : "force-cache"
};
}
async function orCreateContext(node, options) {
return isContext(node) ? node : createContext(node, { ...options, autoDestruct: true });
}
async function createContext(node, options) {
const { scale = 1, workerUrl, workerNumber = 1 } = options || {};
const debug = Boolean(options?.debug);
const features = options?.features ?? true;
const ownerDocument = node.ownerDocument ?? (IN_BROWSER ? window.document : void 0);
const ownerWindow = node.ownerDocument?.defaultView ?? (IN_BROWSER ? window : void 0);
const requests = /* @__PURE__ */ new Map();
const context = {
// Options
width: 0,
height: 0,
quality: 1,
type: "image/png",
scale,
backgroundColor: null,
style: null,
filter: null,
maximumCanvasSize: 0,
timeout: 3e4,
progress: null,
debug,
fetch: {
requestInit: getDefaultRequestInit(options?.fetch?.bypassingCache),
placeholderImage: "data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
bypassingCache: false,
...options?.fetch
},
fetchFn: null,
font: {},
drawImageInterval: 100,
workerUrl: null,
workerNumber,
onCloneNode: null,
onEmbedNode: null,
onCreateForeignObjectSvg: null,
includeStyleProperties: null,
autoDestruct: false,
...options,
// InternalContext
__CONTEXT__: true,
log: createLogger(debug),
node,
ownerDocument,
ownerWindow,
dpi: scale === 1 ? null : 96 * scale,
svgStyleElement: createStyleElement(ownerDocument),
svgDefsElement: ownerDocument?.createElementNS(XMLNS, "defs"),
svgStyles: /* @__PURE__ */ new Map(),
defaultComputedStyles: /* @__PURE__ */ new Map(),
workers: [
...Array.from({
length: SUPPORT_WEB_WORKER && workerUrl && workerNumber ? workerNumber : 0
})
].map(() => {
try {
const worker = new Worker(workerUrl);
worker.onmessage = async (event) => {
const { url, result } = event.data;
if (result) {
requests.get(url)?.resolve?.(result);
} else {
requests.get(url)?.reject?.(new Error(`Error receiving message from worker: ${url}`));
}
};
worker.onmessageerror = (event) => {
const { url } = event.data;
requests.get(url)?.reject?.(new Error(`Error receiving message from worker: ${url}`));
};
return worker;
} catch (error) {
context.log.warn("Failed to new Worker", error);
return null;
}
}).filter(Boolean),
fontFamilies: /* @__PURE__ */ new Map(),
fontCssTexts: /* @__PURE__ */ new Map(),
acceptOfImage: `${[
supportWebp(ownerDocument) && "image/webp",
"image/svg+xml",
"image/*",
"*/*"
].filter(Boolean).join(",")};q=0.8`,
requests,
drawImageCount: 0,
tasks: [],
features,
isEnable: (key) => {
if (key === "restoreScrollPosition") {
return typeof features === "boolean" ? false : features[key] ?? false;
}
if (typeof features === "boolean") {
return features;
}
return features[key] ?? true;
}
};
context.log.time("wait until load");
await waitUntilLoad(node, { timeout: context.timeout, onWarn: context.log.warn });
context.log.timeEnd("wait until load");
const { width, height } = resolveBoundingBox(node, context);
context.width = width;
context.height = height;
return context;
}
function createStyleElement(ownerDocument) {
if (!ownerDocument)
return void 0;
const style = ownerDocument.createElement("style");
const cssText = style.ownerDocument.createTextNode(`
.______background-clip--text {
background-clip: text;
-webkit-background-clip: text;
}
`);
style.appendChild(cssText);
return style;
}
function resolveBoundingBox(node, context) {
let { width, height } = context;
if (isElementNode(node) && (!width || !height)) {
const box = node.getBoundingClientRect();
width = width || box.width || Number(node.getAttribute("width")) || 0;
height = height || box.height || Number(node.getAttribute("height")) || 0;
}
return { width, height };
}
async function imageToCanvas(image, context) {
const {
log,
timeout,
drawImageCount,
drawImageInterval
} = context;
log.time("image to canvas");
const loaded = await loadMedia(image, { timeout, onWarn: context.log.warn });
const { canvas, context2d } = createCanvas(image.ownerDocument, context);
const drawImage = () => {
try {
context2d?.drawImage(loaded, 0, 0, canvas.width, canvas.height);
} catch (error) {
context.log.warn("Failed to drawImage", error);
}
};
drawImage();
if (context.isEnable("fixSvgXmlDecode")) {
for (let i = 0; i < drawImageCount; i++) {
await new Promise((resolve) => {
setTimeout(() => {
drawImage();
resolve();
}, i + drawImageInterval);
});
}
}
context.drawImageCount = 0;
log.timeEnd("image to canvas");
return canvas;
}
function createCanvas(ownerDocument, context) {
const { width, height, scale, backgroundColor, maximumCanvasSize: max } = context;
const canvas = ownerDocument.createElement("canvas");
canvas.width = Math.floor(width * scale);
canvas.height = Math.floor(height * scale);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
if (max) {
if (canvas.width > max || canvas.height > max) {
if (canvas.width > max && canvas.height > max) {
if (canvas.width > canvas.height) {
canvas.height *= max / canvas.width;
canvas.width = max;
} else {
canvas.width *= max / canvas.height;
canvas.height = max;
}
} else if (canvas.width > max) {
canvas.height *= max / canvas.width;
canvas.width = max;
} else {
canvas.width *= max / canvas.height;
canvas.height = max;
}
}
}
const context2d = canvas.getContext("2d");
if (context2d && backgroundColor) {
context2d.fillStyle = backgroundColor;
context2d.fillRect(0, 0, canvas.width, canvas.height);
}
return { canvas, context2d };
}
function cloneCanvas(canvas, context) {
if (canvas.ownerDocument) {
try {
const dataURL = canvas.toDataURL();
if (dataURL !== "data:,") {
return createImage(dataURL, canvas.ownerDocument);
}
} catch (error) {
context.log.warn("Failed to clone canvas", error);
}
}
const cloned = canvas.cloneNode(false);
const ctx = canvas.getContext("2d");
const clonedCtx = cloned.getContext("2d");
try {
if (ctx && clonedCtx) {
clonedCtx.putImageData(
ctx.getImageData(0, 0, canvas.width, canvas.height),
0,
0
);
}
return cloned;
} catch (error) {
context.log.warn("Failed to clone canvas", error);
}
return cloned;
}
function cloneIframe(iframe, context) {
try {
if (iframe?.contentDocument?.body) {
return cloneNode(iframe.contentDocument.body, context);
}
} catch (error) {
context.log.warn("Failed to clone iframe", error);
}
return iframe.cloneNode(false);
}
function cloneImage(image) {
const cloned = image.cloneNode(false);
if (image.currentSrc && image.currentSrc !== image.src) {
cloned.src = image.currentSrc;
cloned.srcset = "";
}
if (cloned.loading === "lazy") {
cloned.loading = "eager";
}
return cloned;
}
async function cloneVideo(video, context) {
if (video.ownerDocument && !video.currentSrc && video.poster) {
return createImage(video.poster, video.ownerDocument);
}
const cloned = video.cloneNode(false);
cloned.crossOrigin = "anonymous";
if (video.currentSrc && video.currentSrc !== video.src) {
cloned.src = video.currentSrc;
}
const ownerDocument = cloned.ownerDocument;
if (ownerDocument) {
let canPlay = true;
await loadMedia(cloned, { onError: () => canPlay = false, onWarn: context.log.warn });
if (!canPlay) {
if (video.poster) {
return createImage(video.poster, video.ownerDocument);
}
return cloned;
}
cloned.currentTime = video.currentTime;
await new Promise((resolve) => {
cloned.addEventListener("seeked", resolve, { once: true });
});
const canvas = ownerDocument.createElement("canvas");
canvas.width = video.offsetWidth;
canvas.height = video.offsetHeight;
try {
const ctx = canvas.getContext("2d");
if (ctx)
ctx.drawImage(cloned, 0, 0, canvas.width, canvas.height);
} catch (error) {
context.log.warn("Failed to clone video", error);
if (video.poster) {
return createImage(video.poster, video.ownerDocument);
}
return cloned;
}
return cloneCanvas(canvas, context);
}
return cloned;
}
function cloneElement(node, context) {
if (isCanvasElement(node)) {
return cloneCanvas(node, context);
}
if (isIFrameElement(node)) {
return cloneIframe(node, context);
}
if (isImageElement(node)) {
return cloneImage(node);
}
if (isVideoElement(node)) {
return cloneVideo(node, context);
}
return node.cloneNode(false);
}
function getSandBox(context) {
let sandbox = context.sandbox;
if (!sandbox) {
const { ownerDocument } = context;
try {
if (ownerDocument) {
sandbox = ownerDocument.createElement("iframe");
sandbox.id = `__SANDBOX__-${uuid()}`;
sandbox.width = "0";
sandbox.height = "0";
sandbox.style.visibility = "hidden";
sandbox.style.position = "fixed";
ownerDocument.body.appendChild(sandbox);
sandbox.contentWindow?.document.write('<!DOCTYPE html><meta charset="UTF-8"><title></title><body>');
context.sandbox = sandbox;
}
} catch (error) {
context.log.warn("Failed to getSandBox", error);
}
}
return sandbox;
}
const ignoredStyles = [
"width",
"height",
"-webkit-text-fill-color"
];
const includedAttributes = [
"stroke",
"fill"
];
function getDefaultStyle(node, pseudoElement, context) {
const { defaultComputedStyles } = context;
const nodeName = node.nodeName.toLowerCase();
const isSvgNode = isSVGElementNode(node) && nodeName !== "svg";
const attributes = isSvgNode ? includedAttributes.map((name) => [name, node.getAttribute(name)]).filter(([, value]) => value !== null) : [];
const key = [
isSvgNode && "svg",
nodeName,
attributes.map((name, value) => `${name}=${value}`).join(","),
pseudoElement
].filter(Boolean).join(":");
if (defaultComputedStyles.has(key))
return defaultComputedStyles.get(key);
const sandbox = getSandBox(context);
const sandboxWindow = sandbox?.contentWindow;
if (!sandboxWindow)
return /* @__PURE__ */ new Map();
const sandboxDocument = sandboxWindow?.document;
let root;
let el;
if (isSvgNode) {
root = sandboxDocument.createElementNS(XMLNS, "svg");
el = root.ownerDocument.createElementNS(root.namespaceURI, nodeName);
attributes.forEach(([name, value]) => {
el.setAttributeNS(null, name, value);
});
root.appendChild(el);
} else {
root = el = sandboxDocument.createElement(nodeName);
}
el.textContent = " ";
sandboxDocument.body.appendChild(root);
const computedStyle = sandboxWindow.getComputedStyle(el, pseudoElement);
const styles = /* @__PURE__ */ new Map();
for (let len = computedStyle.length, i = 0; i < len; i++) {
const name = computedStyle.item(i);
if (ignoredStyles.includes(name))
continue;
styles.set(name, computedStyle.getPropertyValue(name));
}
sandboxDocument.body.removeChild(root);
defaultComputedStyles.set(key, styles);
return styles;
}
function getDiffStyle(style, defaultStyle, includeStyleProperties) {
const diffStyle = /* @__PURE__ */ new Map();
const prefixs = [];
const prefixTree = /* @__PURE__ */ new Map();
if (includeStyleProperties) {
for (const name of includeStyleProperties) {
applyTo(name);
}
} else {
for (let len = style.length, i = 0; i < len; i++) {
const name = style.item(i);
applyTo(name);
}
}
for (let len = prefixs.length, i = 0; i < len; i++) {
prefixTree.get(prefixs[i])?.forEach((value, name) => diffStyle.set(name, value));
}
function applyTo(name) {
const value = style.getPropertyValue(name);
const priority = style.getPropertyPriority(name);
const subIndex = name.lastIndexOf("-");
const prefix = subIndex > -1 ? name.substring(0, subIndex) : void 0;
if (prefix) {
let map = prefixTree.get(prefix);
if (!map) {
map = /* @__PURE__ */ new Map();
prefixTree.set(prefix, map);
}
map.set(name, [value, priority]);
}
if (defaultStyle.get(name) === value && !priority)
return;
if (prefix) {
prefixs.push(prefix);
} else {
diffStyle.set(name, [value, priority]);
}
}
return diffStyle;
}
function copyCssStyles(node, cloned, isRoot, context) {
const { ownerWindow, includeStyleProperties, currentParentNodeStyle } = context;
const clonedStyle = cloned.style;
const computedStyle = ownerWindow.getComputedStyle(node);
const defaultStyle = getDefaultStyle(node, null, context);
currentParentNodeStyle?.forEach((_, key) => {
defaultStyle.delete(key);
});
const style = getDiffStyle(computedStyle, defaultStyle, includeStyleProperties);
style.delete("transition-property");
style.delete("all");
style.delete("d");
style.delete("content");
if (isRoot) {
style.delete("margin-top");
style.delete("margin-right");
style.delete("margin-bottom");
style.delete("margin-left");
style.delete("margin-block-start");
style.delete("margin-block-end");
style.delete("margin-inline-start");
style.delete("margin-inline-end");
style.set("box-sizing", ["border-box", ""]);
}
if (style.get("background-clip")?.[0] === "text") {
cloned.classList.add("______background-clip--text");
}
if (IN_CHROME) {
if (!style.has("font-kerning"))
style.set("font-kerning", ["normal", ""]);
if ((style.get("overflow-x")?.[0] === "hidden" || style.get("overflow-y")?.[0] === "hidden") && style.get("text-overflow")?.[0] === "ellipsis" && node.scrollWidth === node.clientWidth) {
style.set("text-overflow", ["clip", ""]);
}
}
for (let len = clonedStyle.length, i = 0; i < len; i++) {
clonedStyle.removeProperty(clonedStyle.item(i));
}
style.forEach(([value, priority], name) => {
clonedStyle.setProperty(name, value, priority);
});
return style;
}
function copyInputValue(node, cloned) {
if (isTextareaElement(node) || isInputElement(node) || isSelectElement(node)) {
cloned.setAttribute("value", node.value);
}
}
const pseudoClasses = [
":before",
":after"
// ':placeholder', TODO
];
const scrollbarPseudoClasses = [
":-webkit-scrollbar",
":-webkit-scrollbar-button",
// ':-webkit-scrollbar:horizontal', TODO
":-webkit-scrollbar-thumb",
":-webkit-scrollbar-track",
":-webkit-scrollbar-track-piece",
// ':-webkit-scrollbar:vertical', TODO
":-webkit-scrollbar-corner",
":-webkit-resizer"
];
function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFamilies) {
const { ownerWindow, svgStyleElement, svgStyles, currentNodeStyle } = context;
if (!svgStyleElement || !ownerWindow)
return;
function copyBy(pseudoClass) {
const computedStyle = ownerWindow.getComputedStyle(node, pseudoClass);
let content = computedStyle.getPropertyValue("content");
if (!content || content === "none")
return;
addWordToFontFamilies?.(content);
content = content.replace(/(')|(")|(counter\(.+\))/g, "");
const klasses = [uuid()];
const defaultStyle = getDefaultStyle(node, pseudoClass, context);
currentNodeStyle?.forEach((_, key) => {
defaultStyle.delete(key);
});
const style = getDiffStyle(computedStyle, defaultStyle, context.includeStyleProperties);
style.delete("content");
style.delete("-webkit-locale");
if (style.get("background-clip")?.[0] === "text") {
cloned.classList.add("______background-clip--text");
}
const cloneStyle = [
`content: '${content}';`
];
style.forEach(([value, priority], name) => {
cloneStyle.push(`${name}: ${value}${priority ? " !important" : ""};`);
});
if (cloneStyle.length === 1)
return;
try {
cloned.className = [cloned.className, ...klasses].join(" ");
} catch (err) {
context.log.warn("Failed to copyPseudoClass", err);
return;
}
const cssText = cloneStyle.join("\n ");
let allClasses = svgStyles.get(cssText);
if (!allClasses) {
allClasses = [];
svgStyles.set(cssText, allClasses);
}
allClasses.push(`.${klasses[0]}:${pseudoClass}`);
}
pseudoClasses.forEach(copyBy);
if (copyScrollbar)
scrollbarPseudoClasses.forEach(copyBy);
}
const excludeParentNodes = /* @__PURE__ */ new Set([
"symbol"
// test/fixtures/svg.symbol.html
]);
async function appendChildNode(node, cloned, child, context, addWordToFontFamilies) {
if (isElementNode(child) && (isStyleElement(child) || isScriptElement(child)))
return;
if (context.filter && !context.filter(child))
return;
if (excludeParentNodes.has(cloned.nodeName) || excludeParentNodes.has(child.nodeName)) {
context.currentParentNodeStyle = void 0;
} else {
context.currentParentNodeStyle = context.currentNodeStyle;
}
const childCloned = await cloneNode(child, context, false, addWordToFontFamilies);
if (context.isEnable("restoreScrollPosition")) {
restoreScrollPosition(node, childCloned);
}
cloned.appendChild(childCloned);
}
async function cloneChildNodes(node, cloned, context, addWordToFontFamilies) {
const firstChild = (isElementNode(node) ? node.shadowRoot?.firstChild : void 0) ?? node.firstChild;
for (let child = firstChild; child; child = child.nextSibling) {
if (isCommentNode(child))
continue;
if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === "function") {
const nodes = child.assignedNodes();
for (let i = 0; i < nodes.length; i++) {
await appendChildNode(node, cloned, nodes[i], context, addWordToFontFamilies);
}
} else {
await appendChildNode(node, cloned, child, context, addWordToFontFamilies);
}
}
}
function restoreScrollPosition(node, chlidCloned) {
if (!isHTMLElementNode(node) || !isHTMLElementNode(chlidCloned))
return;
const { scrollTop, scrollLeft } = node;
if (!scrollTop && !scrollLeft) {
return;
}
const { transform } = chlidCloned.style;
const matrix = new DOMMatrix(transform);
const { a, b, c, d } = matrix;
matrix.a = 1;
matrix.b = 0;
matrix.c = 0;
matrix.d = 1;
matrix.translateSelf(-scrollLeft, -scrollTop);
matrix.a = a;
matrix.b = b;
matrix.c = c;
matrix.d = d;
chlidCloned.style.transform = matrix.toString();
}
function applyCssStyleWithOptions(cloned, context) {
const { backgroundColor, width, height, style: styles } = context;
const clonedStyle = cloned.style;
if (backgroundColor)
clonedStyle.setProperty("background-color", backgroundColor, "important");
if (width)
clonedStyle.setProperty("width", `${width}px`, "important");
if (height)
clonedStyle.setProperty("height", `${height}px`, "important");
if (styles) {
for (const name in styles) clonedStyle[name] = styles[name];
}
}
const NORMAL_ATTRIBUTE_RE = /^[\w-:]+$/;
async function cloneNode(node, context, isRoot = false, addWordToFontFamilies) {
const { ownerDocument, ownerWindow, fontFamilies } = context;
if (ownerDocument && isTextNode(node)) {
if (addWordToFontFamilies && /\S/.test(node.data)) {
addWordToFontFamilies(node.data);
}
return ownerDocument.createTextNode(node.data);
}
if (ownerDocument && ownerWindow && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
const cloned2 = await cloneElement(node, context);
if (context.isEnable("removeAbnormalAttributes")) {
const names = cloned2.getAttributeNames();
for (let len = names.length, i = 0; i < len; i++) {
const name = names[i];
if (!NORMAL_ATTRIBUTE_RE.test(name)) {
cloned2.removeAttribute(name);
}
}
}
const style = context.currentNodeStyle = copyCssStyles(node, cloned2, isRoot, context);
if (isRoot)
applyCssStyleWithOptions(cloned2, context);
let copyScrollbar = false;
if (context.isEnable("copyScrollbar")) {
const overflow = [
style.get("overflow-x")?.[0],
style.get("overflow-y")?.[0]
];
copyScrollbar = overflow.includes("scroll") || (overflow.includes("auto") || overflow.includes("overlay")) && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth);
}
const textTransform = style.get("text-transform")?.[0];
const families = splitFontFamily(style.get("font-family")?.[0]);
const addWordToFontFamilies2 = families ? (word) => {
if (textTransform === "uppercase") {
word = word.toUpperCase();
} else if (textTransform === "lowercase") {
word = word.toLowerCase();
} else if (textTransform === "capitalize") {
word = word[0].toUpperCase() + word.substring(1);
}
families.forEach((family) => {
let fontFamily = fontFamilies.get(family);
if (!fontFamily) {
fontFamilies.set(family, fontFamily = /* @__PURE__ */ new Set());
}
word.split("").forEach((text) => fontFamily.add(text));
});
} : void 0;
copyPseudoClass(
node,
cloned2,
copyScrollbar,
context,
addWordToFontFamilies2
);
copyInputValue(node, cloned2);
if (!isVideoElement(node)) {
await cloneChildNodes(
node,
cloned2,
context,
addWordToFontFamilies2
);
}
return cloned2;
}
const cloned = node.cloneNode(false);
await cloneChildNodes(node, cloned, context);
return cloned;
}
function destroyContext(context) {
context.ownerDocument = void 0;
context.ownerWindow = void 0;
context.svgStyleElement = void 0;
context.svgDefsElement = void 0;
context.svgStyles.clear();
context.defaultComputedStyles.clear();
if (context.sandbox) {
try {
context.sandbox.remove();
} catch (err) {
context.log.warn("Failed to destroyContext", err);
}
context.sandbox = void 0;
}
context.workers = [];
context.fontFamilies.clear();
context.fontCssTexts.clear();
context.requests.clear();
context.tasks = [];
}
function baseFetch(options) {
const { url, timeout, responseType, ...requestInit } = options;
const controller = new AbortController();
const timer = timeout ? setTimeout(() => controller.abort(), timeout) : void 0;
return fetch(url, { signal: controller.signal, ...requestInit }).then((response) => {
if (!response.ok) {
throw new Error("Failed fetch, not 2xx response", { cause: response });
}
switch (responseType) {
case "arrayBuffer":
return response.arrayBuffer();
case "dataUrl":
return response.blob().then(blobToDataUrl);
case "text":
default:
return response.text();
}
}).finally(() => clearTimeout(timer));
}
function contextFetch(context, options) {
const { url: rawUrl, requestType = "text", responseType = "text", imageDom } = options;
let url = rawUrl;
const {
timeout,
acceptOfImage,
requests,
fetchFn,
fetch: {
requestInit,
bypassingCache,
placeholderImage
},
font,
workers,
fontFamilies
} = context;
if (requestType === "image" && (IN_SAFARI || IN_FIREFOX)) {
context.drawImageCount++;
}
let request = requests.get(rawUrl);
if (!request) {
if (bypassingCache) {
if (bypassingCache instanceof RegExp && bypassingCache.test(url)) {
url += (/\?/.test(url) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime();
}
}
const canFontMinify = requestType.startsWith("font") && font && font.minify;
const fontTexts = /* @__PURE__ */ new Set();
if (canFontMinify) {
const families = requestType.split(";")[1].split(",");
families.forEach((family) => {
if (!fontFamilies.has(family))
return;
fontFamilies.get(family).forEach((text) => fontTexts.add(text));
});
}
const needFontMinify = canFontMinify && fontTexts.size;
const baseFetchOptions = {
url,
timeout,
responseType: needFontMinify ? "arrayBuffer" : responseType,
headers: requestType === "image" ? { accept: acceptOfImage } : void 0,
...requestInit
};
request = {
type: requestType,
resolve: void 0,
reject: void 0,
response: null
};
request.response = (async () => {
if (fetchFn && requestType === "image") {
const result = await fetchFn(rawUrl);
if (result)
return result;
}
if (!IN_SAFARI && rawUrl.startsWith("http") && workers.length) {
return new Promise((resolve, reject) => {
const worker = workers[requests.size & workers.length - 1];
worker.postMessage({ rawUrl, ...baseFetchOptions });
request.resolve = resolve;
request.reject = reject;
});
}
return baseFetch(baseFetchOptions);
})().catch((error) => {
requests.delete(rawUrl);
if (requestType === "image" && placeholderImage) {
context.log.warn("Failed to fetch image base64, trying to use placeholder image", url);
return typeof placeholderImage === "string" ? placeholderImage : placeholderImage(imageDom);
}
throw error;
});
requests.set(rawUrl, request);
}
return request.response;
}
async function replaceCssUrlToDataUrl(cssText, baseUrl, context, isImage) {
if (!hasCssUrl(cssText))
return cssText;
for (const [rawUrl, url] of parseCssUrls(cssText, baseUrl)) {
try {
const dataUrl = await contextFetch(
context,
{
url,
requestType: isImage ? "image" : "text",
responseType: "dataUrl"
}
);
cssText = cssText.replace(toRE(rawUrl), `$1${dataUrl}$3`);
} catch (error) {
context.log.warn("Failed to fetch css data url", rawUrl, error);
}
}
return cssText;
}
function hasCssUrl(cssText) {
return /url\((['"]?)([^'"]+?)\1\)/.test(cssText);
}
const URL_RE = /url\((['"]?)([^'"]+?)\1\)/g;
function parseCssUrls(cssText, baseUrl) {
const result = [];
cssText.replace(URL_RE, (raw, quotation, url) => {
result.push([url, resolveUrl(url, baseUrl)]);
return raw;
});
return result.filter(([url]) => !isDataUrl(url));
}
function toRE(url) {
const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1");
return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g");
}
const properties = [
"background-image",
"border-image-source",
"-webkit-border-image",
"-webkit-mask-image",
"list-style-image"
];
function embedCssStyleImage(style, context) {
return properties.map((property) => {
const value = style.getPropertyValue(property);
if (!value || value === "none") {
return null;
}
if (IN_SAFARI || IN_FIREFOX) {
context.drawImageCount++;
}
return replaceCssUrlToDataUrl(value, null, context, true).then((newValue) => {
if (!newValue || value === newValue)
return;
style.setProperty(
property,
newValue,
style.getPropertyPriority(property)
);
});
}).filter(Boolean);
}
function embedImageElement(cloned, context) {
if (isImageElement(cloned)) {
const originalSrc = cloned.currentSrc || cloned.src;
if (!isDataUrl(originalSrc)) {
return [
contextFetch(context, {
url: originalSrc,
imageDom: cloned,
requestType: "image",
responseType: "dataUrl"
}).then((url) => {
if (!url)
return;
cloned.srcset = "";
cloned.dataset.originalSrc = originalSrc;
cloned.src = url || "";
})
];
}
if (IN_SAFARI || IN_FIREFOX) {
context.drawImageCount++;
}
} else if (isSVGElementNode(cloned) && !isDataUrl(cloned.href.baseVal)) {
const originalSrc = cloned.href.baseVal;
return [
contextFetch(context, {
url: originalSrc,
imageDom: cloned,
requestType: "image",
responseType: "dataUrl"
}).then((url) => {
if (!url)
return;
cloned.dataset.originalSrc = originalSrc;
cloned.href.baseVal = url || "";
})
];
}
return [];
}
function embedSvgUse(cloned, context) {
const { ownerDocument, svgDefsElement } = context;
const href = cloned.getAttribute("href") ?? cloned.getAttribute("xlink:href");
if (!href)
return [];
const [svgUrl, id] = href.split("#");
if (id) {
const query = `#${id}`;
const definition = ownerDocument?.querySelector(`svg ${query}`);
if (svgUrl) {
cloned.setAttribute("href", query);
}
if (svgDefsElement?.querySelector(query))
return [];
if (definition) {
svgDefsElement?.appendChild(definition.cloneNode(true));
return [];
} else if (svgUrl) {
return [
contextFetch(context, {
url: svgUrl,
responseType: "text"
}).then((svgData) => {
svgDefsElement?.insertAdjacentHTML("beforeend", svgData);
})
];
}
}
return [];
}
function embedNode(cloned, context) {
const { tasks } = context;
if (isElementNode(cloned)) {
if (isImageElement(cloned) || isSVGImageElementNode(cloned)) {
tasks.push(...embedImageElement(cloned, context));
}
if (isSVGUseElementNode(cloned)) {
tasks.push(...embedSvgUse(cloned, context));
}
}
if (isHTMLElementNode(cloned)) {
tasks.push(...embedCssStyleImage(cloned.style, context));
}
cloned.childNodes.forEach((child) => {
embedNode(child, context);
});
}
async function embedWebFont(clone, context) {
const {
ownerDocument,
svgStyleElement,
fontFamilies,
fontCssTexts,
tasks,
font
} = context;
if (!ownerDocument || !svgStyleElement || !fontFamilies.size) {
return;
}
if (font && font.cssText) {
const cssText = filterPreferredFormat(font.cssText, context);
svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText}
`));
} else {
const styleSheets = Array.from(ownerDocument.styleSheets).filter((styleSheet) => {
try {
return "cssRules" in styleSheet && Boolean(styleSheet.cssRules.length);
} catch (error) {
context.log.warn(`Error while reading CSS rules from ${styleSheet.href}`, error);
return false;
}
});
await Promise.all(
styleSheets.flatMap((styleSheet) => {
return Array.from(styleSheet.cssRules).map(async (cssRule, index) => {
if (isCSSImportRule(cssRule)) {
let importIndex = index + 1;
const baseUrl = cssRule.href;
let cssText = "";
try {
cssText = await contextFetch(context, {
url: baseUrl,
requestType: "text",
responseType: "text"
});
} catch (error) {
context.log.warn(`Error fetch remote css import from ${baseUrl}`, error);
}
const replacedCssText = cssText.replace(
URL_RE,
(raw, quotation, url) => raw.replace(url, resolveUrl(url, baseUrl))
);
for (const rule of parseCss(replacedCssText)) {
try {
styleSheet.insertRule(
rule,
rule.startsWith("@import") ? importIndex += 1 : styleSheet.cssRules.length
);
} catch (error) {
context.log.warn("Error inserting rule from remote css import", { rule, error });
}
}
}
});
})
);
const cssRules = styleSheets.flatMap((styleSheet) => Array.from(styleSheet.cssRules));
cssRules.filter((cssRule) => isCssFontFaceRule(cssRule) && hasCssUrl(cssRule.style.getPropertyValue("src")) && splitFontFamily(cssRule.style.getPropertyValue("font-family"))?.some((val) => fontFamilies.has(val))).forEach((value) => {
const rule = value;
const cssText = fontCssTexts.get(rule.cssText);
if (cssText) {
svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText}
`));
} else {
tasks.push(
replaceCssUrlToDataUrl(
rule.cssText,
rule.parentStyleSheet ? rule.parentStyleSheet.href : null,
context
).then((cssText2) => {
cssText2 = filterPreferredFormat(cssText2, context);
fontCssTexts.set(rule.cssText, cssText2);
svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText2}
`));
})
);
}
});
}
}
const COMMENTS_RE = /(\/\*[\s\S]*?\*\/)/g;
const KEYFRAMES_RE = /((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi;
function parseCss(source) {
if (source == null)
return [];
const result = [];
let cssText = source.replace(COMMENTS_RE, "");
while (true) {
const matches = KEYFRAMES_RE.exec(cssText);
if (!matches)
break;
result.push(matches[0]);
}
cssText = cssText.replace(KEYFRAMES_RE, "");
const IMPORT_RE = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
const UNIFIED_RE = new RegExp(
// eslint-disable-next-line
"((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})",
"gi"
);
while (true) {
let matches = IMPORT_RE.exec(cssText);
if (!matches) {
matches = UNIFIED_RE.exec(cssText);
if (!matches) {
break;
} else {
IMPORT_RE.lastIndex = UNIFIED_RE.lastIndex;
}
} else {
UNIFIED_RE.lastIndex = IMPORT_RE.lastIndex;
}
result.push(matches[0]);
}
return result;
}
const URL_WITH_FORMAT_RE = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
const FONT_SRC_RE = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;
function filterPreferredFormat(str, context) {
const { font } = context;
const preferredFormat = font ? font?.preferredFormat : void 0;
return preferredFormat ? str.replace(FONT_SRC_RE, (match) => {
while (true) {
const [src, , format] = URL_WITH_FORMAT_RE.exec(match) || [];
if (!format)
return "";
if (format === preferredFormat)
return `src: ${src};`;
}
}) : str;
}
async function domToForeignObjectSvg(node, options) {
const context = await orCreateContext(node, options);
if (isElementNode(context.node) && isSVGElementNode(context.node))
return context.node;
const {
ownerDocument,
log,
tasks,
svgStyleElement,
svgDefsElement,
svgStyles,
font,
progress,
autoDestruct,
onCloneNode,
onEmbedNode,
onCreateForeignObjectSvg
} = context;
log.time("clone node");
const clone = await cloneNode(context.node, context, true);
if (svgStyleElement && ownerDocument) {
let allCssText = "";
svgStyles.forEach((klasses, cssText) => {
allCssText += `${klasses.join(",\n")} {
${cssText}
}
`;
});
svgStyleElement.appendChild(ownerDocument.createTextNode(allCssText));
}
log.timeEnd("clone node");
await onCloneNode?.(clone);
if (font !== false && isElementNode(clone)) {
log.time("embed web font");
await embedWebFont(clone, context);
log.timeEnd("embed web font");
}
log.time("embed node");
embedNode(clone, context);
const count = tasks.length;
let current = 0;
const runTask = async () => {
while (true) {
const task = tasks.pop();
if (!task)
break;
try {
await task;
} catch (error) {
context.log.warn("Failed to run task", error);
}
progress?.(++current, count);
}
};
progress?.(current, count);
await Promise.all([...Array.from({ length: 4 })].map(runTask));
log.timeEnd("embed node");
await onEmbedNode?.(clone);
const svg = createForeignObjectSvg(clone, context);
svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0]);
svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0]);
autoDestruct && destroyContext(context);
await onCreateForeignObjectSvg?.(svg);
return svg;
}
function createForeignObjectSvg(clone, context) {
const { width, height } = context;
const svg = createSvg(width, height, clone.ownerDocument);
const foreignObject = svg.ownerDocument.createElementNS(svg.namespaceURI, "foreignObject");
foreignObject.setAttributeNS(null, "x", "0%");
foreignObject.setAttributeNS(null, "y", "0%");
foreignObject.setAttributeNS(null, "width", "100%");
foreignObject.setAttributeNS(null, "height", "100%");
foreignObject.append(clone);
svg.appendChild(foreignObject);
return svg;
}
async function domToCanvas(node, options) {
const context = await orCreateContext(node, options);
const svg = await domToForeignObjectSvg(context);
const dataUrl = svgToDataUrl(svg, context.isEnable("removeControlCharacter"));
if (!context.autoDestruct) {
context.svgStyleElement = createStyleElement(context.ownerDocument);
context.svgDefsElement = context.ownerDocument?.createElementNS(XMLNS, "defs");
context.svgStyles.clear();
}
const image = createImage(dataUrl, svg.ownerDocument);
return await imageToCanvas(image, context);
}
async function domToBlob(node, options) {
const context = await orCreateContext(node, options);
const { log, type, quality, dpi } = context;
const canvas = await domToCanvas(context);
log.time("canvas to blob");
const blob = await canvasToBlob(canvas, type, quality);
if (["image/png", "image/jpeg"].includes(type) && dpi) {
const arrayBuffer = await blobToArrayBuffer(blob.slice(0, 33));
let uint8Array = new Uint8Array(arrayBuffer);
if (type === "image/png") {
uint8Array = changePngDpi(uint8Array, dpi);
} else if (type === "image/jpeg") {
uint8Array = changeJpegDpi(uint8Array, dpi);
}
log.timeEnd("canvas to blob");
return new Blob([uint8Array, blob.s