乐趣区

关于openharmony: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 即可,如下代码所示:

//javascript
const 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 暂停动画

组件外部会监听该参数的变动,用户只有扭转值即可达到管制成果

  1. 重置 GIF 的播放

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

    因为重置不须要状态治理,所以组件内监听到数据变动就会重置 gif 播放

  2. 设置 GIF 动画播放速度

    let modelx = new GIFComponent.ControllerOptions()modelx.setSpeedFactor(2)// 将速率晋升到 2 倍

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

  3. 监听 GIF 动画播放回调(比方第一次动画完结)和获取动画理论播放总时长

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

    调用 setLoopFinish(fn: (loopTime?) => void)能够通过回调失去 GIF 动画运行一周期耗时和一周期完结工夫节点。

  4. 显示 GIF 任意一帧

    let modelx = new GIFComponent.ControllerOptions()modelx.setSeekTo(5) // 间接展现该 gif 第 5 帧图像

    调用 setSeekTo(gifPosition: number)能够间接展现该 gif 的某一帧图像。

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

  1. 适配组件的大小

    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/11…

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

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

4. 高级滤镜算法借鉴 https://gitee.com/openharmony…

5.OpenHarmony 环境下 Worker 的 API 接口列表 https://gitee.com/openharmony…

6.Worker 的应用简略案例 https://gitee.com/wang_zhaoyo…

7.Web Worker API 参考 https://developer.mozilla.org…

8.OpenHarmony 的 Canvas 文档 https://gitee.com/openharmony…

9.OpenHarmony 的 CanvasRenderingContext2D 对象文档 https://gitee.com/openharmony…

退出移动版