ImageKnifePro 源码解读(十四):EXIF 方向处理、HDR 显示与内存分配模式
图片从文件字节流变成屏幕上的像素,中间有几个容易被忽略的环节:手机拍的照片可能带有 EXIF 方向标记,不处理的话图片会显示成横的或倒的;HDR 图片需要在支持的设备上以高动态范围模式渲染;解码后的 PixelMap 内存从哪里分配,DMA 还是共享内存,直接影响送显效率和内存占用。ImageKnifePro 在解码拦截器里统一处理了这些问题。 数码相机和手机拍照时,传感器捕获的原始像素排列方式和用户看到的画面方向可能不一致。EXIF 元数据中的 Orientation 字段记录了这个偏差。EXIF 标准定义了八种 Orientation 值,ImageKnifePro 在 值 1(Top-left)表示图片像素排列和显示方向一致,不需要做任何变换。值 5、7 是最复杂的情况,需要先翻转再旋转,两步操作不能颠倒顺序——先旋转再翻转会得到错误的结果。 方向修正发生在 这里有几个设计考虑。只对主图执行方向修正——占位图、错误图一般是本地资源,不存在方向问题。只在 当用户设置了具体的方向枚举(UP、RIGHT、DOWN、LEFT),ImageKnifePro 不读 EXIF,直接按枚举值执行旋转操作。枚举和 EXIF 字符串之间有一个映射表: 动图的方向修正也走同一套逻辑。 HDR 显示涉及两个阶段:解码时告诉系统期望的动态范围,送显时设置 Image 组件的渲染模式。 只有主图才尊重用户的 用户强制指定 解码 PixelMap 时可以选择不同的内存分配方式。ImageKnifePro 通过 DMA Buffer 是 GPU 和显示子系统可以直接访问的内存区域。使用 DMA Buffer 分配的 PixelMap 在送显时不需要额外的内存拷贝,图形合成器可以直接读取。SharedMemory 是进程间共享的内存段,分配和管理开销较低,但送显时可能需要一次拷贝到 GPU 可见的缓冲区。 当指定的分配方式不被设备支持时( JPEG 图片的原始编码格式是 YUV。默认解码流程把 YUV 转换成 RGBA,每像素占 4 字节。ImageKnifePro 提供了一个优化选项:如果不需要做图形变换,直接以 NV21(YUV 的一种平面排列格式)输出,跳过 YUV-to-RGBA 的转换。NV21 每像素约 1.5 字节,内存占用是 RGBA 的 37.5%。 启用条件有两个:全局开关 图形变换操作(裁剪、圆角、模糊等)需要逐像素操作 RGBA 数据,NV21 格式不适用。所以有变换时自动跳过优化,回退到标准解码路径。 在 只有 JPEG 格式才走这个优化路径。PNG、WEBP 等格式的原始编码方式不同,强制设置 NV21 没有意义。 有个需要注意的点:通过 期望尺寸来自降采样计算结果。如果宽高都为零,表示不需要降采样,系统按原图解码。动图的逐帧解码通过 以上就是本篇内容的所有了~有什么问题欢迎在评论区提出 项目地址:ImageKnifePro
一、EXIF 方向:八种旋转翻转组合
PixelmapUtils::GetOrientateFunc 中为每种值注册了对应的操作函数:static std::unordered_map<std::string, std::function<void(OH_PixelmapNative *)>> funcMap = {
{"Top-left", nullptr}, // 值1:无需操作
{"Top-right", [](OH_PixelmapNative *p) {
OH_PixelmapNative_Flip(p, true, false); // 值2:水平镜像
}},
{"Bottom-right", [rotate180](OH_PixelmapNative *p) {
OH_PixelmapNative_Rotate(p, rotate180); // 值3:旋转180度
}},
{"Bottom-left", [](OH_PixelmapNative *p) {
OH_PixelmapNative_Flip(p, false, true); // 值4:垂直镜像
}},
{"Left-top", [rotate270](OH_PixelmapNative *p) {
OH_PixelmapNative_Flip(p, true, false); // 值5:水平镜像+旋转270度
OH_PixelmapNative_Rotate(p, rotate270);
}},
{"Right-top", [rotate90](OH_PixelmapNative *p) {
OH_PixelmapNative_Rotate(p, rotate90); // 值6:旋转90度
}},
{"Right-bottom", [rotate90](OH_PixelmapNative *p) {
OH_PixelmapNative_Flip(p, true, false); // 值7:水平镜像+旋转90度
OH_PixelmapNative_Rotate(p, rotate90);
}},
{"Left-bottom", [rotate270](OH_PixelmapNative *p) {
OH_PixelmapNative_Rotate(p, rotate270); // 值8:旋转270度
}},
};Orientate 的调用时机
DecodeInterceptorDefault::Decode 的最后一步。解码完成后,Orientate 方法读取 ImageSource 的 Orientation 属性,然后对 PixelMap 执行对应的变换:void Orientate(std::shared_ptr<ImageKnifeTask> task, OH_ImageSourceNative *source)
{
if (task->GetImageRequestType() != ImageRequestType::MAIN_SRC) {
return;
}
auto option = request->GetImageKnifeOption();
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);
}orientation == AUTO 时才读 EXIF,因为用户可以通过设置 Orientation::UP、Orientation::RIGHT 等枚举来强制指定显示方向,跳过 EXIF 读取。Image_String 返回的 data 不包含尾零,需要用长度构造 std::string。最后还要 free(value.data) 释放系统分配的内存,这在 API 文档里有说明,漏掉会造成内存泄漏。枚举驱动的强制方向
static std::unordered_map<Orientation, std::string> enumMap = {
{Orientation::DOWN, "Bottom-right"},
{Orientation::LEFT, "Left-bottom"},
{Orientation::RIGHT, "Right-top"},
};Orientation::UP 不在 map 里,因为 UP 表示"按像素原始排列显示",不需要任何操作。这个映射让枚举路径和 EXIF 路径复用了同一套变换逻辑,避免了代码重复。Orientate 方法遍历 ImageData 的所有帧(data->GetFrameCount()),对每一帧的 PixelMap 执行相同的变换操作。二、HDR 动态范围处理
解码阶段的动态范围配置
DecodeInterceptorDefault::GetDesiredDynamicRange 根据用户设置返回解码期望的动态范围:IMAGE_DYNAMIC_RANGE DecodeInterceptorDefault::GetDesiredDynamicRange(
std::shared_ptr<ImageKnifeTask> task)
{
if (task->GetImageRequestType() == ImageRequestType::MAIN_SRC) {
return static_cast<IMAGE_DYNAMIC_RANGE>(option->dynamicRange);
}
return IMAGE_DYNAMIC_RANGE_AUTO;
}dynamicRange 设置,占位图、缩略图统一用 AUTO。三种可选值:AUTO 由系统根据图片内容自动决定,SDR 强制标准动态范围,HDR 强制高动态范围。送显阶段的组件模式设置
ImageKnifeNodeImage::SetDynamicRangeMode 在图片显示前设置 Image 组件的渲染模式:void ImageKnifeNodeImage::SetDynamicRangeMode(OH_PixelmapNative *pixelmap)
{
ArkUI_NumberValue num = {.i32 = ARKUI_DYNAMIC_RANGE_MODE_STANDARD};
if (option->dynamicRange == DynamicRange::HDR) {
num.i32 = ARKUI_DYNAMIC_RANGE_MODE_HIGH;
} else if (option->dynamicRange == DynamicRange::AUTO && pixelmap != nullptr) {
OH_Pixelmap_ImageInfo *imageInfo = nullptr;
if (OH_PixelmapImageInfo_Create(&imageInfo) == IMAGE_SUCCESS) {
OH_PixelmapNative_GetImageInfo(pixelmap, imageInfo);
bool isHdr = false;
OH_PixelmapImageInfo_GetDynamicRange(imageInfo, &isHdr);
if (isHdr) {
num.i32 = ARKUI_DYNAMIC_RANGE_MODE_HIGH;
}
OH_PixelmapImageInfo_Release(imageInfo);
}
}
const int dynamicRangeModeAttr = 4018;
GetNodeAPI()->setAttribute(handle,
static_cast<ArkUI_NodeAttributeType>(dynamicRangeModeAttr), &item);
}DynamicRange::HDR 时,直接设为高动态范围模式。AUTO 模式下需要检测解码后的 PixelMap 是否真的包含 HDR 信息——通过 OH_PixelmapImageInfo_GetDynamicRange 查询。dynamicRangeModeAttr 用硬编码值 4018 而不是枚举名 NODE_IMAGE_DYNAMIC_RANGE_MODE,是一种 API 版本兼容手法。这个枚举在 API 21 才引入,用硬编码值可以在低版本 IDE 上编译通过,运行时在低版本设备上等同于空操作。同样的手法也用在了 NODE_IMAGE_SYNC_LOAD(值 4012)上。
三、内存分配模式:DMA vs SharedMemory
AllocationMode 枚举提供了三种选择:AUTO(系统自动选择)、DMA(DMA Buffer)、SHARE_MEMORY(共享内存)。CreatePixelmapByAllocator 方法根据 option 中的 allocationMode 选择分配方式:OH_PixelmapNative *DecodeInterceptorDefault::CreatePixelmapByAllocator(
OH_ImageSourceNative *source, OH_DecodingOptions *decodeOption,
std::shared_ptr<ImageKnifeTaskInternal> task)
{
static auto apiFunc = ImageKnifeApi::GetInstance()
.GetImageSourceApi().GetApiCreatePixelmapByAllocator();
if (apiFunc != nullptr) {
IMAGE_ALLOCATOR_TYPE allocatorType = IMAGE_ALLOCATOR_TYPE_AUTO;
if (task->GetImageRequestType() == ImageRequestType::MAIN_SRC) {
allocatorType = static_cast<IMAGE_ALLOCATOR_TYPE>(option->allocationMode);
}
errCode = apiFunc(source, decodeOption, allocatorType, &pixelmap);
if (errCode == IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE
&& option->allocationMode != AllocationMode::AUTO) {
errCode = apiFunc(source, decodeOption, IMAGE_ALLOCATOR_TYPE_AUTO, &pixelmap);
}
} else {
errCode = OH_ImageSourceNative_CreatePixelmap(source, decodeOption, &pixelmap);
}
// ...
}GetApiCreatePixelmapByAllocator 通过动态符号查找获取 API 函数指针。这个 API 在 API 15 才引入,旧版本系统上返回 nullptr,此时走标准的 OH_ImageSourceNative_CreatePixelmap。分配方式只对主图生效——占位图等体积小、生命周期短,用 AUTO 即可。IMAGE_SOURCE_UNSUPPORTED_ALLOCATOR_TYPE),自动降级到 AUTO 模式重试。这种降级策略保证了接口在不同硬件上的可用性,不会因为用户设置了 DMA 而在不支持的设备上崩溃。四、JPEG NV21 优化解码
setJpegOptimizeDecoding(true) 打开,且当前图片没有设置图形变换。bool DecodeInterceptorDefault::IsYuvEnable(std::shared_ptr<ImageKnifeTaskInternal> &task)
{
if (!ImageKnifeInternal::GetInstance().GetJpegOptimizeDecoding()) {
return false;
}
auto option = request->GetImageKnifeOption();
return option->multiTransformation == nullptr && option->transformation == nullptr;
}ConfigDecodeOption 中,条件满足时设置解码像素格式:if (task->GetFileTypeInfo()->format == ImageFormat::JPG && IsYuvEnable(task)) {
OH_DecodingOptions_SetPixelFormat(args.decodeOption, PIXEL_FORMAT_NV21);
}getCacheImage 获取到的 PixelMap 在 NV21 优化开启后可能是 NV21 格式,不是 RGBA。如果使用方需要对像素数据做操作,要先检查格式或自行转换。五、解码选项的完整配置
ConfigDecodeOption 方法把上述所有配置项集中在一处:bool ConfigDecodeOption(DecodeArgs &args, std::shared_ptr<ImageKnifeTaskInternal> &task)
{
OH_DecodingOptions_Create(&args.decodeOption);
// 1. 期望解码尺寸(降采样后的目标尺寸)
Image_Size imageSize = task->GetDesiredImageSize();
if (imageSize.width != 0 && imageSize.height != 0) {
OH_DecodingOptions_SetDesiredSize(args.decodeOption, &imageSize);
}
// 2. JPEG NV21 优化
if (task->GetFileTypeInfo()->format == ImageFormat::JPG && IsYuvEnable(task)) {
OH_DecodingOptions_SetPixelFormat(args.decodeOption, PIXEL_FORMAT_NV21);
}
// 3. 动态范围
OH_DecodingOptions_SetDesiredDynamicRange(args.decodeOption, GetDesiredDynamicRange(task));
// 4. 动图指定帧
if (task->IsFrameDecodeMode()) {
OH_DecodingOptions_SetIndex(args.decodeOption, task->GetDecodeFrameIndex());
}
return true;
}SetIndex 指定当前帧序号。这些选项互不冲突,可以同时设置。DecodeArgs 结构体利用 RAII:析构函数自动释放 OH_DecodingOptions 和 OH_ImageSourceNative,即使中间步骤提前 return 也不会泄漏资源。