科技微讯

小程序在一个 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,00,sWidth,sHeight,005050)
      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,00,sWidth,sHeight,005050)
      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,00,sWidth,sHeight,50 * index,05050)
    })
    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 api,对于新的 api 即 canvas 2d,如果没有在 query 之前设置 canvas 的 css 尺寸会导致 ios 端无法成功导出图片,具体看我之前的文章

暂无评论