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

44次阅读

共计 5285 个字符,预计需要花费 14 分钟才能阅读完成。

笔者开源了一个 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 这个库实际上有一千多行代码,那么它到底多做了些什么呢,点个关注,咱们下一篇文章再见。

正文完
 0