Search K
Appearance
Appearance
高效率图像文件格式(英语:High Efficiency Image File Format,简称 HEIF),是一种用于存储单张图像或图像序列的文件格式,而 HEIC 是 HEIF 文件的扩展名。作为 HEVC 编码的容器,HEIF 提供了比 JPEG 更高的压缩效率, 同时保持相同的图像质量。
HEIF 文件格式被苹果公司广泛应用于其 iOS 设备,但由于浏览器原生并不支持 HEIC 格式 ,因此需要使用额外的方法来处理和显示 HEIC 图片。
由于浏览器无法直接处理 HEIC 格式图片,我们需要将其转换为浏览器支持的格式,例如 JPEG 或 PNG。目前常见的解决方案包括:
heic2any
)来转换 HEIC 文件。libheif
,将其编译为 WebAssembly (WASM) 模块,在浏览器端处理 HEIC 图片。推荐使用 libheif
,因为 heic2any
库已停止维护且存在许多问题。以下是如何使 用 libheif
并将其编译为 WASM 的具体步骤。
强烈建议在类 Unix 环境下(如 WSL Ubuntu)进行编译。
libheif
仓库自带编译脚本位于 libheif/build-emscripten.sh
,以下为修改后的脚本 内容:
#!/bin/bash
emcc -Wl,--whole-archive "$LIBHEIFA" -Wl,--no-whole-archive \
-sEXPORTED_FUNCTIONS="$EXPORTED_FUNCTIONS,_free,_malloc,_memcpy" \
-sMODULARIZE \
-sEXPORT_NAME="libheif" \
-sWASM_ASYNC_COMPILATION=0 \
-sALLOW_MEMORY_GROWTH \
-std=c++11 \
$LIBRARY_INCLUDE_FLAGS \
$LIBRARY_LINKER_FLAGS \
$BUILD_FLAGS \
$RELEASE_BUILD_FLAGS
# 使用 WSL Ubuntu 编译
#!/bin/bash
# 下载并安装 Emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
# 安装必要的工具链
sudo apt install build-essential cmake
# 克隆 libheif 仓库并开始编译
git clone https://github.com/strukturag/libheif
cd libheif
mkdir buildjs
cd buildjs
# 使用仓库自带脚本支持 ES6 模块导出
USE_WASM=1 ../build-emscripten.sh ..
编译产物位于 libheif/buildjs/libheif.js
import type { MainModule, heif_context } from '../lib/libheif';
import loadModule from '../lib/libheif';
/**
* @description heif reader
*/
export class HEIFReader {
/**
* @description heif解码器
*/
static heifModule?: MainModule | null;
#heifDecoder?: heif_context | null;
/**
* 初始化heif解码器
*/
static async initHeif() {
HEIFReader.heifModule = await loadModule();
}
#blobToUnit8Array(blob: Blob) {
return new Promise<Uint8Array>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(new Uint8Array(reader.result as ArrayBuffer));
};
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
});
}
/**
* @description heif解码以流式方式解码
* @param imgFile Uint8Array | Blob | File
*/
async heifDecode(imgFile: Uint8Array | Blob | File) {
if (!HEIFReader.heifModule) throw new Error('heif未初始化');
if (this.#heifDecoder) {
HEIFReader.heifModule.heif_context_free(this.#heifDecoder);
}
const start = Date.now();
console.log(new Date().toLocaleTimeString(), '开始heif解码');
let data = imgFile as Uint8Array;
if (imgFile instanceof Blob) {
data = await this.#blobToUnit8Array(imgFile);
}
this.#heifDecoder = HEIFReader.heifModule.heif_context_alloc();
if (!this.#heifDecoder) {
throw new Error('heif解码器初始化失败');
}
const error = HEIFReader.heifModule.heif_context_read_from_memory(
this.#heifDecoder,
data,
);
if (error.code !== HEIFReader.heifModule.heif_error_code.heif_error_Ok) {
throw new Error('heif解码失败');
}
const ids =
HEIFReader.heifModule.heif_js_context_get_list_of_top_level_image_IDs(
this.#heifDecoder,
);
if (!ids || ids.code) {
throw new Error('加载图片ids失败');
}
if (!ids.length) {
throw new Error('heif容器内没有图片');
}
const result: Promise<DecodeResult | undefined>[] = [];
const handleTo = async (id: number) => {
const heifModule = HEIFReader.heifModule!;
const handle = heifModule.heif_js_context_get_image_handle(
this.#heifDecoder!,
id,
);
if (!handle || handle.code) {
console.log('没有获取到图片句柄', id, handle);
return;
}
const width = heifModule.heif_image_handle_get_width(handle);
const height = heifModule.heif_image_handle_get_height(handle);
const isPrimary = heifModule.heif_image_handle_is_primary_image(handle);
// 以RGB格式解码
const img = heifModule.heif_js_decode_image2(
handle,
heifModule.heif_colorspace.heif_colorspace_RGB,
heifModule.heif_chroma.heif_chroma_interleaved_RGBA,
);
if (!img || img.code) {
throw new Error('heif handle解码失败');
}
const imageData = new ImageData(width, height);
for (const c of img.channels) {
if (c.id === heifModule.heif_channel.heif_channel_interleaved) {
// 复制值
if (c.stride === c.width * 4) {
imageData.data.set(c.data);
} else {
for (let y = 0; y < c.height; y++) {
imageData.data.set(
// slice
c.data.slice(y * c.stride, y * c.stride + c.width * 4),
y * c.width * 4,
);
}
}
}
}
heifModule.heif_image_handle_release(handle);
const blob = await this.#imageDataToBlob(imageData);
return {
data: blob,
width,
height,
isPrimary: Boolean(isPrimary),
};
};
for (let i = 0; i < ids.length; i++) {
result.push(handleTo(ids[i]));
}
const imgs = (await Promise.all(result)).filter(
(item) => item,
) as DecodeResult[];
console.log(`heif解码:耗时${Date.now() - start}ms`);
return imgs;
}
/**
* 释放解码器
*/
free() {
if (this.#heifDecoder) {
if (HEIFReader.heifModule) {
HEIFReader.heifModule.heif_context_free(this.#heifDecoder);
}
this.#heifDecoder = null;
}
}
/**
* 释放导入的模块
*/
static freeModule() {
if (HEIFReader.heifModule) {
HEIFReader.heifModule = null;
}
}
#imageDataToBlob(imageData: ImageData): Promise<Blob> {
// 1. 创建临时canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imageData.width;
canvas.height = imageData.height;
// 2. 将ImageData绘制到canvas上
ctx!.putImageData(imageData, 0, 0);
// 3. 将canvas转换为blob
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve(blob!);
}, 'image/png');
});
}
}
export interface DecodeResult {
/**
* 以二进制形式存储的图片数据
*/
data: Blob;
width: number;
height: number;
/**
* 是否是主图
*/
isPrimary: boolean;
}
可以将图片解析放到 web worker 中。同时使用 OffscreenCanvas 来绘制图片,提升性能 。