内网通去广告小程序_微信小程序内玩转GIF

纯技术贴:

要在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 实现字节流本地化的一些操作。

6fc897e4f7ce443fb6bba418b29fab34.png

随着人们精神需求的不断提高,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 的。

2371b111ea8a8ffcb574aebf046fcecc.png

但是知道了这样的原理,我们可以有条理地实现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 层。

一个简单的前端架构图:

83570859662d4e16a7152493e87eb3f5.png

当然更理想的情况是: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 才能绘制完成,而这还是我的机器相对性能较好的情况,性能差一点的机器会花掉更多的时间。

8884aef6704221106f4ba97115d9ab5c.png

但在H5端,情况可以有效改观。如果你的手机拥有多核的CPU,意味着整个过程可以大大缩短。至少我们必须编写额外的脚本处理进程的队列。不过相信对于坚强的996们来说,这些应该都不是什么难事。

e269478263898feb72809148e364ce5c.png

而这样的过程如果放在服务端,使用FFMPEG 完成这样的过程,整个gif的编码都可以控制在1s 以内,可见利用服务端构建此类的服务会更符合实际应用的需求。

所以如果你们的产品有这样的需求,最好的方案依旧是将gif编码放置在服务端,服务端需要克服的就只是并发问题。

但作为小程序端的一种尝试,帮助我们探究图像编码的一些过程,也可以作为一种技术经验,让我大家收获颇多吧~

c5b274581448c7e1a62d26bef9f04a46.png

希望下次可以写一些非技术类的文章,比如如何把家里的猫养得更胖一点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值