写点什么

OpenHarmony 轻松玩转 GIF 数据渲染

  • 2022-10-20
    上海
  • 本文字数:8297 字

    阅读完需:约 1 分钟

OpenHarmony轻松玩转GIF数据渲染

OpenAtom OpenHarmony(以下简称“OpenHarmony”)提供了 Image 组件支持 GIF 动图的播放,但是缺乏扩展能力,不支持播放控制等。今天介绍一款三方库——ohos-gif-drawable 三方组件,带大家一起玩转 GIF 的数据渲染,搞定 GIF 动图的各种需求。

效果演示



本文将从 5 个小节来带领大家使用 ohos-gif-drawable 这一款三方库,其中 1、2、3 这 3 个小节,主要介绍了 ohos-gif-drawable 的核心能力、GIF 软解码和 GIF 绘制。4 和 5 小节主要是扩展讨论,如何添加滤镜效果和软解码遇到的耗时问题。



1.GIF 的文件格式理论基础

工欲善其事必先利其器。首先我们需要为自己打下理论基础。了解 GIF 的数据格式,为后续解码 GIF 提供理论支持。


通过学习 GIF 的文件格式,我们对于 GIF 的组成格式有了一定的了解,并且有助于理解后面 GIF 的解码。


在开始介绍之前,我想让大家了解一下整体的结构思路如下图:



其中 gifuct-js 三方库主要完成了解码的工作。


ohos-gif-drawable 三方库则是在 gifuct-js 的三方库之上,进行了封装。并结合了 OpenHarmony 的 Canvas 绘制能力,达到了播放和控制 GIF 的能力。


2.GIF 软解码:gifuct-js 三方库介绍

GIF 解码我们使用了 gifuct-js 这个库,它是一个纯 JavaScript 的 GIF 解码库。首先我们需要了解基础用法。


2.1 参考样例将一个文件 ArrayBuffer 转换为 GIF 解码后的帧数据数组。

//javascriptvar gif = parseGIF(arraybuffer)var frames = decompressFrames(gif, true)
复制代码


2.2 由于 OpenHarmony 的 Image 生成 PixelMap 需要的数据是 BGRA 数据,而 2.1 生成的 frames 所有数组中的 patch 字段则是 RGBA 数据,所以我们需要使用

//javascriptvar gif = parseGIF(arraybuffer)var frames = decompressFrames(gif, false)
复制代码


然后将 frame 目前还未生成的 patch 字段数据,通过 generatePatch 函数,将 RGBA 的数据更换为 BGRA 即可,如下代码所示:

//javascriptconst generatePatch = image => {  const totalPixels = image.pixels.length  const patchData = new Uint8ClampedArray(totalPixels * 4)  for (var i = 0; i < totalPixels; i++) {    const pos = i * 4    const colorIndex = image.pixels[i]    const color = image.colorTable[colorIndex] || [0, 0, 0]    patchData[pos] = color[2] // B    patchData[pos + 1] = color[1]// G    patchData[pos + 2] = color[0] // R    patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0//A  }  return patchData}
复制代码


generatePatch 函数,在这里会根据颜色表 colorTable 和基于颜色表的图像数据 pixels 以及透明度 transparentIndex 生成 BGRA 格式的 patchData,这个数据和 Canvas 中 getImageData 获取的 ImageData 数据是一致的,都是 Uint8ClampedArray 类型,可以直接使用 putImageData 让 canvas 绘制。


最后,生成的 patchData 赋值给 Frame 的 patch 字段。


这里我们并没有直接使用 Canvas 的 putImageData 直接绘制。为了提升扩展性,我们使用了 Image 的能力来生成 PixelMap,这样处理为后续滤镜效果提供了可能,也方便后续绘制流程。


好了,到这里我们就基本上把 gifuct-js 库的基础使用简单介绍完了。


如何使用 GIF:ohos-gif-drawable 三方库的介绍。


