Leon4gr45's picture
Include updated package-lock.json (part 8)
3530df7 verified
Raw
History Blame Contribute Delete
21.7 kB
import { Bitmap, Format, JimpClass, Edge } from "@jimp/types";
import { cssColorToHex, scan, scanIterator } from "@jimp/utils";
import { to } from "await-to-js";
import { existsSync, readFile, writeFile } from "@jimp/file-ops";
import mime from "mime/lite.js";
import { composite } from "./utils/composite.js";
import { BlendMode } from "./index.js";
import { attemptExifRotate } from "./utils/image-bitmap.js";
const emptyBitmap: Bitmap = {
data: Buffer.alloc(0),
width: 0,
height: 0,
};
/**
* Prepare a Buffer object from the arrayBuffer.
*/
function bufferFromArrayBuffer(arrayBuffer: ArrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i]!;
}
return buffer;
}
async function detectFileTypeFromBuffer(buffer: Buffer | ArrayBuffer) {
const { fileTypeFromBuffer } = await import("file-type");
return fileTypeFromBuffer(
buffer instanceof ArrayBuffer
? buffer
: new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
);
}
export { getExifOrientation } from "./utils/image-bitmap.js";
export { composite } from "./utils/composite.js";
export * from "./utils/constants.js";
export interface RawImageData {
width: number;
height: number;
data: Buffer | Uint8Array | Uint8ClampedArray | number[];
}
/**
* Instead of loading an image into an instance you can initialize a new Jimp instance with a empty bitmap.
*/
export interface JimpSimpleConstructorOptions {
height: number;
width: number;
/**
* Initialize the image with a color for each pixel
*/
color?: number | string;
}
export type JimpConstructorOptions = Bitmap | JimpSimpleConstructorOptions;
/** Converts a jimp plugin function to a Jimp class method */
type JimpInstanceMethod<ClassInstance, MethodMap, Method> =
Method extends JimpChainableMethod<infer Args>
? (
...args: Args
) => JimpInstanceMethods<ClassInstance, MethodMap> & ClassInstance
: Method extends JimpMethod<infer Args, infer Return>
? (...args: Args) => Return
: never;
/** Converts a Record of jimp plugin functions to a Record of Jimp class methods */
export type JimpInstanceMethods<ClassInstance, MethodMap> = {
[Key in keyof MethodMap]: JimpInstanceMethod<
ClassInstance,
MethodMap,
MethodMap[Key]
>;
};
/** A Jimp instance method that can be chained. */
type JimpChainableMethod<
Args extends any[] = any[],
J extends JimpClass = JimpClass,
> = (img: J, ...args: Args) => J;
/** A Jimp instance method that returns anything. */
type JimpMethod<
Args extends any[] = any[],
ReturnType = any,
J extends JimpClass = JimpClass,
> = (img: J, ...args: Args) => ReturnType;
export interface JimpPlugin {
[key: string]: JimpChainableMethod | JimpMethod;
}
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Constructor<T> = new (...args: any[]) => T;
type JimpFormat<
MimeType extends string = string,
EncodeOptions extends Record<string, any> | undefined = undefined,
DecodeOptions extends Record<string, any> | undefined = undefined,
T extends Format<MimeType, EncodeOptions, DecodeOptions> = Format<
MimeType,
EncodeOptions,
DecodeOptions
>,
> = () => T;
type CreateMimeTypeToExportOptions<T extends Format<string, any>> =
T extends Format<infer M, infer O> ? Record<M, O> : never;
type CreateMimeTypeToDecodeOptions<T extends Format<string, any>> =
T extends Format<infer M, any, infer O> ? Record<M, O> : never;
type GetOptionsForMimeType<Mime extends string, MimeTypeMap> =
MimeTypeMap extends Record<Mime, infer O> ? O : never;
type PathWithExtension<E extends string> = `${string}.${E}`;
type MimeToExtension<M extends string> = `${string}/${M}`;
type CreateExtensionToMimeType<M extends string> =
M extends MimeToExtension<infer E> ? Record<E, M> : never;
type GetMimeTypeForExtension<Mime extends string, MimeTypeMap> =
MimeTypeMap extends Record<Mime, infer M> ? M : never;
/**
* Create a Jimp class that support the given image formats and methods
*/
export function createJimp<
Methods extends JimpPlugin[],
Formats extends JimpFormat[],
>({
plugins: pluginsArg,
formats: formatsArg,
}: {
/** Plugins that add methods to the created Jimp class */
plugins?: Methods;
/** Image formats the Jimp class should support */
formats?: Formats;
} = {}) {
type ExtraMethodMap = JimpInstanceMethods<
InstanceType<typeof CustomJimp>,
UnionToIntersection<Methods[number]>
>;
type SupportedMimeTypes = ReturnType<Formats[number]>["mime"];
type MimeTypeToExportOptions = CreateMimeTypeToExportOptions<
ReturnType<Formats[number]>
>;
type MimeTypeToDecodeOptions = CreateMimeTypeToDecodeOptions<
ReturnType<Formats[number]>
>;
type ExtensionToMimeType = CreateExtensionToMimeType<SupportedMimeTypes>;
const plugins = pluginsArg || [];
const formats = (formatsArg || []).map((format) => format());
const CustomJimp = class Jimp implements JimpClass {
/**
* The bitmap data of the image
*/
bitmap: Bitmap = emptyBitmap;
/** Default color to use for new pixels */
background = 0x00000000;
/** Formats that can be used with Jimp */
formats: Format<any>[] = [];
/** The original MIME type of the image */
mime?: string;
constructor(options: JimpConstructorOptions = emptyBitmap) {
// Add the formats
this.formats = formats;
if ("data" in options) {
this.bitmap = options;
} else {
this.bitmap = {
data: Buffer.alloc(options.width * options.height * 4),
width: options.width,
height: options.height,
};
if (options.color) {
this.background =
typeof options.color === "string"
? cssColorToHex(options.color)
: options.color;
for (let i = 0; i < this.bitmap.data.length; i += 4) {
this.bitmap.data.writeUInt32BE(this.background, i);
}
}
}
// Add the plugins
for (const methods of plugins) {
for (const key in methods) {
(this as any)[key] = (...args: any[]) => {
const result = methods[key]?.(this, ...args);
if (typeof result === "object" && "bitmap" in result) {
this.bitmap = result.bitmap;
return this;
}
return result;
};
}
}
}
/**
* Create a Jimp instance from a URL, a file path, or a Buffer
* @example
* ```ts
* import { Jimp } from "jimp";
*
* // Read from a file path
* const image = await Jimp.read("test/image.png");
*
* // Read from a URL
* const image = await Jimp.read("https://upload.wikimedia.org/wikipedia/commons/0/01/Bot-Test.jpg");
* ```
*/
static async read(
url: string | Buffer | ArrayBuffer,
options?: MimeTypeToDecodeOptions
) {
if (Buffer.isBuffer(url) || url instanceof ArrayBuffer) {
return this.fromBuffer(url);
}
if (existsSync(url)) {
return this.fromBuffer(await readFile(url));
}
const [fetchErr, response] = await to(fetch(url));
if (fetchErr) {
throw new Error(`Could not load Buffer from URL: ${url}`);
}
if (!response.ok) {
throw new Error(`HTTP Status ${response.status} for url ${url}`);
}
const [arrayBufferErr, data] = await to(response.arrayBuffer());
if (arrayBufferErr) {
throw new Error(`Could not load Buffer from ${url}`);
}
const buffer = bufferFromArrayBuffer(data);
return this.fromBuffer(buffer, options);
}
/**
* Create a Jimp instance from a bitmap.
* The difference between this and just using the constructor is that this will
* convert raw image data into the bitmap format that Jimp uses.
*
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = Jimp.fromBitmap({
* data: Buffer.from([
* 0xffffffff, 0xffffffff, 0xffffffff,
* 0xffffffff, 0xffffffff, 0xffffffff,
* 0xffffffff, 0xffffffff, 0xffffffff,
* ]),
* width: 3,
* height: 3,
* });
* ```
*/
static fromBitmap(bitmap: RawImageData) {
let data: Buffer | undefined;
if (bitmap.data instanceof Buffer) {
data = Buffer.from(bitmap.data);
}
if (
bitmap.data instanceof Uint8Array ||
bitmap.data instanceof Uint8ClampedArray
) {
data = Buffer.from(bitmap.data.buffer);
}
if (Array.isArray(bitmap.data)) {
data = Buffer.concat(
bitmap.data.map((hex) =>
Buffer.from(hex.toString(16).padStart(8, "0"), "hex")
)
);
}
if (!data) {
throw new Error("data must be a Buffer");
}
if (
typeof bitmap.height !== "number" ||
typeof bitmap.width !== "number"
) {
throw new Error("bitmap must have width and height");
}
return new CustomJimp({
height: bitmap.height,
width: bitmap.width,
data,
}) as InstanceType<typeof CustomJimp> & ExtraMethodMap;
}
/**
* Parse a bitmap with the loaded image types.
*
* @param buffer Raw image data
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const buffer = await fs.readFile("test/image.png");
* const image = await Jimp.fromBuffer(buffer);
* ```
*/
static async fromBuffer(
buffer: Buffer | ArrayBuffer,
options?: MimeTypeToDecodeOptions
) {
const actualBuffer =
buffer instanceof ArrayBuffer ? bufferFromArrayBuffer(buffer) : buffer;
const mime = await detectFileTypeFromBuffer(actualBuffer);
if (!mime || !mime.mime) {
throw new Error("Could not find MIME for Buffer");
}
const format = formats.find((format) => format.mime === mime.mime);
if (!format || !format.decode) {
throw new Error(`Mime type ${mime.mime} does not support decoding`);
}
const image = new CustomJimp(
await format.decode(actualBuffer, options?.[format.mime])
) as InstanceType<typeof CustomJimp> & ExtraMethodMap;
image.mime = mime.mime;
attemptExifRotate(image, actualBuffer);
return image;
}
/**
* Nicely format Jimp object when sent to the console e.g. console.log(image)
* @returns Pretty printed jimp object
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = await Jimp.read("test/image.png");
*
* console.log(image);
* ```
*/
inspect() {
return (
"<Jimp " +
(this.bitmap === emptyBitmap
? "pending..."
: this.bitmap.width + "x" + this.bitmap.height) +
">"
);
}
/**
* Nicely format Jimp object when converted to a string
* @returns pretty printed
*/
toString() {
return "[object Jimp]";
}
/** Get the width of the image */
get width() {
return this.bitmap.width;
}
/** Get the height of the image */
get height() {
return this.bitmap.height;
}
/**
* Converts the Jimp instance to an image buffer
* @param mime The mime type to export to
* @param options The options to use when exporting
* @example
* ```ts
* import { Jimp } from "jimp";
* import { promises as fs } from "fs";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* await image.getBuffer("image/jpeg", {
* quality: 50,
* });
* ```
*/
async getBuffer<
ProvidedMimeType extends SupportedMimeTypes,
Options extends GetOptionsForMimeType<
ProvidedMimeType,
MimeTypeToExportOptions
>,
>(mime: ProvidedMimeType, options?: Options) {
const format = this.formats.find((format) => format.mime === mime);
if (!format || !format.encode) {
throw new Error(`Unsupported MIME type: ${mime}`);
}
let outputImage: Jimp;
if (format.hasAlpha) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
outputImage = this;
} else {
outputImage = new CustomJimp({
width: this.bitmap.width,
height: this.bitmap.height,
color: this.background,
});
composite(outputImage, this);
}
return format.encode(outputImage.bitmap, options);
}
/**
* Converts the image to a base 64 string
*
* @param mime The mime type to export to
* @param options The options to use when exporting
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = Jimp.fromBuffer(Buffer.from([
* 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
* 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
* 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
* ]));
*
* const base64 = image.getBase64("image/jpeg", {
* quality: 50,
* });
* ```
*/
async getBase64<
ProvidedMimeType extends SupportedMimeTypes,
Options extends GetOptionsForMimeType<
ProvidedMimeType,
MimeTypeToExportOptions
>,
>(mime: ProvidedMimeType, options?: Options) {
const data = await this.getBuffer(mime, options);
return "data:" + mime + ";base64," + data.toString("base64");
}
/**
* Write the image to a file
* @param path the path to write the image to
* @param options the options to use when writing the image
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = Jimp.fromBuffer(Buffer.from([
* 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
* 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
* 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
* ]));
*
* await image.write("test/output.png");
* ```
*/
async write<
Extension extends string,
Mime extends GetMimeTypeForExtension<Extension, ExtensionToMimeType>,
Options extends GetOptionsForMimeType<Mime, MimeTypeToExportOptions>,
>(path: PathWithExtension<Extension>, options?: Options) {
const mimeType = mime.getType(path);
await writeFile(
path,
await this.getBuffer(mimeType as SupportedMimeTypes, options)
);
}
/**
* Clone the image into a new Jimp instance.
* @param this
* @returns A new Jimp instance
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* const clone = image.clone();
* ```
*/
clone<S extends typeof CustomJimp>(this: S) {
return new CustomJimp({
...(this as any).bitmap,
data: Buffer.from((this as any).bitmap.data),
}) as unknown as S;
}
/**
* Returns the offset of a pixel in the bitmap buffer
* @param x the x coordinate
* @param y the y coordinate
* @param edgeHandling (optional) define how to sum pixels from outside the border
* @returns the index of the pixel or -1 if not found
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* image.getPixelIndex(1, 1); // 2
* ```
*/
getPixelIndex(x: number, y: number, edgeHandling?: Edge) {
let xi;
let yi;
if (!edgeHandling) {
edgeHandling = Edge.EXTEND;
}
if (typeof x !== "number" || typeof y !== "number") {
throw new Error("x and y must be numbers");
}
// round input
x = Math.round(x);
y = Math.round(y);
xi = x;
yi = y;
if (edgeHandling === Edge.EXTEND) {
if (x < 0) xi = 0;
if (x >= this.bitmap.width) xi = this.bitmap.width - 1;
if (y < 0) yi = 0;
if (y >= this.bitmap.height) yi = this.bitmap.height - 1;
}
if (edgeHandling === Edge.WRAP) {
if (x < 0) {
xi = this.bitmap.width + x;
}
if (x >= this.bitmap.width) {
xi = x % this.bitmap.width;
}
if (y < 0) {
yi = this.bitmap.height + y;
}
if (y >= this.bitmap.height) {
yi = y % this.bitmap.height;
}
}
let i = (this.bitmap.width * yi + xi) << 2;
// if out of bounds index is -1
if (xi < 0 || xi >= this.bitmap.width) {
i = -1;
}
if (yi < 0 || yi >= this.bitmap.height) {
i = -1;
}
return i;
}
/**
* Returns the hex color value of a pixel
* @param x the x coordinate
* @param y the y coordinate
* @returns the color of the pixel
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* image.getPixelColor(1, 1); // 0xffffffff
* ```
*/
getPixelColor(x: number, y: number) {
if (typeof x !== "number" || typeof y !== "number") {
throw new Error("x and y must be numbers");
}
const idx = this.getPixelIndex(x, y);
return this.bitmap.data.readUInt32BE(idx);
}
/**
* Sets the hex colour value of a pixel
*
* @param hex color to set
* @param x the x coordinate
* @param y the y coordinate
*
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* image.setPixelColor(0xff0000ff, 0, 0);
* ```
*/
setPixelColor(hex: number, x: number, y: number) {
if (
typeof hex !== "number" ||
typeof x !== "number" ||
typeof y !== "number"
) {
throw new Error("hex, x and y must be numbers");
}
const idx = this.getPixelIndex(x, y);
this.bitmap.data.writeUInt32BE(hex, idx);
return this;
}
/**
* Determine if the image contains opaque pixels.
*
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffaa });
* const image2 = new Jimp({ width: 3, height: 3, color: 0xff0000ff });
*
* image.hasAlpha(); // false
* image2.hasAlpha(); // true
* ```
*/
hasAlpha() {
const { width, height, data } = this.bitmap;
const byteLen = (width * height) << 2;
for (let idx = 3; idx < byteLen; idx += 4) {
if (data[idx] !== 0xff) {
return true;
}
}
return false;
}
/**
* Composites a source image over to this image respecting alpha channels
* @param src the source Jimp instance
* @param x the x position to blit the image
* @param y the y position to blit the image
* @param options determine what mode to use
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 10, height: 10, color: 0xffffffff });
* const image2 = new Jimp({ width: 3, height: 3, color: 0xff0000ff });
*
* image.composite(image2, 3, 3);
* ```
*/
composite<I extends typeof this>(
src: I,
x = 0,
y = 0,
options: {
mode?: BlendMode;
opacitySource?: number;
opacityDest?: number;
} = {}
) {
return composite(this, src, x, y, options);
}
/**
* Scan through the image and call the callback for each pixel
*
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* image.scan((x, y, idx) => {
* // do something with the pixel
* });
*
* // Or scan through just a region
* image.scan(0, 0, 2, 2, (x, y, idx) => {
* // do something with the pixel
* });
* ```
*/
scan(f: (x: number, y: number, idx: number) => any): this;
scan(
x: number,
y: number,
w: number,
h: number,
cb: (x: number, y: number, idx: number) => any
): this;
scan(
x: number | ((x: number, y: number, idx: number) => any),
y?: number,
w?: number,
h?: number,
f?: (x: number, y: number, idx: number) => any
): this {
return scan(this, x as any, y as any, w as any, h as any, f as any);
}
/**
* Iterate scan through a region of the bitmap
* @param x the x coordinate to begin the scan at
* @param y the y coordinate to begin the scan at
* @param w the width of the scan region
* @param h the height of the scan region
* @example
* ```ts
* import { Jimp } from "jimp";
*
* const image = new Jimp({ width: 3, height: 3, color: 0xffffffff });
*
* for (const { x, y, idx, image } of j.scanIterator()) {
* // do something with the pixel
* }
* ```
*/
scanIterator(x = 0, y = 0, w = this.bitmap.width, h = this.bitmap.height) {
if (typeof x !== "number" || typeof y !== "number") {
throw new Error("x and y must be numbers");
}
if (typeof w !== "number" || typeof h !== "number") {
throw new Error("w and h must be numbers");
}
return scanIterator(this, x, y, w, h);
}
};
return CustomJimp as typeof CustomJimp & Constructor<ExtraMethodMap>;
}