纯技术贴:
要在H5或者小程序内实现图片的制作比如:编辑,合成,滤镜等功能,不是什么难事。现有的API在你对图片格式以及区别不慎了解的情况下就可以轻松完成。
例如我想在图片上添加一个水印,是一个非常简单的操作。
let ctx = canvasElelement.getContext('2d');ctx.drawImage(img,0,0,width,width)ctx.drawText("我是水印",100,100)let data = canvasElelement.toDataUrl();
基本上通过以上的方式就可以实现一个图片添加文字并导出为base64的操作。通过最新的Blob相关的api,可以方便的对得到的图片进行本地化的操作:比如保存在本地,上传file 文件至服务器,这些操作小微信小程序以及H5端都可适用,区别在于小程序没有Blob api,但可以通过小程序的
wx.getFileSystemManager()
相关的API 实现字节流本地化的一些操作。
随着人们精神需求的不断提高,GIF 的日常操作也出现在了产品的需求日志中。
GIF 和 其他图片的格式一样,都可以在img 标签内得到访问,但区别于其他图片资源的绘制,H5 和 小程序没有直接通过canvas 的方式去绘制GIF 的方式。
大家可以简单的把GIF 当作是由若干张图片构成的图片序列帧格式,需要在canvas 上绘制gif,只需要得到这张gif 每一张的帧信息即可。canvas 除了提供比较可视化的绘制方式:drawImage 以外,还提供了 putImageData这个api。
putImageData 需要提供的canvas 的内容是一个Uint8Array:一个由8比特位组成的数组。例如:
[71, 73, 70, 56, 57, 97, 44, 1, 44, 1, 247, 0, 0, 1, 10, 14, 1, 10, 14, 1, 11, 15, 4, 14, 18, 6, 16, 21]
其中每4个元素组成一个canvas 上绘制的像素点的RGBA 信息。一个400*400像素的图像,需要的Uint8Array 的长度为400*400*4=64000。这些信息都通过一定的压缩算法,存在于jpg,png 以及 gif 中,我们称之为字节流信息。
通过arraybuffer 的方式去访问你想要的图片,你可以得到类似于以上Uint8Array的信息。但通过这种方式获取到的字节流信息包含了文件头,调色板,压缩数据 等非图像RGBA内容。最终的RGBA 需要通过文件内包含的这些信息计算出来,大致的原理就是这样。
gif 由于包含的帧内容为:每帧的图像信息,延迟,是否循环 等其他内容,而canvas 的功能目前都是在于处理单张图片上,所以canvas 的toDataUrl 能够导出为 jpg 和 png 格式的文件流,就是不能导出为gif 的。
但是知道了这样的原理,我们可以有条理地实现gif 内容在canvas 上的绘制。
在H5端,有两个第三方库:
1. libgif:实现gif文件到 图像序列的解码。
2.gif.worker.js 实现 图像序列 到gif 文件的编码。
他们的github地址分别为:
https://github.com/kelyvin/libgif-js#readme
以及:
https://github.com/jnordberg/gif.js
其中 gif.worker.js 工作在 webworker 层。
一个简单的前端架构图:
当然更理想的情况是:libgif.js 也可以工作在webworker 层。
H5 端实现的架构和在小程序端实现的架构一致。
但是小程序和H5 有几个重要的区别:
包括canvas元素在内的canvas 元素的动态创建。
webWorker 的数量。
前者直接影响了gif 二进制数据的获取,后者影响了合成文件的最终效率。
libgif.js 中有一个
get_frames
的方法用于获取所有的帧数据。但是目前版本的libgif 内有很多数据是通过创建动态的canvas 内容获取的。比如
frames.push({data: frame.getImageData(0, 0, hdr.width, hdr.height), delay: delay});
其中frame 是创建的临时的canvas 绘制到的信息。
当然想要在小程序段实现gif 的绘制功能,在渲染进程创建一个 canvas 标签并隐藏即可。
但欣仔希望在这个过程中是没有任何额外的元素参与的,即一切都在操作字节层面完成。所以欣仔在对libgif的源码进行了修改,去除掉所有与dom元素相关的代码,ImageData的获取以及合成由 canvas 的绘制改制成纯 ArrayBuffer 的操作。得到一个全新的libgif.js
源码可以在以下 github 上找到:
https://github.com/shinku/gif
最后小程序端的业务代码为:
import S_GIF from '@utils/libgif'let decoder = new S_GIF();.....wx.request({url:"https://www.abc.gif", responseType:"arraybuffer"}).then(res=>decoder.load_raw(res[1]['data'],(gifs,frames)=>{ console.log({frames}); //{data:[0,0,0,255,0,0,0,255,0,0,0,255.....],delay:10}}))
得到了frames 和 delay 之后,就可以通过wx 的canvas 操作的相关API 实现动图的绘制。
PS:目前的小程序版本的SDK 版本依旧支持以下API方法
canvas.getContext('2d').putImageData(data,w,h)
你根据自己的业务需求,完成了gif 每帧的操作:比如加了水印或者额外增加了字幕。然后通过
getImageData
重新获取到每一帧的信息之后。将他们导出成gif文件。
这个时候需要加使用gif.worker.js
const gworker = wx.createWorker('workers/gif.worker.js');
我在原来的 gif.worker.js 中做了简单的修改:将H5 环境下worker 的this 指向变成worker api。以适应 小程序端worker的使用。
例如我已经得到了最新的 数据帧数组:
//frames 为最新的gif 帧数组const tasks=[];const newData = [];frames.forEach((item,index)=>{ tasks.push({ index, data:item.data last: index === frames.length - 1, delay: item.delay*10, width:item.w, height:item.h, quality:10, debug: false, dither: true, transparent:true, globalPalette:false, repeat:0, background:"#000000", canTransfer:true }) gworker.postMessage(tasks[index]); }); gworker.onMessage(item=>{ newData.push(item); if(newData.length == tasks.length){ newData.sort((a,b)=>{ return a.index>b.index; }) }; // })
以上对newData的的得到chunk进行 合并,将得到一个完整的gif 的chunk,就是最终的ArrayBuffer。拼接的过程我也封装在了
https://github.com/shinku/gif
这个git中。最后将这个ArrayBuffer保存为 一个本地文件:
const fs = wx.getFileSystemManager(); let _path = wx.env.USER_DATA_PATH; let filePath = _path+"/temp.gif"; let buffer = arrayToGif(arrays) fs.writeFile({ filePath, data:buffer, encoding:'binary', success:()=>{ //在这里你可以通过filePath 这个本地路径实现访问以及文件d的上传。 } })
以上是一个完整的链路。
我尝试过使用postMessage传递所有的帧数据,而不是通过数据循环传递,从而减少进程间通信的开销,虽然这个过程相对于编码的消耗来说不值一提吗。但是在设备上,小程序postMessage 可以传递的数据的大小有限制,所以只能作罢。
一张300*300的gif的编码过程中,真实设备上每一帧的编码时间要1s左右。意味着一张 6帧的gif 至少要花6s 才能绘制完成,而这还是我的机器相对性能较好的情况,性能差一点的机器会花掉更多的时间。
但在H5端,情况可以有效改观。如果你的手机拥有多核的CPU,意味着整个过程可以大大缩短。至少我们必须编写额外的脚本处理进程的队列。不过相信对于坚强的996们来说,这些应该都不是什么难事。
而这样的过程如果放在服务端,使用FFMPEG 完成这样的过程,整个gif的编码都可以控制在1s 以内,可见利用服务端构建此类的服务会更符合实际应用的需求。
所以如果你们的产品有这样的需求,最好的方案依旧是将gif编码放置在服务端,服务端需要克服的就只是并发问题。
但作为小程序端的一种尝试,帮助我们探究图像编码的一些过程,也可以作为一种技术经验,让我大家收获颇多吧~
希望下次可以写一些非技术类的文章,比如如何把家里的猫养得更胖一点。