1
0
Fork 0
edp445/node_modules/@skyra/gifenc/dist/lib/GifEncoder.js

562 lines
20 KiB
JavaScript

"use strict";
/**
* GifEncoder
*
* Authors
* - Kevin Weiner (original Java version - kweiner@fmsware.com)
* - Thibault Imbert (AS3 version - bytearray.org)
* - Johan Nordberg (JS version - code@johan-nordberg.com)
* - Eugene Ware (node.js streaming version - eugene@noblesmaurai.com)
* - Antonio Román (TS version - kyradiscord@gmail.com)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GifEncoder = void 0;
const stream_1 = require("stream");
const util_1 = require("util");
const ByteBuffer_1 = require("./ByteBuffer");
const LZWEncoder_1 = require("./LZWEncoder");
const NeuQuant_1 = require("./NeuQuant");
const NOP = () => {
// no-op
};
const GIF_HEADER = new TextEncoder().encode('GIF89a');
const NETSCAPE_HEADER = new Uint8Array([0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30]); // NETSCAPE2.0
/**
* The color table size (bits - 1).
*/
const PALETTE_SIZE = 7;
class GifEncoder {
/**
* Constructs the GIF encoder.
* @param width An integer representing the GIF image's width, between `1` and `65536`.
* @param height An integer representing the GIF image's height, between `1` and `65536`.
*/
constructor(width, height) {
/**
* The GIF image's width, between `1` and `65536`.
*/
Object.defineProperty(this, "width", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* The GIF image's height, between `1` and `65536`.
*/
Object.defineProperty(this, "height", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* The transparent color, `null` if no transparent color is given.
*/
Object.defineProperty(this, "transparent", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* The transparent index in the color table.
*/
Object.defineProperty(this, "transparentIndex", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
/**
* Number between `-1` and `65536`, `-1` indicating no repeat (GIF89a specification), otherwise repeating `repeat`
* times with the exception of `0`, which repeats indefinitely.
*/
Object.defineProperty(this, "repeat", {
enumerable: true,
configurable: true,
writable: true,
value: -1
});
/**
* Frame delay in hundredths of a second (1 = 10ms).
*/
Object.defineProperty(this, "delay", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
/**
* The current frame.
*/
Object.defineProperty(this, "image", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* The BGR byte array from the current frame.
*/
Object.defineProperty(this, "pixels", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* The converted frame indexed to the palette.
*/
Object.defineProperty(this, "indexedPixels", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* The number of bit planes.
*/
Object.defineProperty(this, "colorDepth", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* The RGB palette.
*/
Object.defineProperty(this, "colorPalette", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
/**
* The active palette entries.
*/
Object.defineProperty(this, "usedEntry", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
/**
* The disposal code (`-1` = determine defaults).
*/
Object.defineProperty(this, "disposalMode", {
enumerable: true,
configurable: true,
writable: true,
value: -1
});
/**
* Whether or not this is the first frame.
*/
Object.defineProperty(this, "firstFrame", {
enumerable: true,
configurable: true,
writable: true,
value: true
});
/**
* The sample interval for the quantifier.
*/
Object.defineProperty(this, "sample", {
enumerable: true,
configurable: true,
writable: true,
value: 10
});
/**
* Whether or not we started encoding.
*/
Object.defineProperty(this, "started", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
/**
* The readable streams.
*/
Object.defineProperty(this, "readableStreams", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
/**
* The output buffer.
*/
Object.defineProperty(this, "byteBuffer", {
enumerable: true,
configurable: true,
writable: true,
value: new ByteBuffer_1.ByteBuffer()
});
this.width = ~~width;
this.height = ~~height;
}
createReadStream(readable) {
if (!readable) {
readable = new stream_1.Readable();
readable._read = NOP;
}
this.readableStreams.push(readable);
return readable;
}
/**
* Creates a write stream.
* @param options The options for the write stream.
* @returns A {@link Duplex}.
* @example
* ```typescript
* const { GifEncoder } = require('@skyra/gifenc');
* const encoder = new GifEncoder(400, 200);
*
* pngStreamGenerator() // A user-defined `Readable`.
* .pipe(encoder.createWriteStream({ repeat: -1, delay: 500, quality: 10 }))
* .pipe(fs.createWriteStream('runningKitten.gif'));
* ```
*/
createWriteStream(options) {
if (options) {
if (options.delay !== undefined)
this.setDelay(options.delay);
if (options.framerate !== undefined)
this.setFramerate(options.framerate);
if (options.dispose !== undefined)
this.setDispose(options.dispose);
if (options.repeat !== undefined)
this.setRepeat(options.repeat);
if (options.transparent !== undefined)
this.setTransparent(options.transparent);
if (options.quality !== undefined)
this.setQuality(options.quality);
}
const duplex = new stream_1.Duplex({ objectMode: true });
duplex._read = NOP;
this.createReadStream(duplex);
duplex._write = (data, _enc, next) => {
if (!this.started)
this.start();
this.addFrame(data);
next();
};
const end = duplex.end.bind(duplex);
// @ts-expect-error This is a Node 17 issue and it should not break using the library
duplex.end = (...args) => {
end(...args);
this.finish();
};
return duplex;
}
/**
* Sets the delay time between each frame, or changes it for subsequent frames (applies to the next frame added).
* @param delay The delay between frames, in milliseconds. Must be a number between `655360` and `10`.
*/
setDelay(delay) {
this.delay = Math.round(delay / 10);
return this;
}
/**
* Sets frame rate in frames per second.
* @param fps The amount of frames per second, maximum is `100` frames per second.
*/
setFramerate(fps) {
this.delay = Math.round(100 / fps);
return this;
}
/**
* Sets the GIF frame disposal code for the last added frame and any subsequent frames.
*
* Defaults to one of the following values:
* - `0` : If `transparent` is set
* - `2` : Otherwise
*
* @param disposalCode The disposal code.
* @see {@link DisposalCode}
*/
setDispose(disposalCode) {
if (disposalCode >= 0)
this.disposalMode = disposalCode;
return this;
}
/**
* Sets the number of times the set of GIF frames should be played.
* @param repeat The number of times between `-1` and `65536` to repeat the GIF, with two special cases:
* - `-1` (**default**): play once
* - `0`: repeat indefinitely
*
* @note This method has no effect after the first image was added.
*/
setRepeat(repeat) {
this.repeat = repeat;
return this;
}
/**
* Sets the transparent color for the last added frame and any subsequent frames. Since all colors are subject to
* modification in the quantization process, the color in the final palette for each frame closest to the given
* color becomes the transparent color for that frame. May be set to null to indicate no transparent color.
* @param color The color to be set in transparent pixels.
*/
setTransparent(color) {
this.transparent = color;
return this;
}
/**
* Sets the quality of color quantization (conversion of images to the maximum 256 colors allowed by the GIF
* specification). Lower values (`minimum` = 1) produce better colors, but slow processing significantly. `10` is
* the default, and produces good color mapping at reasonable speeds. Values greater than 20 do not yield
* significant improvements in speed.
* @param quality A number between `1` and `30`.
*/
setQuality(quality) {
if (quality < 1)
quality = 1;
this.sample = quality;
return this;
}
/**
* Adds the next GIF frame. The frame is not written immediately, but is actually deferred until the next frame is
* received so that timing data can be inserted. Calling {@link GifEncoder.finish} will flush all frames.
* @param imageData The image data to add into the next frame.
*/
addFrame(imageData) {
if (util_1.types.isUint8ClampedArray(imageData)) {
this.image = imageData;
}
else {
this.image = imageData.getImageData(0, 0, this.width, this.height).data;
}
this.getImagePixels(); // convert to correct format if necessary
this.analyzePixels(); // build color table & map pixels
if (this.firstFrame) {
this.writeLogicalScreenDescriptor(); // logical screen descriptor
this.writePalette(); // global color table
if (this.repeat >= 0) {
// use NS app extension to indicate reps
this.writeNetscapeExtension();
}
}
this.writeGraphicControlExtension(); // write graphic control extension
this.writeImageDescriptor(); // image descriptor
if (!this.firstFrame)
this.writePalette(); // local color table
this.writePixels(); // encode and write pixel data
this.firstFrame = false;
this.emit();
}
/**
* Adds final trailer to the GIF stream, if you don't call the finish method the GIF stream will not be valid.
*/
finish() {
this.byteBuffer.writeByte(0x3b); // gif trailer
this.end();
}
/**
* Writes the GIF file header
*/
start() {
this.byteBuffer.writeBytes(GIF_HEADER);
this.started = true;
this.emit();
}
end() {
if (this.readableStreams.length === 0)
return;
this.emit();
for (const stream of this.readableStreams) {
stream.push(null);
}
this.readableStreams = [];
}
emit() {
if (this.readableStreams.length === 0 || this.byteBuffer.length === 0)
return;
const data = this.byteBuffer.toArray();
for (const stream of this.readableStreams) {
stream.push(Buffer.from(data));
}
this.byteBuffer.reset();
}
/**
* Analyzes current frame colors and creates a color map.
*/
analyzePixels() {
const pixels = this.pixels;
const pixelByteCount = pixels.length;
const pixelCount = pixelByteCount / 3;
this.indexedPixels = new Uint8Array(pixelCount);
const quantifier = new NeuQuant_1.NeuQuant(pixels, this.sample);
this.colorPalette = quantifier.getColorMap();
// Map image pixels to new palette:
let k = 0;
for (let j = 0; j < pixelCount; j++) {
const r = pixels[k++] & 0xff;
const g = pixels[k++] & 0xff;
const b = pixels[k++] & 0xff;
const index = quantifier.lookupRGB(r, g, b);
this.usedEntry[index] = true;
this.indexedPixels[j] = index;
}
this.pixels = null;
this.colorDepth = 8;
// Get closest match to transparent color if specified:
if (this.transparent === null)
return;
this.transparentIndex = this.findClosest(this.transparent);
// Ensure that pixels with full transparency in the RGBA image are using
// the selected transparent color index in the indexed image.
for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
if (this.image[pixelIndex * 4 + 3] === 0) {
this.indexedPixels[pixelIndex] = this.transparentIndex;
}
}
}
/**
* Returns index of palette color closest to c.
* @param color The color to compare.
*/
findClosest(color) {
if (this.colorPalette === null)
return -1;
const r = (color & 0xff0000) >> 16;
const g = (color & 0x00ff00) >> 8;
const b = color & 0x0000ff;
let minimumIndex = 0;
let distanceMinimum = 256 * 256 * 256;
const len = this.colorPalette.length;
for (let i = 0; i < len;) {
const index = i / 3;
const dr = r - (this.colorPalette[i++] & 0xff);
const dg = g - (this.colorPalette[i++] & 0xff);
const db = b - (this.colorPalette[i++] & 0xff);
const d = dr * dr + dg * dg + db * db;
if (this.usedEntry[index] && d < distanceMinimum) {
distanceMinimum = d;
minimumIndex = index;
}
}
return minimumIndex;
}
/**
* Updates {@link GifEncoder.pixels} by creating an RGB-formatted {@link Uint8Array} from the RGBA-formatted data.
*/
getImagePixels() {
const w = this.width;
const h = this.height;
this.pixels = new Uint8Array(w * h * 3);
const data = this.image;
for (let i = 0, count = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
const b = i * w * 4 + j * 4;
this.pixels[count++] = data[b];
this.pixels[count++] = data[b + 1];
this.pixels[count++] = data[b + 2];
}
}
}
/**
* Writes the GCE (Graphic Control Extension).
*/
writeGraphicControlExtension() {
this.byteBuffer.writeByte(0x21); // extension introducer
this.byteBuffer.writeByte(0xf9); // GCE label
this.byteBuffer.writeByte(4); // data block size
let transparency;
let dispose;
if (this.transparent === null) {
transparency = 0;
dispose = 0; // dispose = no action
}
else {
transparency = 1;
dispose = 2; // force clear if using transparent color
}
if (this.disposalMode >= 0) {
dispose = this.disposalMode & 7; // user override
}
dispose <<= 2;
// Write GCP's packed fields
const fields = 0 | // XXX0_0000 : Reserved
dispose | // 000X_XX00 : Disposal Method
0 | // 0000_00X0 : User Input Flag
transparency; // 0000_000X : Transparent Color Flag
this.byteBuffer.writeByte(fields);
this.writeShort(this.delay); // delay x 1 / 100 sec
this.byteBuffer.writeByte(this.transparentIndex); // transparent color index
this.byteBuffer.writeByte(0); // block terminator
}
/**
* Writes the ID (Image Descriptor).
*/
writeImageDescriptor() {
this.byteBuffer.writeByte(0x2c); // Image Descriptor block identifier
this.writeShort(0); // Image Left Position
this.writeShort(0); // Image Top Position
this.writeShort(this.width); // Image Width
this.writeShort(this.height); // Image Height
// Write the LCT (Local Color Table):
const fields = this.firstFrame
? 0 // The first frame uses the GCT (Global Color Table)
: 128 | // X000_0000 : Local Color Table Flag = 1
0 | // 0X00_0000 : Interlace Flag = 0
0 | // 00X0_0000 : Sort Flag = 0
0 | // 000X_X000 : Reserved
PALETTE_SIZE; // 0000_0XXX : Size of Local Color Table
this.byteBuffer.writeByte(fields);
}
/**
* Writes the LSD (Logical Screen Descriptor)
*/
writeLogicalScreenDescriptor() {
// logical screen size
this.writeShort(this.width);
this.writeShort(this.height);
// Write the GCT (Global Color Table):
const fields = 128 | // X000_0000 : GCT (Global Color Table) flag = 1
112 | // 0XXX_0000 : Color Resolution = 7
0 | // 0000_X000 : GCT sort flag = 0
0 | // 0000_0X00 : Reserved
PALETTE_SIZE; // 0000_00XX : GCT (Global Color Table) size
this.byteBuffer.writeByte(fields);
this.byteBuffer.writeByte(0x000000); // background color index
this.byteBuffer.writeByte(0); // pixel aspect ratio - assume 1:1
}
/**
* Writes the Netscape application extension to define repeat count.
*/
writeNetscapeExtension() {
// Reference: http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
this.byteBuffer.writeByte(0x21); // Extension
this.byteBuffer.writeByte(0xff); // Application Extension
this.byteBuffer.writeByte(0x0b); // Block Size
this.byteBuffer.writeBytes(NETSCAPE_HEADER); // Application Identifier + Application Authentication Code
this.byteBuffer.writeByte(0x03); // Sub-block data size
this.byteBuffer.writeByte(0x01); // Sub-block ID
this.writeShort(this.repeat); // Loop Count (up to 2 bytes, `0` = repeat forever)
this.byteBuffer.writeByte(0); // Block Terminator
}
/**
* Writes the color table palette.
*/
writePalette() {
this.byteBuffer.writeBytes(this.colorPalette);
this.byteBuffer.writeTimes(0, 3 * 256 - this.colorPalette.length);
}
writeShort(pValue) {
this.byteBuffer.writeByte(pValue & 0xff);
this.byteBuffer.writeByte((pValue >> 8) & 0xff);
}
/**
* Encodes and writes pixel data into {@link GifEncoder.byteBuffer}.
*/
writePixels() {
const enc = new LZWEncoder_1.LZWEncoder(this.width, this.height, this.indexedPixels, this.colorDepth);
enc.encode(this.byteBuffer);
}
}
exports.GifEncoder = GifEncoder;
//# sourceMappingURL=GifEncoder.js.map