Skip to content

Commit

Permalink
Add TIFF decoding support
Browse files Browse the repository at this point in the history
  • Loading branch information
andylizi committed May 21, 2024
1 parent 19beb1a commit 4c2efd4
Show file tree
Hide file tree
Showing 15 changed files with 688 additions and 0 deletions.
2 changes: 2 additions & 0 deletions codecs/tiff/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**
!/patches
2 changes: 2 additions & 0 deletions codecs/tiff/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
compile_flags.txt
*.wat
160 changes: 160 additions & 0 deletions codecs/tiff/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
CODEC_URL = https://download.osgeo.org/libtiff/tiff-4.6.0.tar.xz
CODEC_PACKAGE = node_modules/tiff.tar.xz
CODEC_PACKAGE_HASH = e178649607d1e22b51cf361dd20a3753f244f022eefab1f2f218fc62ebaf87d2

ZLIB_URL = https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.xz
ZLIB_PACKAGE = node_modules/zlib.tar.xz
ZLIB_PACKAGE_HASH = 38ef96b8dfe510d42707d9c781877914792541133e1870841463bfa73f883e32

JPEG_URL = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.2/libjpeg-turbo-3.0.2.tar.gz
JPEG_PACKAGE = node_modules/libjpeg-turbo.tar.gz
JPEG_PACKAGE_HASH = c2ce515a78d91b09023773ef2770d6b0df77d674e144de80d63e0389b3a15ca6

export CODEC_DIR = node_modules/tiff
export ZLIB_DIR = node_modules/zlib
export JPEG_DIR = node_modules/jpeg

.PHONY all: clean_binary dec/tiff_dec.wasm

# ===============================================================
# zlib

$(ZLIB_PACKAGE):
mkdir -p "$(@D)"
curl -sSL "$(ZLIB_URL)" -o "$@"
echo "$(ZLIB_PACKAGE_HASH) $(ZLIB_PACKAGE)" | sha256sum -c -

$(ZLIB_DIR)/CMakeLists.txt: $(ZLIB_PACKAGE)
mkdir -p "$(@D)"
tar xJm --strip 1 -C "$(@D)" -f "$(ZLIB_PACKAGE)"
# remove dummy Makefile
rm -f "$(ZLIB_DIR)/Makefile"

$(ZLIB_DIR)/Makefile: $(ZLIB_DIR)/CMakeLists.txt
cmake -S "$(ZLIB_DIR)" -B "$(ZLIB_DIR)" \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DBUILD_SHARED_LIBS=OFF

ZLIB_LIB=$(ZLIB_DIR)/libz.a

$(ZLIB_LIB): $(ZLIB_DIR)/Makefile
$(MAKE) -C "$(ZLIB_DIR)" zlibstatic
mv -f "$(ZLIB_DIR)/libzlibstatic.a" "$(ZLIB_LIB)"

# ===============================================================
# libjpeg-turbo

$(JPEG_PACKAGE):
mkdir -p "$(@D)"
curl -sSL "$(JPEG_URL)" -o "$@"
echo "$(JPEG_PACKAGE_HASH) $(JPEG_PACKAGE)" | sha256sum -c -

