现在只有一个 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 的尺寸,然后首先把收到的图片全部 draw 进这个 canvas 的不同区域,最后把这个 canvas 的不同区域分别导出成不同的图片。
这种方法需要注意的问题是,动态设置 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 api,对于新的 api 即 canvas 2d,如果没有在 query 之前设置 canvas 的 css 尺寸会导致 ios 端无法成功导出图片,具体看我之前的文章。