node-libpng
Version:
Unofficial bindings for node to libpng.
359 lines • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PngImage = exports.convertNativeBackgroundColor = exports.convertNativeTime = exports.InterlaceType = void 0;
const encode_1 = require("./encode");
const colors_1 = require("./colors");
const xy_1 = require("./xy");
const rect_1 = require("./rect");
const color_type_1 = require("./color-type");
const native_1 = require("./native");
/**
* The interlace type from libpng.
*/
var InterlaceType;
(function (InterlaceType) {
/**
* Libpng interlace type `PNG_INTERLACE_NONE` (0).
*/
InterlaceType["NONE"] = "none";
/**
* Libpng interlace type `PNG_INTERLACE_ADAM7` (1).
*/
InterlaceType["ADAM7"] = "adam7";
/**
* Interlace type parsing failed.
*/
InterlaceType["UNKNOWN"] = "unknown";
})(InterlaceType = exports.InterlaceType || (exports.InterlaceType = {}));
/**
* Converts the native time from the libpng bindings into a javascript `Date` object.
*
* @param nativeTime The time as returned by the bindings.
*
* @return The time converted to a javascript `Date` object or `undefined` if the time was
* not set in the PNG's header.
*/
function convertNativeTime(nativeTime) {
if (!nativeTime) {
return;
}
const { year, month, day, hour, minute, second } = nativeTime;
return new Date(year, month - 1, day, hour, minute, second);
}
exports.convertNativeTime = convertNativeTime;
/**
* Converts the background color from the libpng bindings into a color.
*
* @param nativeBackgroundColor The background color as returned by the native bindings.
* @param colorType The color type of the image of which the background color should be converted.
*
* @return The converted background color in `ColorRGB`, `ColorPalette` or `ColorGrayScale` format
* or undefined if the background color was not set in the PNG.
*/
function convertNativeBackgroundColor(nativeBackgroundColor, colorType) {
if (!nativeBackgroundColor) {
return;
}
switch (colorType) {
case color_type_1.ColorType.GRAY_SCALE:
case color_type_1.ColorType.GRAY_SCALE_ALPHA:
return colors_1.colorGrayScale(nativeBackgroundColor.gray);
case color_type_1.ColorType.PALETTE:
return colors_1.colorPalette(nativeBackgroundColor.index);
case color_type_1.ColorType.RGB:
case color_type_1.ColorType.RGBA:
const { red, green, blue } = nativeBackgroundColor;
return colors_1.colorRGB(red, green, blue);
default:
return undefined;
}
}
exports.convertNativeBackgroundColor = convertNativeBackgroundColor;
/**
* Converts a native palette as returned by the bindings into a Map.
*
* @param nativePalette The native palette which should be converted.
*
* @return The palette as a `Palette` (Javascript Map with the key being the palette index and
* the value being a color.
*/
function convertNativePalette(nativePalette) {
if (!nativePalette) {
return;
}
return nativePalette.reduce((result, current, index) => {
result.set(index, colors_1.colorRGB(current.red, current.green, current.blue));
return result;
}, new Map());
}
/**
* Decodes and wraps a PNG image. Will call the native bindings under the hood and provides
* a high-level access to read- and write operations on the image.
*/
class PngImage {
constructor(buffer) {
if (!Buffer.isBuffer(buffer)) {
throw new Error("Error decoding PNG. Input is not a buffer.");
}
const nativePng = new native_1.__native_PngImage(buffer);
this.bitDepth = nativePng.bitDepth;
this.channels = nativePng.channels;
this.colorType = nativePng.colorType;
this.height = nativePng.height;
this.width = nativePng.width;
this.interlaceType = nativePng.interlaceType;
this.rowBytes = nativePng.rowBytes;
this.offsetX = nativePng.offsetX;
this.offsetY = nativePng.offsetY;
this.pixelsPerMeterX = nativePng.pixelsPerMeterX;
this.pixelsPerMeterY = nativePng.pixelsPerMeterY;
this.data = nativePng.data;
this.palette = convertNativePalette(nativePng.palette);
this.gamma = nativePng.gamma;
this.time = convertNativeTime(nativePng.time);
this.backgroundColor = convertNativeBackgroundColor(nativePng.backgroundColor, this.colorType);
}
/**
* Will be `true` if the image's color type has an alpha channel and `false` otherwise.
*/
get alpha() {
switch (this.colorType) {
case color_type_1.ColorType.RGBA:
case color_type_1.ColorType.GRAY_SCALE_ALPHA:
return true;
default:
return false;
}
}
/**
* Returns the amount of bytes per pixel (depending on the color type) for the image.
*/
get bytesPerPixel() {
const bytesPerColor = Math.ceil(this.bitDepth / 8);
switch (this.colorType) {
case color_type_1.ColorType.GRAY_SCALE_ALPHA:
return 2 * bytesPerColor;
case color_type_1.ColorType.RGBA:
return 4 * bytesPerColor;
case color_type_1.ColorType.GRAY_SCALE:
case color_type_1.ColorType.PALETTE:
return 1 * bytesPerColor;
case color_type_1.ColorType.RGB:
return 3 * bytesPerColor;
default:
return undefined;
}
}
/**
* Convert a set of coordinates to index in the buffer.
*/
toIndex(x, y) {
return (x + y * this.width) * this.bytesPerPixel;
}
/**
* Convert an index in the buffer to a set of coordinates.
*/
toXY(index) {
const colorIndex = index / this.bytesPerPixel;
const x = Math.floor(colorIndex % this.width);
const y = Math.floor(colorIndex / this.width);
return xy_1.xy(x, y);
}
/**
* Retrieves the color in the image's color format at the specified position.
*
* @param x The x position of the pixel in the image of which to retrieve the color.
* @param y The y position of the pixel in the image of which to retrieve the color.
*
* @return The color at the given pixel in the image's color format.
*/
at(x, y) {
const index = this.toIndex(x, y);
if (index > this.data.length || index < 0) {
throw new Error("Index out of range when reading pixel from image.");
}
const { data } = this;
switch (this.colorType) {
case color_type_1.ColorType.GRAY_SCALE:
return colors_1.colorGrayScale(data[index]);
case color_type_1.ColorType.GRAY_SCALE_ALPHA:
return colors_1.colorGrayScaleAlpha(data[index], data[index + 1]);
case color_type_1.ColorType.PALETTE:
return colors_1.colorPalette(data[index]);
case color_type_1.ColorType.RGB:
return colors_1.colorRGB(data[index], data[index + 1], data[index + 2]);
case color_type_1.ColorType.RGBA:
return colors_1.colorRGBA(data[index], data[index + 1], data[index + 2], data[index + 3]);
default:
return undefined;
}
}
/**
* Retrieves the color in rgba format, converting from the image's color format.
* This will automatically convert from indexed or grayscale images to rgba. If
* the image's color format doesn't provide an alpha channel, `255` is returned as alpha.
*
* @param x The x position of the pixel in the image of which to retrieve the color.
* @param y The y position of the pixel in the image of which to retrieve the color.
*
* @return The color at the given pixel in rgba format.
*/
rgbaAt(x, y) {
return colors_1.convertToRGBA(this.at(x, y), this.palette);
}
/**
* A convenience wrapper around `resizeCanvas`. Crops the image to a specified sub-rectangle.
* Modifies this image and the underlying buffer.
*
* @see PngImage.resizeCanvas
* @see ResizeCanvasArguments
*
* @param clip A sub-rectangle which should be cropped out of the image.
*/
crop(clip) {
this.resizeCanvas({
clip,
dimensions: clip.dimensions,
});
}
/**
* Resizes the canvas with while optionally adding padding and cropping regions from the image.
* Modifies this image and the underlying buffer.
*
* @see ResizeCanvasArguments
*/
resizeCanvas({ dimensions, offset, clip, fillColor }) {
const safeOffset = typeof offset === "undefined" ? xy_1.xy(0, 0) : offset;
const safeDimensions = typeof dimensions === "undefined" ? xy_1.xy(this.width, this.height) : dimensions;
const safeClip = typeof clip === "undefined" ? rect_1.rect(0, 0, this.width, this.height) : clip;
const safeFillColor = typeof fillColor === "undefined" ? colors_1.defaultBackgroundColor(this.colorType) : fillColor;
if (typeof fillColor !== "undefined" && !colors_1.colorTypeToColorChecker(this.colorType)(fillColor)) {
throw new Error("Fill color must be of same color type as image.");
}
if (safeDimensions.x < 1 || safeDimensions.y < 1) {
throw new Error("Invalid dimensions.");
}
if (safeOffset.x + safeClip.width > safeDimensions.x || safeOffset.y + safeClip.height > safeDimensions.y) {
throw new Error("Dimensions and offset are out of range for new dimensions.");
}
if (safeClip.x + safeClip.width > this.width || safeClip.y + safeClip.height > this.height) {
throw new Error("Provided clipping rectangle is out of range for current dimensions.");
}
if (safeClip.x < 0 || safeClip.y < 0 || safeClip.width < 1 || safeClip.height < 1) {
throw new Error("Invalid clipping rectangle.");
}
if (safeOffset.x < 0 || safeOffset.y < 0) {
throw new Error("Invalid offset.");
}
const newBuffer = native_1.__native_resize(this.data, this.width, this.height, ...safeDimensions, ...safeOffset, ...safeClip, safeFillColor, this.bitDepth);
this.data = newBuffer;
this.width = safeDimensions.x;
this.height = safeDimensions.y;
}
/**
* Copies the specified rectangle from the other image (or the whole other image if rectangle is omitted)
* into this image at the current offset (or to the top left if the offset is omitted).
* Modifies this image and the underlying buffer.
*
* @param other The other image which should be copied into this image.
* @param offset The target position in this image to which the other image should be copied.
* @param source The clipping rectangle of the other image which should be copied.
*/
copyFrom(other, offset, source) {
const safeOffset = typeof offset === "undefined" ? xy_1.xy(0, 0) : offset;
const safeSource = typeof source === "undefined" ? rect_1.rect(0, 0, other.width, other.height) : source;
if (safeSource.x < 0 || safeSource.y < 0 || safeSource.width < 1 || safeSource.height < 1) {
throw new Error("Invalid source rectangle.");
}
if (safeOffset.x < 0 || safeOffset.y < 0) {
throw new Error("Invalid offset.");
}
if (other.colorType !== this.colorType) {
throw new Error("Cannot copy from image with different color type.");
}
if (safeSource.x + safeSource.width > other.width || safeSource.y + safeSource.height > other.height) {
throw new Error("Provided source rectangle is out of range for source image.");
}
if (safeSource.width + safeOffset.x > this.width || safeSource.height + safeOffset.y > this.height) {
throw new Error("Provided source rectangle and offset are out of range for this image.");
}
native_1.__native_copy(other.data, this.data, other.width, other.height, this.width, this.height, ...safeSource, ...safeOffset);
}
/**
* Fill an area of the image with a specific color.
* This will change the underlying data of this image. The change is in-place.
*
* @param color The color with which the area should be filled.
* @param area The area to fill. Can be omitted to fill the whole image.
*/
fill(color, area) {
const safeArea = typeof area === "undefined" ? rect_1.rect(0, 0, this.width, this.height) : area;
if (typeof color === "undefined") {
throw new Error("Fill color must be specified.");
}
if (!colors_1.colorTypeToColorChecker(this.colorType)(color)) {
throw new Error("Fill color must be of same color type as image.");
}
const notInside = safeArea.x < 0 ||
safeArea.y < 0 ||
safeArea.x + safeArea.width > this.width ||
safeArea.y + safeArea.height > this.height;
if (notInside) {
throw new Error("Provided area is out of range for this image.");
}
native_1.__native_fill(this.data, this.width, this.height, ...safeArea, color, this.bitDepth);
}
/**
* Set the color of one specific pixel on this image.
* This will change the underlying data of this image. The change is in-place.
*
* @param color The color with which the area should be filled.
* @param position The position of the pixel to colorize.
*/
set(color, position) {
this.fill(color, rect_1.rect(position.x, position.y, 1, 1));
}
/**
* Will encode this image to a PNG buffer.
*/
encode() {
const { alpha, width, height } = this;
if (this.colorType !== color_type_1.ColorType.RGB && this.colorType !== color_type_1.ColorType.RGBA) {
throw new Error("Can only encode images with RGB or RGBA color type.");
}
return encode_1.encode(this.data, { width, height });
}
/**
* Will encode this image and write it to the file at the specified path.
*
* @param path Path to the file to which the encoded PNG should be written.
* @param callback An optional callback to use instead of the Promise API.
*
* @see writePngFile
*
* @return A Promise which resolves once the file is written or `undefined` if a callback was specified.
*/
write(path, callback) {
const { alpha, width, height } = this;
if (this.colorType !== color_type_1.ColorType.RGB && this.colorType !== color_type_1.ColorType.RGBA) {
throw new Error("Can only encode images with RGB or RGBA color type.");
}
return encode_1.writePngFile(path, this.data, { width, height }, callback);
}
/**
* Will encode this image and write it to the file at the specified path synchroneously.
*
* @param path Path to the file to which the encoded PNG should be written.
*
* @see writePngFileSync
*/
writeSync(path) {
const { alpha, width, height } = this;
if (this.colorType !== color_type_1.ColorType.RGB && this.colorType !== color_type_1.ColorType.RGBA) {
throw new Error("Can only encode images with RGB or RGBA color type.");
}
return encode_1.writePngFileSync(path, this.data, { width, height });
}
}
exports.PngImage = PngImage;
//# sourceMappingURL=png-image.js.map