SVG与foreignObject元素

可缩放矢量图形Scalable Vector Graphics - SVG基于XML标记语言,用于描述二维的矢量图形。作为一个基于文本的开放网络标准,SVG能够优雅而简洁地渲染不同大小的图形,并和CSSDOMJavaScript等其他网络标准无缝衔接。SVG图像及其相关行为被定义于XML文本文件之中,这意味着可以对其进行搜索、索引、编写脚本以及压缩,此外这也意味着可以使用任何文本编辑器和绘图软件来创建和编辑SVG

SVG

SVG是可缩放矢量图形Scalable Vector Graphics的缩写,其是一种用于描述二维矢量图形的XML可扩展标记语言标准,与基于像素的图像格式(如JPEGPNG)不同,SVG使用数学方程和几何描述来定义图像,这使得其能够无损地缩放和调整大小,而不会失真或模糊。SVG图像由基本形状(如线段、曲线、矩形、圆形等)和路径组成,还可以包含文本、渐变、图案和图像剪裁等元素。SVG图形可以使用文本编辑器手动创建,也可以使用专业的矢量图形编辑软件生成,其可以在Web页面上直接嵌入,也可以通过CSS样式表和JavaScript进行控制和交互,由于SVG图形是基于矢量的,因此在放大或缩小时不会失去清晰度,这使得SVG在响应式设计、图标、地图、数据可视化和动画等领域中非常有用。此外SVG还兼容支持各种浏览器,并且可以与其他Web技术无缝集成。

SVG有着诸多优点,并且拥有通用的标准,但是也存在一些限制,那么在这里我们主要讨论SVGtext元素也就是文本元素的一些局限。SVGtext元素提供了基本的文本渲染功能,可以在指定位置绘制单行或多行文本,然而SVG并没有提供像HTMLCSS中的强大布局功能,比如文本自动换行、对齐方式等,这意味着在SVG中实现复杂的文本布局需要手动计算和调整位置。此外SVGtext元素支持一些基本的文字样式属性,如字体大小、颜色、字体粗细等,然而相对于CSS提供的丰富样式选项,SVG的文字样式相对有限,例如无法直接设置文字阴影、文字间距等效果等。

实际上在平时使用中我们并不需要关注这些问题,但是在一些基于SVG的可视化编辑器中比如DrawIO中这些就是需要重视的问题了,当然现在可能可视化编辑更多的会选择使用Canvas来实现,但是这个复杂度非常高,就不在本文讨论范围内了。那么如果使用text来绘制文本在日常使用中最大的问题实际上就是文本的换行,如果只是平时人工来绘制SVG可能并没有什么问题,text同样提供了大量的属性来展示文本,但是想做一个通用的解决方案可能就麻烦一点了,举个例子如果我想批量生成一些SVG,那么人工单独调整文本是不太可能的,当然在这个例子中我们还是可以批量去计算文字宽度来控制换行的,但是我们更希望的是有一种通用的能力来解决这个问题。我们可以先来看看文本溢出不自动换行的例子:

-----------------------------------
| This is a long text that cannot | automatically wrap
| |
| |
-----------------------------------
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
<g>
<rect width="200" height="100" fill="lightgray" />
<text x="10" y="20" font-size="12" fill="black">
This is a long text that cannot automatically wrap within the rectangle.
</text>
</g>
</svg>

在这个例子中,text元素是无法自动换行的,即使在text元素上添加width属性也是无法实现这个效果的。此外<text>标签不能直接放在<rect>标签内部,其具有严格的嵌套规则,<text>标签是一个独立的元素,用于在SVG画布上绘制文本,而<rect>标签是用于绘制矩形的元素,所以绘制的矩形并没有限制文本展示范围,但是实际上这个文本的长度是超出了整个SVG元素设置的width: 300,也就是说这段文本实际上是没有能够完全显示出来,从图中也可以看出wrap之后的文本没有了,并且其并没有能够自动换行。如果想实现换行效果,则必须要自行计算文本长度与高度进行切割来计算位置:

