Discover how to export html and svg as images

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:

583816e2275beed9e9c40f1276defca1.png

image-20230821105935068.png

Then create a blob data of type image/svg + xml through the Blob[2] constructor, and then use the blobData 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)
    })
}

bf33022061736437cc60fb4c96feda31.png

image-20230821111615758.png

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:

8d081811733173fdf9e23a99bf556431.png

image-20230821112646954.png

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()
}

f95555de0dd32f111df4d66eba562ee3.png

image-20230821112831650.png

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

71d2006ccc9825e1a054260bbbf89cc7.png

image-20230821133232510.png

The first image is inserted using the data:URL method of base64, and the second image is inserted using the ordinary url:

8134e6b2373bb77876d8afbf2a524c6f.png

image-20230821133435201.png

The export results are as follows:

d35e1a7e2aa27c5fb3ef171af5a5ba63.png

image-20230821133613043.png

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:

d37044d414b4b567608aec33d37e1050.png

image-20230821134654842.png

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:

09707dfbf6fcf32cd056be207f39e483.png

image-20230821135644629.png
0fa94455ba0157ae3cdf3c49553f2007.png
image-20230821140215055.png

Next, use the previous method to export. The results are as follows:

7306d46913f198ceab51963f8018bc8f.png

image-20230821140115427.png

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:

8e2e6d76a3f1f6349666ebd5259a9db2.png

image-20230821143605226.png

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:

a99db9cd69528279821b2bfab45c24ad.png

image-20230821144958580.png

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:

004d96dcfd4071129c2e294b398d63a6.png

image-20230821153348800.png

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:

6f0eb551294653b5226bb5a5cc514264.png

image-20230821153828776.png

However, when exported using the previous method, the content of the foreignObject tag is empty as displayed in the firefox browser:

740d0b8b97c0f312ca279ead28a0fdeb.png

image-20230821154023987.png

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:

1e5b1daf3f578af787bbc2b1178650ca.png

image-20230821155328598.png

This should be a bug[4] of html2canvas, but look at the number of issues and the submission record:

ecf3830dc650367d8faea10fc504320f.png

image-20230821161649150.png

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:

ba9752f9d3c9e7ce340c5c78c19822dd.png

image-20230821161939970.png

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. :

891ec4d8fef0138ac0c946b7b9d0cc47.png

image-20230821182444826.png

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 canvasWhen 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

syntaxbug.com © 2021 All Rights Reserved.