The author has open sourced a Web mind map [1], and made many detours when exporting it to pictures, so I will record it through this article.
The nodes and connections of the mind map are all rendered through svg
. As a pure js
library, we do not consider implementing it through the backend, so we can only think about how to do it through Pure front-end method to convert svg
or html
into images.
Use img tag combined with canvas to export
We all know that the img
tag can display svg
, and then canvas
can render img
, so is it just a matter of adding < Just render code>svg into the img
tag, and then export it as a picture through canvas
. The answer is yes.
const svgToPng = async (svgStr) => { //Convert to blob data let blob = new Blob([svgStr], { type: 'image/svg + xml' }) //Convert to data:url data let svgUrl = await blobToUrl(blob) //Draw to canvas let imgData = await drawToCanvas(svgUrl) // download downloadFile(imgData, 'picture.png') }
svgStr
is the svg
string to be exported, for example:
Then create a blob
data of type image/svg + xml
through the Blob[2] constructor, and then use the blob
Data is converted into data:URL
:
const blobToUrl = (blob) => { return new Promise((resolve, reject) => { let reader = new FileReader() reader.onload = evt => { resolve(evt.target.result) } reader.onerror = err => { reject(err) } reader.readAsDataURL(blob) }) }
In fact, it is a string in base64
format.
Next, you can load it through img
and render it into canvas
for export:
const drawToCanvas = (svgUrl) => { return new Promise((resolve, reject) => { const img = new Image() // Cross-domain images need to add this attribute, otherwise the canvas will be contaminated and the image cannot be exported. img.setAttribute('crossOrigin', 'anonymous') img.onload = async () => { try { let canvas = document.createElement('canvas') canvas.width = img.width canvas.height = img.height let ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, img.width, img.height) resolve(canvas.toDataURL()) } catch (error) { reject(error) } } img.onerror = e => { reject(e) } img.src = svgUrl }) }
The canvas.toDataURL()
method also returns a data:URL
string in base64
format:
Finally, you can download it through the a
tag:
const downloadFile = (file, fileName) => { let a = document.createElement('a') a.href = file a.download = fileName a.click() }
The implementation is very simple and the effect is good, but is this okay? Next, let’s insert two pictures to try.
Handling the situation where images exist
The first image is inserted using the data:URL
method of base64
, and the second image is inserted using the ordinary url
:
The export results are as follows:
As you can see, there is no problem with the first picture, but the second picture is cracked. Maybe you think it is a problem with the same-origin policy, but in fact, if you change it to a picture with the same origin, it is also cracked. The solution is very simple, traverse svg
node tree, just convert the images into the form of data:URL
:
//The @svgdotjs/svg.js library is used to operate svg const transfromImg = (svgNode) => { let imageList = svgNode.find('image') let task = imageList.map(async item => { // Get image url let imgUlr = item.attr('href') || item.attr('xlink:href') // Already in the form of data:URL, no need to convert if (/^data:/.test(imgUlr)) { return } // Convert and replace image url let imgData = await drawToCanvas(imgUlr) item.attr('href', imgData) }) await Promise.all(task) return svgNode.svg()// Return svg html string }
The previous drawToCanvas
method is used here to convert the image into data:URL
, so the export is normal:
At this point, converting pure svg
into images is basically no problem.
Handling the presence of foreignObject tag
svg
provides a foreignObject
tag that can be inserted into html
nodes. In fact, the author uses it to achieve the rich text editing effect of nodes:
Next, use the previous method to export. The results are as follows:
It clearly shows that there is no problem, but the content of foreignObject
is offset when exporting. Why is this? In fact, it is because of the default style problem. The page clears margin
and padding
, and set box-sizing
to border-box
:
Then there is no problem when svg
exists in the document tree, but the svg
string is used when exporting, which is separated from the document, so there is no style coverage. Then there will naturally be problems with the display. Knowing the reason, there are two solutions. One is to traverse all embedded html
nodes and manually add inline styles. Be sure to give all html< Adding all /code> nodes, only adding
svg
, foreignObject
or the outermost html
node will not work; the second is Directly add a style
tag in the foreignObject
tag, add styles through the style
tag, and only need to give one of the foreignObject tag. There are two methods, whichever you like. I use the second one:
const transformForeignObject = (svgNode) => { let foreignObjectList = svgNode.find('foreignObject') if (foreignObjectList.length > 0) { foreignObjectList[0].add(SVG(`<style> * { margin: 0; padding: 0; box-sizing: border-box; } </style>`)) } return svgNode.svg() }
The export results are as follows:
As you can see, everything is normal.
Regarding compatibility issues, the author tested the latest chrome
, firefox
, opera
, safari
, 360 Rapid Browser
runs normally.
Hit pit record
The previous introduction is the solution currently adopted by the author. The implementation is actually very simple, but the process is long and bumpy. Next, I will start my performance.
The content of the foreignObject tag cannot be displayed on the firefox browser
For the operation of svg
, I use the svg.js[3] library. The core code for creating rich text nodes is roughly as follows:
import { SVG, ForeignObject } from '@svgdotjs/svg.js' let html = `<div>Node text</div>` let foreignObject = new ForeignObject() foreignObject.add(SVG(html)) g.add(foreignObject)
The SVG
method is used to convert a html
string into a dom
node.
The rendering is normal on the chrome
browser and opera
browser, but the foreignObject
tag on the firefox
browser The content cannot be rendered at all:
I can't see any problems when I check the elements, and the magic is that as long as I edit the embedded html
content in the console element, it can be displayed. I searched around on Baidu and couldn't find a solution. method, and then because the browser share of firefox
was not high, this issue was shelved.
The content of the foreignObject tag in the image exported using img combined with canvas is empty
Although the chrome
browser renders normally:
However, when exported using the previous method, the content of the foreignObject
tag is empty as displayed in the firefox
browser:
firefox
couldn't bear this, so I tried to use some libraries that convert html
into images.
Use html2canvas, dom-to-image and other libraries
Using html2canvas
:
import html2canvas from 'html2canvas' const useHtml2canvas = async (svgNode) => { let el = document.createElement('div') el.style.position = 'absolute' el.style.left = '-9999999px' el.appendChild(svgNode) document.body.appendChild(el)//html2canvas conversion The nodes that need to be converted are in the document let canvas = await html2canvas(el, { backgroundColor: null }) mdocument.body.removeChild(el) return canvas.toDataURL() }
html2canvas
can be exported successfully, but there is a problem, that is, the text style in the foreignObject
tag will be lost:
This should be a bug[4] of html2canvas
, but look at the number of issues
and the submission record:
It is unrealistic to expect html2canvas
to change, so I tried to use dom-to-image
:
import domtoimage from 'dom-to-image' const dataUrl = domtoimage.toPng(el)
I found that dom-to-image
is even worse. The export is completely blank:
And it was last updated five or six years ago, so I have no choice but to go back to using html2canvas
.
Later, someone suggested using dom-to-image-more[5]. After a cursory look, it was modified based on the dom-to-image
library. Try After some research, I found that it was indeed possible, so I switched to using this library. Then someone reported that the exported node content was empty on some browsers, including firefox
and 360
. Even the previous version of chrome
doesn't work. The author can only lament that it is too difficult. Then someone suggested using the previous major version, which can solve the export problem on firefox
. But the author tried it and found that there were still problems on some other browsers, so I was considering whether to switch back to html2canvas
. Although it had certain problems, at least it was not completely empty.
Solve the problem that the content of the foreignObject tag cannot be displayed on the firefox browser
As more people use it, this problem has been raised again, so I tried to see if it can be solved. I always thought it was a problem with the firefox
browser. After all, in chrome
and opera
are normal. This time I wondered if it was a problem with the svgjs
library, so I searched for its issue
, to my surprise, I actually found issue[6]. The general idea is that the dom
node converted through the SVG
method is in svg
namespace, that is, created using the document.createElementNS
method, causing some browsers to fail to render. Ultimately, this is caused by different browsers’ different implementations of the specification. :
You said chrome
is very powerful, yes, but it virtually prevents problems from being exposed.
Once you know the reason, the modification is very simple. Just set the second parameter of the SVG
method to true
, or you can create the node yourself:
foreignObject.add(document.createElemnt('div'))
Sure enough, it rendered normally on the firefox
browser.
Solve the problem that the image exported by img combined with canvas is empty
After solving the problem of the foreignObject
tag being empty on the firefox
browser, you will naturally suspect that you used img
combined with canvas
When exporting pictures, is it possible that the foreignObject
tag is empty because of this problem? I also learned about the implementation principle of the dom-to-image
library and found that it also uses dom-to-image
. The code>dom node is added to the foreignObject
tag of svg
to convert html
into a picture, which would be very funny. The content I want to convert is a svg
embedded with the foreignObject
tag. Use dom-to-image
to convert it, and it will pass it again Its svg
is added to a foreignObject
tag. Isn't this a matryoshka doll? Since dom-to-image-more
can pass The foreignObject
tag is successfully exported, so it will definitely work without it. At this point, I am basically convinced that it didn't work before because of the namespace problem.
Sure enough, after removing the dom-to-image-more
library, the previous method was used to export successfully, and it was successfully exported in firefox
, chrome
, opera
, 360
and other browsers have no problems, and the compatibility is better than the dom-to-image-more
library.
Summary
Although the author's implementation is very simple, the library dom-to-image-more
actually has more than a thousand lines of code, so what does it do more? Please pay attention. Let's see next See you later in this article.
[1]
Web mind map: https://github.com/wanglin2/mind-map
[2]
Blob: https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/Blob
[3]
svg.js: https://github.com/svgdotjs/svg.js
[4]
bug: https://github.com/niklasvh/html2canvas/issues/2799
[5]
dom-to-image-more: https://github.com/1904labs/dom-to-image-more
[6]
issue: https://github.com/svgdotjs/svg.js/issues/1058