-----------------------------------
| This is a long text that |
| cannot automatically wrap |
| within the rectangle. |
-----------------------------------
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
<g>
<rect width="200" height="100" fill="lightgray" />
<text x="10" y="20" font-size="12" fill="black">
<tspan x="10" dy="1.2em">This is a long text that</tspan>
<tspan x="10" dy="1.2em">cannot automatically wrap </tspan>
<tspan x="10" dy="1.2em">within the rectangle.</tspan>
</text>
</g>
</svg>

foreignObject元素

那么如果想以比较低的成本实现接近于HTML的文本绘制体验,可以借助foreignObject元素,<foreignObject>元素允许在SVG文档中嵌入HTMLXML或其他非SVG命名空间的内容,也就是说我们可以直接在SVG中嵌入HTML,借助HTML的能力来展示我们的元素,例如上边的这个例子,我们就可以将其改造为如下的形式:

-----------------------------------
| This is a long text that |
| will automatically wrap |
| within the rectangle. |
-----------------------------------
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
<g>
<rect width="200" height="100" fill="lightgray" />
<foreignObject x="10" width="180" height="80">
<div xmlns="http://www.w3.org/1999/xhtml">
<p>This is a long text that will automatically wrap within the rectangle.</p>
</div>
</foreignObject>
</g>
</svg>

当我们打开DrawIO绘制流程图时,其实也能发现其在绘制文本时使用的就是<foreignObject>元素,当然DrawIO为了更通用的场景做了很多兼容处理,特别是表现在行内样式上,类似于上述例子中的SVGDrawIO表现出来是如下的示例,需要注意的是,直接从DrawIO导出的当前这个文件需要保存为.html文件而不是.svg文件,因为其没有声明命名空间,如果需要要保存为.svg文件并且能够正常展示的话,需要在svg元素上加入xmlns="http://www.w3.org/2000/svg"命名空间声明,但是仅仅加上这一个声明是不够的,如果此时打开.svg文件发现只展示了矩形而没有文字内容,此时我们还需要在<foreignObject>元素的第一个<div>上加入xmlns="http://www.w3.org/1999/xhtml"的命名空间声明,此时就可以将矩形与文字完整地表现出来。

-----------------------------------------------------
| This is a long text that will automatically wrap |
| within the rectangle. |
-----------------------------------------------------
<svg
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="263px"
height="103px"
viewBox="-0.5 -0.5 263 103"
>
<defs></defs>
<g>
<rect
x="1"
y="1"
width="260"
height="100"
fill="#ffffff"
stroke="#000000"
pointer-events="all"
></rect>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject
style="overflow: visible; text-align: left;"
pointer-events="none"
width="100%"
height="100%"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
>
<div style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 258px; height: 1px; padding-top: 51px; margin-left: 2px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<div>
<span>
This is a long text that will automatically wrap within the rectangle.
</span>
</div>
</div>
</div>
</div>
</foreignObject>
<text
x="131"
y="55"
fill="#000000"
font-family="Helvetica"
font-size="12px"
text-anchor="middle"
>
This is a long text that will automatically...
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"></g>
<a
transform="translate(0,-5)"
xlink:href="https://desk.draw.io/support/solutions/articles/16000042487"
target="_blank"
>
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>
</a>
</switch>
</svg>

看起来一切都很完美,我们既能够借助SVG绘制矢量图形,又能够在比较复杂的情况下借助HTML的能力完成需求,但是事情总有两面性,当我们在某一方面享受到便利的时候,就可能在另一处带来意想不到的麻烦。设想一个场景,假设此时我们需要在后端将SVG绘制出来,然后将其转换为PNG格式的图片给予用户下载,在前端做一些批量的操作是不太现实的,再假设我们需要将这个SVG绘制出来拼接到Word或者Excel中,那么这些操作都要求我们需要在后端完整地将整个图片绘制出来,那么此时我们可能会想到node-canvas在后端创建和操作图形,但是当我们真的使用node-canvas绘制我们的SVG图形时例如上边的DrawIO的例子,会发现所有的图形形状是可以被绘制出来的,但是所有的文本都丢失了,那么既然node-canvas做不到,那么我们可能会想到sharp来完成图像处理的相关功能,例如先将SVG转换为PNG,但是很遗憾的是sharp也做不到这一点,最终效果与node-canvas是一致的。