$(JPEG_DIR)/CMakeLists.txt: $(JPEG_PACKAGE)
mkdir -p "$(@D)"
tar xzm --strip 1 -C "$(@D)" -f "$(JPEG_PACKAGE)"
for i in ./patches/libjpeg-turbo/*.patch; do patch -d "$(@D)" -N -p1 < "$$i"; done

$(JPEG_DIR)/Makefile: $(JPEG_DIR)/CMakeLists.txt
cmake -S "$(JPEG_DIR)" -B "$(JPEG_DIR)" \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DBUILD_SHARED_LIBS=OFF \
-DENABLE_SHARED=OFF \
-DWITH_SIMD=OFF \
-DWITH_TURBOJPEG=OFF

JPEG_LIB=$(JPEG_DIR)/libjpeg.a

$(JPEG_LIB): $(JPEG_DIR)/Makefile
$(MAKE) -C "$(JPEG_DIR)" jpeg-static

# ===============================================================
# libtiff

$(CODEC_PACKAGE):
mkdir -p "$(@D)"
curl -sSL "$(CODEC_URL)" -o "$@"
echo "$(CODEC_PACKAGE_HASH) $(CODEC_PACKAGE)" | sha256sum -c -

$(CODEC_DIR)/configure: $(CODEC_PACKAGE)
mkdir -p "$(@D)"
tar xJm --strip 1 -C "$(@D)" -f "$(CODEC_PACKAGE)"
for i in ./patches/libtiff/*.patch; do patch -d "$(@D)" -N -p1 < "$$i"; done

# It's a huge pain telling CMake to find our custom-built libraries, use autoconf instead...
$(CODEC_DIR)/Makefile: $(CODEC_DIR)/configure
cd $(CODEC_DIR); \
./configure \
--host=wasm32-wasi \
--enable-shared=no \
--enable-cxx=no \
--disable-tools \
--disable-tests \
--disable-contrib \
--disable-docs \
--disable-thunder \
--disable-next \
--disable-mdi \
--disable-old-jpeg \
--disable-jbig \
--disable-lerc \
--disable-lzma \
--disable-zstd \
--disable-webp \
--disable-sphinx \
--with-zlib-include-dir="$(shell realpath $(ZLIB_DIR))" \
--with-zlib-lib-dir="$(shell realpath $(ZLIB_DIR))" \
--with-jpeg-include-dir="$(shell realpath $(JPEG_DIR))" \
--with-jpeg-lib-dir="$(shell realpath $(JPEG_DIR))"

CODEC_LIB=$(CODEC_DIR)/libtiff/.libs/libtiff.a

$(CODEC_LIB): $(CODEC_DIR)/Makefile
$(MAKE) -C "$(CODEC_DIR)"

# ===============================================================

dec/tiff_dec.wasm: $(ZLIB_LIB) $(JPEG_LIB) $(CODEC_LIB)
$(CC) \
$(CFLAGS) \
-Wall \
-Wextra \
-Wconversion \
-I"$(CODEC_DIR)/libtiff" \
-g \
-Wl,-z,stack-size=1048576 \
-Wl,--fatal-warnings \
-Wl,--no-entry \
-Wl,--export=malloc \
-Wl,--export=free \
-Wl,--trace-symbol=stderr \
-mexec-model=reactor \
-o dec/tiff_dec.wasm \
dec/tiff_dec.c \
"$(CODEC_LIB)" \
"$(JPEG_LIB)" \
"$(ZLIB_LIB)" \
-lm

# ===============================================================

distclean_zlib:
rm -rf "$(ZLIB_DIR)"

distclean_jpeg:
rm -rf "$(JPEG_DIR)"

distclean_codec:
rm -rf "$(CODEC_DIR)"

.PHONY distclean: distclean_zlib distclean_jpeg distclean_codec

clean_jpeg:
[ -f "$(JPEG_DIR)/Makefile" ] && $(MAKE) -C "$(JPEG_DIR)" clean

clean_codec:
[ -f "$(CODEC_DIR)/Makefile" ] && $(MAKE) -C "$(CODEC_DIR)" clean

clean_binary:
rm -f dec/tiff_dec.wasm

.PHONY clean: clean_binary clean_codec clean_jpeg
8 changes: 8 additions & 0 deletions codecs/tiff/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail

docker build -t squoosh-wasi --network=host -f wasi.Dockerfile .
docker run -it --rm -v "$(pwd)":/src -u "$(id -u):$(id -g)" squoosh-wasi "$@"

# wasm2wat --enable-annotation --enable-code-metadata dec/tiff_dec.wasm > dec/tiff_dec.wat || true
wasm-opt -O3 --strip-debug -o dec/tiff_dec.wasm dec/tiff_dec.wasm
195 changes: 195 additions & 0 deletions codecs/tiff/dec/tiff_dec.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <tiffio.h>

typedef struct cursor {
const char* data;
tmsize_t size;
tmsize_t off;
} cursor_t;

typedef union cd_as_cursor {
thandle_t client;
cursor_t* cursor;
} cd_as_cursor_t;

static tmsize_t _cursor_read_proc(thandle_t clientdata, void* buf, tmsize_t len) {
cd_as_cursor_t cdc;
cdc.client = clientdata;
cursor_t* c = cdc.cursor;

tmsize_t nremain = c->size - c->off;
tmsize_t nread = nremain > len ? len : nremain;

memcpy(buf, (const void*)(c->data + c->off), (size_t)nread);
c->off += nread;
return (tmsize_t)nread;
}

static tmsize_t _cursor_write_proc(thandle_t clientdata, void* buf, tmsize_t size) {
(void)clientdata, (void)buf, (void)size;
return 0;
}

static toff_t _cursor_seek_proc(thandle_t clientdata, toff_t off, int whence) {
cd_as_cursor_t cdc;
cdc.client = clientdata;
cursor_t* c = cdc.cursor;

tmsize_t off_m = (tmsize_t)off;
if ((toff_t)off_m != off) {
errno = EINVAL;
return (toff_t)-1;
}

switch (whence) {
case SEEK_SET:
c->off = off_m;
break;
case SEEK_CUR:
c->off += off_m;
break;
case SEEK_END:
if (off_m <= c->size) {
c->off = c->size - off_m;
break;
}
__attribute__((fallthrough));
default:
errno = EINVAL;
return (toff_t)-1;
}
return (toff_t)c->off;
}

static int _cursor_close_proc(thandle_t clientdata) {
cd_as_cursor_t cdc;
cdc.client = clientdata;
free(cdc.cursor);
return 0;
}

static uint64_t _cursor_size_proc(thandle_t clientdata) {
cd_as_cursor_t cdc;
cdc.client = clientdata;
return (uint64_t)cdc.cursor->size;
}

static int _cursor_map_proc(thandle_t clientdata, void** pbase, toff_t* psize) {
cd_as_cursor_t cdc;
cdc.client = clientdata;
cursor_t* c = cdc.cursor;

// `tif_unix.c` only passes `PROT_READ` to `mmap()`, so dropping `const` should be fine here.
*pbase = (void*)c->data;
*psize = (toff_t)c->size;
return 1;
}

static void _cursor_unmap_proc(thandle_t clientdata, void* base, toff_t size) {
(void)clientdata, (void)base, (void)size;
}

extern int _return_decoded_image(const uint32_t* raster, uint32_t width, uint32_t height)
__attribute__((import_name("return_decoded_image")));

__attribute__((export_name("decode"))) int decode(const char* data, const size_t size) {
static const char* module_name = "tiff_dec";
if (!data)
return 0;

tmsize_t size_m = (tmsize_t)size;
if (((size_t)size_m) != size) {
TIFFErrorExtR(NULL, module_name, "Too large image buffer");
return 0;
}

cursor_t* c = malloc(sizeof(cursor_t));
if (!c)
return 0;
c->data = data;
c->size = size_m;
c->off = 0;

cd_as_cursor_t cdc;
cdc.cursor = c;
TIFF* tif = TIFFClientOpen("dummy.tif", "r", cdc.client, _cursor_read_proc, _cursor_write_proc,
_cursor_seek_proc, _cursor_close_proc, _cursor_size_proc,
_cursor_map_proc, _cursor_unmap_proc);
if (!tif) {
free(cdc.cursor);
return 0;
}

int ret = 0;

uint32_t width, height;
if (TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width) == 0 ||
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &height) == 0) {
TIFFErrorExtR(tif, module_name, "Missing image dimension tag");
goto cleanup_tif;
}

uint32_t npixels;
if (__builtin_umul_overflow(width, height, &npixels)) {
TIFFErrorExtR(tif, module_name, "Too large image dimension");
goto cleanup_tif;
}

uint32_t* raster = (uint32_t*)malloc(npixels * sizeof(uint32_t));
if (!raster) {
TIFFErrorExtR(tif, module_name, "Failed to allocate memory of %d*%d*4 bytes", width, height);
goto cleanup_tif;
}

if (!TIFFReadRGBAImageOriented(tif, width, height, raster, 1, ORIENTATION_TOPLEFT)) {
goto cleanup_raster;
}

ret = _return_decoded_image(raster, width, height);

cleanup_raster:
free(raster);
cleanup_tif:
TIFFClose(tif);
return ret;
}

extern int _log_warning(const char* s, size_t len) __attribute__((import_name("log_warning")));

extern int _log_error(const char* s, size_t len) __attribute__((import_name("log_error")));

// Override unix error handlers from `tiff_unix.c`.
static void _extern_warning_handler(const char* module, const char* fmt, va_list ap) {
(void)module;
char buf[256];
vsnprintf(buf, sizeof(buf), fmt, ap);
_log_warning(buf, strlen(buf));
}
TIFFErrorHandler _TIFFwarningHandler = _extern_warning_handler;

static void _extern_error_handler(const char* module, const char* fmt, va_list ap) {
(void)module;
char buf[256];
vsnprintf(buf, sizeof(buf), fmt, ap);
_log_error(buf, strlen(buf));
}
TIFFErrorHandler _TIFFerrorHandler = _extern_error_handler;

// Stubbing libc to remove `environ_get*` WASI imports.
char* getenv(const char* name) {
(void)name;
return NULL;
}

// Stubbing libc to remove `fd_*` WASI imports.
_Noreturn void __assert_fail(const char* expr, const char* file, int line, const char* func) {
// Modified from
// https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-20/libc-top-half/musl/src/exit/assert.c
char buf[256];
snprintf(buf, sizeof(buf), "Assertion failed: %s (%s: %s: %d)\n", expr, file, func, line);
_log_error(buf, strlen(buf));
abort();
}
Binary file added codecs/tiff/dec/tiff_dec.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions codecs/tiff/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "tiff",
"scripts": {
"build": "./build.sh make"
}
}
Loading

0 comments on commit 4c2efd4

Please sign in to comment.