Skip to content

Commit

Permalink
Create initial version of @wasm-codecs/gifsicle
Browse files Browse the repository at this point in the history
  • Loading branch information
cyrilwanner committed Aug 2, 2020
1 parent df218df commit b3f4c66
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 4 deletions.
19 changes: 19 additions & 0 deletions packages/gifsicle/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
48 changes: 45 additions & 3 deletions packages/gifsicle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions packages/gifsicle/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion packages/gifsicle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"",
Expand Down
12 changes: 12 additions & 0 deletions packages/gifsicle/src/gifsicle.d.ts
Original file line number Diff line number Diff line change
@@ -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<Omit<GifsicleModule, 'then'>>;
105 changes: 105 additions & 0 deletions packages/gifsicle/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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<Buffer> => {
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;
48 changes: 48 additions & 0 deletions packages/gifsicle/src/io.ts
Original file line number Diff line number Diff line change
@@ -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 = '';
}
};
6 changes: 6 additions & 0 deletions packages/gifsicle/src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { EncodeOptions } from './types';

export const defaultEncodeOptions: EncodeOptions = {
optimizationLevel: 3,
interlaced: false,
};
15 changes: 15 additions & 0 deletions packages/gifsicle/src/pre.ts
Original file line number Diff line number Diff line change
@@ -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);
});
7 changes: 7 additions & 0 deletions packages/gifsicle/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type EncodeOptions = {
optimizationLevel?: number;
interlaced?: boolean;
colors?: number;
width?: number;
height?: number;
};
1 change: 1 addition & 0 deletions packages/mozjpeg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b3f4c66

Please sign in to comment.