Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions packages/heic/codec/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# using libheif from https://github.com/strukturag/libheif
# and libde265 for H.265/HEVC decoding (used by iPhone HEIC photos)
LIBHEIF_URL = https://github.com/strukturag/libheif/archive/refs/tags/v1.19.7.tar.gz
LIBHEIF_PACKAGE = node_modules/libheif.tar.gz

LIBDE265_URL = https://github.com/strukturag/libde265/archive/refs/tags/v1.0.15.tar.gz
LIBDE265_PACKAGE = node_modules/libde265.tar.gz

export CODEC_DIR = node_modules/libheif
export BUILD_DIR = node_modules/build
export LIBDE265_DIR = node_modules/libde265

OUT_DEC_JS = dec/heic_dec.js
OUT_DEC_CPP = dec/heic_dec.cpp
OUT_DEC_WASM = dec/heic_dec.wasm
ENVIRONMENT = web,worker

PRE_JS = pre.js

LIBDE265_BUILD_DIR = $(BUILD_DIR)/libde265
LIBDE265_OUT = $(LIBDE265_BUILD_DIR)/libde265/libde265.a

LIBHEIF_BUILD_DIR = $(BUILD_DIR)/libheif
LIBHEIF_OUT = $(LIBHEIF_BUILD_DIR)/libheif/libheif.a

STACK_SIZE = 5242880
INITIAL_MEMORY_SIZE = 16777216

.PHONY: all clean

all: $(OUT_DEC_JS)

# Build the final decoder JS+WASM
$(OUT_DEC_JS): $(OUT_DEC_CPP) $(LIBHEIF_OUT) $(LIBDE265_OUT)
$(CXX) \
-I $(CODEC_DIR)/libheif \
-I $(CODEC_DIR)/libheif/api \
-I $(LIBHEIF_BUILD_DIR) \
$(CXXFLAGS) \
$(LDFLAGS) \
--pre-js $(PRE_JS) \
--bind \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
-s ENVIRONMENT=$(ENVIRONMENT) \
-s EXPORT_ES6=1 \
-s DYNAMIC_EXECUTION=0 \
-s MODULARIZE=1 \
-s STACK_SIZE=$(STACK_SIZE) \
-s INITIAL_MEMORY=$(INITIAL_MEMORY_SIZE) \
-o $@ \
$+

# Build libde265
$(LIBDE265_OUT): $(LIBDE265_DIR)/CMakeLists.txt
emcmake cmake \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DENABLE_SDL=OFF \
-DENABLE_ENCODER=OFF \
-DENABLE_DECODER=ON \
-B $(LIBDE265_BUILD_DIR) \
$(LIBDE265_DIR) && \
$(MAKE) -C $(LIBDE265_BUILD_DIR) && \
cp $(LIBDE265_BUILD_DIR)/libde265/de265-version.h $(LIBDE265_DIR)/libde265/

# Build libheif (decode-only, with libde265)
$(LIBHEIF_OUT): $(CODEC_DIR)/CMakeLists.txt $(LIBDE265_OUT)
emcmake cmake \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DWITH_EXAMPLES=OFF \
-DWITH_GDK_PIXBUF=OFF \
-DENABLE_MULTITHREADING_SUPPORT=OFF \
-DWITH_AOM_DECODER=OFF \
-DWITH_AOM_ENCODER=OFF \
-DWITH_X265=OFF \
-DWITH_DAV1D=OFF \
-DWITH_RAV1E=OFF \
-DWITH_SvtEnc=OFF \
-DWITH_KVAZAAR=OFF \
-DWITH_OpenJPEG_DECODER=OFF \
-DWITH_OpenJPEG_ENCODER=OFF \
-DWITH_JPEG_DECODER=OFF \
-DWITH_JPEG_ENCODER=OFF \
-DWITH_OpenH264_DECODER=OFF \
-DWITH_OpenH264_ENCODER=OFF \
-DWITH_FFMPEG_DECODER=OFF \
-DWITH_UNCOMPRESSED_CODEC=OFF \
-DWITH_DEFLATE_HEADER_COMPRESSION=OFF \
-DBUILD_TESTING=OFF \
-DWITH_LIBDE265=ON \
-DLIBDE265_INCLUDE_DIR=$(LIBDE265_DIR) \
-DLIBDE265_LIBRARY=$(LIBDE265_OUT) \
-B $(LIBHEIF_BUILD_DIR) \
$(CODEC_DIR) && \
$(MAKE) -C $(LIBHEIF_BUILD_DIR)

# Download and extract libheif
$(LIBHEIF_PACKAGE):
mkdir -p $(@D)
curl -sL $(LIBHEIF_URL) -o $@

$(CODEC_DIR)/CMakeLists.txt: $(LIBHEIF_PACKAGE)
mkdir -p $(@D)
tar xzm --strip 1 -C $(@D) -f $(LIBHEIF_PACKAGE)

# Download and extract libde265
$(LIBDE265_PACKAGE):
mkdir -p $(@D)
curl -sL $(LIBDE265_URL) -o $@