https://github.com/lovell/sharp/issues/3668
https://github.com/Automattic/node-canvas/issues/1325

那么既然需求摆在这,而业务上又非常需要这个功能,那么我们应该如何实现这个能力呢,实际上这个问题最终的结局方案反而很简单,既然这个SVG只能在浏览器中绘制,那么我们直接在后端运行一个Headless Chromium就可以了。那么此时我们就可以借助PuppeteerPuppeteer允许我们以编程方式模拟用户在浏览器中的行为,进行网页截图、生成PDF、执行自动化测试、进行数据抓取等任务。那么此时我们的任务就变得简单许多了,主要的麻烦是配置环境,Chromium是有环境要求的,例如在Debian系列的最新版Chromium就需要Debian 10以上的环境,并且还需要安装依赖,可以借助ldd xxxx/chrome | grep no命令来检查未安装的动态链接库。如果碰到安装问题,也可以node node_modules/puppeteer/install.js进行重试,此外还有一些字体的问题,因为是在后端将文本渲染出来的,就需要服务器本身安装一些中文字体,例如思源fonts-noto-cjk、中文语言包language-pack-zh*等等。

那么在我们将环境搭建好了之后,后续就是要将SVG渲染并且转换为Buffer了,这个工作实际上比较简单,只需要在我们的Headless Chromium中将SVG渲染出来,并且将ViewPort截图即可,Puppeteer提供的API比较简单,并且方法有很多,下边是一个例子,此外Puppeteer能够实现的能力还有很多,比如导出PDF等,在这里就不展开了。

const puppeteer = require('puppeteer');
// 实际上可以维护单实例的`browser`对象
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
// 同样也可以维护单实例的`page`对象
const page = await browser.newPage();
// 如果有视窗长宽的话可以直接设置
// 否则先绘制`SVG`获取视窗长宽之后再设置视窗的大小也可以
await page.setViewport({
width: 1000,
height: 1000,
deviceScaleFactor: 3, // 不设置则会导致截图模糊
});
await page.setContent(svg);
const element = await page.$('svg');
let buffer: Buffer | null = null;
if(element){
const box = await element.boundingBox();
if(box){
buffer = await page.screenshot({
clip: {
x: box.x,
y: box.y,
width: box.width,
height: box.height,
},
type: 'png',
omitBackground: true,
});
}
}
await page.close();
await browser.close();
return buffer;

DOM TO IMAGE

让我们想一想,foreignObject元素看起来是个非常神奇的设计,通过foreignObject元素我们可以把HTML绘制到SVG当中,那么我们是不是可以有一个非常神奇的点子,如果我们此时需要将浏览器当中的DOM绘制出来,实现于类似于截图的效果,那么我我们是不是就可以借助foreignObject元素来实现呢。这当然是可行的,而且是一件非常有意思的事情,我们可以将DOM + CSS绘制到SVG当中,紧接着将其转换为DATA URL,借助canvas将其绘制出来,最终我们就可以将DOM生成图像以及导出了。

