From b3f4c667dcc204213b3f07a31e3de8906180ffd4 Mon Sep 17 00:00:00 2001 From: Cyril Wanner Date: Sun, 2 Aug 2020 17:33:49 +0200 Subject: [PATCH] Create initial version of @wasm-codecs/gifsicle --- packages/gifsicle/Dockerfile | 19 +++++ packages/gifsicle/README.md | 48 ++++++++++++- packages/gifsicle/build.sh | 19 +++++ packages/gifsicle/package.json | 2 +- packages/gifsicle/src/gifsicle.d.ts | 12 ++++ packages/gifsicle/src/index.ts | 105 ++++++++++++++++++++++++++++ packages/gifsicle/src/io.ts | 48 +++++++++++++ packages/gifsicle/src/options.ts | 6 ++ packages/gifsicle/src/pre.ts | 15 ++++ packages/gifsicle/src/types.ts | 7 ++ packages/mozjpeg/src/index.ts | 1 + 11 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 packages/gifsicle/src/gifsicle.d.ts create mode 100644 packages/gifsicle/src/index.ts create mode 100644 packages/gifsicle/src/io.ts create mode 100644 packages/gifsicle/src/options.ts create mode 100644 packages/gifsicle/src/pre.ts create mode 100644 packages/gifsicle/src/types.ts diff --git a/packages/gifsicle/Dockerfile b/packages/gifsicle/Dockerfile index 549472a..e198915 100644 --- a/packages/gifsicle/Dockerfile +++ b/packages/gifsicle/Dockerfile @@ -1 +1,20 @@ FROM trzeci/emscripten:1.39.18-upstream + +RUN apt-get update && \ + apt-get install -y \ + autoconf \ + libtool \ + libpng-dev \ + pkg-config && \ + rm -rf /var/lib/apt/lists/* + +RUN curl https://codeload.github.com/kohler/gifsicle/zip/v1.92 -o /tmp/gifsicle.zip && \ + unzip /tmp/gifsicle.zip -d /lib && \ + mv /lib/gifsicle-1.92 /lib/gifsicle && \ + rm /tmp/gifsicle.zip + +RUN cd /lib/gifsicle && \ + sed -i 's/CC="$CC -W -Wall"/CC="$CC -W -Wall -s ERROR_ON_UNDEFINED_SYMBOLS=0"/g' configure.ac && \ + ./bootstrap.sh && \ + emconfigure ./configure --disable-gifview && \ + emmake make diff --git a/packages/gifsicle/README.md b/packages/gifsicle/README.md index 06514d8..9c2afb0 100644 --- a/packages/gifsicle/README.md +++ b/packages/gifsicle/README.md @@ -21,15 +21,57 @@ npm install @wasm-codecs/gifsicle ## Usage -> TODO +```typescript +import encode from '@wasm-codecs/gifsicle'; + +(async () => { + const encodedImage = await encode(image, encodeOptions); +})(); +``` ## API -> TODO +### `encode(image, encodeOptions?): Buffer` + +Returns a buffer containing the compressed image data. + +##### `image: Buffer` + +Buffer of a GIF image. + +##### `encodeOptions?: EncodeOptions` + +All encoding options are optional and fall back to the [default values](https://github.com/cyrilwanner/wasm-codecs/blob/master/packages/gifsicle/src/options.ts#L3-L6). + +```typescript +type EncodeOptions = { + optimizationLevel?: number; + interlaced?: boolean; + colors?: number; + width?: number; + height?: number; +} +``` ## Examples -> TODO +### Using Node.js + +```typescript +import fs from 'fs'; +import encode from '@wasm-codecs/gifsicle'; + +(async () => { + // read input image as a buffer + const data = fs.readFileSync('in.gif'); + + // encode the image using @wasm-codecs/gifsicle + const output = await encode(data); + + // save the image to the file system + fs.writeFileSync('out.png', output); +})(); +``` ## License diff --git a/packages/gifsicle/build.sh b/packages/gifsicle/build.sh index 9336879..e38edee 100755 --- a/packages/gifsicle/build.sh +++ b/packages/gifsicle/build.sh @@ -5,3 +5,22 @@ set -e cd /build echo "Building wasm-codecs-gifsicle.." +emcc \ + /lib/gifsicle/src/clp.o \ + /lib/gifsicle/src/fmalloc.o \ + /lib/gifsicle/src/giffunc.o \ + /lib/gifsicle/src/gifread.o \ + /lib/gifsicle/src/gifunopt.o \ + /lib/gifsicle/src/merge.o \ + /lib/gifsicle/src/optimize.o \ + /lib/gifsicle/src/quantize.o \ + /lib/gifsicle/src/support.o \ + /lib/gifsicle/src/xform.o \ + /lib/gifsicle/src/gifsicle.o \ + /lib/gifsicle/src/gifwrite.o \ + -s MODULARIZE=1 \ + -s EXPORT_NAME=gifsicle \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s ERROR_ON_UNDEFINED_SYMBOLS=0 \ + --pre-js lib/pre.js \ + -o ./lib/gifsicle.js diff --git a/packages/gifsicle/package.json b/packages/gifsicle/package.json index 3b4fffb..169ce91 100644 --- a/packages/gifsicle/package.json +++ b/packages/gifsicle/package.json @@ -14,7 +14,7 @@ "license": "MIT", "scripts": { "prebuild": "rimraf lib && mkdirp lib", - "build": "npm run build:wasm && npm run build:js && npm run build:types", + "build": "npm run build:js && npm run build:wasm && npm run build:types", "prebuild:wasm": "docker build -t cyrilwanner/wasm-codecs-gifsicle .", "build:wasm": "docker run --rm -v `pwd`:/build cyrilwanner/wasm-codecs-gifsicle /build/build.sh", "build:js": "babel src --out-dir lib --extensions \".ts\"", diff --git a/packages/gifsicle/src/gifsicle.d.ts b/packages/gifsicle/src/gifsicle.d.ts new file mode 100644 index 0000000..6ef57ac --- /dev/null +++ b/packages/gifsicle/src/gifsicle.d.ts @@ -0,0 +1,12 @@ +export type GifsicleModule = EmscriptenModule & { + input: Uint8Array; + output: (res: Uint8Array) => void, +} + +export default function(mozjpeg: { + stdout?: (char: number) => void, + stderr?: (char: number) => void, + arguments?: string[], + input: Uint8Array, + output: (res: Uint8Array) => void, +}): Promise>; diff --git a/packages/gifsicle/src/index.ts b/packages/gifsicle/src/index.ts new file mode 100644 index 0000000..dfab45b --- /dev/null +++ b/packages/gifsicle/src/index.ts @@ -0,0 +1,105 @@ +import Gifsicle from './gifsicle'; +import { EncodeOptions } from './types'; +import { defaultEncodeOptions } from './options'; +import { stdout, stderr, flush } from './io'; + +const queue: Array<() => void> = []; + +/** + * Initialize the gifsicle module + */ +const initModule = (): Promise => { + return new Promise((resolve) => { + // add a new job to the queue + queue.push(resolve); + + // start it if there is no queue + if (queue.length === 1) { + queue[0](); + } + }); +}; + +/** + * Reset the gifsicle module + */ +const resetModule = (): void => { + if (queue.length > 0) { + // remove finished job + queue.shift(); + + // trigger next job + if (queue.length > 0) { + queue[0](); + } + } +}; + +/** + * Encode an input image using Gifsicle + * + * @async + * @param {Buffer} image Image input buffer + * @param {EncodeOptions} encodeOptions Encoding options passed to Gifsicle + * @returns {Buffer} Processed image buffer + */ +const encode = async (image: Buffer, encodeOptions: EncodeOptions = {}): Promise => { + await initModule(); + + return new Promise((resolve, reject) => { + // merge default options + const filledEncodeOptions = { ...defaultEncodeOptions, ...encodeOptions }; + + // build arguments + const gifsicleArguments = [ + // ignore gifsicle warnings + '--no-warnings', + + // remove application extensions from the input image + '--no-app-extensions', + + // set optimization level + `--optimize=${filledEncodeOptions.optimizationLevel}`, + + // turn on interlacing + filledEncodeOptions.interlaced === true ? '--interlace' : false, + + // set number of colors + typeof filledEncodeOptions.colors === 'number' ? `--colors=${filledEncodeOptions.colors}` : false, + + // resize image + filledEncodeOptions.width || filledEncodeOptions.height + ? `--resize ${filledEncodeOptions.width || '_'}x${filledEncodeOptions.height || '_'}` + : false, + + // set input & output file names + '-i', + '/input.gif', + '-o', + '/output.gif', + ].filter(Boolean); + + let resolved = false; + + Gifsicle({ + stdout, + stderr, + arguments: gifsicleArguments as string[], + input: new Uint8Array(image.buffer), + output: (res: Uint8Array) => { + resolve(Buffer.from(res)); + resolved = true; + }, + }).then(() => { + flush(); + if (!resolved) { + reject(); + } + resetModule(); + }); + }); +}; + +export default encode; +export type { EncodeOptions } from './types'; +module.exports = encode; diff --git a/packages/gifsicle/src/io.ts b/packages/gifsicle/src/io.ts new file mode 100644 index 0000000..6cb6527 --- /dev/null +++ b/packages/gifsicle/src/io.ts @@ -0,0 +1,48 @@ +/* eslint-disable no-console */ + +let out = ''; + +/** + * Process stdout stream + * + * @param {number} char Next char in stream + */ +export const stdout = (char: number): void => { + out += String.fromCharCode(char); + + if (char === 10) { + console.log(out); + out = ''; + } +}; + +let err = ''; + +/** + * Process stderr stream + * + * @param {number} char Next char in stream + */ +export const stderr = (char: number): void => { + err += String.fromCharCode(char); + + if (char === 10) { + console.error(err); + err = ''; + } +}; + +/** + * Flush remaining buffer + */ +export const flush = (): void => { + if (out.length > 0) { + console.log(out); + out = ''; + } + + if (err.length > 0) { + console.error(err); + err = ''; + } +}; diff --git a/packages/gifsicle/src/options.ts b/packages/gifsicle/src/options.ts new file mode 100644 index 0000000..8b0bd13 --- /dev/null +++ b/packages/gifsicle/src/options.ts @@ -0,0 +1,6 @@ +import { EncodeOptions } from './types'; + +export const defaultEncodeOptions: EncodeOptions = { + optimizationLevel: 3, + interlaced: false, +}; diff --git a/packages/gifsicle/src/pre.ts b/packages/gifsicle/src/pre.ts new file mode 100644 index 0000000..34b0019 --- /dev/null +++ b/packages/gifsicle/src/pre.ts @@ -0,0 +1,15 @@ +import { GifsicleModule } from './gifsicle'; + +// write input file to FS of WASM before executing gifsicle +Module.preRun = Module.preRun || []; +Module.preRun.push(function writeInputFile() { + const data = (Module as GifsicleModule).input; + FS.writeFile('/input.gif', data); +}); + +// send output file from FS of WASM back to the JS +Module.postRun = Module.postRun || []; +Module.postRun.push(function getOutputFile() { + const data = FS.readFile('/output.gif'); + (Module as GifsicleModule).output(data); +}); diff --git a/packages/gifsicle/src/types.ts b/packages/gifsicle/src/types.ts new file mode 100644 index 0000000..1626e35 --- /dev/null +++ b/packages/gifsicle/src/types.ts @@ -0,0 +1,7 @@ +export type EncodeOptions = { + optimizationLevel?: number; + interlaced?: boolean; + colors?: number; + width?: number; + height?: number; +}; diff --git a/packages/mozjpeg/src/index.ts b/packages/mozjpeg/src/index.ts index 0db3b93..37dff1a 100644 --- a/packages/mozjpeg/src/index.ts +++ b/packages/mozjpeg/src/index.ts @@ -50,6 +50,7 @@ const resetModule = (): void => { /** * Encode a raw input image using MozJPEG * + * @async * @param {Buffer} image Raw image input buffer * @param {InputInfo} inputInfo Information about the input image * @param {EncodeOptions} encodeOptions Encoding options passed to MozJPEG