Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.
使用svg的一个特性,允许在<foreignobject>标签中包含任意的html内容。
-
dom 转换为 XHTML
-
XMLSerializer 对象使你能够把一个 XML 文档或 Node 对象转化或“序列化”为未解析的 XML 标记的一个字符串要使用一个 XMLSerializer,使用不带参数的构造函数实例化它 ,然后调用其 serializeToString() 方法;
-
递归去克隆dom节点
- 遇到canvas转为image对象
- 提取元素computed样式,并且插到新建的style标签上面,对于":before,:after"这些伪元素,会提取其样式,放到新建样式名中并且插入到新建的style标签中,供所属的节点使用。
- 处理输入内容和svg。
-
插入字体
- 获取所有样式表并处理为数组,提取包含 rule.type === CSSRule.FONT_FACE_RULE 规则,再提取包含 src 的 rules。
- 下载资源,将资源转为dataUrl并给 src 使用。
-
处理图片 图片都处理为dataUrl
-
示例一
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body onload="load()"> <div id="long"> <span>1111</span> <h1>222222</h1> hello SeriousLose</div> </body> </html> <script> function load() { let long = document.getElementById('long'); let xmlStr = new XMLSerializer(); let xml = xmlStr.serializeToString(long); console.log(xml); let dom = `data:image/svg+xml;charset=utf-8,<svg viewBox="0 0 200 200" xmlns="<http://www.w3.org/2000/svg>"> <style> polygon { fill: black } div { color: white; font:12px serif; height: 100%; overflow: auto; } </style> <polygon points="5,5 195,10 185,185 10,195" /> <!-- Common use case: embed HTML text into SVG --> <foreignObject x="20" y="20" width="160" height="160"> ${xml} </foreignObject> </svg>`; let img = new Image(); img.src = dom; document.body.appendChild(img); img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; ctx.drawImage(img, 0, 0); canvas.toBlob(function (blob) { let url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; document.body.appendChild(a); a.href = url; a.download = 'image'; a.click(); window.URL.revokeObjectURL(url); }, 'image/png'); }; } </script>
-
-
使用svg中的<foreignobject>
- 序列化dom节点为字符串,然后在 foreignObject 嵌入转换好的字符串,
- foreignObject 能够在 svg 内部嵌入XHTML,再将svg处理为dataUrl数据
-
用 canvas 渲染出处理好的 dataUrl 数据
- 新建img
- img赋值已经组合好的dataUrl;
- img 转化为 canvas;
- canvas.drawImage(image, dx, dy)
- CanvasRenderingContext2D.drawImage() - Web API 接口参考 | MDN
-
canvas转化为 bold类型;
- canvas.toBlob(callback, type, encoderOptions);
- HTMLCanvasElement.toBlob() - Web API 接口参考 | MDN
源码地址 dom-to-image/dom-to-image.js at master · tsayen/dom-to-image · GitHub
// toPng 方法
function toPng(node, options) {
return draw(node, options || {}).then(function (canvas) {
return canvas.toDataURL();
});
}
// draw 方法
function draw(domNode, options) {
// 将dom节点转为svg
return toSvg(domNode, options)
// 拿到的svg是image data URL,这里进一步通过svg创建图片
.then(util.makeImage)
.then(util.delay(100))
.then(function (image) {
// 通过图片创建canvas并返回
var canvas = newCanvas(domNode);
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
});
// 新建canvas节点,处理dataUrl资源,和options参数
function newCanvas(domNode) {
var canvas = document.createElement("canvas");
canvas.width = options.width || util.width(domNode);
canvas.height = options.height || util.height(domNode);
if (options.bgcolor) {
var ctx = canvas.getContext("2d");
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
}
// toSvg
function toSvg(node, options) {
options = options || {};
copyOptions(options);
return Promise.resolve(node)
.then(function (node) {
// 递归克隆dom节点
return cloneNode(node, options.filter, true);
})
// 嵌入字体,找出所有font-face样式,添加入一个新的style里面
.then(embedFonts)
// 将图片链接转换为dataUrl形式使用
.then(inlineImages)
// 将options里面的一些style放进style里面
.then(applyOptions)
.then(function (clone) {
// 创建svg,将dom节点通过 XMLSerializer().serializeToString() 序列化为字符串
// 然后用 foreignObject 包裹,就能将dom转为svg。
return makeSvgDataUri(
clone,
options.width || util.width(node),
options.height || util.height(node)
);
});
// 处理一些options的样式
function applyOptions(clone) {
if (options.bgcolor) clone.style.backgroundColor = options.bgcolor;
if (options.width) clone.style.width = options.width + "px";
if (options.height) clone.style.height = options.height + "px";
if (options.style)
Object.keys(options.style).forEach(function (property) {
clone.style[property] = options.style[property];
});
return clone;
}
}
// cloneNode
function cloneNode(node, filter, root) {
if (!root && filter && !filter(node)) return Promise.resolve();
return Promise.resolve(node)
.then(makeNodeCopy)
.then(function (clone) {
return cloneChildren(node, clone, filter);
})
.then(function (clone) {
return processClone(node, clone);
});
function makeNodeCopy(node) {
// 遇到canvas转为image对象
if (node instanceof HTMLCanvasElement)
return util.makeImage(node.toDataURL());
// 克隆第一层
return node.cloneNode(false);
}
// 克隆子节点
function cloneChildren(original, clone, filter) {
var children = original.childNodes;
if (children.length === 0) return Promise.resolve(clone);
return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
function () {
return clone;
}
);
// 递归克隆
function cloneChildrenInOrder(parent, children, filter) {
var done = Promise.resolve();
children.forEach(function (child) {
done = done
.then(function () {
return cloneNode(child, filter);
})
.then(function (childClone) {
if (childClone) parent.appendChild(childClone);
});
});
return done;
}
}
function processClone(original, clone) {
if (!(clone instanceof Element)) return clone;
return Promise.resolve()
.then(cloneStyle)
.then(clonePseudoElements)
.then(copyUserInput)
.then(fixSvg)
.then(function () {
return clone;
});
// 克隆节点上面所有使用的样式。
function cloneStyle() {
// 顺便提提,为什么不用style,因为如果什么样式也没有设置的话,style是光秃秃的
// 而getComputedStyle则能获取到应用在节点上面所有样式
copyStyle(window.getComputedStyle(original), clone.style);
function copyStyle(source, target) {
if (source.cssText) target.cssText = source.cssText;
else copyProperties(source, target);
function copyProperties(source, target) {
util.asArray(source).forEach(function (name) {
target.setProperty(
name,
source.getPropertyValue(name),
source.getPropertyPriority(name)
);
});
}
}
}
// 提取伪类样式,放到css
function clonePseudoElements() {
[":before", ":after"].forEach(function (element) {
clonePseudoElement(element);
});
function clonePseudoElement(element) {
var style = window.getComputedStyle(original, element);
var content = style.getPropertyValue("content");
if (content === "" || content === "none") return;
var className = util.uid();
clone.className = clone.className + " " + className;
var styleElement = document.createElement("style");
styleElement.appendChild(
formatPseudoElementStyle(className, element, style)
);
clone.appendChild(styleElement);
function formatPseudoElementStyle(className, element, style) {
var selector = "." + className + ":" + element;
var cssText = style.cssText
? formatCssText(style)
: formatCssProperties(style);
return document.createTextNode(selector + "{" + cssText + "}");
function formatCssText(style) {
var content = style.getPropertyValue("content");
return style.cssText + " content: " + content + ";";
}
function formatCssProperties(style) {
return util.asArray(style).map(formatProperty).join("; ") + ";";
function formatProperty(name) {
return (
name +
": " +
style.getPropertyValue(name) +
(style.getPropertyPriority(name) ? " !important" : "")
);
}
}
}
}
}
// 处理输入内容
function copyUserInput() {
...
}
// 处理svg,创建命名空间
function fixSvg() {
if (!(clone instanceof SVGElement)) return;
clone.setAttribute("xmlns", "<http://www.w3.org/2000/svg>");
...
}
}
}
// makeSvgDataUri
function makeSvgDataUri(node, width, height) {
return (
Promise.resolve(node)
.then(function (node) {
// 将dom转换为字符串
node.setAttribute("xmlns", "<http://www.w3.org/1999/xhtml>");
return new XMLSerializer().serializeToString(node);
})
.then(util.escapeXhtml)
.then(function (xhtml) {
return (
'<foreignObject x="0" y="0" width="100%" height="100%">' +
xhtml +
"</foreignObject>"
);
})
/**
* 顺带提一提
* 不指定xmlns命名空间是不会渲染的
* xmlns="<http://www.w3.org/2000/svg>"
*/
.then(function (foreignObject) {
return (
'<svg xmlns="<http://www.w3.org/2000/svg>" width="' +
width +
'" height="' +
height +
'">' +
foreignObject +
"</svg>"
);
})
.then(function (svg) {
return "data:image/svg+xml;charset=utf-8," + svg;
})
);
}