我们先来看看整个 ohos-gif-drawable 组件的模型图,通过模型图,我们可以看到,用户只要关注 GIFComponent 组件,和 GIFComponent.ControllerOptions 配置参数以及控制参数 autoPlay 和 resetGif 即可,非常简单!


  1. 支持的功能列表如下

● 支持播放 GIF 图片。

● 支持控制 GIF 播放/暂停。

● 支持重置 GIF 播放动画。

● 支持调节 GIF 播放速率。

● 支持监听 GIF 所有帧显示完成后的回调。

● 支持设置显示大小。

● 支持 7 种不同的展示类型。

● 支持设置显示区域背景颜色。


2.如何使用 ohos-gif-drawable

首先需要使用 npm 下载 ohos-gif-drawable 三方库

npm install @ohos/ohos-gif-drawable --save
复制代码


接下来我们需要配置一个 worker 给 gifuct-js 解码使用。


配置 worker,在应用工程的 entry/src/main/ets/pages 目录下新建 workers 文件夹,并且创建文件 gifParseWorker.ts ,文件内容如下:

import arkWorker from '@ohos.worker';import { handler } from '@ohos/ohos-gif-drawable/src/main/ets/components/gif/worker/GifWorker'// handler封装了子线程逻辑,但worker目前只能在entry中进行创建arkWorker.parentPort.onmessage = handler;
复制代码


然后在 entry 目录的 build-profile.json5 文件中,添加如下内容:

"buildOption": {  "sourceOption": {    "workers": [            "./src/main/ets/pages/workers/gifParseWorker.ts"]  }},
复制代码


到这里我们 worker 就配置好了。


下面就到了正式使用环节,我们只要在 UI 界面需要的地方写上自定义控件 GIFComponent,然后传入 GIFComponent.ControllerOptions,gifAutoPlay,gifReset 这三个参数就能控制 gif 动画。

import { GIFComponent, ResourceLoader } from '@ohos/ohos-gif-drawable'// gif绘制组件用户属性设置@State model:GIFComponent.ControllerOptions = new GIFComponent.ControllerOptions();// 是否自动播放@State gifAutoPlay:boolean = true;// 重置GIF播放,每次取反都能生效@State gifReset:boolean = true;// 在ARKUI的其他容器组件中添加该组件GIFComponent({model:$model, autoPlay:$gifAutoPlay, resetGif:this.gifReset})
复制代码


举个简单的例子说明一下

// 创建worker let worker = new ArkWorker.Worker('entry/ets/pages/workers/gifParseWorker.ts', {type: 'classic',name: 'loadUrlByWorker'})// 关闭动画      this.gifAutoPlay = false;// 销毁上一次资源this.model.destroy();// 新创建一个modelx,用于配置用户参数let modelx = new GIFComponent.ControllerOptions()modelx  // 配置回调动画结束监听,和耗时监听    .setLoopFinish((loopTime) => {   this.gifLoopCount++;   this.loopHint = '当前gif循环了' + this.gifLoopCount + '次,耗时=' + loopTime + 'ms'   })  // 设置组件大小    .setSize({ width: this.compWidth, height: this.compHeight })  // 设置图像和组件的适配类型  .setScaleType(this.scaleType)  // 设置播放速率  .setSpeedFactor(this.speedFactor)  // 设置背景  .setBackgroundColor(Color.Grey)// 加载网络图片,getContext(this)中的this指向page页面或者组件都可以ResourceLoader.downloadDataWithContext(getContext(this), {   url: 'https://pic.ibaotu.com/gif/18/17/16/51u888piCtqj.gif!fwpaa70/fw/700'   }, (sucBuffer) => {    // 网络资源sucBuffer返回后处理   modelx.loadBuffer(sucBuffer, () => {      console.log('网络加载解析成功回调绘制!')    // 开启自动播放      this.gifAutoPlay = true;    // 给组件数据赋新的用户配置参数,达到后续gif动画效果      this.model = modelx;   }, worker)}, (err) => {   // 用户根据返回的错误信息,进行业务处理(展示一张失败占位图、再次加载一次、加载其他图片等)})
复制代码


