1636 lines
52 KiB
JavaScript
1636 lines
52 KiB
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.slice(33)], { type });
|
|
}
|
|
log.timeEnd("canvas to blob");
|
|
return blob;
|
|
}
|
|
|
|
async function domToDataUrl(node, options) {
|
|
const context = await orCreateContext(node, options);
|
|
const { log, quality, type, dpi } = context;
|
|
const canvas = await domToCanvas(context);
|
|
log.time("canvas to data url");
|
|
let dataUrl = canvas.toDataURL(type, quality);
|
|
if (["image/png", "image/jpeg"].includes(type) && dpi && SUPPORT_ATOB && SUPPORT_BTOA) {
|
|
const [format, body] = dataUrl.split(",");
|
|
let headerLength = 0;
|
|
let overwritepHYs = false;
|
|
if (type === "image/png") {
|
|
const b64Index = detectPhysChunkFromDataUrl(body);
|
|
if (b64Index >= 0) {
|
|
headerLength = Math.ceil((b64Index + 28) / 3) * 4;
|
|
overwritepHYs = true;
|
|
} else {
|
|
headerLength = 33 / 3 * 4;
|
|
}
|
|
} else if (type === "image/jpeg") {
|
|
headerLength = 18 / 3 * 4;
|
|
}
|
|
const stringHeader = body.substring(0, headerLength);
|
|
const restOfData = body.substring(headerLength);
|
|
const headerBytes = window.atob(stringHeader);
|
|
const uint8Array = new Uint8Array(headerBytes.length);
|
|
for (let i = 0; i < uint8Array.length; i++) {
|
|
uint8Array[i] = headerBytes.charCodeAt(i);
|
|
}
|
|
const finalArray = type === "image/png" ? changePngDpi(uint8Array, dpi, overwritepHYs) : changeJpegDpi(uint8Array, dpi);
|
|
const base64Header = window.btoa(String.fromCharCode(...finalArray));
|
|
dataUrl = [format, ",", base64Header, restOfData].join("");
|
|
}
|
|
log.timeEnd("canvas to data url");
|
|
return dataUrl;
|
|
}
|
|
|
|
async function domToSvg(node, options) {
|
|
const context = await orCreateContext(node, options);
|
|
const { width, height, ownerDocument } = context;
|
|
const dataUrl = await domToDataUrl(context);
|
|
const svg = createSvg(width, height, ownerDocument);
|
|
const svgImage = svg.ownerDocument.createElementNS(svg.namespaceURI, "image");
|
|
svgImage.setAttributeNS(null, "href", dataUrl);
|
|
svgImage.setAttributeNS(null, "height", "100%");
|
|
svgImage.setAttributeNS(null, "width", "100%");
|
|
svg.appendChild(svgImage);
|
|
return svgToDataUrl(svg, context.isEnable("removeControlCharacter"));
|
|
}
|
|
|
|
async function domToImage(node, options) {
|
|
const context = await orCreateContext(node, options);
|
|
const { ownerDocument, width, height, scale, type } = context;
|
|
const url = type === "image/svg+xml" ? await domToSvg(context) : await domToDataUrl(context);
|
|
const image = createImage(url, ownerDocument);
|
|
image.width = Math.floor(width * scale);
|
|
image.height = Math.floor(height * scale);
|
|
image.style.width = `${width}px`;
|
|
image.style.height = `${height}px`;
|
|
return image;
|
|
}
|
|
|
|
async function domToJpeg(node, options) {
|
|
return domToDataUrl(
|
|
await orCreateContext(node, { ...options, type: "image/jpeg" })
|
|
);
|
|
}
|
|
|
|
async function domToPixel(node, options) {
|
|
const context = await orCreateContext(node, options);
|
|
const canvas = await domToCanvas(context);
|
|
return canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data;
|
|
}
|
|
|
|
async function domToPng(node, options) {
|
|
return domToDataUrl(
|
|
await orCreateContext(node, { ...options, type: "image/png" })
|
|
);
|
|
}
|
|
|
|
async function domToWebp(node, options) {
|
|
return domToDataUrl(
|
|
await orCreateContext(node, { ...options, type: "image/webp" })
|
|
);
|
|
}
|
|
|
|
export { createContext, destroyContext, domToBlob, domToCanvas, domToDataUrl, domToForeignObjectSvg, domToImage, domToJpeg, domToPixel, domToPng, domToSvg, domToWebp, loadMedia, waitUntilLoad };
|