图片从文件字节流变成屏幕上的像素,中间有几个容易被忽略的环节:手机拍的照片可能带有 EXIF 方向标记,不处理的话图片会显示成横的或倒的;HDR 图片需要在支持的设备上以高动态范围模式渲染;解码后的 PixelMap 内存从哪里分配,DMA 还是共享内存,直接影响送显效率和内存占用。ImageKnifePro 在解码拦截器里统一处理了这些问题。

解码阶段处理流程

一、EXIF 方向:八种旋转翻转组合

数码相机和手机拍照时,传感器捕获的原始像素排列方式和用户看到的画面方向可能不一致。EXIF 元数据中的 Orientation 字段记录了这个偏差。EXIF 标准定义了八种 Orientation 值,ImageKnifePro 在 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度
    }},
};

值 1(Top-left)表示图片像素排列和显示方向一致,不需要做任何变换。值 5、7 是最复杂的情况,需要先翻转再旋转,两步操作不能颠倒顺序——先旋转再翻转会得到错误的结果。

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::UPOrientation::RIGHT 等枚举来强制指定显示方向,跳过 EXIF 读取。

Image_String 返回的 data 不包含尾零,需要用长度构造 std::string。最后还要 free(value.data) 释放系统分配的内存,这在 API 文档里有说明,漏掉会造成内存泄漏。

枚举驱动的强制方向

当用户设置了具体的方向枚举(UP、RIGHT、DOWN、LEFT),ImageKnifePro 不读 EXIF,直接按枚举值执行旋转操作。枚举和 EXIF 字符串之间有一个映射表:

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 动态范围处理

HDR 显示涉及两个阶段:解码时告诉系统期望的动态范围,送显时设置 Image 组件的渲染模式。

解码阶段的动态范围配置

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)上。

HDR两阶段流程

三、内存分配模式:DMA vs SharedMemory

解码 PixelMap 时可以选择不同的内存分配方式。ImageKnifePro 通过 AllocationMode 枚举提供了三种选择:AUTO(系统自动选择)、DMA(DMA Buffer)、SHARE_MEMORY(共享内存)。

DMA Buffer 是 GPU 和显示子系统可以直接访问的内存区域。使用 DMA Buffer 分配的 PixelMap 在送显时不需要额外的内存拷贝,图形合成器可以直接读取。SharedMemory 是进程间共享的内存段,分配和管理开销较低,但送显时可能需要一次拷贝到 GPU 可见的缓冲区。

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 优化解码

JPEG 图片的原始编码格式是 YUV。默认解码流程把 YUV 转换成 RGBA,每像素占 4 字节。ImageKnifePro 提供了一个优化选项:如果不需要做图形变换,直接以 NV21(YUV 的一种平面排列格式)输出,跳过 YUV-to-RGBA 的转换。NV21 每像素约 1.5 字节,内存占用是 RGBA 的 37.5%。

启用条件有两个:全局开关 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;
}

图形变换操作(裁剪、圆角、模糊等)需要逐像素操作 RGBA 数据,NV21 格式不适用。所以有变换时自动跳过优化,回退到标准解码路径。

ConfigDecodeOption 中,条件满足时设置解码像素格式:

if (task->GetFileTypeInfo()->format == ImageFormat::JPG && IsYuvEnable(task)) {
    OH_DecodingOptions_SetPixelFormat(args.decodeOption, PIXEL_FORMAT_NV21);
}

只有 JPEG 格式才走这个优化路径。PNG、WEBP 等格式的原始编码方式不同,强制设置 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_DecodingOptionsOH_ImageSourceNative,即使中间步骤提前 return 也不会泄漏资源。

解码选项与分支关系

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

标签: none

添加新评论