- OpenHarmony
- UI组件
- 图像处理
【免费下载链接】gifdrawable4cj
一个gif图像加载库
gifdrawable4cj是一个使用仓颉(Cangjie)语言实现的 GIF 图像渲染库,面向 OpenHarmony/HarmonyOS 应用(HAP)与模块(HAR)场景,提供从 GIF 二进制数据解析、帧解码到画布播放的完整链路。本文以仓库根目录的 README.md 为主线,结合 doc/feature_api.md 中的完整 API 说明与gifdrawable模块源码,带你掌握:如何在工程中引入该库、用GIFComponent组件播放 GIF、通过ControllerOptions精细控制播放行为,以及底层基于 Schema 的流式解析与 LZW 解压是如何工作的。
项目定位与核心特性
gifdrawable4cj 由entry(示例应用 HAP 模块)与gifdrawable(可复用 HAR 库模块)两个工程模块组成,核心代码全部使用仓颉语言编写。作为图像加载库,它解决的问题是:在仓颉 UI 应用中,把工程资源、沙箱文件或网络下载得到的 GIF 数据解码为逐帧图像,并交给自定义组件驱动播放。
仓库 README.md 明确列出以下特性:
- 支持播放 GIF 图片:提供开箱即用的自定义组件
GIFComponent; - 支持控制 GIF 播放/暂停:通过
autoPlay状态变量一键切换; - 支持调节 GIF 播放速率:
ControllerOptions.setSpeedFactor()支持加速与减速; - 支持设置显示大小:组件宽高由
canvasWidth/canvasHeight状态驱动; - 支持不同的拉伸类型:内置
ScaleType枚举,覆盖 7 种缩放与对齐策略。
软件架构:模块划分与数据流
架构图
README 提供了两张与解码主题强相关的示意图。第一张展示 GIF 图片内部的主要结构(逻辑屏幕描述符、颜色表、图像描述符、扩展块等),第二张给出从原始字节到可播放帧的解析流程图:
源码目录结构
README 给出的仓库目录骨架如下,其中gifdrawable/src/main/cangjie/src下四个子目录各司其职:
├─AppScope ├─doc ├─entry │ └─src │ └─main │ ├─cangjie │ └─resources ├─gifdrawable │ └─src │ └─main │ ├─cangjie │ │ └─src │ │ ├─gifdisplay │ │ ├─parser │ │ ├─sparkmd5 │ │ └─utils │ └─resources └─hvigor各目录职责(与 README 目录说明一致):
AppScope:全局资源存放目录和应用全局信息配置目录;doc:API 文档和使用手册存放目录,接口细节见 doc/feature_api.md;entry:工程模块,编译生成一个 HAP,其中entry/src/main/cangjie是仓颉代码目录,resources是资源文件目录;gifdrawable:工程模块,编译生成一个 HAR 包,即本文使用的库本体;gifdrawable/src/main/cangjie/src/gifdisplay:组件主体代码目录,包含GIFComponent、ControllerOptions、ResourceLoader、FileUtils;gifdrawable/src/main/cangjie/src/parser:帧解码目录,包含数据流解析(GIFUnit8)、Schema 解析(parseGIF)、LZW 解压、GIFFrame帧结构;gifdrawable/src/main/cangjie/src/sparkmd5:文件哈希计算代码目录(用于网络下载文件的本地命名缓存);gifdrawable/src/main/cangjie/src/utils:解码工具目录(URI 编解码、位运算等辅助工具);hvigor:构建工具目录。
在gifdisplay目录下,gif_component.cj 是GIFComponent组件实现,controller_options.cj 是配置与解码入口,resource_loader.cj 负责三种数据来源的读取。
编译构建:以 module 方式接入工程
README 的"编译构建"章节给出完整的接入流程,需要在被集成的应用工程中完成以下 5 步:
第 1 步:克隆下载项目,将本仓库拉取到本地。
第 2 步:把gifdrawable模块整体拷贝到你的应用项目目录下(与entry同级)。
第 3 步:修改自身应用entry下的oh-package.json5,在dependencies字段添加对gifdrawable的本地文件依赖:
{ "name": "entry", "version": "1.0.0", "description": "Please describe the basic information.", "main": "", "author": "", "license": "", "dependencies": { "gifdrawable": "file:../gifdrawable" } }第 4 步:修改自身应用entry/src/main/cangjie下的cjpm.toml,在[dependencies]字段下按相对路径声明库的仓颉源码依赖。仓库中 entry/src/main/cangjie/cjpm.toml 的写法即为参考:
[dependencies] [dependencies.gifdrawable] path = "../../../../gifdrawable/src/main/cangjie"说明:该相对路径从
entry/src/main/cangjie出发,向上回退四层到仓库根,再进入gifdrawable/src/main/cangjie,与 README 中的示例一致。实际接入时请以你自己的工程目录层级为准调整。
第 5 步:在项目中引用库的公开 API:
import gifdrawable.gifdisplay.* import gifdrawable.parser.*其中gifdrawable.gifdisplay提供组件与控制器(GIFComponent、ControllerOptions、ResourceLoader),gifdrawable.parser提供解码与帧结构(parseBufferToFrame、GIFFrame、ScaleType等)。
功能示例:从数据加载到播放控制
README 的"功能示例"章节以"GIF 动图数据解析"为主线,给出完整可复制的调用序列,下面按步骤展开。
第 1 步:在 UI 中添加 GIFComponent 组件
在页面状态中声明控制器与各类控制变量,然后在build()中挂载组件:
// 创建一个控制器保证GIFComponent初始化 @State var model: ControllerOptions = ControllerOptions() // 是否自动播放 @State var gifAutoPlay: Bool = true // 重置GIF播放,每次取反都能生效 @State var gifReset: Bool = false // 绘制区域宽度 @State var compWidth: Float64 = 300.0 // 绘制区域高度 @State var compHeight: Float64 = 300.0 // 添加GIFComponent组件 GIFComponent( model: model, // 控制器 autoPlay: gifAutoPlay, // 自动播放标志 resetGif: gifReset, // 重置标志 canvasWidth: compWidth, // 绘制区域宽度 canvasHeight: compHeight // 绘制区域高度 )从组件源码看,gif_component.cj 中model、autoPlay、resetGif都是@Watch+@Link双向绑定的状态变量:autoPlay变化触发watchAutoPlay(),为true时立即调用renderFrames()开始播放;resetGif取反会先取消定时器、清空画布、将帧索引归零后重新渲染;model变化则触发watchModel()等待画布尺寸就绪后重新解析。组件内部使用Canvas+CanvasRenderingContext2D绘制,并开启抗锯齿(RenderingContextSettings(antialias: true))。
第 2 步:初始化控制器
创建独立控制器实例进行数据预处理,处理完成后一次性赋值给组件绑定的model,可以减少组件刷新次数:
// 创建控制器 let modelx: ControllerOptions = ControllerOptions() // 设置是否使用硬解码 modelx.setOpenHardware(true) // 设置拉伸方式 modelx.setScaleType(ScaleType.FIT_CENTER) // 设置速率 modelx.setSpeedFactor(1) // 设置完成一次播放后的回调函数 modelx.setLoopFinish({loopTime: Option<Int64> => })第 3 步:加载数据(三种方式)
ResourceLoader提供三种数据来源的读取接口(对应 resource_loader.cj 的实现):
// 读取工程内资源(resId 为媒体资源 ID,需要传入全局 AbilityContext) let buffer: Array<UInt8> = ResourceLoader.loadMedia(UInt32(@r(app.media.xxx).id), globalAbilityContext) // 读取沙箱路径下资源(仅支持 .gif 后缀文件) let buffer: Array<UInt8> = ResourceLoader.loadRawFile("/data/storage/el2/base/files/xxx.gif") // 下载网络资源(成功回调拿到 Array<Byte>,失败回调拿到错误信息) ResourceLoader.downloadDataWithContext( Model("网络资源地址"), // 下载设置 globalAbilityContext, // AbilityContext {buffer: Array<Byte> => // 下载成功并读取成 Array<Byte> 后的回调函数 }, { str: String => // 下载错误回调函数 } )几个实现细节值得注意:
loadMedia(resId, context)通过ResourceManager.getResourceManager(stageContext).getMediaContent(resId, 0)读取媒体资源,context为Option<AbilityContext>,为None时返回空数组并输出错误日志;loadRawFile(filename)在 resource_loader.cj 中会先校验文件名必须以.gif结尾,否则抛出Exception;- 网络下载走 OpenHarmony 下载代理(
ohos.request.agent):下载前用SparkMD5.hashBinary(model.url, false)生成 URL 的 MD5 作为文件名,缓存到/data/storage/el2/base/files目录;Model(url, progressCallBack)可附带进度回调(进度区间 0%~100%),下载完成事件触发后自动读取文件内容并删除临时文件。
第 4 步:数据转换成可显示内容
将字节数据交给控制器的loadBuffer异步处理,回调中携带Option<String>错误信息:
// 调用数据处理方法 modelx.loadBuffer( buffer, // 步骤3中获取的数据 {error: Option<String> => if (let Some(v) <- error) { // 错误信息处理 } else { // 处理完成,开始显示 gifAutoPlay = true // 将已完成数据处理的控制器赋值给组件关联的初始化控制器 model = modelx } } )loadBuffer在 controller_options.cj 中会根据openHardware分流:软解码走userTaskPoolParse(任务池解析 +createPixelMapAll批量生成 PixelMap),硬解码走useHardwareParse(系统 ImageSource 的createPixelMapList一次取出全部帧)。
显示控制
数据加载完成、开始播放之后,以下状态与接口即构成完整的播放控制面板:
// 开始 gifAutoPlay = true // 暂停 gifAutoPlay = false // 重置(每次取反都能生效) gifReset = !gifReset // 修改宽高 compWidth = 100.0 compHeight = 200.0 // 修改速率 model.setSpeedFactor(2) // 修改拉伸方式 model.setScaleType(ScaleType.FIT_START)解码原理:从字节流到 GIFFrame 的完整链路
README 将详细接口指向 doc/feature_api.md,该文档按"GIF 解码器 → 二进制数据解析器 → 提取 gif 帧数据 → 动图播放 → 其他接口"的层次组织。结合parser目录源码,可以还原一条完整的解码调用链:
parseBufferToFrame(arraybuffer, patchRule) └─ parseGIF(byteData) // 包装 UInt8TypedArray,构造 GIFSchema └─ parse(stream, schema, result, par) // 循环/条件解析各 GIF 块 └─ decompressFrames(gif, false) // 遍历 Schema 帧列表 └─ decompressFrame(gce, image, gct) // 提取单帧像素 └─ lzw(minCodeSize, data, pixelCount) // LZW 解压 └─ deinterlace(pixels, width) // 隔行扫描重排 └─ prePatchGIF(origins, patchFunc) // 颜色表 → 可绘制 patch 流数据流封装与逐字节读取
gif_uint8.cj 定义了UInt8TypedArray(字节数组 + 游标位置封装)与GIFUnit8(GIF 数据流解析类)。GIFUnit8提供一组高阶函数式的读取原语,全部以UInt8TypedArray为入参、移动内部游标:
readByte()/peekByte(offset!: Int64 = 0):读取/查看下一个字节;readBytes(length)/peekBytes(length):读取/查看指定长度字节数组;readString(length):读取指定长度并转成字符串(用于解析 GIF 头、注释等 ASCII 字段);readUnsigned(littleEndian: Bool):将两个字节合成一个 16 位无符号整数,littleEndian决定合成顺序——这是 GIF 文件读取中固定的低位优先(little-endian)读取方式;readArray(byteSize, totalFunc):分组读取数据,totalFunc负责计算读取次数,可结合当前/上次解析的Schema对象动态决定;readBits(params: Array<Int64>):按(起始位置, 切割长度)的成对参数(如[0, 3, 3, 5])把一个字节的 8 个 bit 重新组合;subBitsTotal(bits, startIndex, length):把指定 bit 段计算为十进制值。
Schema 驱动的流式解析框架
GIF 文件本质是若干块(Block)的串联:头、逻辑屏幕描述符、全局颜色表、图形控制扩展(GCE)、图像描述符、图像数据、尾块等。feature_api.md 用Schema体系抽象这一结构,gif_schemas.cj 中Schema是基类,GIFSchema(整体帧集合 + 全局颜色表gct)、ImageSchema(图像描述符,含局部颜色表lct与data_blocks)是其子类,各自重写parse(mStream, result, parent)与getTypeCode()。
配套的四个顶层解析函数构成组合子框架:
parse(stream, loops, result, par)/parse(stream, schema, result, par):按子类数据处理流程解析流中数据;conditional(schema, conditionFunc):条件判断——满足条件才按当前 Schema 逻辑处理;loop(schema, continueFunc):循环处理,continueFunc返回false时终止,防止无限循环。
parseGIF(byteData)(见 gif_parse.cj)就是这一框架的入口:创建UInt8TypedArray(byteData, 0)与GIFSchema,随后调用parse(stream, schema, schema, schema)完成整份数据的结构化解码。
LZW 解压缩与隔行扫描重排
GIF 图像数据采用 LZW 压缩编码。lzw(minCodeSize, data, pixelCount)(实现于 lzw.cj)按 GIF 规范维护prefix、suffix、pixelStack三张表(栈上限 4096),处理Clear Code、End of Information码,动态扩展code_size,最终输出长度恰为pixelCount的像素索引数组。
对于隔行扫描(Interlaced)存储的 GIF,解码后还需deinterlace(pixels, width)(gif_parse.cj)按offsets = [0, 4, 2, 1]、steps = [8, 8, 4, 2]的 4 遍扫描顺序把行数据重排回正常光栅顺序。
帧结构与颜色表 → 可绘制数据
decompressFrame(gceSchema, imageSchema, gct, buildImagePatch)将解析结果封装为GIFFrame(结构定义见 gif_frame.cj):
dims:帧的宽高与上、左边距(Dims类);pixels:当前帧每个像素的颜色表查找索引;colorTable:本帧引用的颜色数组(优先局部颜色表,否则用全局颜色表),3 个连续值表示一个颜色;delay:当前帧到下一帧的间隔时长(由 GCE 字段gce_delay乘 10 得到,gce_delay == 0时按 100 兜底,单位毫秒);disposalType:帧处置方式(0 保留、1 不处置、2 恢复背景、3 恢复上一状态);transparentIndex:透明色索引;patch:颜色转换后的补片信息,用于绘制。
generatePatch(image, flagSwapRB!: Bool = false)完成"颜色表 + 索引"到 RGBA 字节流的转换:每个像素展开为 4 字节(R、G、B 取自颜色表,A 在像素索引等于transparentIndex时为 0,否则为 255),flagSwapRB可交换 R/B 通道以适应目标渲染格式。该流程在 gif_parse.cj 中有完整实现。
parseBufferToFrame(arraybuffer, patchRule)是面向调用者的高层入口:先解析为帧数组,再通过patchRule让用户有机会对像素进行自定义处理(如翻转、裁剪),最后交给prePatchGIF生成可绘制的 patch 数据。
软解码与硬解码两条路径
在 controller_options.cj 中,loadBuffer根据openHardware分流:
- 软解码(默认):
userTaskPoolParse通过parseByTaskpool(buffer)(即 gif_component.cj 封装的parseBufferToFrame)在任务池中完成解码,随后createPixelMapAll逐帧把patch数据通过createPixelMap(patchBuffer, initializationOptions)构建为PixelMap,挂到frame.drawPixelMap上,全部成功后回调readyRender(None); - 硬解码:
useHardwareParse使用系统createImageSource(buffer)+DecodingOptions(sampleSize: 1, rotate: 0, editable: true)的createPixelMapList一次性产出全部PixelMap,再通过getDelayTimeList()补齐每帧delay,帧尺寸统一取首帧getImageInfo()的宽高。
播放引擎:速率、定位与回调
GIFComponent的播放循环集中在renderFrames()(gif_component.cj),其工作机制是"画一帧 → 按 delay 定时 → 画下一帧":
- 速率控制:每帧停留时间
stayTime由帧的delay与speedFactor计算——speedFactor > 0时delay / speedFactor(如 2 表示 2 倍速);speedFactor < 0时delay * |speedFactor|(减速)。实际定时器时长还会减去本次绘制耗时diff,保证节奏准确; - 循环回调:播到最后一帧(
frameIndex == frameLength - 1)时触发model.getLoopFinish()(gifLoopDuration),参数为整轮播放耗时(毫秒),随后帧索引归零继续循环; - 定点回调:调用
setPositionFinish(position, fn)后,播到第position - 1帧时触发getPositionFinish()回调,可用于帧级事件通知; - 跳帧显示:
setSeekTo(gifPosition)让组件直接显示第 N 帧(getSeekTo()返回当前定位)。组件会暂停自动播放并逐帧绘制到目标帧,保证非首帧数据不完整时画面依然正确(见drawSeekToFrame)。
绘制阶段drawFrame→canvasDrawPixelMap根据上一帧的disposalType决定是否清空背景区域(RESTOREBACKGROUND时清除对应区域),随后按ScaleType设置画布变换再drawImage。
ScaleType 拉伸类型一览
ScaleType枚举定义在 gif_type.cj,组件内对应 7 种独立绘制函数(drawFIT_START等):
| 取值 | 值 | 行为描述 |
|---|---|---|
FIT_START | 0 | 图像位于组件左上角,等比缩放至全部展示 |
FIT_END | 1 | 图像位于组件右下角,等比缩放至全部展示 |
FIT_CENTER | 2 | 图像居中,等比缩放至全部展示 |
CENTER | 3 | 图像绝对居中,不缩放 |
CENTER_CROP | 4 | 短边缩放至组件大小,长边超出部分裁剪 |
FIT_XY | 5 | 图像拉伸至组件大小(不保持比例) |
CENTER_INSIDE | 6 | 图像大于组件时按 FIT_CENTER 缩放,小于组件时按 CENTER 居中 |
getTypeString()返回对应字符串(fix_start、fix_center、center_crop等),可用于日志与调试。配套的DisposalType枚举(NOTHING/NOTRESOLVE/RESTOREBACKGROUND/PREVIOUSSTATUS,取值 0~3)对应 GIF 规范中的帧处置方式,播放绘制时据此处理画布清除逻辑。
关键 API 速查
ControllerOptions 完整接口
除上文使用过的接口外,doc/feature_api.md 的"动图播放"章节还列出了以下能力:
getOpenHardware()/setOpenHardware(open: Bool):查询/设置硬解码开关,返回ControllerOptions支持链式调用;getFrames()/setFrames(images):获取/设置已解析的GIFFrame数组;getLoopFinish()/setLoopFinish(fn):读取/设置整轮播放结束回调;getPositionFinish()/setPositionFinish(position, fn):读取/设置指定帧位回调;getPosition():获取当前设定帧位置;getScaleType()/setScaleType(scaletype)、getSpeedFactor()/setSpeedFactor(speed):拉伸类型与播放速率;getSeekTo()/setSeekTo(gifPosition):跳帧显示;destroy():销毁配置——释放所有帧的PixelMap资源、清空帧数组并把各参数复位(controller_options.cj),页面退出时应调用以避免资源泄漏。
文件工具类 FileUtils
feature_api.md 的"文件工具类"章节给出了单例FileUtils.getInstance()下的文件能力:createFile/createFileToID(新建文件)、deleteFile、copyFile/copyFileAsync(异步拷贝)、clearFile、writeData/writeDataAsync/writeNewFile/writeNewFileAsync(写入,其中writeNewFile覆盖式新建)、exist、getFileSize、readFile/readFileAsync、createFolder(path, recursive!: Bool = false)与existFolder。创建的File对象会默认打开,使用完毕后需及时close,否则会造成资源泄漏。
网络下载数据源 Model
public class Model { public var url: String = "" // 下载任务数据源路径 public var progressCallBack: ?((String) -> Unit) = None // 下载进度回调 [0%, 100%] public init() {} public init(url: String, progressCallBack!: Option<(String) -> Unit> = None) }MD5 与内部工具(按需谨慎使用)
sparkmd5目录提供了SparkMD5与SparkMD5ArrayBuffer(append/appendBinary/end(raw)/reset/getState/setState/destroy/ 静态hashSparkMD5、hashSparkMD5ArrayBuffer),网络下载模块内部用它为 URL 生成缓存文件名。feature_api.md 明确指出:encodeURIComponent、UnescapeBuffer等"其他接口"只在内部流程中使用,无需调用者操作,且可能随迭代修改实现,请按需谨慎使用。
命令行示例:纯解析场景验证
feature_api.md 的"示例"章节给出了一个不依赖 UI 的最小验证用例,可直接在控制台运行以确认解码链路正确:
from std import fs.* from std import sync.* from gifdrawable4cj import gifdrawable4cj.* main (){ let filename: String = "./dengdaitu.gif" let byteData: Array<UInt8> = ResourceLoader.loadRawFile(filename) let gifs: Array<GIFFrame> = parseBufferToFrame(byteData, None) println("gifs.size = ${gifs.size}") return 0 }执行结果:
gifs.size = 24示例说明:对任意一个 24 帧的 GIF 文件,loadRawFile读取字节后交给parseBufferToFrame,即可得到 24 个GIFFrame帧对象,验证"字节流 → Schema → GIFFrame"整条解码链工作正常。
约束与限制
README 说明本项目在以下版本组合下验证通过:
- DevEco Studio 5.0.3 Release(5.0.9.300)
- Cangjie Support Plugin 5.0.9.100
- Cangjie Compiler:0.53.18(cjnative)
从 entry/src/main/cangjie/cjpm.toml 可以看到,[package]中cjc-version = "0.53.18"与 README 标注一致,构建目标覆盖aarch64-linux-ohos、x86_64-linux-ohos与x86_64-unknown-windows-gnu。若使用其他版本的工具链,请以实际编译与运行结果为准。
开源协议与参与贡献
本项目基于 Apache License 2.0 开源,可自由使用与参与贡献。欢迎提交 Pull Request 与 Issue,或参与任何形式的贡献。
- OpenHarmony
- UI组件
- 图像处理
【免费下载链接】gifdrawable4cj
一个gif图像加载库
相关推荐
gifdrawable4cj v0.0.1 能力解析:基于仓颉语言的 GIF 动图解码、播放控制与渲染实战指南
gifdrawable4cj v0.0.1 能力解析:基于仓颉语言的 GIF 动图解码、播放控制与渲染实战指南 gifdrawable4cj 是一个基于仓颉(C
OpenHarmonyUI组件图像处理仓颉(Cangjie)APNG 动画解码渲染库 apng4cj:接入配置、核心 API 与源码原理深度解析
仓颉(Cangjie)APNG 动画解码渲染库 apng4cj:接入配置、核心 API 与源码原理深度解析 本篇技术指南以开源仓库 apng4cj https:
OpenHarmony图像处理仓颉图片处理库 image4cj 实战指南:架构设计、JPEG/PNG/GIF 编解码与图像处理 API 全解析
仓颉图片处理库 image4cj 实战指南:架构设计、JPEG/PNG/GIF 编解码与图像处理 API 全解析 image4cj 是一个基于仓颉(Cangji
图像处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考