├── .dockerignore ├── .gitignore ├── .npmrc ├── Dockerfile ├── Makefile ├── index.d.ts ├── index.js ├── package.json ├── readme.md └── test ├── basn3p01.png ├── clock.png ├── decode.js └── encode.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | node_modules/ 3 | lodepng.wasm 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lodepng.wasm 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ######################### 4 | # Install prerequisites # 5 | ######################### 6 | 7 | RUN \ 8 | apt-get update && \ 9 | DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl git libxml2 10 | 11 | ######################### 12 | # Install WASI SDK 15.0 # 13 | ######################### 14 | 15 | RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-15/wasi-sdk-15.0-linux.tar.gz | tar xzk --strip-components=1 -C / 16 | 17 | ######################### 18 | # Install binaryen v110 # 19 | ######################### 20 | 21 | RUN curl -L https://github.com/WebAssembly/binaryen/releases/download/version_110/binaryen-version_110-x86_64-linux.tar.gz | tar xzk --strip-components=1 -C / 22 | 23 | ##################### 24 | # Build actual code # 25 | ##################### 26 | 27 | WORKDIR /code 28 | 29 | RUN git clone https://github.com/lvandeve/lodepng.git && cd lodepng && git checkout 18964554bc769255401942e0e6dfd09f2fab2093 30 | RUN mv lodepng/lodepng.cpp lodepng/lodepng.c 31 | 32 | # Relase build 33 | RUN clang --sysroot=/share/wasi-sysroot --target=wasm32-unknown-wasi -flto -Oz -o lodepng.wasm -DLODEPNG_NO_COMPILE_DISK -DLODEPNG_NO_COMPILE_CPP -mexec-model=reactor -fvisibility=hidden -Wl,--export=malloc,--export=free,--export=lodepng_decode32,--export=lodepng_encode32,--strip-all -- lodepng/lodepng.c 34 | 35 | # Debug build 36 | # RUN clang --sysroot=/share/wasi-sysroot --target=wasm32-unknown-wasi -flto -O0 -g3 -o lodepng.wasm -DLODEPNG_NO_COMPILE_DISK -DLODEPNG_NO_COMPILE_CPP -mexec-model=reactor -fvisibility=hidden -Wl,--export=malloc,--export=free,--export=lodepng_decode32,--export=lodepng_encode32 -- lodepng/lodepng.c 37 | 38 | RUN wasm-opt -Oz lodepng.wasm -o lodepng.wasm 39 | 40 | CMD base64 --wrap=0 lodepng.wasm 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | lodepng.wasm: Dockerfile 4 | docker build --platform linux/amd64 . 5 | sh -c 'docker run --platform linux/amd64 --rm -it $$(docker build --platform linux/amd64 -q .) | base64 -D > lodepng.wasm' 6 | 7 | test: lodepng.wasm 8 | @node_modules/.bin/standard 9 | @node_modules/.bin/mocha 10 | @node_modules/.bin/ts-readme-generator --check 11 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import ImageData = require('@canvas/image-data') 2 | 3 | interface ImageLike { 4 | width: number 5 | height: number 6 | data: Buffer | Int8Array | Uint8Array | Uint8ClampedArray 7 | } 8 | 9 | /** 10 | * @param source - The PNG data 11 | * @returns Decoded width, height and pixel data 12 | */ 13 | export function decode (source: Uint8Array): ImageData 14 | 15 | /** 16 | * @param source - The image data 17 | * @returns Encoded data 18 | */ 19 | export function encode (source: ImageLike): Uint8Array 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global WebAssembly */ 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const ImageData = require('@canvas/image-data') 7 | 8 | const code = fs.readFileSync(path.join(__dirname, 'lodepng.wasm')) 9 | const wasmModule = new WebAssembly.Module(code) 10 | const instance = new WebAssembly.Instance(wasmModule) 11 | 12 | exports.decode = function (input) { 13 | // Allocate memory to hand over the input data to WASM 14 | const inputPointer = instance.exports.malloc(input.byteLength) 15 | const targetView = new Uint8Array(instance.exports.memory.buffer, inputPointer, input.byteLength) 16 | 17 | // Copy input data into WASM readable memory 18 | targetView.set(input) 19 | 20 | // Allocate metadata (outputPointer, width, and height) 21 | const metadataPointer = instance.exports.malloc(12) 22 | 23 | // Decode input data 24 | const error = instance.exports.lodepng_decode32(metadataPointer, metadataPointer + 4, metadataPointer + 8, inputPointer, input.byteLength) 25 | 26 | // Free the input data in WASM land 27 | instance.exports.free(inputPointer) 28 | 29 | // Guard return value for NULL pointer 30 | if (error !== 0) { 31 | instance.exports.free(metadataPointer) 32 | throw new Error('Failed to decode png image') 33 | } 34 | 35 | // Read returned metadata 36 | const metadata = new Uint32Array(instance.exports.memory.buffer, metadataPointer, 3) 37 | const [outputPointer, width, height] = metadata 38 | 39 | // Free the metadata in WASM land 40 | instance.exports.free(metadataPointer) 41 | 42 | // Create an empty buffer for the resulting data 43 | const outputSize = (width * height * 4) 44 | const output = new Uint8ClampedArray(outputSize) 45 | 46 | // Copy decoded data from WASM memory to JS 47 | output.set(new Uint8Array(instance.exports.memory.buffer, outputPointer, outputSize)) 48 | 49 | // Free WASM copy of decoded data 50 | instance.exports.free(outputPointer) 51 | 52 | // Return decoded image as raw data 53 | return new ImageData(output, width, height) 54 | } 55 | 56 | exports.encode = function (input) { 57 | // Allocate memory to hand over the input data to WASM 58 | const inputPointer = instance.exports.malloc(input.data.byteLength) 59 | const targetView = new Uint8Array(instance.exports.memory.buffer, inputPointer, input.data.byteLength) 60 | 61 | // Copy input data into WASM readable memory 62 | targetView.set(input.data) 63 | 64 | // Allocate metadata (outputPointer, outputSize) 65 | const metadataPointer = instance.exports.malloc(8) 66 | 67 | // Encode input data 68 | const error = instance.exports.lodepng_encode32(metadataPointer, metadataPointer + 4, inputPointer, input.width, input.height) 69 | 70 | // Free the input data in WASM land 71 | instance.exports.free(inputPointer) 72 | 73 | // Guard return value for NULL pointer 74 | if (error !== 0) { 75 | instance.exports.free(metadataPointer) 76 | throw new Error('Failed to encode png image') 77 | } 78 | 79 | // Read returned metadata 80 | const metadata = new Uint32Array(instance.exports.memory.buffer, metadataPointer, 2) 81 | const [outputPointer, outputSize] = metadata 82 | 83 | // Free the metadata in WASM land 84 | instance.exports.free(metadataPointer) 85 | 86 | // Create an empty buffer for the resulting data 87 | const output = new Uint8Array(outputSize) 88 | 89 | // Copy encoded data from WASM memory to JS 90 | output.set(new Uint8Array(instance.exports.memory.buffer, outputPointer, outputSize)) 91 | 92 | // Free WASM copy of encoded data 93 | instance.exports.free(outputPointer) 94 | 95 | // Return encoded image as raw data 96 | return output 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cwasm/lodepng", 3 | "version": "0.1.7", 4 | "repository": "LinusU/cwasm-lodepng", 5 | "license": "MIT", 6 | "files": [ 7 | "index.d.ts", 8 | "index.js", 9 | "lodepng.wasm" 10 | ], 11 | "scripts": { 12 | "prepare": "make lodepng.wasm", 13 | "test": "make test" 14 | }, 15 | "dependencies": { 16 | "@canvas/image-data": "^1.0.0" 17 | }, 18 | "devDependencies": { 19 | "image-size": "^1.0.0", 20 | "mocha": "^7.1.1", 21 | "standard": "^14.3.3", 22 | "ts-readme-generator": "^0.4.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # LodePNG 2 | 3 | PNG decoding/encoding for Node.js, using [LodePNG][LodePNG] compiled to [WebAssembly][WebAssembly]. 4 | 5 | [LodePNG]: https://lodev.org/lodepng/ 6 | [WebAssembly]: https://webassembly.org 7 | 8 | ## Installation 9 | 10 | ```sh 11 | npm install --save @cwasm/lodepng 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```js 17 | const fs = require('fs') 18 | const lodepng = require('@cwasm/lodepng') 19 | 20 | const source = fs.readFileSync('image.png') 21 | const image = lodepng.decode(source) 22 | 23 | console.log(image) 24 | // { width: 128, 25 | // height: 128, 26 | // data: 27 | // Uint8ClampedArray [ ... ] } 28 | 29 | const data = lodepng.encode(image) 30 | 31 | console.log(data) 32 | // Uint8Array [ 137, 80, 78, 71, ... ] 33 | ``` 34 | 35 | ## API 36 | 37 | ### `decode(source)` 38 | 39 | - `source` ([`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), required) - The PNG data 40 | - returns [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) - Decoded width, height and pixel data 41 | 42 | ### `encode(source)` 43 | 44 | - `source` (`ImageLike`, required) - The image data 45 | - returns [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) - Encoded data 46 | -------------------------------------------------------------------------------- /test/basn3p01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinusU/cwasm-lodepng/fd65f57a23755e9153e933874dcf74f914772d3d/test/basn3p01.png -------------------------------------------------------------------------------- /test/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinusU/cwasm-lodepng/fd65f57a23755e9153e933874dcf74f914772d3d/test/clock.png -------------------------------------------------------------------------------- /test/decode.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict' 4 | 5 | const ImageData = require('@canvas/image-data') 6 | const fs = require('fs') 7 | const path = require('path') 8 | const assert = require('assert') 9 | 10 | const lodepng = require('../') 11 | 12 | function getPixel (img, x, y) { 13 | return Buffer.from(img.data).readUInt32LE((y * img.width + x) * 4) 14 | } 15 | 16 | describe('Decode', () => { 17 | it('should decode "basn3p01.png"', () => { 18 | const data = fs.readFileSync(path.join(__dirname, 'basn3p01.png')) 19 | const img = lodepng.decode(data) 20 | 21 | assert(img instanceof ImageData) 22 | assert(img.data instanceof Uint8ClampedArray) 23 | 24 | assert.strictEqual(img.width, 32) 25 | assert.strictEqual(img.height, 32) 26 | 27 | assert.strictEqual(getPixel(img, 18, 10), 0xff22ffee) 28 | assert.strictEqual(getPixel(img, 14, 26), 0xffff6622) 29 | }) 30 | 31 | it('should decode "clock.png', () => { 32 | const data = fs.readFileSync(path.join(__dirname, 'clock.png')) 33 | const img = lodepng.decode(data) 34 | 35 | assert(img instanceof ImageData) 36 | assert(img.data instanceof Uint8ClampedArray) 37 | 38 | assert.strictEqual(img.width, 320) 39 | assert.strictEqual(img.height, 320) 40 | 41 | assert.strictEqual(getPixel(img, 57, 57), 0xffa25f32) 42 | assert.strictEqual(getPixel(img, 225, 103), 0xff0000d4) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/encode.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict' 4 | 5 | const assert = require('assert') 6 | const sizeOf = require('image-size') 7 | 8 | const lodepng = require('../') 9 | 10 | function testEncode (src) { 11 | const img = { 12 | data: src, 13 | width: 1, 14 | height: 2 15 | } 16 | 17 | const data = Buffer.from(lodepng.encode(img)) 18 | const size = sizeOf(data) 19 | 20 | assert.strictEqual(size.width, img.width) 21 | assert.strictEqual(size.height, img.height) 22 | 23 | assert.strictEqual(data.readUInt32LE(0), 0x474e5089) 24 | assert.strictEqual(data.readUInt32LE(4), 0x0a1a0a0d) 25 | } 26 | 27 | describe('Encode', function () { 28 | it('should encode from a Buffer', () => { 29 | const src = Buffer.allocUnsafe(8) 30 | 31 | src.writeUInt32LE(0xff22ffee, 0) 32 | src.writeUInt32LE(0xffff6622, 4) 33 | 34 | testEncode(src) 35 | }) 36 | 37 | it('should encode from a Uint8ClampedArray', () => { 38 | const src = new Uint8ClampedArray(8) 39 | 40 | src[0] = 0xff 41 | src[1] = 0x22 42 | src[2] = 0xff 43 | src[3] = 0xee 44 | src[4] = 0xff 45 | src[5] = 0xff 46 | src[6] = 0x66 47 | src[7] = 0x22 48 | 49 | testEncode(src) 50 | }) 51 | 52 | it('should encode from a Uint8Array', () => { 53 | const src = new Uint8Array(8) 54 | 55 | src[0] = 0xff 56 | src[1] = 0x22 57 | src[2] = 0xff 58 | src[3] = 0xee 59 | src[4] = 0xff 60 | src[5] = 0xff 61 | src[6] = 0x66 62 | src[7] = 0x22 63 | 64 | testEncode(src) 65 | }) 66 | 67 | it('should encode from a Int8Array', () => { 68 | const src = new Int8Array(8) 69 | 70 | src[0] = 0xff 71 | src[1] = 0x22 72 | src[2] = 0xff 73 | src[3] = 0xee 74 | src[4] = 0xff 75 | src[5] = 0xff 76 | src[6] = 0x66 77 | src[7] = 0x22 78 | 79 | testEncode(src) 80 | }) 81 | 82 | it('should encode from a ArrayBuffer', () => { 83 | const src = new ArrayBuffer(8) 84 | const view = new Uint8Array(src) 85 | 86 | view[0] = 0xff 87 | view[1] = 0x22 88 | view[2] = 0xff 89 | view[3] = 0xee 90 | view[4] = 0xff 91 | view[5] = 0xff 92 | view[6] = 0x66 93 | view[7] = 0x22 94 | 95 | testEncode(src) 96 | }) 97 | 98 | it('should give error on incorrect length', () => { 99 | const src = Buffer.allocUnsafe(8) 100 | 101 | src.writeUInt32LE(0xff22ffee, 0) 102 | src.writeUInt32LE(0xffff6622, 4) 103 | 104 | const img = { 105 | data: src, 106 | width: 12, 107 | height: 12 108 | } 109 | 110 | return assert.throws(() => lodepng.encode(img)) 111 | }) 112 | }) 113 | --------------------------------------------------------------------------------