这里 ResourceLoader 内置了加载网络资源 GIF,本地工程资源 GIF 和本地路径资源 GIF 文件数据的能力。


如果你已经有了 GIF 文件的 arraybuffer 数据,也可以直接调用 modelx.loadBuffer(buffer: ArrayBuffer, readyRender: (err?) => void, worker: any)进行 GIF 播放。


甚至你已经生成了 GIF 解析数据,比如调用了 2.2 中的解码代码,那么你也可以直接调用 modelx.setFrames(images?: GIFFrame[])来进行 gif 播放。


1.控制 GIF 的播放与暂停:

this.gifAutoPlay = true 开启动画this.gifAutoPlay = false 暂停动画
复制代码


组件内部会监听该参数的变化,用户只要改变值即可达到控制效果


2. 重置 GIF 的播放

this.gifReset = !this.gifReset 每次变化都会重置gif播放。

复制代码


由于重置不需要状态管理,所以组件内监听到数据变化就会重置 gif 播放


3. 设置 GIF 动画播放速度

let modelx = new GIFComponent.ControllerOptions()modelx.setSpeedFactor(2)// 将速率提升到2倍
复制代码


调用 setSpeedFactor(speed: number)即可调整播放速度 speed 为对比原始速率的乘积因子,比如设置 0.5 即为原始速率的 0.5 倍,设置为 2 即为原始速率的 2 倍。


4. 监听 GIF 动画播放回调(比如第一次动画结束)和获取动画实际播放总时长