下面就是个这个能力的实现,当然在这里的实现还是比较简单的,主要处理的部分就是将DOM进行clone以及样式全部内联,由此来生成完整的SVG图像。实际上这其中还有很多需要注意的地方,例如生成伪元素、@font-face字体的声明、BASE64编码的内容、img元素到CSS background属性的转换等等,想要比较完整地实现整个功能还是需要考虑到很多case的,在这里就不涉及具体的实现了,可以参考dom-to-image-more

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DOM IMAGE</title>
<style>
#root {
width: 300px;
border: 1px solid #eee;
} .list > .list-item {
display: flex;
background-color: #aaa;
color: #fff;
align-items: center;
justify-content: space-between;
}
</style>
</head>
<body>
<!-- #root START -->
<!-- `DOM`内容-->
<div id="root">
<h1>Title</h1>
<hr />
<div>Content</div>
<div class="list">
<div class="list-item">
<span>label</span>
<span>value</span>
</div>
<div class="list-item">
<span>label</span>
<span>value</span>
</div>
</div>
</div>
<!-- #root END -->
<button onclick="onDOMToImage()">下载</button>
</body>
<script>
const cloneCSS = (target, origin) => {
const style = window.getComputedStyle(origin);
// 生成所有样式表
const cssText = Array.from(style).reduce((acc, key) => {
return `${acc}${key}:${style.getPropertyValue(key)};`;
}, "");
target.style.cssText = cssText;
}; const cloneDOM = (origin) => {
const target = origin.cloneNode(true);
const targetNodes = target.querySelectorAll("*");
const originNodes = origin.querySelectorAll("*");
// 复制根节点样式
cloneCSS(target, origin);
// 复制所有节点样式
Array.from(targetNodes).forEach((node, index) => {
cloneCSS(node, originNodes[index]);
});
// 去除元素的外边距
target.style.margin =
target.style.marginLeft =
target.style.marginTop =
target.style.marginBottom =
target.style.marginRight =
"";
return target;
}; const buildSVGUrl = (node, width, height) => {
const xml = new XMLSerializer().serializeToString(node);
const data = `
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<foreignObject width="100%" height="100%">
${xml}
</foreignObject>
</svg>
`;
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(data);
}; const onDOMToImage = () => {
const origin = document.getElementById("root");
const { width, height } = root.getBoundingClientRect();
const target = cloneDOM(origin);
const data = buildSVGUrl(target, width, height);
const image = new Image();
image.crossOrigin = "anonymous";
image.src = data;
image.onload = () => {
const canvas = document.createElement("canvas");
// 值越大像素越高
const ratio = window.devicePixelRatio || 1;
canvas.width = width * ratio;
canvas.height = height * ratio;
const ctx = canvas.getContext("2d");
ctx.scale(ratio, ratio);
ctx.drawImage(image, 0, 0);
const a = document.createElement("a");
a.href = canvas.toDataURL("image/png");
a.download = "image.png";
a.click();
};
};
</script>
</html>

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://github.com/jgraph/drawio
https://github.com/pbakaus/domvas
https://github.com/puppeteer/puppeteer
https://www.npmjs.com/package/dom-to-image-more
https://developer.mozilla.org/zh-CN/docs/Web/SVG
https://zzerd.com/blog/2021/04/10/linux/debian_install_puppeteer
https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/foreignObject
https://developer.mozilla.org/en-US/docs/Web/SVG/Namespaces_Crash_Course

