5

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区-51CTO.COM

 1 year ago
source link: https://ost.51cto.com/posts/18278
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

效果演示​

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区
OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区

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

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区

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

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

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区

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

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

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区

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

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

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

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

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

//javascript
var gif = parseGIF(arraybuffer)
var frames = decompressFrames(gif, true)

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

//javascript
var 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即可,非常简单!

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区

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

OpenHarmony轻松玩转GIF数据渲染-开源基础软件社区

为什么要配置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的代码。

//typescript
import image from "@ohos.multimedia.image"
let colorBuffer = patch.buffer
let 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-js

​​https://github.com/matt-way/gifuct-js​​​​

3.GIF解码库底层逻辑jsBinarySchemaParser

​​https://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轻松玩转GIF数据渲染-开源基础软件社区

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK