小程序在一个 canvas 组件画多张图片并导出多张图片的问题

作者: 科技微讯

日期:

现在只有一个 canvas 组件:

<canvas canvas-id="canvas" style="width: 50px; height: 50px"></canvas>

需求是: 获取多张高分辨率图片的缩略图, 比如收到一张 1000 x 2000 的图片, 获取其 50 x 50 分辨率的缩略图.

这个需求可以用 canvas 处理, 如果用小程序旧版的 canvas api, 则是:

(async () => {
  const getThumbnail = function(image){
    return new Promise((resolve, reject) => {
      const ctx = wx.createCanvasContext("canvas")
      let sWidth, sHeight
      if (image.width > image.height) {
        sWidth = sHeight = image.height
      } else {
        sWidth = sHeight = image.width
      }
      ctx.drawImage(image.path, 0, 0, sWidth, sHeight, 0, 0, 50, 50)
      ctx.draw(false, () => {
        wx.canvasToTempFilePath({
          canvasId: "canvas",
          success: res => res.tempFilePath // 这个就是缩略图的本地地址,
          fail: reject,
        })
      })
    })
  }
  const allThumbnails = await Promise.all(images.map(image => getThumbnail(image)))
})();

上面这样的代码可能出错, 因为用了 Promise.all, 最后得到的每张缩略图可能都是相同的. 正确的做法是:

(async () => {
  const getThumbnail = function(image){
    return new Promise((resolve, reject) => {
      const ctx = wx.createCanvasContext("canvas")
      let sWidth, sHeight
      if (image.width > image.height) {
        sWidth = sHeight = image.height
      } else {
        sWidth = sHeight = image.width
      }
      ctx.drawImage(image.path, 0, 0, sWidth, sHeight, 0, 0, 50, 50)
      ctx.draw(false, () => {
        wx.canvasToTempFilePath({
          canvasId: "canvas",
          success: res => resolve(res.tempFilePath), // 这个就是缩略图的本地地址
          fail: reject,
        })
      })
    })
  }
  const allThumbnails = [];
  let (image of images){
    allThumbnails.push(await getThumbnail(image))
  }
})()

这里没有再使用 Promise.all, 而是按顺序, 先获取一个缩略图, 再获取下一个缩略图. 更详细地说, 就是在一个固定大小的 canvas 组件的相同位置中 draw 图片, 然后导出图片, 接着在相同的位置 draw 下一张图片, 覆盖原来的图片, 然后再导出新图片, 直到获得所有缩略图.

canvas 1

另一种方法是: 根据收到的原始图片的尺寸动态设置地 canvas 的尺寸, 然后首先把收到的图片全部 draw 进这个 canvas 的不同区域, 最后把这个 canvas 的不同区域分别导出成不同的图片.

canvas 2

这种方法需要注意的问题是, 动态设置 canvas 的尺寸, 最后通过 canvasToTempFilePath 导出图片时, 可能会出现 canvasToTempFilePath:fail:cearte bitmap failed 错误.

我试验得到的结论是, 用 setData 动态设置 canvas 尺寸, 在 setData 的回调函数中获取 ctx 并 draw, 手机上删除小程序的开发版, 重新扫描二维码预览, 第一次打开小程序并选择图片, 会出现上述错误, 接着退出小程序再打开就好了. 如果没有在 setData 的回调函数中获取 ctx 并 draw, 那会一直出错.

一种不怎么完美的解决方法是, 除了把 ctx 和 draw 放在 setData 的回调函数之外, 还需要让 ctx.draw() 中的回调函数整个地放在 setTime 中延迟执行.

比如 canvas 组件是这样的:

<canvas
  canvas-id="canvas"
  style="width: {{canvasWidth}}px; height: {{canvasHeight}}px"
></canvas>

然后

// 通过 setData 设置 canvas 的尺寸
thi.setData(
  {
    canvasWidth: 50 * images.length,
    canvasHeight: 50,
  },
  // 把代码放在 setData 的回调函数中
  () => {
    const ctx = wx.createCanvasContext("canvas")
    images.forEach((image, index) => {
      let sWidth, sHeight
      if (image.width > image.height) {
        sWidth = sHeight = image.height
      } else {
        sWidth = sHeight = image.width
      }
      ctx.drawImage(image.path, 0, 0, sWidth, sHeight, 50 * index, 0, 50, 50)
    })
    ctx.draw(false, () => {
      // 把 ctx.draw 的整个回调函数包裹在 setTime 中
      setTimeout(async () => {
        const getThumbnail = function(index) {
          return new Promise((resolve, reject) => {
            wx.canvasToTempFilePath({
              canvasId: "canvas",
              success: res => resolve(res.tempFilePath), // 这个就是缩略图的本地地址
              fail: reject,
            })
          })
        }
        const thumbnail = await Promise.all(
          images.map((image, index) => getThumbnail(index))
        )
      }, 200)
    })
  }
)

另一种解决方法是, 可以像第一种写法那样, 给 canvas 设定一个固定的尺寸. 既然缩略图是 50 x 50, 如果限制用户一次最多只能同时生成 9 张图片的缩略图, 那 canvas 的尺寸可以固定设置为 50 x 450.

<canvas canvas-id="canvas" style="width: 450px; height: 50px"></canvas>

可惜的是, 设置一个更大的 canvas, 最后通过 wx.canvasToTempFilePath 导出图片时, 至少 macOS 版的微信会把整个 canvas 导出, 而不是把 wx.canvasToTempFilePath 指定的 canvas 的某区域导出, 这应该是小程序的 bug.

总结一下三种方法:

  • 设置一个小尺寸的固定大小的 canvas, 然后按顺序获取图片的缩略图, 缺点是有点慢;
  • 设置一个足够大的大尺寸的固定大小的 canvas, 先把所有图片 draw 进这个 canvas, 然后把不同的区域分别导出, 缺点是 PC 端似乎不能导出 canvas 的某个区域;
  • 根据收到的图片数量动态设置 canvas 的尺寸, 然后把 draw 函数写在 setData 的回调函数中, 并用 setTimeout 延迟这个 draw 函数, 缺点同上;

这篇文章是关于小程序旧的 canvas api, 对于新的 api 即 canvas 2d, 如果没有在 query 之前设置 canvas 的 css 尺寸会导致 ios 端无法成功导出图片, 具体看我之前的文章.