乐趣区

关于前端:探索如何将html和svg导出为图片

笔者开源了一个 Web 思维导图,在做导出为图片的性能时走了挺多弯路,所以通过本文来记录一下。

思维导图的节点和连线都是通过 svg 渲染的,作为一个纯 js 库,咱们不思考通过后端来实现,所以只能思考如何通过纯前端的形式来实现将 svghtml转换为图片。

应用 img 标签联合 canvas 导出

咱们都晓得 img 标签能够显示 svg,而后 canvas 又能够渲染 img,那么是不是只有将 svg 渲染到 img 标签里,再通过 canvas 导出为图片就能够呢,答案是必定的。

const svgToPng = async (svgStr) => {
    // 转换成 blob 数据
    let blob = new Blob([svgStr], {type: 'image/svg+xml'})
    // 转换成 data:url 数据
    let svgUrl = await blobToUrl(blob)
    // 绘制到 canvas 上
    let imgData = await drawToCanvas(svgUrl)
    // 下载
    downloadFile(imgData, '图片.png')
}

svgStr是要导出的 svg 字符串,比方:

而后通过 Blob 构造函数创立一个类型为 image/svg+xmlblob数据,接下来将 blob 数据转换成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)
    })
}

其实就是 base64 格局的字符串。

接下来就能够通过 img 来加载,并渲染到 canvas 里进行导出:

const drawToCanvas = (svgUrl) => {return new Promise((resolve, reject) => {const img = new Image()
      // 跨域图片须要增加这个属性,否则画布被净化了无奈导出图片
      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
    })
}

canvas.toDataURL()办法返回的也是一个 base64 格局的 data:URL 字符串:

最初就能够通过 a 标签来下载:

const downloadFile = (file, fileName) => {let a = document.createElement('a')
  a.href = file
  a.download = fileName
  a.click()}

实现很简略,成果也不错,不过这样就没问题了吗,接下来咱们插入两张图片试试。

解决存在图片的状况

第一张图片是应用 base64data:URL形式插入的,第二张图片是应用一般 url 插入的:

导出后果如下:

能够看到,第一张图片没有问题,第二张图片裂开了,可能你感觉同源策略的问题,但实际上换成同源的图片,同样也是裂开的,解决办法很简略,遍历 svg 节点树,将图片都转换成 data:URL 的模式即可:

// 操作 svg 应用了 @svgdotjs/svg.js 库
const transfromImg = (svgNode) => {let imageList = svgNode.find('image')
    let task = imageList.map(async item => {
      // 获取图片 url
      let imgUlr = item.attr('href') || item.attr('xlink:href')
      // 曾经是 data:URL 模式不必转换
      if (/^data:/.test(imgUlr)) {return}
      // 转换并替换图片 url
      let imgData = await drawToCanvas(imgUlr)
      item.attr('href', imgData)
    })
    await Promise.all(task)
    return svgNode.svg()// 返回 svg html 字符串}

这里应用了后面的 drawToCanvas 办法来将图片转换成data:URL,这样导出就失常了:

到这里,将纯 svg 转换为图片就根本没啥问题了。

解决存在 foreignObject 标签的状况

svg提供了一个 foreignObject 标签,能够插入 html 节点,实际上,笔者就是应用它来实现节点的富文本编辑成果的:

接下来应用后面的形式来导出,后果如下:

明明显示没有问题,导出时 foreignObject 内容却产生了偏移,这是为啥呢,其实是因为默认款式的问题,页面全局革除了 marginpadding,以及将 box-sizing 设置成了border-box

那么当 svg 存在于文档树中时是没有问题的,然而导出时应用的是 svg 字符串,是脱离于文档的,所以没有这个款式笼罩,那么显示天然会呈现问题,晓得了起因,解决办法有两种,一是遍历所有嵌入的 html 节点,手动增加内联款式,留神肯定要给所有的 html 节点都增加,只给 svgforeignObject 或最外层的 html 节点增加都是不行的;第二种是间接在 foreignObject 标签里增加一个 style 标签,通过 style 标签来加上款式,并且只有给其中一个 foreignObject 标签增加就能够了,两种形式看你喜爱哪种,笔者应用的是第二种:

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

导出后果如下:

能够看到,一切正常。

对于兼容性的问题,笔者测试了最新的chromefirefoxoperasafari360 急速浏览器,运行都是失常的。

踩坑记录

后面介绍的是笔者目前采纳的计划,看着实现其实非常简单,然而过程漫长且崎岖,接下来,开始我的表演。

foreignObject 标签内容在 firefox 浏览器上无奈显示

对于 svg 的操作笔者应用的是 svg.js 库,创立富文本节点的外围代码大抵如下:

import {SVG, ForeignObject} from '@svgdotjs/svg.js'

let html = `<div> 节点文本 </div>`
let foreignObject = new ForeignObject()
foreignObject.add(SVG(html))
g.add(foreignObject)

SVG办法是用来将一段 html 字符串转换为 dom 节点的。

chrome 浏览器和 opera 浏览器上渲染十分失常,然而在 firefox 浏览器上 foreignObject 标签的内容齐全渲染不进去:

查看元素也看不出有任何问题,并且神奇的是只有在控制台元素里编辑一下嵌入的 html 内容,它就能够显示了,百度搜寻了一圈,也没找到解决办法,而后因为 firefox 浏览器占有率并不高,于是这个问题就搁浅了。

应用 img 联合 canvas 导出图片里 foreignObject 标签内容为空

chrome浏览器尽管渲染是失常的:

然而应用后面的形式导出时 foreignObject 标签内容却是跟在 firefox 浏览器里显示一样是空的:

firefox能忍这个不能忍,于是尝试应用一些将 html 转换为图片的库。

应用 html2canvas、dom-to-image 等库

应用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 转换须要被转换的节点在文档中
    let canvas = await html2canvas(el, {backgroundColor: null})
    mdocument.body.removeChild(el)
    return canvas.toDataURL()}

