diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..f4e8c00 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/.gitignore b/.gitignore index b7591b6..a976057 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ # Project specific ignores target -out.jpg +__pycache__ +*.pyc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f07ebfb --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +all: wasm-thumbnail-py/src/wasm_thumbnail/data/wasm_thumbnail.wasm + +wasm-thumbnail-py/src/wasm_thumbnail/data/wasm_thumbnail.wasm: wasm-thumbnail/src/lib.rs + cd wasm-thumbnail && cargo build --release + cp wasm-thumbnail/target/wasm32-unknown-unknown/release/wasm_thumbnail.wasm wasm-thumbnail-py/src/wasm_thumbnail/data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..92df287 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# wasm-thumbnail + +A tiny library for creating padded image thumbnails intended for use inside +webassembly sandboxes. + +Supports resizing gif, jpeg, png and webp images. diff --git a/python-test/requirements.txt b/python-test/requirements.txt deleted file mode 100644 index e70ee79..0000000 --- a/python-test/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -wasmer==1.0.0-alpha3 -wasmer_compiler_cranelift==1.0.0-alpha3 -Pillow==7.1.1 diff --git a/python-test/test.py b/python-test/test.py deleted file mode 100644 index e25b248..0000000 --- a/python-test/test.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -import time -from PIL import Image -import struct - -from wasmer import engine, Store, Module, Instance -from wasmer_compiler_cranelift import Compiler - -path = 'wasm_thumbnail.wasm' -store = Store(engine.JIT(Compiler)) -module = Module(store, open(path, 'rb').read()) - -def decode(data): - """Extract a payload from its padding by reading its length header.""" - data_length_without_header = len(data) - 4 - if data_length_without_header < 0: - raise ValueError('Data must be at least 4 bytes long', len(data)) - - payload_length = struct.unpack('!L', data[0:4])[0] - - if data_length_without_header < payload_length: - raise ValueError('Payload is shorter than the expected length', - data_length_without_header, payload_length) - - return data[4:4 + payload_length] - -def resize_and_pad_image(image_bytes, width, height, size): - instance = Instance(module) - - image_length = len(image_bytes) - input_pointer = instance.exports.allocate(image_length) - memory = instance.exports.memory.uint8_view(input_pointer) - memory[0:image_length] = image_bytes - - output_pointer = instance.exports.resize_and_pad(input_pointer, image_length, width, height, size) - - memory = instance.exports.memory.uint8_view(output_pointer) - out_bytes = bytes(memory[:size]) - - return decode(out_bytes) - -with open('brave.png', 'rb') as image: - image_bytes = image.read() - - tic = time.perf_counter() - - out_bytes = resize_and_pad_image(image_bytes, 500, 500, 250000) - with open('out.jpg', 'wb+') as out_image: - out_image.write(out_bytes) - - toc = time.perf_counter() - print(f"Resized brave.png with WASM in {toc - tic:0.4f} seconds") - -tic = time.perf_counter() - -try: - m = Image.open("brave.png").convert('RGB') - m.thumbnail((100, 100),Image.ANTIALIAS) -except: - print("unknown image.") - -toc = time.perf_counter() -print(f"Resized brave.png with PIL in {toc - tic:0.4f} seconds") diff --git a/python-test/wasm_thumbnail.wasm b/python-test/wasm_thumbnail.wasm deleted file mode 100755 index d31a223..0000000 Binary files a/python-test/wasm_thumbnail.wasm and /dev/null differ diff --git a/wasm-thumbnail-py/pyproject.toml b/wasm-thumbnail-py/pyproject.toml new file mode 100644 index 0000000..a449411 --- /dev/null +++ b/wasm-thumbnail-py/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] diff --git a/wasm-thumbnail-py/setup.cfg b/wasm-thumbnail-py/setup.cfg new file mode 100644 index 0000000..77d0217 --- /dev/null +++ b/wasm-thumbnail-py/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = wasm-thumbnail +version = 0.0.1 +author = ev Quirk +author_email = ev@7pr.xyz +description = WASM based thumbnail library +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/brave-intl/wasm-thumbnail +project_urls = + Bug Tracker = https://github.com/brave-intl/wasm-thumbnail/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MPL License + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.6 +include_package_data = True + +[options.package_data] +wasm_thumbnail = data/*.wasm + +[options.packages.find] +where = src diff --git a/wasm-thumbnail-py/src/wasm_thumbnail/__init__.py b/wasm-thumbnail-py/src/wasm_thumbnail/__init__.py new file mode 100644 index 0000000..18349a5 --- /dev/null +++ b/wasm-thumbnail-py/src/wasm_thumbnail/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +from .thumbnail import * diff --git a/wasm-thumbnail-py/src/wasm_thumbnail/data/__init__.py b/wasm-thumbnail-py/src/wasm_thumbnail/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wasm-thumbnail-py/src/wasm_thumbnail/data/wasm_thumbnail.wasm b/wasm-thumbnail-py/src/wasm_thumbnail/data/wasm_thumbnail.wasm new file mode 100755 index 0000000..d27716c Binary files /dev/null and b/wasm-thumbnail-py/src/wasm_thumbnail/data/wasm_thumbnail.wasm differ diff --git a/wasm-thumbnail-py/src/wasm_thumbnail/thumbnail.py b/wasm-thumbnail-py/src/wasm_thumbnail/thumbnail.py new file mode 100644 index 0000000..49ea797 --- /dev/null +++ b/wasm-thumbnail-py/src/wasm_thumbnail/thumbnail.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +import importlib +from importlib import resources +import struct + +from wasmer import engine, Store, Module, Instance, ImportObject, Function, FunctionType, Type +from wasmer_compiler_cranelift import Compiler + +def decode_padded_image(data): + """Extract a payload from its padding by reading its length header.""" + data_length_without_header = len(data) - 4 + if data_length_without_header < 0: + raise ValueError('Data must be at least 4 bytes long', len(data)) + + payload_length = struct.unpack('!L', data[0:4])[0] + + if data_length_without_header < payload_length: + raise ValueError('Payload is shorter than the expected length', + data_length_without_header, payload_length) + + return data[4:4 + payload_length] + +def resize_and_pad_image(image_bytes, width, height, size): + """Resize an image and pad to fit size, output is prefixed by image length without padding. + Throws an error if the resized image does not fit in size or is not a supported format""" + instance = Instance(module, import_object) + + image_length = len(image_bytes) + input_pointer = instance.exports.allocate(image_length) + memory = instance.exports.memory.uint8_view(input_pointer) + memory[0:image_length] = image_bytes + + output_pointer = instance.exports.resize_and_pad(input_pointer, image_length, width, height, size) + instance.exports.deallocate(input_pointer, image_length) + + memory = instance.exports.memory.uint8_view(output_pointer) + out_bytes = bytes(memory[:size]) + instance.exports.deallocate(output_pointer, size) + + return out_bytes + +def register_panic(msg_ptr: int, msg_len: int, file_ptr: int, file_len: int, line: int, column: int): + """Panic handler to be called from WASM for debugging purposes""" + msg = bytes(instance.exports.memory.uint8_view(msg_ptr)[:msg_len]).decode("utf-8") + file = bytes(instance.exports.memory.uint8_view(file_ptr)[:file_len]).decode("utf-8") + print("wasm panicked at '{}', {}:{}:{}".format(msg, file, line, column)) + +wasm = resources.open_binary('wasm_thumbnail.data', "wasm_thumbnail.wasm") +store = Store(engine.JIT(Compiler)) +module = Module(store, wasm.read()) +import_object = ImportObject() +import_object.register( + "env", + { + "register_panic": Function(store, register_panic) + } +) diff --git a/python-test/brave.png b/wasm-thumbnail-py/tests/brave.png similarity index 100% rename from python-test/brave.png rename to wasm-thumbnail-py/tests/brave.png diff --git a/wasm-thumbnail-py/tests/test.py b/wasm-thumbnail-py/tests/test.py new file mode 100644 index 0000000..652c2dc --- /dev/null +++ b/wasm-thumbnail-py/tests/test.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +import os +import time + +from wasm_thumbnail import * + + +IMAGE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "brave.png") + +with open(IMAGE, 'rb') as image: + image_bytes = image.read() + + tic = time.perf_counter() + + out_bytes = resize_and_pad_image(image_bytes, 100, 100, 240000) + with open('out.jpg', 'wb+') as out_image: + out_image.write(out_bytes) + + toc = time.perf_counter() + print(f"Resized brave.png with WASM in {toc - tic:0.4f} seconds") diff --git a/Cargo.lock b/wasm-thumbnail/Cargo.lock similarity index 59% rename from Cargo.lock rename to wasm-thumbnail/Cargo.lock index c359241..65cac0d 100644 --- a/Cargo.lock +++ b/wasm-thumbnail/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "adler32" version = "1.2.0" @@ -20,33 +22,33 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bytemuck" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41aa2ec95ca3b5c54cf73c91acf06d24f4495d5f1b1c12506ae3483d646177ac" +checksum = "9966d2ab714d0f785dbac0a0396251a35280aeb42413281617d0209ab4898435" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cfg-if" -version = "0.1.10" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "color_quant" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "crc32fast" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ "cfg-if", ] @@ -63,9 +65,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02efba560f227847cb41463a7395c514d127d4f74fff12ef0137fff1b84b96c4" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" dependencies = [ "color_quant", "weezl", @@ -73,36 +75,26 @@ dependencies = [ [[package]] name = "image" -version = "0.23.10" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985fc06b1304d19c28d5c562ed78ef5316183f2b0053b46763a0b94862373c34" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" dependencies = [ "bytemuck", "byteorder", + "color_quant", "gif", "jpeg-decoder", "num-iter", "num-rational", "num-traits", "png", - "scoped_threadpool", - "tiff", ] [[package]] name = "jpeg-decoder" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3" -dependencies = [ - "byteorder", -] - -[[package]] -name = "lzw" -version = "0.10.0" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "miniz_oxide" @@ -115,9 +107,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -125,9 +117,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.41" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ "autocfg", "num-integer", @@ -136,9 +128,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b4d7360f362cfb50dde8143501e6940b22f644be75a4cc90b2d81968908138" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg", "num-integer", @@ -147,18 +139,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] [[package]] name = "png" -version = "0.16.7" +version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe7f9f1c730833200b134370e1d5098964231af8450bce9b78ee3ab5278b970" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" dependencies = [ "bitflags", "crc32fast", @@ -166,23 +158,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - -[[package]] -name = "tiff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f" -dependencies = [ - "byteorder", - "lzw", - "miniz_oxide", -] - [[package]] name = "wasm-thumbnail" version = "0.1.0" @@ -192,6 +167,6 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.1" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e26e7a4d998e3d7949c69444b8b4916bac810da0d3a82ae612c89e952782f4" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" diff --git a/Cargo.toml b/wasm-thumbnail/Cargo.toml similarity index 100% rename from Cargo.toml rename to wasm-thumbnail/Cargo.toml diff --git a/wasm-thumbnail/src/hook.rs b/wasm-thumbnail/src/hook.rs new file mode 100644 index 0000000..5f36971 --- /dev/null +++ b/wasm-thumbnail/src/hook.rs @@ -0,0 +1,58 @@ +/// Function imported into WASM for reporting panic details +extern "C" { + fn register_panic( + msg_ptr: *const u8, + msg_len: u32, + file_ptr: *const u8, + file_len: u32, + line: u32, + column: u32, + ); +} + +/// Panic hook +fn hook(info: &std::panic::PanicInfo<'_>) { + let error_msg = info + .payload() + .downcast_ref::() + .map(String::as_str) + .or_else(|| info.payload().downcast_ref::<&'static str>().copied()) + .unwrap_or(""); + let location = info.location(); + + unsafe { + let _ = match location { + Some(loc) => { + let file = loc.file(); + let line = loc.line(); + let column = loc.column(); + + register_panic( + error_msg.as_ptr(), + error_msg.len() as u32, + file.as_ptr(), + file.len() as u32, + line, + column, + ) + } + None => register_panic( + error_msg.as_ptr(), + error_msg.len() as u32, + std::ptr::null(), + 0, + 0, + 0, + ), + }; + } +} + +/// Register panic hook +pub fn register_panic_hook() { + use std::sync::Once; + static SET_HOOK: Once = Once::new(); + SET_HOOK.call_once(|| { + std::panic::set_hook(Box::new(hook)); + }); +} diff --git a/src/lib.rs b/wasm-thumbnail/src/lib.rs similarity index 56% rename from src/lib.rs rename to wasm-thumbnail/src/lib.rs index 3e9622f..20141e4 100644 --- a/src/lib.rs +++ b/wasm-thumbnail/src/lib.rs @@ -3,8 +3,17 @@ use std::os::raw::c_void; use image; use image::imageops::FilterType; +use image::DynamicImage; +use image::GenericImage; +use image::GenericImageView; use image::ImageOutputFormat; +mod hook; +use hook::register_panic_hook; + +/// Resize the input image specified by pointer and length to nwidth by nheight, +/// returns a pointer to nsize bytes that containing a u32 length followed +/// by the thumbnail bytes and padding #[no_mangle] pub extern "C" fn resize_and_pad( pointer: *mut u8, @@ -13,12 +22,20 @@ pub extern "C" fn resize_and_pad( nheight: u32, nsize: usize, ) -> *const u8 { + register_panic_hook(); + let slice: &[u8] = unsafe { std::slice::from_raw_parts(pointer, length) }; let img = image::load_from_memory(slice).expect("must be a valid image"); - // Preserves aspect ratio - let result = img.resize(nwidth, nheight, FilterType::Lanczos3); + // Resize preserves aspect ratio + let img = img.resize(nwidth, nheight, FilterType::Lanczos3); + + // Copy pixels only + let mut result = DynamicImage::new_rgba8(img.width(), img.height()); + result + .copy_from(&img, 0, 0) + .expect("copy should not fail as output is same dimensions"); let mut out: Vec = Vec::with_capacity(nsize); @@ -36,20 +53,22 @@ pub extern "C" fn resize_and_pad( out.resize(nsize, 0); - let pointer = out.as_slice().as_ptr(); + let pointer = out.as_mut_ptr(); mem::forget(out); pointer } +/// Allocate a new buffer in the wasm memory space #[no_mangle] -pub extern "C" fn allocate(size: usize) -> *mut c_void { - let mut buffer = Vec::with_capacity(size); +pub extern "C" fn allocate(capacity: usize) -> *mut c_void { + let mut buffer = Vec::with_capacity(capacity); let pointer = buffer.as_mut_ptr(); mem::forget(buffer); pointer as *mut c_void } +/// Deallocate a buffer in the wasm memory space #[no_mangle] pub extern "C" fn deallocate(pointer: *mut c_void, capacity: usize) { unsafe {