chore: add jimp dependency, icon padding script, and update configuration

This commit is contained in:
2026-05-31 14:04:37 +02:00
parent 30f81045ac
commit 7cd17307ba
1126 changed files with 166400 additions and 2 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+91
View File
@@ -0,0 +1,91 @@
exports.COLORS = {
'R': 0xFF0000ff, // red
'r': 0xFF00007f, // red half-alpha
'G': 0x00FF00ff, // green
'g': 0x00FF007f, // green half-alpha
'B': 0x0000FFff, // blue
'b': 0x0000FF7f, // blue half-alpha
'*': 0x000000ff, // black
' ': 0x00000000, // fully transparent
'W': 0xFFFFFFff, // white
'_': 0xFFFFFF01, // white transparent
'4': 0x404040ff, // dark grey
'9': 0x909090ff // light grey
};
exports.PREMADE = {
sampleSprite: [
' *',
'* '
],
singleFrameMonoOpaque: [
'RRR',
'RRR',
'RRR'
],
singleFrameMonoOpaqueSpriteAt1x1: [
'RRR',
'RR*',
'R*R'
],
singleFrameNoColorTrans: [
' ',
' ',
' '
],
singleFrameMonoTrans: [
' G ',
'GGG',
' G '
],
singleFrameBWOpaque: [
'*WW*',
'W**W',
'*WW*'
],
singleFrameMultiOpaque: [
'RGBW',
'WRGB'
],
singleFrameMultiTrans: [
'RGB ',
' RGB',
' *'
],
singleFrameMultiPartialTrans: [
'_G ',
'__G ',
'rgb*'
],
twoFrameMultiOpaque: [
['**RR',
'GG**',
'**BB'],
['RR**',
'**GG',
'BB**']
],
threeFrameMonoTrans: [
['* ',
' ',
' '],
[' ',
' * ',
' '],
[' ',
' ',
' *']
]
};
+202
View File
@@ -0,0 +1,202 @@
'use strict';
const assert = require('chai').assert;
const Path = require('path');
const Jimp = require('jimp');
const Bitmaps = require('./bitmaps');
const { BitmapImage } = require('../../src/index');
const { GifFrame } = require('../../src/gifframe');
exports.checkBitmap = function (bitmap, width, height, rgbaOrBuf) {
assert.strictEqual(bitmap.width, width, "width");
assert.strictEqual(bitmap.height, height, "height");
if (Buffer.isBuffer(rgbaOrBuf)) {
assert.strictEqual(bitmap.data.length, rgbaOrBuf.length, "length");
for (let i = 0; i < rgbaOrBuf.length; ++i ) {
assert.strictEqual(rgbaOrBuf[i], bitmap.data[i], "at buffer index "+ i);
}
}
else if (typeof rgbaOrBuf === 'number') {
const rgba = rgbaOrBuf
const buf = bitmap.data;
for (let bi = 0; bi < buf.length; bi += 4) {
const found = buf.readUInt32BE(bi);
if (found !== rgba) {
assert.fail(found, rgba, `buffer fill color ${found} != ${rgba}`);
}
}
}
else {
throw new Error("last checkBitmap() param must be a color or a Buffer");
}
}
exports.checkFrameDefaults = function (actualInfo, options, frameIndex = 0) {
options = Object.assign({}, options); // don't munge caller
options.xOffset = options.xOffset || 0;
options.yOffset = options.yOffset || 0;
options.delayCentisecs = options.delayCentisecs || 8;
options.interlaced = (options.interlaced === true);
options.disposalMethod =
(options.disposalMethod || GifFrame.DisposeToBackgroundColor);
exports.verifyFrameInfo(actualInfo, options, frameIndex);
};
exports.compareToFrameDump = function (actualFrames, expectedDump) {
assert(Array.isArray(actualFrames));
assert.strictEqual(actualFrames.length, expectedDump.length);
for (let i = 0; i < actualFrames.length; ++i) {
const actualFrame = actualFrames[i];
const dump = expectedDump[i];
exports.verifyFrameInfo(actualFrame, {
xOffset: dump[0],
yOffset: dump[1],
bitmap: {
width: dump[2],
height: dump[3]
},
delayCentisecs: dump[4],
interlaced: dump[5],
disposalMethod: dump[6]
}, i);
}
};
exports.dumpFramesAsCode = function (frames) {
let first = true;
process.stdout.write("[\n");
frames.forEach(f => {
if (!first) {
process.stdout.write(",\n");
}
process.stdout.write(` [${f.xOffset}, ${f.yOffset}, `+
`${f.bitmap.width}, ${f.bitmap.height}, `+
`${f.delayCentisecs}, ${f.interlaced}, ${f.disposalMethod}]`);
first = false;
});
process.stdout.write("\n]\n");
};
exports.getBitmap = function (bitmapName, transparentRGB) {
const stringPic = Bitmaps.PREMADE[bitmapName];
if (stringPic === undefined) {
throw new Error(`Bitmap '${bitmapName}' not found`);
}
if (Array.isArray(stringPic[0])) {
throw new Error(`'${bitmapName}' is a bitmap series`);
}
return _stringsToBitmap(stringPic, transparentRGB);
};
exports.getFixturePath = function (filename) {
return Path.join(__dirname, "../fixtures", filename);
};
exports.getGifPath = function (filenameMinusExtension) {
return exports.getFixturePath(filenameMinusExtension + '.gif');
};
exports.getImagePath = function (filename) {
return exports.getFixturePath(filename);
};
exports.getSeries = function (seriesName, transparentRGB) {
const series = Bitmaps.PREMADE[seriesName];
if (series === undefined) {
throw new Error(`Bitmap series '${seriesName}' not found`);
}
if (!Array.isArray(series[0])) {
throw new Error(`'${seriesName}' is not a bitmap series`);
}
return series.map(stringPic =>
(_stringsToBitmap(stringPic, transparentRGB)));
};
exports.loadBitmapImage = function (imagePath) {
return new Promise((resolve, reject) => {
new Jimp(imagePath, (err, jimp) => {
if (err) return reject(err);
resolve(new BitmapImage(jimp.bitmap));
});
});
};
exports.saveBitmapImage = function (bitmapImage, path) {
let jimp = new Jimp(1, 1, 0);
jimp.bitmap = bitmapImage.bitmap;
return new Promise((resolve, reject) => {
jimp.write(path, (err) => {
if (err) return reject(err);
resolve();
});
});
};
exports.verifyFrameInfo = function (actual, expected, frameIndex=0, note='') {
expected = Object.assign({}, expected); // don't munge caller
if (expected.xOffset !== undefined) {
assert.strictEqual(actual.xOffset, expected.xOffset,
`frame ${frameIndex} same x offset${note}`);
}
if (expected.yOffset !== undefined) {
assert.strictEqual(actual.yOffset, expected.yOffset,
`frame ${frameIndex} same y offset${note}`);
}
if (expected.bitmap !== undefined) {
assert.strictEqual(actual.bitmap.width, expected.bitmap.width,
`frame ${frameIndex} same width${note}`);
assert.strictEqual(actual.bitmap.height, expected.bitmap.height,
`frame ${frameIndex} same height${note}`);
}
if (expected.delayCentisecs !== undefined) {
assert.strictEqual(actual.delayCentisecs, expected.delayCentisecs,
`frame ${frameIndex} same delay${note}`);
}
if (expected.disposalMethod !== undefined) {
assert.strictEqual(actual.disposalMethod, expected.disposalMethod,
`frame ${frameIndex} same disposal method${note}`);
}
assert.strictEqual(actual.interlaced, (expected.interlaced === true),
`frame ${frameIndex} same interlacing${note}`);
};
function _stringsToBitmap(stringPic, transparentRGB) {
const trans = transparentRGB; // shortens code, leaves parameter clear
const width = stringPic[0].length;
const height = stringPic.length;
const data = new Buffer(width * height * 4);
let offset = 0;
for (let y = 0; y < height; ++y) {
const row = stringPic[y];
if (row.length !== width) {
throw new Error("Inconsistent pixel string length");
}
for (let x = 0; x < width; ++x) {
if (Bitmaps.COLORS[row[x]] !== undefined) {
const color = Bitmaps.COLORS[row[x]];
const alpha = color & 0xff;
if (alpha !== 0 || trans === undefined) {
data[offset] = (color >> 24) & 0xff;
data[offset + 1] = (color >> 16) & 0xff;
data[offset + 2] = (color >> 8) & 0xff;
data[offset + 3] = color & 0xff;
}
else {
// not concerned about speed
data[offset] = (trans >> 16) & 0xff;
data[offset + 1] = (trans >> 8) & 0xff;
data[offset + 2] = trans & 0xff;
data[offset + 3] = 0;
}
offset += 4;
}
else {
const validChars = Object.keys(Bitmaps.COLORS).join('');
throw new Error(`Invalid pixel char '${row[x]}'. `+
`Valid chars are "${validChars}".`);
}
}
}
return { width, height, data };
}
+157
View File
@@ -0,0 +1,157 @@
'use strict';
const assert = require('chai').assert;
const Jimp = require('jimp');
const Tools = require('./lib/tools');
const { BitmapImage, GifUtil } = require('../src/index');
const SAMPLE_PNG_PATH = Tools.getFixturePath('lenna.png');
const SAMPLE_JPG_PATH = Tools.getFixturePath('pelagrina.jpg');
describe("BitmapImage construction behavior", () => {
it("constructs an empty uncolored image", (done) => {
const i = new BitmapImage(10, 5);
Tools.checkBitmap(i.bitmap, 10, 5, 0);
done();
});
it("constructs an empty colored image", (done) => {
const color = 0x01020304;
const i = new BitmapImage(10, 5, color);
Tools.checkBitmap(i.bitmap, 10, 5, color);
done();
});
it("constructs a buffer-sourced image", (done) => {
const buf = new Buffer(10 * 5);
const i = new BitmapImage(10, 5, buf);
Tools.checkBitmap(i.bitmap, 10, 5, buf);
done();
});
it("sourced bitmaps are shared", (done) => {
const b = { width: 5, height: 6, data: new Buffer(5 * 6) };
const j = new BitmapImage(b);
assert.strictEqual(b, j.bitmap);
done();
});
it("sourced images are copied", (done) => {
const color = 0x01020304;
const j1 = new BitmapImage(10, 5, color);
const j2 = new BitmapImage(j1);
assert.notStrictEqual(j1.bitmap, j2.bitmap);
assert.deepStrictEqual(j1.bitmap, j2.bitmap);
done();
});
});
describe("GifFrame bad construction behavior", () => {
it("won't accept garbage", (done) => {
assert.throws(() => {
new BitmapImage();
}, /requires parameters/);
assert.throws(() => {
new BitmapImage(null);
}, /unrecognized/);
assert.throws(() => {
new BitmapImage("string");
}, /unrecognized/);
assert.throws(() => {
new BitmapImage(() => {});
}, /unrecognized/);
assert.throws(() => {
new BitmapImage({});
}, /unrecognized/);
assert.throws(() => {
new BitmapImage(new Buffer(25));
}, /unrecognized/);
done();
});
it("width requires height", (done) => {
assert.throws(() => {
new BitmapImage(5);
}, /unrecognized/);
assert.throws(() => {
new BitmapImage(5, new Buffer(5));
}, /unrecognized/);
assert.throws(() => {
new BitmapImage(5, {});
}, /unrecognized/);
done();
});
});
// TBD: test BitmapImage transformation methods
describe("Jimp compatibility", () => {
it("works when sourced from Jimp", (done) => {
new Jimp(SAMPLE_PNG_PATH, (err, j1) => {
if (err) return done(err);
assert.strictEqual(err, null);
const initialColor = j1.getPixelColor(5, 5);
const i = new BitmapImage(j1.bitmap);
assert.strictEqual(i.getRGBA(5, 5), initialColor);
const newColor = initialColor + 0x01010101;
i.fillRGBA(newColor);
assert.strictEqual(i.getRGBA(5, 5), newColor);
const j2 = GifUtil.shareAsJimp(Jimp, i);
assert.strictEqual(j2.getPixelColor(5, 5), newColor);
done();
});
});
it("works when sourcing Jimp via sharing", (done) => {
const initialColor = 0x12344321;
const i1 = new BitmapImage(10, 5, initialColor);
const j = GifUtil.shareAsJimp(Jimp, i1);
assert.strictEqual(j.getPixelColor(3, 3), initialColor);
const newColor = initialColor + 0x01010101;
j.setPixelColor(newColor, 3, 3);
assert.strictEqual(j.getPixelColor(3, 3), newColor);
const i2 = new BitmapImage(j.bitmap);
assert.strictEqual(i2.getRGBA(3,3), newColor);
done();
});
it("works when sourcing Jimp via copying", (done) => {
const initialColor = 0x12344321;
const i1 = new BitmapImage(10, 5, initialColor);
const j = GifUtil.copyAsJimp(Jimp, i1);
assert.strictEqual(j.getPixelColor(3, 3), initialColor);
const newColor = initialColor + 0x01010101;
j.setPixelColor(newColor, 3, 3);
assert.strictEqual(j.getPixelColor(3, 3), newColor);
const i2 = new BitmapImage(j.bitmap);
assert.strictEqual(i2.getRGBA(3,3), newColor);
done();
});
it("composing with a sprite having transparency", (done) => {
const i1 = new BitmapImage(Tools.getBitmap('singleFrameMonoOpaque'));
const j1 = GifUtil.shareAsJimp(Jimp, i1);
const i2 = new BitmapImage(Tools.getBitmap('sampleSprite'));
const j2 = GifUtil.shareAsJimp(Jimp, i2);
j1.composite(j2, 1, 1);
const result = new BitmapImage(j1.bitmap);
const expected = new BitmapImage(Tools.getBitmap('singleFrameMonoOpaqueSpriteAt1x1'));
assert.deepStrictEqual(result.bitmap, expected.bitmap);
done();
});
});
+252
View File
@@ -0,0 +1,252 @@
'use strict';
const assert = require('chai').assert;
const Tools = require('./lib/tools');
const { Gif, GifFrame, GifCodec, GifUtil, GifError } =
require('../src/index');
describe("single frame decoding", () => {
it("reads an opaque monocolor file", () => {
const name = 'singleFrameMonoOpaque';
const bitmap = Tools.getBitmap(name);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, [bitmap], {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: false
});
});
});
it("reads an opaque multi-color file", () => {
const name = 'singleFrameMultiOpaque';
const bitmap = Tools.getBitmap(name);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, [bitmap], {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: false
});
});
});
it("reads a purely transparent file", () => {
const name = 'singleFrameNoColorTrans';
const bitmap = Tools.getBitmap(name, 0);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, [bitmap], {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: true
});
});
});
it("reads a monochrome file with transparency", () => {
const name = 'singleFrameMonoTrans';
const bitmap = Tools.getBitmap(name, 0);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, [bitmap], {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: true
});
});
});
it("reads a multicolor file with transparency", () => {
const name = 'singleFrameMultiTrans';
const bitmap = Tools.getBitmap(name, 0);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, [bitmap], {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: true
});
});
});
it("reads a multicolor file with custom transparency color", () => {
const transRGB = 0x123456;
const name = 'singleFrameMultiTrans';
const bitmap = Tools.getBitmap(name, transRGB);
const decoder = new GifCodec({ transparentRGB: transRGB });
return GifUtil.read(Tools.getGifPath(name), decoder)
.then(gif => {
_compareGifToSeries(gif, [bitmap], {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: true
});
});
});
});
describe("multiframe decoding", () => {
it("reads a 2-frame multicolor file without transparency", () => {
const name = 'twoFrameMultiOpaque';
const series = Tools.getSeries(name);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, series, {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: false,
delayCentisecs: 50
});
});
});
it("reads a 3-frame monocolor file with transparency", () => {
const name = 'threeFrameMonoTrans';
const series = Tools.getSeries(name);
return GifUtil.read(Tools.getGifPath(name))
.then(gif => {
_compareGifToSeries(gif, series, {
disposalMethod: GifFrame.DisposeToBackgroundColor,
usesTransparency: true,
delayCentisecs: 25
});
});
});
it("reads a large multiframe file w/out transparency", () => {
return GifUtil.read(Tools.getGifPath('nburling-public'))
.then(gif => {
assert.strictEqual(gif.width, 238);
assert.strictEqual(gif.height, 372);
assert.strictEqual(gif.loops, 0);
assert.strictEqual(gif.usesTransparency, false);
assert(Array.isArray(gif.frames));
assert.strictEqual(gif.frames.length, 24);
for (let i = 0; i < gif.frames.length; ++i) {
const frame = gif.frames[i];
assert.strictEqual(frame.bitmap.width, gif.width);
assert.strictEqual(frame.bitmap.height, gif.height);
Tools.checkFrameDefaults(frame, {
xOffset: 0,
yOffset: 0,
disposalMethod: GifFrame.DisposeNothing,
delayCentisecs: 20
}, i);
}
assert(Buffer.isBuffer(gif.buffer));
});
});
it("reads a large multiframe file w/ offsets, w/out transparency", () => {
return GifUtil.read(Tools.getGifPath('rnaples-offsets-public'))
.then(gif => {
assert.strictEqual(gif.width, 480);
assert.strictEqual(gif.height, 693);
assert.strictEqual(gif.loops, 0);
assert.strictEqual(gif.usesTransparency, true);
const frameDump = [ // generated via Tools.dumpFramesAsCode()
[0, 0, 480, 693, 10, false, 1],
[208, 405, 130, 111, 10, false, 1],
[85, 0, 395, 516, 10, false, 1],
[85, 0, 395, 309, 10, false, 1],
[85, 0, 395, 309, 10, false, 1],
[85, 0, 395, 309, 10, false, 1],
[208, 0, 272, 516, 10, false, 1],
[85, 0, 375, 516, 10, false, 1],
[85, 2, 365, 514, 10, false, 1],
[85, 36, 346, 480, 10, false, 1],
[167, 79, 271, 213, 10, false, 1],
[85, 103, 353, 413, 10, false, 1],
[191, 142, 279, 374, 20, false, 1],
[0, 0, 1, 1, 20, false, 1],
[191, 142, 279, 374, 20, false, 1],
[191, 142, 279, 374, 30, false, 1],
[191, 142, 279, 374, 10, false, 1],
[85, 183, 395, 211, 10, false, 1],
[85, 183, 395, 258, 10, false, 1],
[85, 183, 395, 333, 10, false, 1],
[85, 183, 395, 363, 10, false, 1],
[85, 183, 395, 405, 10, false, 1],
[85, 183, 395, 442, 10, false, 1],
[208, 405, 272, 284, 10, false, 1],
[324, 499, 156, 194, 10, false, 1],
[394, 546, 86, 147, 10, false, 1],
[85, 183, 395, 510, 10, false, 1],
[0, 0, 1, 1, 10, false, 1],
[85, 183, 338, 333, 10, false, 1],
[208, 405, 130, 111, 10, false, 1],
[208, 247, 215, 269, 10, false, 1]
];
Tools.compareToFrameDump(gif.frames, frameDump);
assert(Buffer.isBuffer(gif.buffer));
});
});
});
describe("partial frame decoding", () => {
it("renders partial frames properly onto full frames", () => {
let actualBitmapImage;
return GifUtil.read(Tools.getGifPath('rnaples-offsets-public'))
.then(actualGif => {
actualBitmapImage = actualGif.frames[10];
return Tools.loadBitmapImage(Tools.getImagePath('rnaples-frame-10.png'));
})
.then(expectedBitmapImage => {
const expectedBitmap = expectedBitmapImage.bitmap;
Tools.checkBitmap(
actualBitmapImage.bitmap,
expectedBitmap.width,
expectedBitmap.height,
expectedBitmap.data
);
});
});
});
function _compareGifToSeries(actualGif, expectedSeries, options) {
assert.strictEqual(actualGif.width, expectedSeries[0].width);
assert.strictEqual(actualGif.height, expectedSeries[0].height);
if (options.loops === undefined) {
assert.strictEqual(actualGif.loops, 0);
}
else {
assert.strictEqual(actualGif.loops, options.loops);
}
if (options.usesTransparency !== undefined) {
assert.strictEqual(actualGif.usesTransparency,
options.usesTransparency);
}
if (options.optionization !== undefined) {
assert.strictEqual(actualGif.optionization, options.optionization);
}
assert(Array.isArray(actualGif.frames));
assert.strictEqual(actualGif.frames.length, expectedSeries.length);
for (let i = 0; i < actualGif.frames.length; ++i) {
const f = actualGif.frames[i];
Tools.checkFrameDefaults(f, options, i);
assert.deepStrictEqual(f.bitmap, expectedSeries[i],
`frame ${i} same bitmap`);
}
assert(Buffer.isBuffer(actualGif.buffer));
}
+307
View File
@@ -0,0 +1,307 @@
'use strict';
const assert = require('chai').assert;
const Tools = require('./lib/tools');
const { Gif, GifFrame, GifCodec, GifUtil, GifError } = require('../src/index');
// compare decoded encodings with decodings intead of comparing buffers, because there are many ways to encode the same data
const defaultCodec = new GifCodec();
describe("single-frame encoding", () => {
it("encodes an opaque monochrome GIF", () => {
const name = 'singleFrameMonoOpaque';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a transparent GIF", () => {
const name = 'singleFrameNoColorTrans';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a monochrome GIF with transparency", () => {
const name = 'singleFrameMonoTrans';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a opaque two-color GIF", () => {
const name = 'singleFrameBWOpaque';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a opaque multi-color GIF", () => {
const name = 'singleFrameMultiOpaque';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a 4-color GIF w/ transparency", () => {
const name = 'singleFrameMultiTrans';
return _encodeDecodeFile(name, Gif.LocalColorsOnly); // simple code 1st
// .then(() => {
// return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
// });
});
});
describe("multi-frame encoding", () => {
it("encodes a 2-frame multi-color opaque GIF", () => {
const name = 'twoFrameMultiOpaque';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a 3-frame monocolor GIF with transparency", () => {
const name = 'threeFrameMonoTrans';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a large multiframe file w/out transparency", () => {
const name = 'nburling-public';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes a large multiframe file w/ offsets, w/out transparency", () => {
const name = 'rnaples-offsets-public';
return _encodeDecodeFile(name, Gif.LocalColorsOnly) // simple code 1st
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly);
});
});
it("encodes large multiframe files w/ forced buffer-size scaling (1)", () => {
const scalingCodec = new GifCodec();
scalingCodec._testInitialBufferSize = 2048; // big enough for header
const name = 'nburling-public';
return _encodeDecodeFile(name, Gif.LocalColorsOnly, scalingCodec)
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly, scalingCodec);
});
});
it("encodes large multiframe files w/ forced buffer-size scaling (2)", () => {
const scalingCodec = new GifCodec();
scalingCodec._testInitialBufferSize = 2048; // big enough for header
const name = 'rnaples-offsets-public';
return _encodeDecodeFile(name, Gif.LocalColorsOnly, scalingCodec)
.then(() => {
return _encodeDecodeFile(name, Gif.GlobalColorsOnly, scalingCodec);
});
});
});
describe("encoding GlobalColorsPreferred", () => {
it("uses a global color table when 256 colors in 1 frame", () => {
const frames = [];
const options = { colorScope: Gif.GlobalColorsPreferred };
frames.push(_get256ColorFrame());
return defaultCodec.encodeGif(frames, options)
.then(encodedGif => {
options.colorScope = Gif.GlobalColorsOnly;
return defaultCodec.encodeGif(frames, options);
})
.then(encodedGif => {
assert(true);
})
});
it("uses a global color table when 256 colors in each of 2 frames", () => {
const frames = [];
const options = { colorScope: Gif.GlobalColorsPreferred };
frames.push(_get256ColorFrame());
frames.push(_get256ColorFrame());
return defaultCodec.encodeGif(frames, options)
.then(encodedGif => {
options.colorScope = Gif.GlobalColorsOnly;
return defaultCodec.encodeGif(frames, options);
})
.then(encodedGif => {
assert(true);
})
});
it("uses a global color table when 255 colors + transparency in each of 2 frames", () => {
const frames = [];
const options = { colorScope: Gif.GlobalColorsPreferred };
frames.push(_get256ColorFrame());
frames[0].bitmap.data[3] = 0;
frames.push(_get256ColorFrame());
frames[1].bitmap.data[3] = 0;
return defaultCodec.encodeGif(frames, options)
.then(encodedGif => {
options.colorScope = Gif.GlobalColorsOnly;
return defaultCodec.encodeGif(frames, options);
})
.then(encodedGif => {
assert(true);
})
});
it("uses a local color table when there are 257 opaque colors", () => {
const frames = [];
const options = { colorScope: Gif.GlobalColorsPreferred };
frames.push(_get256ColorFrame());
// put a 257th opaque color in the second frame
let buf = new Buffer(256 * 4); // defaults to zeroes
buf[0] = 0; buf[1] = 2; buf[2] = 3; buf[3] = 255;
frames.push(new GifFrame(16, 16, buf));
return defaultCodec.encodeGif(frames, options)
.then(encodedGif => {
options.colorScope = Gif.GlobalColorsOnly;
return defaultCodec.encodeGif(frames, options);
})
.then(encodedGif => {
assert.fail("should not encode");
})
.catch(err => {
if (!(err instanceof GifError)) {
throw err;
}
assert.strictEqual(err.message,
"Too many color indexes for global color table");
});
});
it("uses a local color table when there are 256 opaque colors + transparency", () => {
const frames = [];
const options = { colorScope: Gif.GlobalColorsPreferred };
frames.push(_get256ColorFrame());
frames.push(_get256ColorFrame());
frames[1].bitmap.data[3] = 0;
return defaultCodec.encodeGif(frames, options)
.then(encodedGif => {
options.colorScope = Gif.GlobalColorsOnly;
return defaultCodec.encodeGif(frames, options);
})
.then(encodedGif => {
assert.fail("should not encode");
})
.catch(err => {
if (!(err instanceof GifError)) {
throw err;
}
assert.strictEqual(err.message,
"Too many color indexes for global color table");
});
});
});
function _compareGifs(actual, expected, filename, note) {
note = `file '${filename}' (${note})`;
assert.strictEqual(actual.width, expected.width, note);
assert.strictEqual(actual.height, expected.height, note);
assert.strictEqual(actual.loops, expected.loops, note);
assert.strictEqual(actual.usesTransparency, expected.usesTransparency,
note);
assert(Buffer.isBuffer(actual.buffer), note);
assert(Array.isArray(actual.frames));
assert.strictEqual(actual.frames.length, expected.frames.length);
note = ` in ${note}`;
for (let i = 0; i < actual.frames.length; ++i) {
const actualFrame = actual.frames[i];
const expectedFrame = expected.frames[i];
Tools.verifyFrameInfo(actualFrame, expectedFrame, i, note);
}
}
function _encodeDecodeFile(filename, colorScope, codec) {
let expectedGif;
codec = codec || defaultCodec;
return GifUtil.read(Tools.getGifPath(filename), codec)
.then(readGif => {
expectedGif = readGif;
return codec.encodeGif(readGif.frames,
{ loops: readGif.loops, colorScope: colorScope });
})
.then(encodedGif => {
_compareGifs(encodedGif, expectedGif, filename,
`encoded == read (colorScope ${colorScope})`);
return codec.decodeGif(encodedGif.buffer);
})
.then(decodedGif => {
_compareGifs(decodedGif, expectedGif, filename,
`decoded == read (colorScope ${colorScope})`);
})
}
function _get256ColorFrame() {
let buf = new Buffer(256 * 4);
for (let i = 0; i < 256; ++i) {
const offset = i * 4;
buf[offset + 2] = buf[offset + 1] = buf[offset] = i;
buf[offset + 3] = 255;
}
return new GifFrame(16, 16, buf);
}
+276
View File
@@ -0,0 +1,276 @@
'use strict';
const assert = require('chai').assert;
const Jimp = require('jimp');
const Tools = require('./lib/tools');
const { BitmapImage, Gif, GifFrame, GifError } = require('../src/index');
const SAMPLE_PNG_PATH = Tools.getFixturePath('lenna.png');
const SAMPLE_JPG_PATH = Tools.getFixturePath('pelagrina.jpg');
describe("GifFrame good construction behavior", () => {
it("constructs an uncolored bitmap", (done) => {
const f = new GifFrame(10, 5);
Tools.checkBitmap(f.bitmap, 10, 5, 0);
Tools.checkFrameDefaults(f);
done();
});
it("initializes options in an uncolored bitmap", (done) => {
const f = new GifFrame(10, 5, { delayCentisecs: 100 });
Tools.checkBitmap(f.bitmap, 10, 5, 0);
Tools.checkFrameDefaults(f, {
delayCentisecs: 100
});
done();
});
it("constructs an empty colored bitmap", (done) => {
const color = 0x01020300;
const f = new GifFrame(10, 5, color);
Tools.checkBitmap(f.bitmap, 10, 5, color);
Tools.checkFrameDefaults(f);
done();
});
it("initializes options in a colored bitmap", (done) => {
const color = 0x010203ff;
const f = new GifFrame(10, 5, color, { delayCentisecs: 100 });
Tools.checkBitmap(f.bitmap, 10, 5, color);
Tools.checkFrameDefaults(f, {
delayCentisecs: 100
});
done();
});
it("sources from an existing bitmap, sharing", (done) => {
const color = 0x010203ff;
const j = new BitmapImage(10, 5, color);
const f = new GifFrame(j.bitmap);
assert.strictEqual(f.bitmap, j.bitmap);
Tools.checkFrameDefaults(f);
done();
});
it("sources from an existing bitmap, sharing, with options", (done) => {
const color = 0x010203ff;
const j = new BitmapImage(10, 5, color);
const f = new GifFrame(j.bitmap, { delayCentisecs: 100 });
assert.strictEqual(f.bitmap, j.bitmap);
Tools.checkFrameDefaults(f, {
delayCentisecs: 100
});
done();
});
it("sources from an existing BitmapImage, copying", (done) => {
const color = 0x010203ff;
const j = new BitmapImage(10, 5, color);
const f = new GifFrame(j);
assert.notStrictEqual(f.bitmap, j.bitmap);
assert.deepStrictEqual(f.bitmap, j.bitmap);
Tools.checkFrameDefaults(f);
done();
});
it("sources from an existing BitmapImage, copying, with options", (done) => {
const color = 0x010203ff;
const j = new BitmapImage(10, 5, color);
const f = new GifFrame(j, { delayCentisecs: 100 });
assert.notStrictEqual(f.bitmap, j.bitmap);
assert.deepStrictEqual(f.bitmap, j.bitmap);
Tools.checkFrameDefaults(f, {
delayCentisecs: 100
});
done();
});
it("initializes data params without options", (done) => {
const bitmap = Tools.getBitmap('singleFrameBWOpaque');
const f = new GifFrame(bitmap.width, bitmap.height, bitmap.data);
assert.strictEqual(f.bitmap.width, bitmap.width);
assert.strictEqual(f.bitmap.height, bitmap.height);
assert.strictEqual(f.bitmap.data, bitmap.data);
Tools.checkFrameDefaults(f);
done();
});
it("initializes data params with options", (done) => {
const bitmap = Tools.getBitmap('singleFrameBWOpaque');
const f = new GifFrame(bitmap.width, bitmap.height, bitmap.data,
{ delayCentisecs: 200, interlaced: true });
assert.strictEqual(f.bitmap.width, bitmap.width);
assert.strictEqual(f.bitmap.height, bitmap.height);
assert.strictEqual(f.bitmap.data, bitmap.data);
Tools.checkFrameDefaults(f, {
delayCentisecs: 200,
interlaced: true
});
done();
});
it("initializes bitmap without options", (done) => {
const bitmap = Tools.getBitmap('singleFrameBWOpaque');
const f = new GifFrame(bitmap);
assert.strictEqual(f.bitmap, bitmap);
Tools.checkFrameDefaults(f);
done();
});
it("initializes bitmap with options", (done) => {
const bitmap = Tools.getBitmap('singleFrameBWOpaque');
const f = new GifFrame(bitmap,
{ delayCentisecs: 200, interlaced: true });
assert.strictEqual(f.bitmap, bitmap);
Tools.checkFrameDefaults(f, {
delayCentisecs: 200,
interlaced: true
});
done();
});
it("clones an existing frame", (done) => {
const f1 = new GifFrame(5, 5, {
delayCentisecs: 100,
isInterlace: true
});
f1.bitmap = Tools.getBitmap('singleFrameBWOpaque');
const f2 = new GifFrame(f1);
Tools.verifyFrameInfo(f2, f1);
assert.notStrictEqual(f2.bitmap, f1.bitmap);
assert.deepStrictEqual(f2.bitmap, f1.bitmap);
done();
});
});
describe("GifFrame bad construction behavior", () => {
it("won't accept garbage", (done) => {
assert.throws(() => {
new GifFrame();
}, /requires parameters/);
assert.throws(() => {
new GifFrame(null);
}, /unrecognized/);
assert.throws(() => {
new GifFrame(null, { interlaced: true });
}, /unrecognized/);
assert.throws(() => {
new GifFrame("string");
}, /unrecognized/);
assert.throws(() => {
new GifFrame("string", { interlaced: true });
}, /unrecognized/);
assert.throws(() => {
new GifFrame(() => {});
}, /unrecognized/);
assert.throws(() => {
new GifFrame({});
}, /unrecognized/);
assert.throws(() => {
new GifFrame({ interlaced: true });
}, /unrecognized/);
assert.throws(() => {
new GifFrame(new Buffer(25));
}, /unrecognized/);
done();
});
it("width requires height", (done) => {
assert.throws(() => {
new GifFrame(5);
}, /unrecognized/);
assert.throws(() => {
new GifFrame(5, { interlaced: true });
}, /unrecognized/);
assert.throws(() => {
new GifFrame(5, new Buffer(5));
}, /unrecognized/);
done();
});
});
describe("GifFrame palette", () => {
it("is monocolor without transparency", (done) => {
const bitmap = Tools.getBitmap('singleFrameMonoOpaque');
const f = new GifFrame(bitmap);
const p = f.getPalette();
assert.deepStrictEqual(p.colors, [0xFF0000]);
assert.strictEqual(p.usesTransparency, false);
done();
});
it("includes two colors without transparency", (done) => {
const bitmap = Tools.getBitmap('singleFrameBWOpaque');
const f = new GifFrame(bitmap);
const p = f.getPalette();
assert.deepStrictEqual(p.colors, [0x000000, 0xffffff]);
assert.strictEqual(p.usesTransparency, false);
done();
});
it("includes multiple colors without transparency", (done) => {
const bitmap = Tools.getBitmap('singleFrameMultiOpaque');
const f = new GifFrame(bitmap);
const p = f.getPalette();
assert.deepStrictEqual(p.colors,
[0x0000ff, 0x00ff00, 0xff0000, 0xffffff]);
assert.strictEqual(p.usesTransparency, false);
done();
});
it("has only transparency", (done) => {
const bitmap = Tools.getBitmap('singleFrameNoColorTrans');
const f = new GifFrame(bitmap);
const p = f.getPalette();
assert.deepStrictEqual(p.colors, []);
assert.strictEqual(p.usesTransparency, true);
done();
});
it("is monocolor with transparency", (done) => {
const bitmap = Tools.getBitmap('singleFrameMonoTrans');
const f = new GifFrame(bitmap);
const p = f.getPalette();
assert.deepStrictEqual(p.colors, [0x00ff00]);
assert.strictEqual(p.usesTransparency, true);
done();
});
it("includes multiple colors with transparency", (done) => {
const bitmap = Tools.getBitmap('singleFrameMultiPartialTrans');
const f = new GifFrame(bitmap);
const p = f.getPalette();
assert.deepStrictEqual(p.colors,
[0x000000, 0x0000ff, 0x00ff00, 0xff0000, 0xffffff]);
assert.strictEqual(p.usesTransparency, true);
done();
});
});
function _assertDefaultFrameOptions(frame) {
assert.strictEqual(frame.xOffset, 0);
assert.strictEqual(frame.yOffset, 0);
assert.strictEqual(frame.disposalMethod, GifFrame.DisposeToBackgroundColor);
assert.strictEqual(typeof frame.delayCentisecs, 'number');
assert.strictEqual(frame.interlaced, false);
}
+277
View File
@@ -0,0 +1,277 @@
'use strict';
const assert = require('chai').assert;
const Tools = require('./lib/tools');
const { Gif, GifFrame, GifCodec, GifError, GifUtil } = require('../src/index');
const defaultCodec = new GifCodec();
describe("Gif width/height", () => {
it("sets width/height = size of the frames", () => {
const w = 24, h = 10;
const frames = [
new GifFrame(w, h),
new GifFrame(w, h, 0xffffffff)
];
return defaultCodec.encodeGif(frames)
.then(gif => {
assert.strictEqual(gif.width, w);
assert.strictEqual(gif.height, h);
});
});
it("sets width/height = size of largest frame", () => {
const w = 24, h = 10;
const frames = [
new GifFrame(w - 1, h - 1),
new GifFrame(w, h, 0xffffffff)
];
return defaultCodec.encodeGif(frames)
.then(gif => {
assert.strictEqual(gif.width, w);
assert.strictEqual(gif.height, h);
});
});
it("sets width/height = largest frame boundary", () => {
const w = 24, h = 10;
const frames = [
new GifFrame(w - 1, h - 1, {
xOffset: 1,
yOffset: 1
}),
new GifFrame(w, h, 0xffffffff)
];
return defaultCodec.encodeGif(frames)
.then(gif => {
assert.strictEqual(gif.width, w);
assert.strictEqual(gif.height, h);
});
});
});
describe("Gif loop count", () => {
it("decodes an infinite loop", () => {
return GifUtil.read(Tools.getGifPath('countConstantDelay0'))
.then(gif => {
assert.strictEqual(gif.loops, 0);
});
});
it("decodes a single-pass loop", () => {
return GifUtil.read(Tools.getGifPath('countConstantDelay1'))
.then(gif => {
assert.strictEqual(gif.loops, 1);
});
});
it("decodes a three-pass loop", () => {
return GifUtil.read(Tools.getGifPath('countConstantDelay3'))
.then(gif => {
assert.strictEqual(gif.loops, 3);
});
});
it("encodes an infinite loop", () => {
return _verifyEncodesLoopCount('countConstantDelay3', 0);
});
it("encodes a single-pass loop", () => {
return _verifyEncodesLoopCount('countConstantDelay0', 1);
});
it("encodes a three-pass loop", () => {
return _verifyEncodesLoopCount('countConstantDelay1', 3);
});
});
describe("Gif transparency", () => {
it("indicates no transparency", () => {
return GifUtil.read(Tools.getGifPath('twoFrameMultiOpaque'))
.then(readGif => {
assert.strictEqual(readGif.usesTransparency, false);
return defaultCodec.encodeGif(readGif.frames);
})
.then(encodedGif => {
assert.strictEqual(encodedGif.usesTransparency, false);
})
});
it("indicates transparency", () => {
return GifUtil.read(Tools.getGifPath('threeFrameMonoTrans'))
.then(readGif => {
assert.strictEqual(readGif.usesTransparency, true);
return defaultCodec.encodeGif(readGif.frames);
})
.then(encodedGif => {
assert.strictEqual(encodedGif.usesTransparency, true);
})
});
});
describe("GifFrame x/y-offsets", () => {
it("accommodates/encodes offsets", () => {
return GifUtil.read(Tools.getGifPath('count5x7'))
.then(readGif => {
const frames = readGif.frames;
assert(frames.length >= 4, "precondition for test");
assert.strictEqual(readGif.width, 5);
assert.strictEqual(readGif.height, 7);
frames.forEach(frame => {
_verifyFrameOffsets(frame, 0, 0);
});
frames[1].xOffset = 1;
frames[1].yOffset = 2;
frames[2].xOffset = 2;
frames[2].yOffset = 1;
return defaultCodec.encodeGif(frames);
})
.then(encodedGif => {
assert.strictEqual(encodedGif.width, 7);
assert.strictEqual(encodedGif.height, 9);
return defaultCodec.decodeGif(encodedGif.buffer);
})
.then(decodedGif => {
assert.strictEqual(decodedGif.width, 7);
assert.strictEqual(decodedGif.height, 9);
const frames = decodedGif.frames;
_verifyFrameOffsets(frames[0], 0, 0);
_verifyFrameOffsets(frames[1], 1, 2);
_verifyFrameOffsets(frames[2], 2, 1);
_verifyFrameOffsets(frames[3], 0, 0);
});
});
});
describe("GifFrame delay", () => {
it("confirms a constant frame-delay GIF", () => {
return GifUtil.read(Tools.getGifPath('countConstantDelay0'))
.then(readGif => {
readGif.frames.forEach(frame => {
assert.strictEqual(frame.delayCentisecs, 33);
});
});
});
it("confirms a variable frame-delay GIF", () => {
return GifUtil.read(Tools.getGifPath('countIncreasingDelay'))
.then(readGif => {
const frames = readGif.frames;
for (let i = 0; i < frames.length; ++i) {
assert.strictEqual(frames[i].delayCentisecs, (i + 1)*25);
}
});
});
it("encodes varying frame delays", () => {
return GifUtil.read(Tools.getGifPath('countConstantDelay0'))
.then(readGif => {
const frames = readGif.frames;
for (let i = 0; i < frames.length; ++i) {
frames[i].delayCentisecs = (i + 1)*25;
}
return defaultCodec.encodeGif(frames, { loops: readGif.loops });
})
.then(encodedGif => {
return defaultCodec.decodeGif(encodedGif.buffer);
})
.then(decodedGif => {
const frames = decodedGif.frames;
for (let i = 0; i < frames.length; ++i) {
assert.strictEqual(frames[i].delayCentisecs, (i + 1)*25);
}
});
});
});
describe("GifFrame disposal method", () => {
it("encodes/decodes disposal", () => {
return GifUtil.read(Tools.getGifPath('countConstantDelay0'))
.then(readGif => {
const frames = readGif.frames;
assert(frames.length >= 4, "precondition for test");
frames.forEach(frame => {
assert.strictEqual(frame.disposalMethod,
GifFrame.DisposeToBackgroundColor);
});
for (let i = 0; i < 4; ++i) {
frames[i].disposalMethod = i;
}
return defaultCodec.encodeGif(frames, { loops: readGif.loops });
})
.then(encodedGif => {
return defaultCodec.decodeGif(encodedGif.buffer);
})
.then(decodedGif => {
const frames = decodedGif.frames;
for (let i = 0; i < 4; ++i) {
assert.strictEqual(frames[i].disposalMethod, i);
}
});
});
});
function _verifyEncodesLoopCount(sourceFilename, loopCount) {
return GifUtil.read(Tools.getGifPath(sourceFilename))
.then(readGif => {
return defaultCodec.encodeGif(readGif.frames, { loops: loopCount });
})
.then(encodedGif => {
assert.strictEqual(encodedGif.loops, loopCount);
return defaultCodec.decodeGif(encodedGif.buffer);
})
.then(decodedGif => {
assert.strictEqual(decodedGif.loops, loopCount);
});
}
function _verifyFrameOffsets(frame, xOffset, yOffset) {
assert.strictEqual(frame.xOffset, xOffset);
assert.strictEqual(frame.yOffset, yOffset);
}
+292
View File
@@ -0,0 +1,292 @@
'use strict';
const assert = require('chai').assert;
const Jimp = require('jimp');
const Tools = require('./lib/tools');
const { BitmapImage, GifUtil } = require('../src/index');
describe("graphics color index reduction", () => {
it("Dekker-quantizes down to 32 colors", () => {
return _graphicsTest('quantizeDekker', 32);
});
it("Dekker-quantizes down to 256 colors", () => {
return _graphicsTest('quantizeDekker', 256);
});
it("Sorokin-quantizes down to 32 colors", () => {
return _graphicsTest('quantizeSorokin', 32);
});
it("Sorokin-quantizes down to 256 colors", () => {
return _graphicsTest('quantizeSorokin', 256);
});
it("Wu-quantizes down to 32 colors", () => {
return _graphicsTest('quantizeWu', 32);
});
it("Wu-quantizes down to 256 colors", () => {
return _graphicsTest('quantizeWu', 256);
});
});
describe("photo color index reduction", () => {
it("Dekker-quantizes down to 32 colors", () => {
return _photoTest('quantizeDekker', 32);
});
it("Dekker-quantizes down to 256 colors", () => {
return _photoTest('quantizeDekker', 256);
});
it("Sorokin-quantizes down to 32 colors", () => {
return _photoTest('quantizeSorokin', 32);
});
it("Sorokin-quantizes down to 256 colors", () => {
return _photoTest('quantizeSorokin', 256);
});
it("Wu-quantizes down to 32 colors", () => {
return _photoTest('quantizeWu', 32);
});
it("Wu-quantizes down to 256 colors", () => {
return _photoTest('quantizeWu', 256);
});
});
describe("dithering", () => {
it("FloydSteinberg-dither of Dekker-quantized 256 colors", () => {
return _ditherTest('quantizeDekker', 256, null, 'FloydSteinberg');
});
it("FloydSteinberg-dither of Sorokin-quantized 256 colors", () => {
return _ditherTest('quantizeSorokin', 256, 'min-pop', 'FloydSteinberg');
});
it("FloydSteinberg-dither of Wu-quantized 256 colors", () => {
return _ditherTest('quantizeWu', 256, 5, 'FloydSteinberg');
});
});
describe("reduce colors across a series of images", () => {
const specialOpaque = 0xffffffff;
const specialTransparent = 0;
it("Dekker-quantizes an opaque image series down to 256 colors", (done) => {
_seriesTest('quantizeDekker', specialOpaque);
done();
});
it("Dekker-quantizes an image series with transparency down to 256 colors", (done) => {
_seriesTest('quantizeDekker', specialTransparent);
done();
});
it("Sorokin-quantizes an opaque image series down to 256 colors", (done) => {
_seriesTest('quantizeSorokin', specialOpaque);
done();
});
it("Sorokin-quantizes an image series with transparency down to 256 colors", (done) => {
_seriesTest('quantizeSorokin', specialTransparent);
done();
});
it("Wu-quantizes an opaque image series down to 256 colors", (done) => {
_seriesTest('quantizeWu', specialOpaque);
done();
});
it("Wu-quantizes an image series with transparency down to 256 colors", (done) => {
_seriesTest('quantizeWu', specialTransparent);
done();
});
});
function _ditherTest(method, maxColors, modifier, ditherAlg) {
return _reductionTest("sculptmap.png", false, method, maxColors, modifier, {
ditherAlgorithm: ditherAlg
});
}
function _graphicsTest(method, maxColors) {
return _reductionTest("sculptmap.png", false, method, maxColors)
.then(() => {
return _reductionTest("rosewithtrans.png", true, method, maxColors);
})
}
function _hasTransparency(colorSet) {
for (let rgba of colorSet.values()) {
if ((rgba & 0xff) === 0x00) {
return true;
}
}
return false;
}
function _photoTest(method, maxColors) {
return _reductionTest("hairstreak.jpg", false, method, maxColors);
}
function _reductionTest(sourceFile, usesTransparency, method, maxColors, modifier, dither) {
const baseFile = sourceFile.substr(0, sourceFile.length - 4);
const suffix = method.substr(8);
let expectedFile = `quantized/${baseFile}${maxColors}_${suffix}`;
if (dither) {
expectedFile += '_' + dither.ditherAlgorithm;
}
expectedFile += '.png';
const label = `${method}(${maxColors}) - ${baseFile}`;
const writeFile = null; //expectedFile;
let work;
return new Promise((resolve, reject) => {
new Jimp(Tools.getFixturePath(sourceFile), (err, manyJimp) => {
if (err) return reject(err);
new Jimp(Tools.getFixturePath(expectedFile), (err, limitedJimp) => {
if (err) return reject(err);
work = new BitmapImage(manyJimp.bitmap);
const inputColorSet = work.getRGBASet();
assert.strictEqual(_hasTransparency(inputColorSet), usesTransparency, label);
assert.isAtLeast(inputColorSet.size, maxColors + 1, label);
if (method === 'quantizeDekker') {
GifUtil[method](work, maxColors, dither);
}
else {
GifUtil[method](work, maxColors, modifier, dither);
}
const workBuf = work.bitmap.data;
const limitedBuf = limitedJimp.bitmap.data;
assert.strictEqual(workBuf.length, manyJimp.bitmap.data.length, label);
assert.strictEqual(workBuf.compare(limitedBuf), 0, label);
const outputColorSet = work.getRGBASet();
assert.strictEqual(_hasTransparency(outputColorSet), usesTransparency, label);
assert.isAtMost(outputColorSet.size, maxColors, label);
resolve();
});
});
})
.then(() => {
if (writeFile) {
return _writeJimp(writeFile, work.bitmap);
}
});
}
function _seriesTest(method, specialColor) {
const images = [];
const width = 32, height = 32;
const maxColors = 256;
let image = new BitmapImage(width, height);
let buf = image.bitmap.data;
let bi = 0;
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
buf[bi] = x * 8;
buf[bi + 1] = y * 8;
buf[bi + 3] = 255;
bi += 4;
}
}
images.push(image); // 32 * 32 = 1024 colors
image = new BitmapImage(width, height);
buf = image.bitmap.data;
bi = 0;
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
buf[bi] = x * 8 + 4;
buf[bi + 1] = y * 8 + 4;
buf[bi + 3] = 255;
bi += 4;
}
}
images.push(image); // 32 * 32 = 1024 more colors
image = new BitmapImage(width, height);
buf = image.bitmap.data;
bi = 0;
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
if (x < width / 2) {
buf.writeUInt32BE(255, bi);
}
else {
buf.writeUInt32BE(specialColor, bi);
}
bi += 4;
}
}
images.push(image); // 1 more color (white), which should be there
GifUtil[method](images, maxColors);
const colorSet = new Set();
images.forEach(image => {
buf = image.bitmap.data;
for (let bi = 0; bi < buf.length; bi += 4) {
colorSet.add(buf.readUInt32BE(bi, true));
}
});
assert.isAtMost(colorSet.size, maxColors);
assert(colorSet.has(specialColor), "has special color");
}
function _writeJimp(filename, bitmap) {
const jimp = new Jimp(1, 1);
jimp.bitmap = bitmap;
return new Promise((resolve, reject) => {
jimp.write(filename, (err) => {
if (err) return reject(err);
resolve();
});
}).then(() => {
console.log(`WROTE ${filename}`);
})
.catch((err) => {
console.log(`WRITE FAILED ${err.stack}`);
});
}