SVG与foreignObject元素的更多相关文章

  1. [翻译svg教程]Path元素 svg中最神奇的元素!

    先看一个实例 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999 ...

  2. [翻译svg教程]svg中矩形元素 rect

    svg 元素<rect> 是一个矩形元素,用这个元素,可以你可以绘制矩形,设置矩形宽高,边框的宽度颜色,矩形的填充颜色,是否用圆角等 rect 示例 <svg xmlns=" ...

  3. SVG中的元素属性

    SVG attributes by category Animation event attributes onbegin, onend, onload, onrepeat Animation att ...

  4. D3js怎么获得SVG及其子元素在屏幕中的坐标

    var clientRects = svg.select("image").node().getBoundingClientRect(); var coordinates = [ ...

  5. 用SVG绕过浏览器XSS审计

    [Translated From]:http://insert-script.blogspot.com/2014/02/svg-fun-time-firefox-svg-vector.html === ...

  6. o'Reill的SVG精髓(第二版)学习笔记——第二章

    在网页中使用SVG 将SVG作为图像: SVG是一种图像格式,因此可以使用与其他图像类型相同的方式包含在HTML页面中,具体可以采用两种方法:将图像包含在HTML标记的<img>元素内(当 ...

  7. HTML5学习笔记简明版(1):HTML5介绍与语法

    HTML5介绍 HTML5是继HTML4以后的下一代HTML标准规范,它提供了一些新的元素和属性(例如<nav>网站导航块和<footer>).新型的标签有利于搜索引擎和语义分 ...

  8. CSS遮罩——如何在CSS中使用遮罩

    Css遮罩是2008年4月由苹果公司添加到webkit引擎中的.遮罩提供一种基于像素级别的,可以控制元素透明度的能力,类似于png24位或png32位中的alpha透明通道的效果. 图像是由rgb三个 ...

  9. [转]CSS遮罩——如何在CSS中使用遮罩

    特别声明:此篇文章由D姐根据Christian Schaefer的英文文章原名<CSS Masks – How To Use Masking In CSS Now>进行翻译,整个译文带有我 ...

  10. css Masks

    css Masks:添加蒙板: 测试在微信端可以支持了.谷歌浏览器支持.safari应该也是支持的. 效果:http://runjs.cn/code/xrrgmgmk 但是谷歌可以支持这样子的:htt ...

随机推荐

  1. Icoding 链表 删除范围内结点

    1.题目: 已知线性表中的元素(整数)以值递增有序排列,并以单链表作存储结构.试写一高效算法,删除表中所有大于mink且小于maxk的元素(若表中存在这样的元素),分析你的算法的时间复杂度. 链表结点 ...

  2. 痞子衡嵌入式:我拿到了2023年度电子星球(eestar)年度黑马作者

    今天收到了「电源网旗下电子星球」 颁发的 2023 年度黑马作者奖牌,这是痞子衡继 2019 年和与非网合作后的第二个媒体平台颁发的奖项.这个奖牌做得很有质感,拿在手里沉甸甸的.此外与奖牌配套的还有一 ...

  3. [转帖]linux audit审计(8)--开启audit对系统性能的影响

    https://www.cnblogs.com/xingmuxin/p/8875783.html 我们使用测试性能的工具,unixbench,它有一下几项测试项目: Execl Throughput ...

  4. Kafka的部分初始化参数的学习与整理

    Kafka的部分初始化参数的学习与整理 背景 前段时间跟同事一起处理过kafka的topic offset的retention 时间与 log 的retention时间不一致. 导致消息还有, 但是o ...

  5. [转帖]redis缓存命中率介绍

    缓存命中率的介绍 命中:可以直接通过缓存获取到需要的数据. 不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其它的操作.原因可能是由于缓存中根本不存在,或者缓存已经过期. 通常来讲 ...

  6. [转帖]RocksDB 简介

    https://docs.pingcap.com/zh/tidb/stable/rocksdb-overview RocksDB 是由 Facebook 基于 LevelDB 开发的一款提供键值存储与 ...

  7. 【转帖】Java Full GC (Ergonomics) 的排查

    文章目录 1. Full GC (Ergonomics) 1.1 Java 进程一直进行 Full GC 1.2 Full GC 的原因 1.3 检查堆占用 2. 代码检查 3. 解决方式 1. Fu ...

  8. [转帖]IPv6地址解析库,窥探IPv6地址中包含的信息

    https://zhuanlan.zhihu.com/p/479028720 大家好,我是明说网络的小明同学. 今天和大家介绍一个IPv6 地址解析库IPv6 address Parser :http ...

  9. [转帖]kubelet 原理解析五: exec的背后

    https://segmentfault.com/a/1190000022163850 概述 线上排查pod 问题一般有两种方式,kubectl log或者kubectl exec调试.如果你的 lo ...

  10. CentOS firewall简单总结

    CentOS firewall简单总结 简介 防火墙是安全的重要道防线. 硬件防火墙一般部署再内网的边界区域.作为最外层的防护. 但是一般硬件的防火墙会比较宽松. 不然会导致很多业务不可用 软件防火墙 ...