let modelx = new GIFComponent.ControllerOptions()modelx.setLoopFinish((loopTime?) => {// loopTime为GIF动画一周期耗时,回调时间为GIF动画一周期结束时间节点})

复制代码


调用 setLoopFinish(fn: (loopTime?) => void)可以通过回调得到 GIF 动画运行一周期耗时和一周期结束时间节点。


5. 显示 GIF 任意一帧

let modelx = new GIFComponent.ControllerOptions()modelx.setSeekTo(5) // 直接展示该gif第5帧图像
复制代码


调用 setSeekTo(gifPosition: number)可以直接展示该 gif 的某一帧图像。


到这里 ohos-gif-drawable 三方库的主要能力都介绍完了,是不是很简单呢!


6. 适配组件的大小

let modelx = new GIFComponent.ControllerOptions()
复制代码


modelx.setScaleType(ScaleType.FIT_CENTER) // 将图像缩放适配组件大小调用 setScaleType(scaletype: ScaleType)可以将图像和组件大小进行适配。


目前支持的类型如下图所示:

GIFComponent.ScaleType


为什么要配置 worker

在具体实践过程中我们会发现,当我们按下解码按钮的时候,主界面会有一点卡顿的情况。特别是大的 GIF 文件进行解码的时候效果更明显。这是因为我们在主线程中进行了 CPU 的密集型计算,这是一个耗时且占用 CPU 的操作。主线程中是不能执行耗时操作的。但是 JavaScript 只有一个线程啊?那么解码这一块操作该如何处理会比较好呢?带着疑惑,我去查阅了资料发现 JavaScript 虽然属于单线程环境。但是通过引入 Worker 的能力,引入子线程 worker,可以实现 JavaScript 的“多线程”技术。


OpenHarmony 如何在子线程中处理耗时任务

为了争取良好的用户体验,我们需要将耗时操作封装至子线程中。


这里简单描述一下 worker 的能力:

能够让主页面运行的 JavaScript 线程中加载运行另外单独的一个或者多个 JavaScript 线程,但是它的多线程编程能力区别于传统意义上的多线程编程。主线程和 Worker 线程之间,不会共享任何作用域和资源,他们的通信方式是基于事件监听机制的 message。


接下来我们参考 OpenHarmony 文档下的 worker 能力


  1. OpenHarmony 环境下 Worker 的 API 接口列表

  2. Worker 的使用简单案例经过了解之后,我们可以把解码的耗时封装到 worker 中处理,避免主线程耗时操作占用 CPU 导致卡顿问题。提升用户体验。这也是使用 ohos-gif-drawable 三方库需要配置 worker 的原因。

扩展部分

GIF 的滤镜效果

  1. 灰白滤镜

//javascript// 重点代码更改    let avg = (color[0] + color[1] + color[2]) / 3  patchData[pos] = avg;  patchData[pos + 1] = avg;  patchData[pos + 2] = avg;  patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
复制代码

2.反转滤镜

//javascript// 重点代码更改  patchData[pos] = 255 - color[0];  patchData[pos + 1] = 255 - color[1];  patchData[pos + 2] = 255 - color[2];  patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
复制代码


3.高级滤镜效果

假设我们这边已经拿到了 patch: Uint8ClampedArray 像素数据,这里我需要先将其变换为一张 PixelMap 数据,参考 GIFComponent 中 patch 数据转换为 PixelMap 的代码。

//typescriptimport image from "@ohos.multimedia.image"let colorBuffer = patch.bufferlet pixelmap = await image.createPixelMap(colorBuffer, {  'size': {    'height': frame.dims.height as number,    'width': frame.dims.width as number  }})
复制代码

4.高斯模糊

然后对 PixelMap 像素数据进行高斯模糊, 调用 blur(pixelmap,10,true, (outPixelMap)=>{ // 模糊后的pixelmap数据})在回调中获取模糊后的 pixelmap。以下是模糊处理的算法:

export async function blur(bitmap: any, radius: number, canReuseInBitmap: boolean, func: AsyncTransform<PixelMap>) {  if (radius < 1) {    func("error,radius must be greater than 1 ", null);    return;  }
let imageInfo = await bitmap.getImageInfo(); let size = { width: imageInfo.size.width, height: imageInfo.size.height }
if (!size) { func(new Error("fastBlur The image size does not exist."), null) return; }
let w = size.width; let h = size.height; var pixEntry: Array<PixelEntry> = new Array() var pix: Array<number> = new Array()

let bufferData = new ArrayBuffer(bitmap.getPixelBytesNumber()); await bitmap.readPixelsToBuffer(bufferData); let dataArray = new Uint8Array(bufferData);
for (let index = 0; index < dataArray.length; index+=4) { const r = dataArray[index]; const g = dataArray[index+1]; const b = dataArray[index+2]; const f = dataArray[index+3];
let entry = new PixelEntry(); entry.a = 0; entry.b = b; entry.g = g; entry.r = r; entry.f = f; entry.pixel = ColorUtils.rgb(entry.r, entry.g, entry.b); pixEntry.push(entry); pix.push(ColorUtils.rgb(entry.r, entry.g, entry.b)); }
let wm = w - 1; let hm = h - 1; let wh = w * h; let div = radius + radius + 1;
let r = CalculatePixelUtils.createIntArray(wh); let g = CalculatePixelUtils.createIntArray(wh); let b = CalculatePixelUtils.createIntArray(wh);
let rsum, gsum, bsum, x, y, i, p, yp, yi, yw: number; let vmin = CalculatePixelUtils.createIntArray(Math.max(w, h));
let divsum = (div + 1) >> 1; divsum *= divsum; let dv = CalculatePixelUtils.createIntArray(256 * divsum); for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); }
yw = yi = 0; let stack = CalculatePixelUtils.createInt2DArray(div, 3); let stackpointer, stackstart, rbs, routsum, goutsum, boutsum, rinsum, ginsum, binsum: number; let sir: Array<number>; let r1 = radius + 1; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = pix[yi + Math.min(wm, Math.max(i, 0))]; sir = stack[i + radius]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rbs = r1 - Math.abs(i); rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } } stackpointer = radius;
for (x = 0; x < w; x++) {
r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum];
rsum -= routsum; gsum -= goutsum; bsum -= boutsum;
stackstart = stackpointer - radius + div; sir = stack[stackstart % div];
routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2];
if (y == 0) { vmin[x] = Math.min(x + radius + 1, wm); } p = pix[yw + vmin[x]];
sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff);
rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2];
rsum += rinsum; gsum += ginsum; bsum += binsum;
stackpointer = (stackpointer + 1) % div; sir = stack[(stackpointer) % div];
routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2];
rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2];
yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = Math.max(0, yp) + x;
sir = stack[i + radius];
sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi];
rbs = r1 - Math.abs(i);
rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs;
if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; }
if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { // Preserve alpha channel: ( 0xff000000 & pix[yi] ) pix[yi] = (0xff000000 & pix[Math.round(yi)]) | (dv[Math.round(rsum)] << 16) | (dv[ Math.round(gsum)] << 8) | dv[Math.round(bsum)];
rsum -= routsum; gsum -= goutsum; bsum -= boutsum;
stackstart = stackpointer - radius + div; sir = stack[stackstart % div];
routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2];
if (x == 0) { vmin[y] = Math.min(y + r1, hm) * w; } p = x + vmin[y];
sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p];
rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2];
rsum += rinsum; gsum += ginsum; bsum += binsum;
stackpointer = (stackpointer + 1) % div; sir = stack[stackpointer];
routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2];
rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2];
yi += w; } }
let bufferNewData = new ArrayBuffer(bitmap.getPixelBytesNumber()); let dataNewArray = new Uint8Array(bufferNewData); let index = 0;
for (let i = 0; i < dataNewArray.length; i += 4) { dataNewArray[i] = ColorUtils.red(pix[index]); dataNewArray[i+1] = ColorUtils.green(pix[index]); dataNewArray[i+2] = ColorUtils.blue(pix[index]); dataNewArray[i+3] = pixEntry[index].f; index++; } await bitmap.writeBufferToPixels(bufferNewData); if (func) { func("success", bitmap); }}

