ImageKnifePro 源码解读(六):图片解码——从格式识别到 AVIF 动态导入
图片加载的后半段是把字节流变成可渲染的 PixelMap。ImageKnifePro 用 C++ 拦截器链驱动解码分发,并通过 解码之前得知道拿到的是什么格式。ImageKnifePro 不依赖文件扩展名,直接读取字节流头部的魔数(magic number)判断。网络请求拿回来的数据不一定带扩展名,缓存文件的命名经常用 hash 替代原名,只有二进制头部的魔数是可靠的。 默认解码拦截器 多帧解码走 单帧解码走 解码完成后还有一步 EXIF 方向修正。 EXIF 标准定义了 8 种 Orientation 值,分别对应不同的翻转/旋转组合。 AVIF 由独立拦截器 如果设备不支持 AVIF,这个拦截器压根不会被添加。遇到 AVIF 图片时,默认解码器返回 根据 批量模式有一个防御设计:循环开始前就构造 函数指针缓存在 如果 CMakeLists.txt 通过 不启用时编译的是 这种双重防线的设计:编译期通过 第一步, 第二步,检查是否需要缩放。如果 第三步,YUV 转 RGB。 第四步, 比如 10bit 图片的 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifeProdlopen 在运行时动态加载 AVIF 解码器。编译期通过 #ifdef 控制头文件引入,运行时通过 dlopen 检测设备能力,两层防线保证在不支持 AVIF 的环境下安全降级。一、FileTypeUtil 的文件头识别
FileTypeUtil 用 std::map<ImageFormat, std::vector<std::vector<uint8_t>>> 做签名表。每种格式可以有多条候选签名(比如 AVIF 有 ftyp avif 和 ftyp avis 两条,前者是静态 AVIF,后者是序列图/动图)。static std::map<ImageFormat, std::vector<std::vector<uint8_t>>> fileSignatureMap_ = {
{ImageFormat::JPG, {{0xFF, 0xD8}}},
{ImageFormat::PNG, {{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}}},
{ImageFormat::GIF, {{0x47, 0x49, 0x46}}},
{ImageFormat::WEBP, {{0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50}}},
{ImageFormat::HEIC, {{0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63}, ...}},
{ImageFormat::AVIF, {{0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66},
{0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x73}}},
// ...
};MatchesSignature 方法有两处特殊处理。HEIC 和 AVIF 都基于 ISOBMFF 容器格式,头部结构是一个 Box:前 4 字节是 Box 的 size,后面才是类型标识 ftyp。比对时从第 4 字节开始,跳过 size 字段。WEBP 文件以 RIFF 四字节开头,紧跟 4 字节的 fileSize,然后才是 WEBP 标识。比对逻辑拆成两段:先比前 4 字节 RIFF,跳过 4-8 字节的 size 字段,再从第 8 字节开始比对 WEBP。if (fileType == ImageFormat::WEBP) {
constexpr int riffEnd = 4;
for (int i = 0; i < riffEnd; i++) {
if (data[i] != signature[i]) return false;
}
constexpr int typeOffset = 8;
for (int i = typeOffset; i < signature.size(); i++) {
if (data[i] != signature[i]) return false;
}
return true;
}CheckImageFormat 入口处有一个前置长度校验:文件字节流不足 32 字节直接返回 UNKNOWN,避免后续比对越界。32 字节足够覆盖所有已知格式的签名长度。
二、DecodeInterceptorDefault 的标准解码路径
DecodeInterceptorDefault 处理系统 OH_ImageSourceNative 支持的所有格式——JPG、PNG、GIF、WebP、BMP、HEIC、TIFF、ICO 等。Resolve() 开头检查文件格式,遇到 AVIF、UNKNOWN 或 CUSTOM_FORMAT 就返回 false,任务自动滑到链上的下一个拦截器。bool Resolve(std::shared_ptr<ImageKnifeTask> task) override
{
auto fileTypeInfo = task->GetFileTypeInfo();
if (fileTypeInfo->format == ImageFormat::UNKNOWN ||
fileTypeInfo->format == ImageFormat::CUSTOM_FORMAT ||
fileTypeInfo->format == ImageFormat::AVIF) {
return false;
}
if (taskInternal->IsFrameDecodeMode()) {
return DecodeFrame(taskInternal);
} else {
return Decode(taskInternal);
}
}Decode 方法的流程分三个阶段。GetImageSource 从 imageBuffer 创建 OH_ImageSourceNative。ConfigDecodeOption 装配 OH_DecodingOptions——设置期望解码尺寸(降采样)、JPEG 的 NV21 像素格式优化、期望动态范围。JPEG NV21 优化的判断条件是:全局开启 jpegOptimizeDecoding 且当前请求没有 multiTransformation 和 transformation。出于"NV21 格式不支持后续图形变换"的限制,有变换需求时不能走这条优化路径。bool ConfigDecodeOption(DecodeArgs &args, std::shared_ptr<ImageKnifeTaskInternal> &task)
{
OH_DecodingOptions_Create(&args.decodeOption);
Image_Size imageSize = task->GetDesiredImageSize();
if (imageSize.width != 0 && imageSize.height != 0) {
OH_DecodingOptions_SetDesiredSize(args.decodeOption, &imageSize);
}
if (task->GetFileTypeInfo()->format == ImageFormat::JPG && IsYuvEnable(task)) {
OH_DecodingOptions_SetPixelFormat(args.decodeOption, PIXEL_FORMAT_NV21);
}
OH_DecodingOptions_SetDesiredDynamicRange(args.decodeOption, GetDesiredDynamicRange(task));
return true;
}DecodePixelmapList(),调 OH_ImageSourceNative_CreatePixelmapList() 一次解出所有帧。帧延迟信息优先从 FileTypeInfo 的缓存中获取;缓存没有就重新调 OH_ImageSourceNative_GetDelayTimeList() 获取,这个 fallback 避免了解码阶段因缺少帧时长而失败。CreatePixelmapByAllocator(),优先尝试 DMA 内存分配模式。如果指定模式不被支持(返回 IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE),自动回退到 IMAGE_ALLOCATOR_TYPE_AUTO。如果封装的 API 函数指针为空(老版本 SDK),走标准的 OH_ImageSourceNative_CreatePixelmap。三、EXIF 方向校正
Orientate() 只在主图请求且 option->orientation == AUTO 时执行。它从 OH_ImageSourceNative 读出 Orientation 字符串,传给 PixelmapUtils::Orientate()。void Orientate(std::shared_ptr<ImageKnifeTask> task, OH_ImageSourceNative *source)
{
if (task->GetImageRequestType() != ImageRequestType::MAIN_SRC) return;
if (option->orientation != Orientation::AUTO) return;
std::string keyStr = "Orientation";
Image_String key = {.data = (char *)keyStr.c_str(), .size = keyStr.length()};
Image_String value = {.data = nullptr, .size = 0};
OH_ImageSourceNative_GetImageProperty(source, &key, &value);
std::string valueStr(value.data, value.size);
PixelmapUtils::Orientate(task->product.imageData, valueStr);
free(value.data);
}PixelmapUtils::Orientate() 用 unordered_map<string, function<void(OH_PixelmapNative*)>> 把每种方向映射到操作函数,对每一帧都执行。Image_String 返回的 data 不包含尾 0,必须用 std::string(value.data, value.size) 构造,并且按接口说明 free(value.data) 释放分配的内存。四、DecodeInterceptorAvif 的独立拦截器
DecodeInterceptorAvif 处理。CreateDefaultImageLoader 组装默认链时有条件地添加它:loader->AddDecodeInterceptor(decodeInterceptor); // Default 在链头
if (ImageKnifeDecoderAvif::IsAvifEnable()) {
loader->AddDecodeInterceptor(decodeInterceptorAvif, Position::END); // AVIF 在链尾
}false,链尾没有下一个节点,整条解码链返回 false,上层报解码失败。格式支持的增减变成了拦截器的有无,不影响已有拦截器的任何代码。Resolve() 只在 fileTypeInfo->format == ImageFormat::AVIF 时接管。Decode() 方法先尝试从 task 上获取可复用的 ImageKnifeDecoderAvif 实例——AVIF 序列图的逐帧解码场景下同一个 decoder 实例会被多次复用,避免重复解析 AVIF 容器头部。IsFrameDecodeMode() 的值决定走向。逐帧模式调 DecodeFrame(task->GetDecodeFrameIndex(), &pixelmap) 只解一帧。批量模式分配 OH_PixelmapNative* 数组,循环调用 DecodeFrame(i, &pixelmapList[i])。ImageData 对象持有 pixelmapList 指针。如果中途某一帧解码失败,函数 return false 时 imageData 是局部变量,析构时自动释放已解出的帧,不会泄漏。auto imageData = std::make_shared<ImageData>(pixelmapList, decoder->GetDelayTimeList(), frameCount);
for (int i = 0; i < frameCount; i++) {
std::string errorInfo = decoder->DecodeFrame(i, &pixelmapList[i]);
if (!errorInfo.empty()) {
task->EchoError(errorInfo);
return false; // imageData 析构释放 pixelmapList
}
}
task->product.imageData = imageData;
五、AvifApi 的 dlopen 动态导入
ImageKnifeDecoderAvif 内部嵌套了一个 AvifApi 单例类,负责在运行时通过 dlopen 加载 libavif.so.16。出于"AVIF 解码依赖系统是否安装了 libavif 动态库"的考虑,不能在编译期硬链接,否则在不带 libavif 的设备上程序直接加载失败。AvifApi 的构造函数调 dlopen("libavif.so.16", RTLD_NOW)。RTLD_NOW 表示立即解析所有符号——如果 so 内部有未解析的依赖,dlopen 阶段就会失败,而不是等到实际调用时才崩溃。dlopen 成功后批量 dlsym 导出 11 个 libavif 的公开接口:std::vector<std::string> funcNames = {
"avifDecoderCreate", "avifDecoderDestroy", "avifRGBImageFreePixels",
"avifDecoderSetIOMemory", "avifDecoderParse", "avifDecoderNthImageTiming",
"avifDecoderNthImage", "avifRGBImageSetDefaults", "avifRGBImageAllocatePixels",
"avifImageYUVToRGB", "avifImageScale"
};
for (auto &funcName : funcNames) {
void *func = dlsym(handle_, funcName.c_str());
if (func == nullptr) {
available_ = false;
} else {
apiMap_[funcName] = func;
}
}apiMap_(unordered_map<string, void*>)中。每个包装方法(如 AvifDecoderCreate())内部用 static auto func = GetApiFuncByName<...>(name) 取函数指针。static 局部变量保证每个函数指针只查一次 map,后续调用零开销直接走缓存好的裸指针。dlsym 有任何一个符号失败,available_ 设 false 并立即 dlclose(handle_) 释放句柄,不让半初始化的 so 占用资源。析构函数对 handle_ 做了空指针检查后再 dlclose,确保正常路径也能正确释放。六、编译期控制与 mock 实现
AvifApi 内部类、avifDecoder_ 指针、avifRGBImage rgbImage_ 等成员全部包裹在 #ifdef IMAGE_KNIFE_ENABLE_AVIF_DECODER 条件编译内。没有这个宏时,头文件中 ImageKnifeDecoderAvif 只剩公开方法声明,没有任何 libavif 类型依赖。IMAGEKNIFE_USING_LIBAVIF 选项控制:if (IMAGEKNIFE_USING_LIBAVIF)
add_definitions(-DIMAGE_KNIFE_ENABLE_AVIF_DECODER)
include_directories(imageknifepro PUBLIC
${NATIVERENDER_ROOT_PATH}/thirdparty/libavif/${OHOS_ARCH}/include)
target_sources(imageknifepro PRIVATE decoder/imageknife_decoder_avif.cpp)
else()
target_sources(imageknifepro PRIVATE decoder/imageknife_decoder_avif_mock.cpp)
endif()imageknife_decoder_avif_mock.cpp——所有方法都是空实现:IsAvifEnable() 返回 false,Init() 和 DecodeFrame() 返回固定错误字符串 "ImageKnife Avif Decoder Not Enable",GetWidth()/GetHeight()/GetFrameCount() 返回 0。这保证了没有 libavif 头文件和库的环境下也能正常编译链接。#ifdef 决定是否引入 avif 头文件和真实实现代码,解决"编译环境没有 libavif SDK"的问题;运行时通过 dlopen 决定是否有可用的 so 库,解决"目标设备没装 libavif 运行时"的问题。即便编译时开启了 AVIF 支持,到了不带 libavif 的设备上,dlopen 返回 nullptr,available_ 设 false,Loader 初始化时不挂载 AVIF 拦截器,整条链路安全降级。七、DecodeFrame 的单帧解码
ImageKnifeDecoderAvif::DecodeFrame() 的四步流程构成了 AVIF 像素解码的核心。AvifDecoderNthImage(decoder_, frameIndex) 定位到指定帧。libavif 的 avifDecoderNthImage 支持随机访问——AVIF 序列图的每一帧都是独立可寻址的,不需要从第 0 帧开始顺序解码。desiredWidth_ 和 desiredHeight_ 与原图尺寸不一致且不为 0,调 AvifImageScale() 在 YUV 域完成缩放。YUV 域缩放比先转 RGB 再缩放少一次完整的色彩空间转换,对大尺寸图片能省下可观的计算量。AvifRGBImageSetDefaults() 根据 avifImage 初始化 RGB 参数,AvifRGBImageAllocatePixels() 分配像素缓冲区,AvifImageYUVToRGB() 执行 YUV-to-RGBA 转换。CreatePixelmapNative() 把 RGBA 像素数据写进 OH_PixelmapNative。这里有一个位深适配处理:libavif 支持 10bit 和 12bit 图片,以 uint16 存储每个通道,但 OH_PixelmapNative 的 PIXEL_FORMAT_RGBA_8888 只支持 8bit。CovertToRGBA8888() 通过右移操作把每个通道从高位深缩放到 8bit:size_t byteGap = rgbImage.depth - 8;
outData[pos] = data[pos] >> byteGap; // R
outData[pos + 1] = data[pos + 1] >> byteGap; // G
outData[pos + 2] = data[pos + 2] >> byteGap; // B
outData[pos + 3] = data[pos + 3] >> byteGap; // Adepth 是 10,byteGap 为 2,每个 uint16 值右移 2 位截取高 8 位。这种处理会丢失低位精度,但在 8bit 显示设备上差异肉眼不可见。创建 PixelMap 时同样优先尝试 DMA allocator 接口,不支持再回退 AUTO 模式。