$(LIBDE265_DIR)/CMakeLists.txt: $(LIBDE265_PACKAGE)
mkdir -p $(@D)
tar xzm --strip 1 -C $(@D) -f $(LIBDE265_PACKAGE)

clean:
$(RM) $(OUT_DEC_JS) $(OUT_DEC_WASM)
$(RM) -r $(BUILD_DIR)
74 changes: 74 additions & 0 deletions packages/heic/codec/dec/heic_dec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include "libheif/heif.h"

using namespace emscripten;

thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");

val decode(std::string heicimage) {
heif_context* ctx = heif_context_alloc();
heif_error error;

error = heif_context_read_from_memory_without_copy(
ctx, heicimage.c_str(), heicimage.length(), nullptr);

if (error.code != heif_error_Ok) {
heif_context_free(ctx);
return val::null();
}

// Get the primary image handle
heif_image_handle* handle;
error = heif_context_get_primary_image_handle(ctx, &handle);
if (error.code != heif_error_Ok) {
heif_context_free(ctx);
return val::null();
}

// Decode image to RGBA
heif_image* image;
error = heif_decode_image(handle, &image, heif_colorspace_RGB,
heif_chroma_interleaved_RGBA, nullptr);
if (error.code != heif_error_Ok) {
heif_image_handle_release(handle);
heif_context_free(ctx);
return val::null();
}

int width = heif_image_get_width(image, heif_channel_interleaved);
int height = heif_image_get_height(image, heif_channel_interleaved);
int stride;
const uint8_t* data =
heif_image_get_plane_readonly(image, heif_channel_interleaved, &stride);

// Copy pixel data row by row (stride may differ from width * 4)
size_t row_bytes = width * 4;
size_t total_bytes = row_bytes * height;

val result = val::null();

if (stride == (int)row_bytes) {
result = ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(total_bytes, data)),
width, height);
} else {
// Need to remove stride padding
val pixel_data = Uint8ClampedArray.new_(total_bytes);
for (int y = 0; y < height; y++) {
pixel_data.call<void>("set", val(typed_memory_view(row_bytes, data + y * stride)), y * row_bytes);
}
result = ImageData.new_(pixel_data, width, height);
}

heif_image_release(image);
heif_image_handle_release(handle);
heif_context_free(ctx);

return result;
}

EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
}
7 changes: 7 additions & 0 deletions packages/heic/codec/dec/heic_dec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface HEICModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
}

declare var moduleFactory: EmscriptenWasm.ModuleFactory<HEICModule>;

export default moduleFactory;
15 changes: 15 additions & 0 deletions packages/heic/codec/dec/heic_dec.js

Large diffs are not rendered by default.

Binary file added packages/heic/codec/dec/heic_dec.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions packages/heic/codec/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"scripts": {
"build": "EMSDK_VERSION=3.1.57 DEFAULT_CFLAGS='-Oz -flto' ../../../tools/build-cpp.sh"
},
"type": "module"
}
24 changes: 24 additions & 0 deletions packages/heic/codec/pre.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const isServiceWorker = globalThis.ServiceWorkerGlobalScope !== undefined;
const isRunningInCloudFlareWorkers = isServiceWorker && typeof self !== 'undefined' && globalThis.caches && globalThis.caches.default !== undefined;
const isRunningInNode = typeof process === 'object' && process.release && process.release.name === 'node';

if (isRunningInCloudFlareWorkers || isRunningInNode) {
if (!globalThis.ImageData) {
// Simple Polyfill for ImageData Object
globalThis.ImageData = class ImageData {
constructor(data, width, height) {
this.data = data;
this.width = width;
this.height = height;
}
};
}

if (import.meta.url === undefined) {
import.meta.url = 'https://localhost';
}

if (typeof self !== 'undefined' && self.location === undefined) {
self.location = { href: '' };
}
}
43 changes: 43 additions & 0 deletions packages/heic/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { HEICModule } from './codec/dec/heic_dec.js';
import { initEmscriptenModule } from './utils.js';

import heic_dec from './codec/dec/heic_dec.js';

let emscriptenModule: Promise<HEICModule>;

export async function init(
moduleOptionOverrides?: Partial<EmscriptenWasm.ModuleOpts>,
): Promise<void>;
export async function init(
module?: WebAssembly.Module,
moduleOptionOverrides?: Partial<EmscriptenWasm.ModuleOpts>,
): Promise<void> {
let actualModule: WebAssembly.Module | undefined = module;
let actualOptions: Partial<EmscriptenWasm.ModuleOpts> | undefined =
moduleOptionOverrides;

// If only one argument is provided and it's not a WebAssembly.Module
if (arguments.length === 1 && !(module instanceof WebAssembly.Module)) {
actualModule = undefined;
actualOptions = module as unknown as Partial<EmscriptenWasm.ModuleOpts>;
}

emscriptenModule = initEmscriptenModule(
heic_dec,
actualModule,
actualOptions,
);
}

export default async function decode(
buffer: ArrayBuffer,
): Promise<ImageData> {
if (!emscriptenModule) {
init();
}

const module = await emscriptenModule;
const result = module.decode(buffer);
if (!result) throw new Error('Decoding error');
return result;
}
Loading