html2canvas能够胜利导出,然而存在一个问题,就是 foreignObject 标签里的文本款式会失落:

这应该是 html2canvas 的一个 bug,不过看它这 issues 数量和提交记录:

指望 html2canvas 改是不事实的,于是又尝试应用dom-to-image

import domtoimage from 'dom-to-image'

const dataUrl = domtoimage.toPng(el)

发现 dom-to-image 更不行,导出齐全是空白的:

并且它上一次更新工夫曾经是五六年前,所以没方法,只能回头应用html2canvas

起初有人倡议应用 dom-to-image-more,粗略看了一下,它是在 dom-to-image 库的根底上批改的,尝试了一下,发现的确能够,于是就改为应用这个库,而后又有人反馈在一些浏览器上导出节点内容是空的,包含 firefox360,甚至chrome 之前的版本都不行,笔者只能感叹,太难了,而后又有人倡议应用上一个大版本,能够解决在 firefox 上的导出问题,然而笔者试了一下,在其余一些浏览器上仍旧存在问题,于是又在思考要不要换回html2canvas,尽管它存在肯定问题,但至多不是齐全空的。

解决 foreignObject 标签内容在 firefox 浏览器上无奈显示的问题

用的人多了,这个问题又有人提了进去,于是笔者又尝试看看能不能解决,之前始终认为是 firefox 浏览器的问题,毕竟在 chromeopera上都是失常的,这一次就想会不会是 svgjs 库的问题,于是就去搜它的 issue,没想到,还真的搜进去了 issue,粗心就是因为通过SVG 办法转换的 dom 节点是在 svg 的命名空间下,也就是应用 document.createElementNS 办法创立的,导致局部浏览器渲染不进去,归根结底,这还是不同浏览器对于标准的不同实现导致的:

你说 chrome 很强吧,的确,然而无形中它阻止了问题的裸露。

晓得了起因,那么批改也很简略了,只有将 SVG 办法第二个参数设为 true 即可,或者本人来创立节点也能够:

foreignObject.add(document.createElemnt('div'))

果然,在 firefox 浏览器上失常渲染了。

解决 img 联合 canvas 导出图片为空的问题

解决了在 firefox 浏览器上 foreignObject 标签为空的问题后,天然会狐疑之前应用 img 联合 canvas 导出图片时 foreignObject 标签为空会不会也是因为这个问题,同时理解了一下 dom-to-image 库的实现原理,发现它也是通过将 dom 节点增加到 svgforeignObject标签中实现将 html 转换成图片的,那么就很搞笑了,我自身要转换的内容就是一个嵌入了 foreignObject 标签的 svg,应用dom-to-image 转换,它会再次把传给它的 svg 增加到一个 foreignObject 标签中,这不是套娃吗,既然 dom-to-image-more 能通过 foreignObject 标签胜利导出,那么不必它必然也能够,到这里根本确信之前不行就是因为命名空间的问题。

果然,在去掉了 dom-to-image-more 库后,从新应用之前的形式胜利导出了,并且在 firefoxchromeopera360 等浏览器中都不存在问题,兼容性反而比 dom-to-image-more 库好。

总结

尽管笔者的实现很简略,然而 dom-to-image-more 这个库实际上有一千多行代码,那么它到底多做了些什么呢,点个关注,咱们下一篇文章再见。

退出移动版