Scalable Vector Graphics (SVG) is based on the XML markup language and is used to describe two-dimensional vector graphics. As an open web standard based on text, SVG can elegantly and concisely render graphics of different sizes and seamlessly integrate with other web standards such as CSS, DOM, and JavaScript. SVG images and their related behaviors are defined in XML text files, which means they can be searched, indexed, scripted, and compressed. Additionally, this also means that SVG can be created and edited using any text editor and drawing software.
SVG stands for Scalable Vector Graphics, which is a standard XML-based markup language for describing two-dimensional vector graphics. Unlike pixel-based image formats such as JPEG and PNG, SVG uses mathematical equations and geometric descriptions to define images. This allows for lossless scaling and resizing without distortion or blurring. SVG images consist of basic shapes (such as lines, curves, rectangles, circles, etc.) and paths, and can also include elements such as text, gradients, patterns, and image clipping. SVG graphics can be manually created using a text editor or generated using professional vector graphics editing software. They can be directly embedded in web pages and controlled and interacted with using CSS stylesheets and JavaScript. Because SVG graphics are vector-based, they do not lose clarity when zoomed in or out, making SVG very useful in responsive design, icons, maps, data visualization, and animation. Additionally, SVG is compatible with various browsers and can seamlessly integrate with other web technologies.
SVG has many advantages and a universal standard, but it also has some limitations. Here, we mainly discuss the limitations of the text element in SVG. The text element in SVG provides basic text rendering capabilities and can draw single or multiple lines of text at specified positions. However, SVG does not provide powerful layout features like HTML and CSS, such as automatic text wrapping and alignment. This means that complex text layouts in SVG require manual calculation and adjustment of positions. Additionally, the text element in SVG supports some basic text style properties such as font size, color, and font weight. However, compared to the rich style options provided by CSS, SVG's text styles are relatively limited. For example, it cannot directly set text shadows or letter spacing effects.
In practice, we don't need to pay attention to these issues in our daily use. However, in some SVG-based visualization editors, such as DrawIO, these issues need to be taken seriously. Of course, nowadays, visual editing may choose to use Canvas to implement more, but the complexity of Canvas is beyond the scope of this article. So, the biggest problem with using text to draw text in daily use is actually text wrapping. If you only manually draw SVG in your daily life, there may not be any problems. The text element also provides a lot of properties to display text. However, it may be a bit troublesome to create a universal solution. For example, if I want to generate a batch of SVGs, it is not feasible to manually adjust the text separately. Of course, in this example, we can still calculate the width of the text in batches to control the line breaks, but what we hope for is a universal ability to solve this problem. Let's first look at an example of text overflow without automatic line wrapping:
In this example, the text
element cannot automatically wrap, even if the width
property is added to the text
element. In addition, the <text>
tag cannot be directly placed inside the <rect>
tag. It has strict nesting rules. The <text>
tag is an independent element used to draw text on the SVG canvas, while the <rect>
tag is used to draw rectangles. Therefore, the drawn rectangle does not limit the display range of the text. However, in reality, the length of this text exceeds the width: 300
set for the entire SVG element, which means that this text cannot be fully displayed. From the figure, it can also be seen that the text after wrap
is gone, and it does not automatically wrap. If you want to achieve a line break effect, you must manually calculate the length and height of the text to determine the position:
So, if you want to achieve a text drawing experience similar to HTML at a relatively low cost, you can use the foreignObject
element. The <foreignObject>
element allows embedding HTML, XML, or other non-SVG namespace content in an SVG document. In other words, we can directly embed HTML in SVG and use the capabilities of HTML to display our elements. For example, in the above example, we can transform it into the following form:
When we open DrawIO to draw flowcharts, we can actually find that it uses the <foreignObject>
element to draw text. Of course, DrawIO has made a lot of compatibility processing for more general scenarios, especially in terms of inline styles. Similar to the example above, the SVG displayed in DrawIO is as follows. It is important to note that the current file exported directly from DrawIO needs to be saved as an .html
file instead of an .svg
file because it does not declare a namespace. If you need to save it as an .svg
file and display it correctly, you need to add the namespace declaration xmlns="http://www.w3.org/2000/svg"
to the svg
element. However, adding only this declaration is not enough. If you open the .svg
file and find that only the rectangle is displayed without the text content, you also need to add the namespace declaration xmlns="http://www.w3.org/1999/xhtml"
to the first <div>
element of the <foreignObject>
element. At this time, the rectangle and text can be displayed completely.
Everything seems perfect. We can use SVG
to draw vector graphics and leverage the capabilities of HTML
to fulfill complex requirements. However, things always have two sides. While we enjoy convenience in one aspect, it may bring unexpected troubles in another. Let's imagine a scenario where we need to generate an SVG
on the backend and convert it to a PNG
image for user download. It is not practical to perform batch operations on the frontend. Furthermore, if we need to render the SVG
and insert it into a Word
or Excel
document, it requires us to fully draw the entire image on the backend. In such cases, we might consider using node-canvas
to create and manipulate graphics on the backend. However, when we actually use node-canvas
to draw our SVG
graphics, as in the example of DrawIO
mentioned above, we find that all the shapes can be drawn, but all the text is lost. Since node-canvas
cannot achieve the desired result, we might think of using sharp
for image processing, such as converting SVG
to PNG
. Unfortunately, sharp
also cannot accomplish this, and the final result is the same as node-canvas
.
Given that the requirement is there and the business strongly needs this functionality, how can we implement it? In fact, the solution to this problem is quite simple. Since the SVG
can only be rendered in a browser, we can simply run a Headless Chromium
on the backend. In this case, we can leverage Puppeteer
, which allows us to simulate user behavior in the browser programmatically, enabling tasks such as taking screenshots, generating PDFs, performing automated testing, and scraping data. With Puppeteer
, our task becomes much simpler. The main challenge lies in configuring the environment. Chromium
has specific environment requirements. For example, the latest version of Chromium
in the Debian series requires Debian 10
or above, and dependencies need to be installed. You can use the command ldd xxxx/chrome | grep no
to check for missing dynamic link libraries. If you encounter installation issues, you can retry with node node_modules/puppeteer/install.js
. Additionally, there may be font-related issues because text rendering is done on the backend, so the server itself needs to have certain Chinese fonts installed, such as Source fonts-noto-cjk
and Chinese language packs like language-pack-zh*
, etc.
Once we have set up the environment, the next step is to render the SVG
and convert it to a Buffer
. This task is relatively simple. We just need to render the SVG
in our Headless Chromium
and take a screenshot of the Viewport
. Puppeteer
provides a simple API with many methods. Here is an example. Moreover, Puppeteer
has many other capabilities, such as exporting to PDF
, but we won't go into detail here.
Let's think about it, the foreignObject
element seems to be a very magical design. With the foreignObject
element, we can draw HTML
into an SVG
. So, do we have a very magical idea? If we need to draw the DOM
in the browser and achieve a screenshot-like effect, can we use the foreignObject
element to achieve it? This is certainly feasible, and it is a very interesting thing. We can draw DOM + CSS
into an SVG
, then convert it to a DATA URL
, and finally draw it using canvas
. In the end, we can generate and export the image of the DOM
.
Below is an implementation of this capability. Of course, the implementation here is relatively simple. The main part is to clone
the DOM
and inline all the styles to generate a complete SVG
image. In fact, there are many things to consider, such as generating pseudo-elements, declaring @font-face
fonts, converting BASE64
encoded content, converting img
elements to CSS background
properties, and so on. To implement the entire functionality more completely, you need to consider many cases. Here, we will not go into the specific implementation, but you can refer to dom-to-image-more
.