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. Qt5.9 UI设计(一)——在win10系统上安装Qt5.9

    安装环境 win10 系统 Qt5.9.0 版本 官方下载地址 可以从官方网站获取:https://www.qt.io/zh-cn/ 现在的安装包跟以前的有些区别,加上网络比较慢,我这里演示的是Qt5 ...

  2. [转帖]PD Control 使用说明

    https://docs.pingcap.com/zh/tidb/stable/pd-control PD Control 是 PD 的命令行工具,用于获取集群状态信息和调整集群. 安装方式   注意 ...

  3. Jmeter学习之五_跟踪被测试服务器的performance

    Jmeter学习之五_跟踪被测试服务器的performance 背景 这几天简单学习了一些基本的测试过程. 可以实现一些简单基本的功能了. 今天晚上继续进行了jmeter的一些学习. 想着可以在测试人 ...

  4. 【转帖】eBay 云计算“网”事|网络重传篇

    https://www.infoq.cn/article/iy1drmf6wk8up14dchq8/ #导读 在之前的eBay云计算"网"事|网络超时篇和eBay云计算" ...

  5. [转帖]linux 系统级性能分析工具 perf 的介绍与使用

    目录 1. 背景知识 1.1 tracepoints 1.2 硬件特性之cache 2. 主要关注点 3. perf的使用 3.0 perf引入的overhead 3.1 perf list 3.2 ...

  6. 基于CefSharp开发浏览器(十)浏览器CefSharp.Wpf中文输入法偏移处理

    一.前言 两年多来未曾更新博客,最近一位朋友向我咨询中文输入法问题.具体而言,他在使用CefSharp WPF版本时遇到了一个问题,即输入法突然出现在屏幕的左上角.在这里记录下处理这个问题的过程,希望 ...

  7. 【JS 逆向百例】拉勾网爬虫,traceparent、__lg_stoken__、X-S-HEADER 等参数分析

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后 ...

  8. ios马甲包过审

    说明:这篇文章写的比较早了,大概是2021年上半年写的,一直放在草稿箱,目前这些方法是否被屏蔽有待验证. App Store审核机制 机器审核 人工审核 人工审核大概是玩15分钟的样子,同时有上百审核 ...

  9. VRAR概念的定义和要素以及技术定义和应用

    1.概念 一.三个概念的定义和要素. 1.VR,Virtual Reality,虚拟现实 是一种通过计算机模拟真实感的图像,声音和其他感觉,从而复制出一个真实或者假想的场景,并且让人觉得身处这个场景之 ...

  10. 【3】opencv_contrib 4.3.0库配置+opencv安装

    相关文章: [1]windows下安装OpenCV(4.3)+VS2017安装+opencv_contrib4.3.0配置 [2]Visual Studio 2017同时配置OpenCV2.4 以及O ...