复制代码


如果需要高级滤镜效果可以参考 ImageKnife 组件的 transform 部分,这里仅仅展示模糊效果。由于滤镜效果目前 ohos-gif-drawable 三方库并没有开发接口提供出来,所以开发者可以根据实际需求重写自定义组件 GIFComponent.,只需要在生成 PixelMap 的代码片段中加入滤镜代码,即可利用滤镜效果开发更多精彩的应用。


参考资料

1.《GIF 文件格式解析》https://segmentfault.com/a/1190000022866045

2.GIF 解码库 gifuct-jshttps://github.com/matt-way/gifuct-js

3.GIF 解码库底层逻辑 jsBinarySchemaParserhttps://github.com/matt-way/jsBinarySchemaParser

4.高级滤镜算法借鉴https://gitee.com/openharmony-tpc/ImageKnife/tree/master/imageknife/src/main/ets/components/imageknife/transform

5.OpenHarmony 环境下 Worker 的 API 接口列表https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-worker.md

6.Worker 的使用简单案例https://gitee.com/wang_zhaoyong/js_worker_module/wikis/Worker%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8

7.Web Worker API 参考https://developer.mozilla.org/zh-CN/docs/Web/API/Worker

8.OpenHarmony 的 Canvas 文档https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-components-canvas-canvas.md

9.OpenHarmony 的 CanvasRenderingContext2D 对象文档https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/arkui-ts/ts-canvasrenderingcontext2d.md


用户头像

OpenHarmony开发者官方账号 2021-12-15 加入

OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展

评论

发布
暂无评论
OpenHarmony轻松玩转GIF数据渲染_OpenHarmony_OpenHarmony开发者社区_InfoQ写作社区