chore: add jimp dependency, icon padding script, and update configuration
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 580 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 791 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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: [
|
||||
['* ',
|
||||
' ',
|
||||
' '],
|
||||
[' ',
|
||||
' * ',
|
||||
' '],
|
||||
[' ',
|
||||
' ',
|
||||
' *']
|
||||
]
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||