diff --git a/TODO.md b/TODO.md index d8572061..009636a9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,7 @@ -# TODO - -- [ ] Object APIs - ## Guides - [ ] Getting Started - [ ] Custom Jimp - [ ] Authoring Plugins -- [ ] Using in Browser +- [] Using in Browser - [ ] Worker diff --git a/packages/core/src/utils/image-bitmap.ts b/packages/core/src/utils/image-bitmap.ts index 268b4614..49203ea2 100644 --- a/packages/core/src/utils/image-bitmap.ts +++ b/packages/core/src/utils/image-bitmap.ts @@ -141,7 +141,6 @@ function exifRotate(img: I) { if (transformation) { transformBitmap(img, newWidth, newHeight, transformation); - console.log("done", img); } } diff --git a/packages/diff/src/index.ts b/packages/diff/src/index.ts index afdf10da..8fa01925 100644 --- a/packages/diff/src/index.ts +++ b/packages/diff/src/index.ts @@ -31,10 +31,16 @@ export function diff(img1: I, img2: I, threshold = 0.1) { if (bmp1.width !== bmp2.width || bmp1.height !== bmp2.height) { if (bmp1.width * bmp1.height > bmp2.width * bmp2.height) { // img1 is bigger - img1 = methods.resize(clone(img1), bmp2.width, bmp2.height); + img1 = methods.resize(clone(img1), { + w: bmp2.width, + h: bmp2.height, + }); } else { // img2 is bigger (or they are the same in area) - img2 = methods.resize(clone(img2), bmp1.width, bmp1.height); + img2 = methods.resize(clone(img2), { + w: bmp1.width, + h: bmp1.height, + }); } } diff --git a/packages/jimp/src/callbacks.test.ts b/packages/jimp/src/callbacks.test.ts index bfa372d8..92488b71 100644 --- a/packages/jimp/src/callbacks.test.ts +++ b/packages/jimp/src/callbacks.test.ts @@ -2,21 +2,30 @@ import { expect, test, describe } from "vitest"; import { makeTestImage } from "@jimp/test-utils"; import { HorizontalAlign, Jimp, VerticalAlign } from "./index.js"; +import { CropOptions } from "@jimp/plugin-crop"; +import { FlipOptions } from "@jimp/plugin-flip"; +import { ResizeOptions, ScaleToFitOptions } from "@jimp/plugin-resize"; +import { CoverOptions } from "@jimp/plugin-cover"; +import { ContainOptions } from "@jimp/plugin-contain"; describe("Callbacks", () => { const targetImg = Jimp.fromBitmap(makeTestImage("▴▸▾", "◆▪▰", "▵▹▿")); const miniImg = Jimp.fromBitmap(makeTestImage("□▥", "▥■")); const operations = { - crop: { args: [1, 1, 2, 1] }, + crop: { + args: [{ x: 1, y: 1, w: 2, h: 1 } as CropOptions], + }, invert: { args: [] }, - flip: { args: [true, false] }, + flip: { + args: [{ horizontal: true, vertical: false } as FlipOptions], + }, gaussian: { args: [1] }, blur: { args: [1] }, greyscale: { args: [] }, sepia: { args: [] }, opacity: { args: [0.5] }, - resize: { args: [2, 2] }, + resize: { args: [{ w: 2, h: 2 } as ResizeOptions] }, scale: { args: [0.5] }, brightness: { args: [0.5] }, contrast: { args: [0.75] }, @@ -24,14 +33,26 @@ describe("Callbacks", () => { dither: { args: [] }, // background: { args: [0xffffffff] }, cover: { - args: [3, 2, HorizontalAlign.LEFT | VerticalAlign.TOP], + args: [ + { + w: 3, + h: 2, + align: HorizontalAlign.LEFT | VerticalAlign.TOP, + } as CoverOptions, + ], }, contain: { - args: [3, 2, HorizontalAlign.LEFT | VerticalAlign.TOP], + args: [ + { + w: 3, + h: 2, + align: HorizontalAlign.LEFT | VerticalAlign.TOP, + } as ContainOptions, + ], }, opaque: { args: [] }, fade: { args: [0.5] }, - scaleToFit: { args: [3, 2] }, + scaleToFit: { args: [{ w: 3, h: 2 } as ScaleToFitOptions] }, blit: { args: [{ src: miniImg }] }, composite: { args: [miniImg, 0, 0] }, }; diff --git a/packages/jimp/src/index.ts b/packages/jimp/src/index.ts index 88dbd89d..93a44c8c 100644 --- a/packages/jimp/src/index.ts +++ b/packages/jimp/src/index.ts @@ -63,7 +63,7 @@ import { createJimp } from "@jimp/core"; * then write it back to a file. * * ```ts - * import { Jimp, AutoSize } from "jimp"; + * import { Jimp } from "jimp"; * import { promises as fs } from "fs"; * * const image = await Jimp.read("test/image.png"); @@ -143,7 +143,7 @@ export type { } from "@jimp/plugin-color"; export type { CircleOptions } from "@jimp/plugin-circle"; export type { AutocropOptions } from "@jimp/plugin-crop"; -export { AutoSize, ResizeStrategy } from "@jimp/plugin-resize"; +export { ResizeStrategy } from "@jimp/plugin-resize"; export type { ThresholdOptions } from "@jimp/plugin-threshold"; export { distance, compareHashes } from "@jimp/plugin-hash"; export type { JPEGOptions } from "@jimp/js-jpeg"; diff --git a/packages/types/package.json b/packages/types/package.json index 0ddf4b32..222875b0 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -42,5 +42,8 @@ }, "main": "./dist/commonjs/index.js", "types": "./dist/commonjs/index.d.ts", - "type": "module" + "type": "module", + "dependencies": { + "zod": "^3.22.4" + } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index cc64e872..ab770980 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export enum Edge { EXTEND = 1, WRAP = 2, @@ -33,6 +35,14 @@ export interface RGBAColor { a: number; } +export const JimpClassSchema = z.object({ + bitmap: z.object({ + data: z.instanceof(Buffer), + width: z.number(), + height: z.number(), + }), +}); + export interface JimpClass { background: number; bitmap: Bitmap; diff --git a/plugins/plugin-blit/package.json b/plugins/plugin-blit/package.json index ef4bac7a..f77c6522 100644 --- a/plugins/plugin-blit/package.json +++ b/plugins/plugin-blit/package.json @@ -13,23 +13,24 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { + "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "@jimp/types": "workspace:*" + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/js-jpeg": "workspace:*", - "@jimp/js-png": "workspace:*", - "@jimp/core": "workspace:*", - "@jimp/test-utils": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/core": "workspace:*", + "@jimp/js-jpeg": "workspace:*", + "@jimp/js-png": "workspace:*", + "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exports": { diff --git a/plugins/plugin-blit/src/index.test.ts b/plugins/plugin-blit/src/index.test.ts index 0e218847..6def15c7 100644 --- a/plugins/plugin-blit/src/index.test.ts +++ b/plugins/plugin-blit/src/index.test.ts @@ -33,7 +33,7 @@ describe("Blit over image", function () { )); test("blit on top, with no crop", () => { - expect(targetImg.clone().blit({ src: srcImg })).toMatchSnapshot(); + expect(targetImg.clone().blit(srcImg)).toMatchSnapshot(); }); test("blit on middle, with no crop", () => { diff --git a/plugins/plugin-blit/src/index.ts b/plugins/plugin-blit/src/index.ts index bb6487d7..8559d3aa 100644 --- a/plugins/plugin-blit/src/index.ts +++ b/plugins/plugin-blit/src/index.ts @@ -1,22 +1,28 @@ -import { JimpClass } from "@jimp/types"; +import { JimpClass, JimpClassSchema } from "@jimp/types"; import { limit255, scan } from "@jimp/utils"; +import { z } from "zod"; -export interface BlitOptions { - /** This image to blit on to the current image */ - src: I; +const BlitOptionsSchemaComplex = z.object({ + src: JimpClassSchema, /** the x position to blit the image */ - x?: number; + x: z.number().optional(), /** the y position to blit the image */ - y?: number; + y: z.number().optional(), /** the x position from which to crop the source image */ - srcX?: number; + srcX: z.number().optional(), /** the y position from which to crop the source image */ - srcY?: number; + srcY: z.number().optional(), /** the width to which to crop the source image */ - srcW?: number; + srcW: z.number().optional(), /** the height to which to crop the source image */ - srcH?: number; -} + srcH: z.number().optional(), +}); + +type BlitOptionsComplex = z.infer; + +const BlitOptionsSchema = z.union([JimpClassSchema, BlitOptionsSchemaComplex]); + +export type BlitOptions = z.infer; export const methods = { /** @@ -34,7 +40,8 @@ export const methods = { * image.blit(parrot, x, y); * ``` */ - blit(image: I, options: BlitOptions) { + blit(image: I, options: BlitOptions) { + const parsed = BlitOptionsSchema.parse(options); let { src, x = 0, @@ -43,7 +50,7 @@ export const methods = { srcY = 0, srcW = src.bitmap.width, srcH = src.bitmap.height, - } = options; + } = "bitmap" in parsed ? ({ src: parsed } as BlitOptionsComplex) : parsed; if (!("bitmap" in src)) { throw new Error("The source must be a Jimp image"); diff --git a/plugins/plugin-blur/package.json b/plugins/plugin-blur/package.json index af8dcc92..17581bc1 100644 --- a/plugins/plugin-blur/package.json +++ b/plugins/plugin-blur/package.json @@ -18,12 +18,12 @@ "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-circle/package.json b/plugins/plugin-circle/package.json index c6964acd..9b1dedd4 100644 --- a/plugins/plugin-circle/package.json +++ b/plugins/plugin-circle/package.json @@ -12,20 +12,21 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { - "@jimp/types": "workspace:*" + "@jimp/types": "workspace:*", + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/core": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/core": "workspace:*", "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-circle/src/index.ts b/plugins/plugin-circle/src/index.ts index cbb9b944..b83edde4 100644 --- a/plugins/plugin-circle/src/index.ts +++ b/plugins/plugin-circle/src/index.ts @@ -1,13 +1,16 @@ import { JimpClass } from "@jimp/types"; +import { z } from "zod"; -export interface CircleOptions { - /** the x position to draw the image */ - x?: number; - /** the y position to draw the image */ - y?: number; +const CircleOptionsSchema = z.object({ + /** the x position to draw the circle */ + x: z.number().optional(), + /** the y position to draw the circle */ + y: z.number().optional(), /** the radius of the circle */ - radius?: number; -} + radius: z.number().min(0).optional(), +}); + +export type CircleOptions = z.infer; export const methods = { /** @@ -24,15 +27,16 @@ export const methods = { * ``` */ circle(image: I, options: CircleOptions = {}) { + const parsed = CircleOptionsSchema.parse(options); const radius = - options.radius || + parsed.radius || (image.bitmap.width > image.bitmap.height ? image.bitmap.height : image.bitmap.width) / 2; const center = { - x: typeof options.x === "number" ? options.x : image.bitmap.width / 2, - y: typeof options.y === "number" ? options.y : image.bitmap.height / 2, + x: typeof parsed.x === "number" ? parsed.x : image.bitmap.width / 2, + y: typeof parsed.y === "number" ? parsed.y : image.bitmap.height / 2, }; image.scan((x, y, idx) => { diff --git a/plugins/plugin-color/package.json b/plugins/plugin-color/package.json index 23a22c2d..ea520cb5 100644 --- a/plugins/plugin-color/package.json +++ b/plugins/plugin-color/package.json @@ -19,18 +19,19 @@ "@jimp/js-png": "workspace:*", "@jimp/test-utils": "workspace:*", "@types/tinycolor2": "^1.4.6", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "dependencies": { "@jimp/core": "workspace:*", "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "tinycolor2": "^1.6.0" + "tinycolor2": "^1.6.0", + "zod": "^3.22.4" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-color/src/convolution.test.ts b/plugins/plugin-color/src/convolution.test.ts index 250ee457..dadba3af 100644 --- a/plugins/plugin-color/src/convolution.test.ts +++ b/plugins/plugin-color/src/convolution.test.ts @@ -48,12 +48,24 @@ describe("Convolution", function () { }); test("3x3 sharp matrix on EDGE_WRAP", () => { - expect(imgMid.clone().convolution(sharpM, Edge.WRAP)).toMatchSnapshot(); - expect(imgTopLeft.clone().convolution(sharpM, Edge.WRAP)).toMatchSnapshot(); + expect( + imgMid.clone().convolution({ kernel: sharpM, edgeHandling: Edge.WRAP }) + ).toMatchSnapshot(); + expect( + imgTopLeft + .clone() + .convolution({ kernel: sharpM, edgeHandling: Edge.WRAP }) + ).toMatchSnapshot(); }); test("3x3 sharp matrix on EDGE_CROP", () => { - expect(imgMid.clone().convolution(sharpM, Edge.CROP)).toMatchSnapshot(); - expect(imgTopLeft.clone().convolution(sharpM, Edge.CROP)).toMatchSnapshot(); + expect( + imgMid.clone().convolution({ kernel: sharpM, edgeHandling: Edge.CROP }) + ).toMatchSnapshot(); + expect( + imgTopLeft + .clone() + .convolution({ kernel: sharpM, edgeHandling: Edge.CROP }) + ).toMatchSnapshot(); }); }); diff --git a/plugins/plugin-color/src/index.ts b/plugins/plugin-color/src/index.ts index add62264..3ea47564 100644 --- a/plugins/plugin-color/src/index.ts +++ b/plugins/plugin-color/src/index.ts @@ -1,6 +1,62 @@ import tinyColor from "tinycolor2"; import { clone, limit255, scan } from "@jimp/utils"; import { Edge, JimpClass, RGBColor } from "@jimp/types"; +import { z } from "zod"; + +const ConvolutionMatrixSchema = z.array(z.number()).min(1).array(); +const ConvolutionComplexOptionsSchema = z.object({ + /** a matrix to weight the neighbors sum */ + kernel: ConvolutionMatrixSchema, + /**define how to sum pixels from outside the border */ + edgeHandling: z.nativeEnum(Edge).optional(), +}); +const ConvolutionOptionsSchema = z.union([ + ConvolutionMatrixSchema, + ConvolutionComplexOptionsSchema, +]); + +type ConvolutionOptions = z.infer; + +const ConvoluteComplexOptionsSchema = z.object({ + /** the convolution kernel */ + kernel: ConvolutionMatrixSchema, + /** the x position of the region to apply convolution to */ + x: z.number().optional(), + /** the y position of the region to apply convolution to */ + y: z.number().optional(), + /** the width of the region to apply convolution to */ + w: z.number().optional(), + /** the height of the region to apply convolution to */ + h: z.number().optional(), +}); +const ConvoluteOptionsSchema = z.union([ + ConvolutionMatrixSchema, + ConvoluteComplexOptionsSchema, +]); + +type ConvoluteComplexOptions = z.infer; +type ConvoluteOptions = z.infer; + +const PixelateSize = z.number().min(1).max(Infinity); +const PixelateComplexOptionsSchema = z.object({ + /** the size of the pixels */ + size: PixelateSize, + /** the x position of the region to pixelate */ + x: z.number().optional(), + /** the y position of the region to pixelate */ + y: z.number().optional(), + /** the width of the region to pixelate */ + w: z.number().optional(), + /** the height of the region to pixelate */ + h: z.number().optional(), +}); +const PixelateOptionsSchema = z.union([ + PixelateSize, + PixelateComplexOptionsSchema, +]); + +type PixelateComplexOptions = z.infer; +type PixelateOptions = z.infer; function applyKernel( image: JimpClass, @@ -32,97 +88,123 @@ function mix(clr: RGBColor, clr2: RGBColor, p = 50) { }; } -export interface HueAction { - apply: "hue"; - params: [number]; -} +const HueActionSchema = z.object({ + apply: z.literal("hue"), + params: z.tuple([z.number().min(-360).max(360)]), +}); +export type HueAction = z.infer; -export interface SpinAction { - apply: "spin"; - params: [number]; -} +const SpinActionSchema = z.object({ + apply: z.literal("spin"), + params: z.tuple([z.number().min(-360).max(360)]), +}); +export type SpinAction = z.infer; -export interface LightenAction { - apply: "lighten"; - params?: [number]; -} +const LightenActionSchema = z.object({ + apply: z.literal("lighten"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type LightenAction = z.infer; -export interface MixAction { - apply: "mix"; - params: [RGBColor, number] | [RGBColor]; -} +const RGBColorSchema = z.object({ + r: z.number().min(0).max(255), + g: z.number().min(0).max(255), + b: z.number().min(0).max(255), +}); -export interface TintAction { - apply: "tint"; - params?: [number]; -} +const MixActionSchema = z.object({ + apply: z.literal("mix"), + params: z.union([ + z.tuple([RGBColorSchema]), + z.tuple([RGBColorSchema, z.number().min(0).max(100)]), + ]), +}); +export type MixAction = z.infer; -export interface ShadeAction { - apply: "shade"; - params?: [number]; -} +const TintActionSchema = z.object({ + apply: z.literal("tint"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type TintAction = z.infer; -export interface XorAction { - apply: "xor"; - params: [RGBColor]; -} +const ShadeActionSchema = z.object({ + apply: z.literal("shade"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type ShadeAction = z.infer; -export interface RedAction { - apply: "red"; - params: [number]; -} +const XorActionSchema = z.object({ + apply: z.literal("xor"), + params: z.tuple([RGBColorSchema]), +}); +export type XorAction = z.infer; -export interface GreenAction { - apply: "green"; - params: [number]; -} +const RedActionSchema = z.object({ + apply: z.literal("red"), + params: z.tuple([z.number().min(-255).max(255)]), +}); +export type RedAction = z.infer; -export interface BlueAction { - apply: "blue"; - params: [number]; -} +const GreenActionSchema = z.object({ + apply: z.literal("green"), + params: z.tuple([z.number().min(-255).max(255)]), +}); +export type GreenAction = z.infer; -export interface BrightenAction { - apply: "brighten"; - params?: [number]; -} +const BlueActionSchema = z.object({ + apply: z.literal("blue"), + params: z.tuple([z.number().min(-255).max(255)]), +}); +export type BlueAction = z.infer; -export interface DarkenAction { - apply: "darken"; - params?: [number]; -} +const BrightenActionSchema = z.object({ + apply: z.literal("brighten"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type BrightenAction = z.infer; -export interface DesaturateAction { - apply: "desaturate"; - params?: [number]; -} +const DarkenActionSchema = z.object({ + apply: z.literal("darken"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type DarkenAction = z.infer; -export interface SaturateAction { - apply: "saturate"; - params?: [number]; -} +const DesaturateActionSchema = z.object({ + apply: z.literal("desaturate"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type DesaturateAction = z.infer; -export interface GrayscaleAction { - apply: "greyscale"; - params?: [number]; -} +const SaturateActionSchema = z.object({ + apply: z.literal("saturate"), + params: z.tuple([z.number().min(0).max(100)]).optional(), +}); +export type SaturateAction = z.infer; -export type ColorAction = - | HueAction - | SpinAction - | LightenAction - | MixAction - | TintAction - | ShadeAction - | XorAction - | RedAction - | GreenAction - | BlueAction - | BrightenAction - | DarkenAction - | DesaturateAction - | SaturateAction - | GrayscaleAction; +const GrayscaleActionSchema = z.object({ + apply: z.literal("greyscale"), + params: z.tuple([]).optional(), +}); +export type GrayscaleAction = z.infer; + +const ColorActionNameSchema = z.union([ + HueActionSchema, + SpinActionSchema, + LightenActionSchema, + MixActionSchema, + TintActionSchema, + ShadeActionSchema, + XorActionSchema, + RedActionSchema, + GreenActionSchema, + BlueActionSchema, + BrightenActionSchema, + DarkenActionSchema, + DesaturateActionSchema, + SaturateActionSchema, + GrayscaleActionSchema, +]); +export type ColorAction = z.infer; export const ColorActionName = Object.freeze({ LIGHTEN: "lighten", @@ -362,6 +444,7 @@ export const methods = { return image; }, + /** * Removes colour from the image using ITU Rec 709 luminance values * @example @@ -393,6 +476,7 @@ export const methods = { return image; }, + /** * Multiplies the opacity of each pixel by a factor between 0 and 1 * @param f A number, the factor by which to multiply the opacity of each pixel @@ -478,8 +562,6 @@ export const methods = { /** * Adds each element of the image to its local neighbors, weighted by the kernel - * @param kernel a matrix to weight the neighbors sum - * @param edgeHandling (optional) define how to sum pixels from outside the border * @example * ```ts * import { Jimp } from "jimp"; @@ -493,14 +575,10 @@ export const methods = { * ]); * ``` */ - convolution( - image: I, - kernel: number[][], - edgeHandling?: number - ) { - if (!edgeHandling) { - edgeHandling = Edge.EXTEND; - } + convolution(image: I, options: ConvolutionOptions) { + const parsed = ConvolutionOptionsSchema.parse(options); + const { kernel, edgeHandling = Edge.EXTEND } = + "kernel" in parsed ? parsed : { kernel: parsed, edgeHandling: undefined }; if (!kernel[0]) { throw new Error("kernel must be a matrix"); @@ -608,11 +686,6 @@ export const methods = { /** * Pixelates the image or a region - * @param size the size of the pixels - * @param x (optional) the x position of the region to pixelate - * @param y (optional) the y position of the region to pixelate - * @param w (optional) the width of the region to pixelate - * @param h (optional) the height of the region to pixelate * @example * ```ts * import { Jimp } from "jimp"; @@ -626,25 +699,24 @@ export const methods = { * image.pixelate(10, 10, 10, 20, 20); * ``` */ - pixelate( - image: I, - size: number, - x?: number, - y?: number, - w?: number, - h?: number - ) { + pixelate(image: I, options: PixelateOptions) { + const parsed = PixelateOptionsSchema.parse(options); + let { + size, + x = 0, + y = 0, + w = image.bitmap.width - x, + h = image.bitmap.height - y, + } = typeof parsed === "number" + ? ({ size: parsed } as PixelateComplexOptions) + : parsed; + const kernel = [ [1 / 16, 2 / 16, 1 / 16], [2 / 16, 4 / 16, 2 / 16], [1 / 16, 2 / 16, 1 / 16], ]; - x = x || 0; - y = y || 0; - w = w || image.bitmap.width - x; - h = h || image.bitmap.height - y; - const source = clone(image); scan(source, x, y, w, h, (xx, yx, idx) => { @@ -663,11 +735,6 @@ export const methods = { /** * Applies a convolution kernel to the image or a region - * @param kernel the convolution kernel - * @param x (optional) the x position of the region to apply convolution to - * @param y (optional) the y position of the region to apply convolution to - * @param w (optional) the width of the region to apply convolution to - * @param h (optional) the height of the region to apply convolution to * @example * ```ts * import { Jimp } from "jimp"; @@ -689,14 +756,18 @@ export const methods = { * ], 10, 10, 10, 20); * ``` */ - convolute( - image: I, - kernel: number[][], - x = 0, - y = 0, - w = image.bitmap.width - x, - h = image.bitmap.height - y - ) { + convolute(image: I, options: ConvoluteOptions) { + const parsed = ConvoluteOptionsSchema.parse(options); + const { + kernel, + x = 0, + y = 0, + w = image.bitmap.width - x, + h = image.bitmap.height - y, + } = "kernel" in parsed + ? parsed + : ({ kernel: parsed } as ConvoluteComplexOptions); + const source = clone(image); scan(source, x, y, w, h, (xx, yx, idx) => { @@ -731,6 +802,8 @@ export const methods = { throw new Error("actions must be an array"); } + actions.forEach((action) => ColorActionNameSchema.parse(action)); + actions = actions.map((action) => { if (action.apply === "xor" || action.apply === "mix") { action.params[0] = tinyColor(action.params[0]).toRgb(); diff --git a/plugins/plugin-contain/package.json b/plugins/plugin-contain/package.json index 67d6e049..2447be6a 100644 --- a/plugins/plugin-contain/package.json +++ b/plugins/plugin-contain/package.json @@ -12,23 +12,24 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { + "@jimp/core": "workspace:*", "@jimp/plugin-blit": "workspace:*", "@jimp/plugin-resize": "workspace:*", "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "@jimp/core": "workspace:*" + "zod": "^3.22.4" }, "devDependencies": { "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-contain/src/index.test.ts b/plugins/plugin-contain/src/index.test.ts index e68889de..0ac2322a 100644 --- a/plugins/plugin-contain/src/index.test.ts +++ b/plugins/plugin-contain/src/index.test.ts @@ -59,11 +59,15 @@ describe("All align combinations for contain", () => { (VerticalAlign as any)[verticalAlign]; test("vertical contain aligned to " + align, () => { - expect(vertical.clone().contain(6, 6, alignValue)).toMatchSnapshot(); + expect( + vertical.clone().contain({ w: 6, h: 6, align: alignValue }) + ).toMatchSnapshot(); }); test("horizontal contain aligned to " + align, () => { - expect(horizontal.clone().contain(6, 6, alignValue)).toMatchSnapshot(); + expect( + horizontal.clone().contain({ w: 6, h: 6, align: alignValue }) + ).toMatchSnapshot(); }); }); }); diff --git a/plugins/plugin-contain/src/index.ts b/plugins/plugin-contain/src/index.ts index e4d80449..9052b22e 100644 --- a/plugins/plugin-contain/src/index.ts +++ b/plugins/plugin-contain/src/index.ts @@ -3,13 +3,27 @@ import { HorizontalAlign, VerticalAlign } from "@jimp/core"; import { clone } from "@jimp/utils"; import { ResizeStrategy, methods as resizeMethods } from "@jimp/plugin-resize"; import { methods as blitMethods } from "@jimp/plugin-blit"; +import { z } from "zod"; + +const ContainOptionsSchema = z.object({ + /** the width to resize the image to */ + w: z.number(), + /** the height to resize the image to */ + h: z.number(), + /** A bitmask for horizontal and vertical alignment */ + align: z.number().optional(), + /** a scaling method (e.g. Jimp.RESIZE_BEZIER) */ + mode: z.nativeEnum(ResizeStrategy).optional(), +}); + +export type ContainOptions = z.infer; export const methods = { /** * Scale the image to the given width and height keeping the aspect ratio. Some parts of the image may be letter boxed. * @param w the width to resize the image to * @param h the height to resize the image to - * @param alignBits A bitmask for horizontal and vertical alignment + * @param align A bitmask for horizontal and vertical alignment * @param mode a scaling method (e.g. Jimp.RESIZE_BEZIER) * @example * ```ts @@ -20,17 +34,16 @@ export const methods = { * image.contain(150, 100); * ``` */ - contain( - image: I, - w: number, - h: number, - alignBits?: number, - mode?: ResizeStrategy - ) { - alignBits = alignBits || HorizontalAlign.CENTER | VerticalAlign.MIDDLE; + contain(image: I, options: ContainOptions) { + const { + w, + h, + align = HorizontalAlign.CENTER | VerticalAlign.MIDDLE, + mode, + } = ContainOptionsSchema.parse(options); - const hbits = alignBits & ((1 << 3) - 1); - const vbits = alignBits >> 3; + const hbits = align & ((1 << 3) - 1); + const vbits = align >> 3; // check if more flags than one is in the bit sets if ( @@ -50,9 +63,9 @@ export const methods = { ? h / image.bitmap.height : w / image.bitmap.width; - const c = resizeMethods.scale(clone(image), f, mode); + const c = resizeMethods.scale(clone(image), { f, mode }); - image = resizeMethods.resize(image, w, h, mode); + image = resizeMethods.resize(image, { w, h, mode }); image.scan((_, __, idx) => { image.bitmap.data.writeUInt32BE(image.background, idx); diff --git a/plugins/plugin-cover/package.json b/plugins/plugin-cover/package.json index f9775845..5f3f3f44 100644 --- a/plugins/plugin-cover/package.json +++ b/plugins/plugin-cover/package.json @@ -12,23 +12,24 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { + "@jimp/core": "workspace:*", "@jimp/plugin-crop": "workspace:*", "@jimp/plugin-resize": "workspace:*", - "@jimp/core": "workspace:*", "@jimp/types": "workspace:*", - "@jimp/utils": "workspace:*" + "@jimp/utils": "workspace:*", + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/test-utils": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-cover/src/index.test.ts b/plugins/plugin-cover/src/index.test.ts index 615a41b8..e88b0518 100644 --- a/plugins/plugin-cover/src/index.test.ts +++ b/plugins/plugin-cover/src/index.test.ts @@ -58,11 +58,15 @@ describe("All align combinations for cover", () => { (VerticalAlign as any)[verticalAlign]; test("vertical contain aligned to " + align, () => { - expect(vertical.clone().cover(4, 4, alignValue)).toMatchSnapshot(); + expect( + vertical.clone().cover({ w: 4, h: 4, align: alignValue }) + ).toMatchSnapshot(); }); test("horizontal contain aligned to " + align, () => { - expect(horizontal.clone().cover(4, 4, alignValue)).toMatchSnapshot(); + expect( + horizontal.clone().cover({ w: 4, h: 4, align: alignValue }) + ).toMatchSnapshot(); }); }); }); diff --git a/plugins/plugin-cover/src/index.ts b/plugins/plugin-cover/src/index.ts index 21327290..b2bb65e0 100644 --- a/plugins/plugin-cover/src/index.ts +++ b/plugins/plugin-cover/src/index.ts @@ -2,14 +2,24 @@ import { JimpClass } from "@jimp/types"; import { VerticalAlign, HorizontalAlign } from "@jimp/core"; import { ResizeStrategy, methods as resizeMethods } from "@jimp/plugin-resize"; import { methods as cropMethods } from "@jimp/plugin-crop"; +import { z } from "zod"; + +const CoverOptionsSchema = z.object({ + /** the width to resize the image to */ + w: z.number(), + /** the height to resize the image to */ + h: z.number(), + /** A bitmask for horizontal and vertical alignment */ + align: z.number().optional(), + /** a scaling method (e.g. ResizeStrategy.BEZIER) */ + mode: z.nativeEnum(ResizeStrategy).optional(), +}); + +export type CoverOptions = z.infer; export const methods = { /** * Scale the image so the given width and height keeping the aspect ratio. Some parts of the image may be clipped. - * @param w the width to resize the image to - * @param h the height to resize the image to - * @param alignBits A bitmask for horizontal and vertical alignment - * @param mode a scaling method (e.g. Jimp.RESIZE_BEZIER) * @example * ```ts * import { Jimp } from "jimp"; @@ -19,16 +29,15 @@ export const methods = { * image.cover(150, 100); * ``` */ - cover( - image: I, - w: number, - h: number, - alignBits?: number, - mode?: ResizeStrategy - ) { - alignBits = alignBits || HorizontalAlign.CENTER | VerticalAlign.MIDDLE; - const hbits = alignBits & ((1 << 3) - 1); - const vbits = alignBits >> 3; + cover(image: I, options: CoverOptions) { + const { + w, + h, + align = HorizontalAlign.CENTER | VerticalAlign.MIDDLE, + mode, + } = CoverOptionsSchema.parse(options); + const hbits = align & ((1 << 3) - 1); + const vbits = align >> 3; // check if more flags than one is in the bit sets if ( @@ -48,14 +57,16 @@ export const methods = { ? w / image.bitmap.width : h / image.bitmap.height; - image = resizeMethods.scale(image, f, mode); - image = cropMethods.crop( - image, - ((image.bitmap.width - w) / 2) * alignH, - ((image.bitmap.height - h) / 2) * alignV, + image = resizeMethods.scale(image, { + f, + mode, + }); + image = cropMethods.crop(image, { + x: ((image.bitmap.width - w) / 2) * alignH, + y: ((image.bitmap.height - h) / 2) * alignV, w, - h - ); + h, + }); return image; }, diff --git a/plugins/plugin-crop/package.json b/plugins/plugin-crop/package.json index 5bcca4ba..858aa152 100644 --- a/plugins/plugin-crop/package.json +++ b/plugins/plugin-crop/package.json @@ -15,19 +15,20 @@ "dependencies": { "@jimp/core": "workspace:*", "@jimp/types": "workspace:*", - "@jimp/utils": "workspace:*" + "@jimp/utils": "workspace:*", + "zod": "^3.22.4" }, "devDependencies": { "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-crop/src/__snapshots__/autocrop.test.ts.snap b/plugins/plugin-crop/src/__snapshots__/autocrop.test.ts.snap index d59465aa..8d727b35 100644 --- a/plugins/plugin-crop/src/__snapshots__/autocrop.test.ts.snap +++ b/plugins/plugin-crop/src/__snapshots__/autocrop.test.ts.snap @@ -87,27 +87,19 @@ Data: exports[`Autocrop > image border with small variation 2`] = ` Visualization: -323232323232 -232323232323 -32 ◆◆ 32 -23 ◆▦▦◆ 23 -32 ◆▦▦▦▦◆ 32 -23 ◆▦▦◆ 23 -32 ◆◆ 32 -232323232323 -323232323232 + ◆◆ + ◆▦▦◆ + ◆▦▦▦▦◆ + ◆▦▦◆ + ◆◆ Data: -33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ -22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ -33-33-33ᶠᶠ 22-22-22ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ 33-33-33ᶠᶠ 22-22-22ᶠᶠ -22-22-22ᶠᶠ 33-33-33ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 22-22-22ᶠᶠ 33-33-33ᶠᶠ -33-33-33ᶠᶠ 22-22-22ᶠᶠ 00-00-00⁰⁰ FF-FF-00ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 33-33-33ᶠᶠ 22-22-22ᶠᶠ -22-22-22ᶠᶠ 33-33-33ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 22-22-22ᶠᶠ 33-33-33ᶠᶠ -33-33-33ᶠᶠ 22-22-22ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ 33-33-33ᶠᶠ 22-22-22ᶠᶠ -22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ -33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ 33-33-33ᶠᶠ 22-22-22ᶠᶠ +00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ +00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ +00-00-00⁰⁰ FF-FF-00ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ +00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ 80-80-80ᶠᶠ 80-80-80ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ +00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ FF-FF-00ᶠᶠ FF-FF-00ᶠᶠ 00-00-00⁰⁰ 00-00-00⁰⁰ 00-00-00⁰⁰ `; exports[`Autocrop > image border with small variation configured by options 1`] = ` diff --git a/plugins/plugin-crop/src/crop.test.ts b/plugins/plugin-crop/src/crop.test.ts index 2cd4d457..fff1a9ba 100644 --- a/plugins/plugin-crop/src/crop.test.ts +++ b/plugins/plugin-crop/src/crop.test.ts @@ -17,26 +17,38 @@ describe("crop", () => { ); test("full width from top", () => { - expect(jimp.fromBitmap(testImage).crop(0, 0, 6, 2)).toMatchSnapshot(); + expect( + jimp.fromBitmap(testImage).crop({ x: 0, y: 0, w: 6, h: 2 }) + ).toMatchSnapshot(); }); test("full width from bottom", () => { - expect(jimp.fromBitmap(testImage).crop(0, 3, 6, 2)).toMatchSnapshot(); + expect( + jimp.fromBitmap(testImage).crop({ x: 0, y: 3, w: 6, h: 2 }) + ).toMatchSnapshot(); }); test("full width from middle", () => { - expect(jimp.fromBitmap(testImage).crop(0, 2, 6, 2)).toMatchSnapshot(); + expect( + jimp.fromBitmap(testImage).crop({ x: 0, y: 2, w: 6, h: 2 }) + ).toMatchSnapshot(); }); test("full height from left", () => { - expect(jimp.fromBitmap(testImage).crop(0, 0, 2, 5)).toMatchSnapshot(); + expect( + jimp.fromBitmap(testImage).crop({ x: 0, y: 0, w: 2, h: 5 }) + ).toMatchSnapshot(); }); test("full height from right", () => { - expect(jimp.fromBitmap(testImage).crop(4, 0, 2, 5)).toMatchSnapshot(); + expect( + jimp.fromBitmap(testImage).crop({ x: 4, y: 0, w: 2, h: 5 }) + ).toMatchSnapshot(); }); test("full height from middle", () => { - expect(jimp.fromBitmap(testImage).crop(2, 0, 2, 5)).toMatchSnapshot(); + expect( + jimp.fromBitmap(testImage).crop({ x: 2, y: 0, w: 2, h: 5 }) + ).toMatchSnapshot(); }); }); diff --git a/plugins/plugin-crop/src/index.ts b/plugins/plugin-crop/src/index.ts index 022b5811..151c5db0 100644 --- a/plugins/plugin-crop/src/index.ts +++ b/plugins/plugin-crop/src/index.ts @@ -2,25 +2,44 @@ import { JimpClass } from "@jimp/types"; import { colorDiff, intToRGBA, scan } from "@jimp/utils"; +import { z } from "zod"; -export interface AutocropOptions { +const CropOptionsSchema = z.object({ + x: z.number(), + y: z.number(), + w: z.number(), + h: z.number(), +}); + +export type CropOptions = z.infer; + +const AutocropComplexOptionsSchema = z.object({ /** percent of color difference tolerance (default value) */ - tolerance?: number; + tolerance: z.number().min(0).max(1).optional(), /** flag to force cropping only if the image has a real "frame" i.e. all 4 sides have some border (default value) */ - cropOnlyFrames?: boolean; - /** - * force cropping top be symmetric - */ - cropSymmetric?: boolean; + cropOnlyFrames: z.boolean().optional(), + /** force cropping top be symmetric */ + cropSymmetric: z.boolean().optional(), /** Amount of pixels in border to leave */ - leaveBorder?: number; - ignoreSides?: { - north?: boolean; - south?: boolean; - east?: boolean; - west?: boolean; - }; -} + leaveBorder: z.number().optional(), + ignoreSides: z + .object({ + north: z.boolean().optional(), + south: z.boolean().optional(), + east: z.boolean().optional(), + west: z.boolean().optional(), + }) + .optional(), +}); +const AutocropOptionsSchema = z.union([ + z.number().min(0).max(1), + AutocropComplexOptionsSchema, +]); + +export type AutocropComplexOptions = z.infer< + typeof AutocropComplexOptionsSchema +>; +export type AutocropOptions = z.infer; export const methods = { /** @@ -34,21 +53,8 @@ export const methods = { * const cropped = image.crop(150, 100); * ``` */ - crop( - image: I, - x: number, - y: number, - w: number, - h: number - ) { - 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"); - } - + crop(image: I, options: CropOptions) { + let { x, y, w, h } = CropOptionsSchema.parse(options); // round input x = Math.round(x); y = Math.round(y); @@ -99,7 +105,9 @@ export const methods = { cropSymmetric = false, leaveBorder = 0, ignoreSides: ignoreSidesArg, - } = options; + } = typeof options === "number" + ? ({ tolerance: options } as AutocropComplexOptions) + : options; const w = image.bitmap.width; const h = image.bitmap.height; const minPixelsPerSide = 1; // to avoid cropping completely the image, resulting in an invalid 0 sized image @@ -261,13 +269,12 @@ export const methods = { if (doCrop) { // do the real crop - this.crop( - image, - westPixelsToCrop, - northPixelsToCrop, - widthOfRemainingPixels, - heightOfRemainingPixels - ); + this.crop(image, { + x: westPixelsToCrop, + y: northPixelsToCrop, + w: widthOfRemainingPixels, + h: heightOfRemainingPixels, + }); } return image; diff --git a/plugins/plugin-displace/package.json b/plugins/plugin-displace/package.json index 436c2fa4..2be23c68 100644 --- a/plugins/plugin-displace/package.json +++ b/plugins/plugin-displace/package.json @@ -10,8 +10,9 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { + "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "@jimp/types": "workspace:*" + "zod": "^3.22.4" }, "devDependencies": { "@jimp/config-eslint": "workspace:*", diff --git a/plugins/plugin-displace/src/index.ts b/plugins/plugin-displace/src/index.ts index 3f9a20e2..8b4b1508 100644 --- a/plugins/plugin-displace/src/index.ts +++ b/plugins/plugin-displace/src/index.ts @@ -1,11 +1,21 @@ -import { JimpClass } from "@jimp/types"; +import { JimpClass, JimpClassSchema } from "@jimp/types"; import { clone } from "@jimp/utils"; +import { z } from "zod"; + +const DisplaceOptionsSchema = z.object({ + /** the source Jimp instance */ + map: JimpClassSchema, + /** the maximum displacement value */ + offset: z.number(), +}); + +export type DisplaceOptions = z.infer; export const methods = { /** * Displaces the image based on the provided displacement map * @param map the source Jimp instance - * @param offset the maximum displacement value + * @param offset * @example * ```ts * import { Jimp } from "jimp"; @@ -16,15 +26,8 @@ export const methods = { * image.displace(map, 10); * ``` */ - displace(image: I, map: I, offset: number) { - if (typeof map !== "object" || !map.bitmap) { - throw new Error("The source must be a Jimp image"); - } - - if (typeof offset !== "number") { - throw new Error("offset must be a number"); - } - + displace(image: I, options: DisplaceOptions) { + const { map, offset } = DisplaceOptionsSchema.parse(options); const source = clone(image); image.scan((x, y, idx) => { diff --git a/plugins/plugin-fisheye/package.json b/plugins/plugin-fisheye/package.json index 19901b60..3e1f444f 100644 --- a/plugins/plugin-fisheye/package.json +++ b/plugins/plugin-fisheye/package.json @@ -12,21 +12,22 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { + "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "@jimp/types": "workspace:*" + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/core": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/core": "workspace:*", "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-fisheye/src/index.test.ts b/plugins/plugin-fisheye/src/index.test.ts index 12918aa7..f23fdf4a 100644 --- a/plugins/plugin-fisheye/src/index.test.ts +++ b/plugins/plugin-fisheye/src/index.test.ts @@ -43,6 +43,6 @@ describe("Fisheye", () => { ) ); - expect(imgNormal.fisheye({ r: 1.8 })).toMatchSnapshot(); + expect(imgNormal.fisheye({ radius: 1.8 })).toMatchSnapshot(); }); }); diff --git a/plugins/plugin-fisheye/src/index.ts b/plugins/plugin-fisheye/src/index.ts index 6ca0d055..ced41505 100644 --- a/plugins/plugin-fisheye/src/index.ts +++ b/plugins/plugin-fisheye/src/index.ts @@ -1,9 +1,13 @@ import { JimpClass } from "@jimp/types"; import { clone } from "@jimp/utils"; +import { z } from "zod"; -export interface FisheyeOptions { - r?: number; -} +const FisheyeOptionsSchema = z.object({ + /** the radius of the circle */ + radius: z.number().min(0).optional(), +}); + +export type FisheyeOptions = z.infer; export const methods = { /** @@ -18,7 +22,7 @@ export const methods = { * ``` */ fisheye(image: I, options: FisheyeOptions = {}) { - const r = options.r || 2.5; + const { radius = 2.5 } = FisheyeOptionsSchema.parse(options); const source = clone(image); const { width, height } = source.bitmap; @@ -26,7 +30,7 @@ export const methods = { const hx = x / width; const hy = y / height; const rActual = Math.sqrt(Math.pow(hx - 0.5, 2) + Math.pow(hy - 0.5, 2)); - const rn = 2 * Math.pow(rActual, r); + const rn = 2 * Math.pow(rActual, radius); const cosA = (hx - 0.5) / rActual; const sinA = (hy - 0.5) / rActual; const newX = Math.round((rn * cosA + 0.5) * width); diff --git a/plugins/plugin-flip/package.json b/plugins/plugin-flip/package.json index c7af9bc2..69876ba4 100644 --- a/plugins/plugin-flip/package.json +++ b/plugins/plugin-flip/package.json @@ -12,7 +12,8 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { - "@jimp/types": "workspace:*" + "@jimp/types": "workspace:*", + "zod": "^3.22.4" }, "devDependencies": { "@jimp/config-eslint": "workspace:*", @@ -20,12 +21,12 @@ "@jimp/config-vitest": "workspace:*", "@jimp/core": "workspace:*", "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-flip/src/index.test.ts b/plugins/plugin-flip/src/index.test.ts index eaf402fa..b2313a7d 100644 --- a/plugins/plugin-flip/src/index.test.ts +++ b/plugins/plugin-flip/src/index.test.ts @@ -20,7 +20,7 @@ describe("Flipping plugin", () => { ) ); - expect(src.flip(true, false)).toMatchSnapshot(); + expect(src.flip({ horizontal: true })).toMatchSnapshot(); }); test("can flip vertically", () => { @@ -36,7 +36,7 @@ describe("Flipping plugin", () => { ) ); - expect(src.flip(false, true)).toMatchSnapshot(); + expect(src.flip({ vertical: true })).toMatchSnapshot(); }); test("can flip both horizontally and vertically at once", async () => { @@ -52,6 +52,6 @@ describe("Flipping plugin", () => { ) ); - expect(src.flip(true, true)).toMatchSnapshot(); + expect(src.flip({ horizontal: true, vertical: true })).toMatchSnapshot(); }); }); diff --git a/plugins/plugin-flip/src/index.ts b/plugins/plugin-flip/src/index.ts index 9b834eb4..3fff7038 100644 --- a/plugins/plugin-flip/src/index.ts +++ b/plugins/plugin-flip/src/index.ts @@ -1,4 +1,14 @@ import { JimpClass } from "@jimp/types"; +import { z } from "zod"; + +const FlipOptionsSchema = z.object({ + /** if true the image will be flipped horizontally */ + horizontal: z.boolean().optional(), + /** if true the image will be flipped vertically */ + vertical: z.boolean().optional(), +}); + +export type FlipOptions = z.infer; export const methods = { /** @@ -14,11 +24,8 @@ export const methods = { * image.flip(true, false); * ``` */ - flip(image: I, horizontal: boolean, vertical: boolean) { - if (typeof horizontal !== "boolean" || typeof vertical !== "boolean") { - throw new Error("horizontal and vertical must be Booleans"); - } - + flip(image: I, options: FlipOptions) { + const { horizontal, vertical } = FlipOptionsSchema.parse(options); const bitmap = Buffer.alloc(image.bitmap.data.length); image.scan((x, y, idx) => { diff --git a/plugins/plugin-hash/src/phash.ts b/plugins/plugin-hash/src/phash.ts index e3514776..a70747fd 100644 --- a/plugins/plugin-hash/src/phash.ts +++ b/plugins/plugin-hash/src/phash.ts @@ -65,7 +65,7 @@ class ImagePHash { * This is really done to simplify the DCT computation and not * because it is needed to reduce the high frequencies. */ - img = methods.resize(clone(img), this.size, this.size); + img = methods.resize(clone(img), { w: this.size, h: this.size }); /* 2. Reduce color. * The image is reduced to a grayscale just to further simplify diff --git a/plugins/plugin-mask/package.json b/plugins/plugin-mask/package.json index f4c6df4e..9014b5fd 100644 --- a/plugins/plugin-mask/package.json +++ b/plugins/plugin-mask/package.json @@ -12,20 +12,21 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { - "@jimp/types": "workspace:*" + "@jimp/types": "workspace:*", + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/core": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/core": "workspace:*", "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-mask/src/index.test.ts b/plugins/plugin-mask/src/index.test.ts index b788d8e7..6114522e 100644 --- a/plugins/plugin-mask/src/index.test.ts +++ b/plugins/plugin-mask/src/index.test.ts @@ -56,11 +56,15 @@ describe("Mask", () => { }); test("Affect opaque image with a gray mask with the same size, blited", () => { - expect(imgSrcOpaq.clone().mask(maskGrayBig, 1, 1)).toMatchSnapshot(); + expect( + imgSrcOpaq.clone().mask({ src: maskGrayBig, x: 1, y: 1 }) + ).toMatchSnapshot(); }); test("Affect opaque image with a gray mask with the same size, blited negative", () => { - expect(imgSrcOpaq.clone().mask(maskGrayBig, -1, -1)).toMatchSnapshot(); + expect( + imgSrcOpaq.clone().mask({ src: maskGrayBig, x: -1, y: -1 }) + ).toMatchSnapshot(); }); test("Affect opaque image with a smaller gray mask", () => { @@ -68,7 +72,9 @@ describe("Mask", () => { }); test("Affect opaque image with a smaller gray mask, blited", () => { - expect(imgSrcOpaq.clone().mask(maskGraySmall, 1, 1)).toMatchSnapshot(); + expect( + imgSrcOpaq.clone().mask({ src: maskGraySmall, x: 1, y: 1 }) + ).toMatchSnapshot(); }); test("Affect alpha image with a bigger gray mask", () => { @@ -76,10 +82,14 @@ describe("Mask", () => { }); test("Affect alpha image with a bigger gray mask, blited", () => { - expect(imgSrcAlpa.clone().mask(maskGrayBig, -1, -1)).toMatchSnapshot(); + expect( + imgSrcAlpa.clone().mask({ src: maskGrayBig, x: -1, y: -1 }) + ).toMatchSnapshot(); }); test("Affect opaque image with a colored mask", () => { - expect(imgSrcOpaq.clone().mask(maskColor, 1, 1)).toMatchSnapshot(); + expect( + imgSrcOpaq.clone().mask({ src: maskColor, x: 1, y: 1 }) + ).toMatchSnapshot(); }); }); diff --git a/plugins/plugin-mask/src/index.ts b/plugins/plugin-mask/src/index.ts index be55929b..9135831c 100644 --- a/plugins/plugin-mask/src/index.ts +++ b/plugins/plugin-mask/src/index.ts @@ -1,4 +1,17 @@ -import { JimpClass } from "@jimp/types"; +import { JimpClass, JimpClassSchema } from "@jimp/types"; +import { z } from "zod"; + +const MaskOptionsObjectSchema = z.object({ + src: JimpClassSchema, + /** the x position to draw the image */ + x: z.number().optional(), + /** the y position to draw the image */ + y: z.number().optional(), +}); + +const MaskOptionsSchema = z.union([JimpClassSchema, MaskOptionsObjectSchema]); + +export type MaskOptions = z.infer; export const methods = { /** @@ -16,7 +29,23 @@ export const methods = { * image.mask(mask); * ``` */ - mask(image: I, src: I, x = 0, y = 0) { + mask(image: I, options: MaskOptions) { + MaskOptionsSchema.parse(options); + + let src: JimpClass; + let x: number; + let y: number; + + if ("bitmap" in options) { + src = options as unknown as JimpClass; + x = 0; + y = 0; + } else { + src = options.src as unknown as JimpClass; + x = options.x ?? 0; + y = options.y ?? 0; + } + // round input x = Math.round(x); y = Math.round(y); diff --git a/plugins/plugin-print/package.json b/plugins/plugin-print/package.json index 1d65bb60..a36a8a6e 100644 --- a/plugins/plugin-print/package.json +++ b/plugins/plugin-print/package.json @@ -73,8 +73,10 @@ "@jimp/js-png": "workspace:*", "@jimp/plugin-blit": "workspace:*", "@jimp/types": "workspace:*", + "@jimp/utils": "workspace:*", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", - "parse-bmfont-xml": "^1.1.6" + "parse-bmfont-xml": "^1.1.6", + "zod": "^3.22.4" } } diff --git a/plugins/plugin-print/src/index.node.test.ts b/plugins/plugin-print/src/index.node.test.ts index e859df9e..32959a46 100644 --- a/plugins/plugin-print/src/index.node.test.ts +++ b/plugins/plugin-print/src/index.node.test.ts @@ -38,7 +38,7 @@ async function createTextImage( const image = new Jimp({ width, height, color: 0xffffffff }); return image - .print(loadedFont, x, y, text, maxWidth, maxHeight) + .print({ font: loadedFont, x, y, text, maxWidth, maxHeight }) .getBuffer("image/png"); } @@ -62,7 +62,13 @@ describe("Write text over image", function () { const font = await loadFont(fonts[fontName as keyof typeof fonts]); const image = new Jimp({ width: conf.w, height: conf.h, color: conf.bg }); const output = await image - .print(font, 0, 0, "This is only a test.", image.bitmap.width) + .print({ + font, + x: 0, + y: 0, + text: "This is only a test.", + maxWidth: image.bitmap.width, + }) .getBuffer("image/png"); expect(output).toMatchImageSnapshot(); @@ -73,7 +79,13 @@ describe("Write text over image", function () { const font = await loadFont(fonts.SANS_16_BLACK); const image = new Jimp({ width: 300, height: 100, color: 0xff8800ff }); const output = await image - .print(font, 150, 50, "This is only a test.", 100) + .print({ + font, + x: 150, + y: 50, + text: "This is only a test.", + maxWidth: 100, + }) .getBuffer("image/png"); expect(output).toMatchImageSnapshot(); @@ -85,7 +97,13 @@ describe("Write text over image", function () { ); const image = new Jimp({ width: 300, height: 100, color: 0xff8800ff }); const output = await image - .print(font, 150, 50, "This is only a test.", 100) + .print({ + font, + x: 150, + y: 50, + text: "This is only a test.", + maxWidth: 100, + }) .getBuffer("image/png"); expect(output).toMatchImageSnapshot(); @@ -95,7 +113,7 @@ describe("Write text over image", function () { const font = await loadFont(fonts.SANS_16_BLACK); const image = new Jimp({ width: 300, height: 100, color: 0xff8800ff }); const output = await image - .print(font, 0, 0, "ツ ツ ツ", 100) + .print({ font, x: 0, y: 0, text: "ツ ツ ツ", maxWidth: 100 }) .getBuffer("image/png"); expect(output).toMatchImageSnapshot(); @@ -105,7 +123,7 @@ describe("Write text over image", function () { const font = await loadFont(fonts.SANS_16_BLACK); const image = new Jimp({ width: 300, height: 100, color: 0xff8800ff }); const output = await image - .print(font, 0, 0, 12345678, 100) + .print({ font, x: 0, y: 0, text: 12345678, maxWidth: 100 }) .getBuffer("image/png"); expect(output).toMatchImageSnapshot(); @@ -223,23 +241,22 @@ describe("Write text over image", function () { const loadedFont = await loadFont(fonts.SANS_16_BLACK); const image = new Jimp({ width: 500, height: 500, color: 0xffffffff }); - image.print( - loadedFont, - 0, - 0, - "One two three four fix six seven eight nine ten eleven twelve", - 250, - undefined, - ({ x, y }) => { - image.print( - loadedFont, + image.print({ + font: loadedFont, + x: 0, + y: 0, + text: "One two three four fix six seven eight nine ten eleven twelve", + maxWidth: 250, + cb: ({ x, y }) => { + image.print({ + font: loadedFont, x, - y + 50, - "thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty", - 250 - ); - } - ); + y: y + 50, + text: "thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty", + maxWidth: 250, + }); + }, + }); const output = await image.getBuffer("image/png"); expect(output).toMatchImageSnapshot(); diff --git a/plugins/plugin-print/src/index.ts b/plugins/plugin-print/src/index.ts index 27b52407..7a65dbed 100644 --- a/plugins/plugin-print/src/index.ts +++ b/plugins/plugin-print/src/index.ts @@ -1,6 +1,7 @@ import { HorizontalAlign, VerticalAlign } from "@jimp/core"; import { JimpClass } from "@jimp/types"; import { methods as blitMethods } from "@jimp/plugin-blit"; +import { z } from "zod"; import { measureText, measureTextHeight, splitLines } from "./measure-text.js"; import { BmFont } from "./types.js"; @@ -8,6 +9,32 @@ import { BmFont } from "./types.js"; export { measureText, measureTextHeight } from "./measure-text.js"; export * from "./types.js"; +const PrintOptionsSchema = z.object({ + /** the x position to draw the image */ + x: z.number(), + /** the y position to draw the image */ + y: z.number(), + /** the text to print */ + text: z.union([ + z.union([z.string(), z.number()]), + z.object({ + text: z.union([z.string(), z.number()]), + alignmentX: z.nativeEnum(HorizontalAlign).optional(), + alignmentY: z.nativeEnum(VerticalAlign).optional(), + }), + ]), + /** the boundary width to draw in */ + maxWidth: z.number().optional(), + /** the boundary height to draw in */ + maxHeight: z.number().optional(), + /** a callback for when complete that ahs the end co-ordinates of the text */ + cb: z + .function(z.tuple([z.object({ x: z.number(), y: z.number() })])) + .optional(), +}); + +export type PrintOptions = z.infer; + function xOffsetBasedOnAlignment( font: BmFont, line: string, @@ -94,9 +121,6 @@ export const methods = { * @param x the x position to start drawing the text * @param y the y position to start drawing the text * @param text the text to draw (string or object with `text`, `alignmentX`, and/or `alignmentY`) - * @param maxWidth the boundary width to draw in - * @param maxHeight the boundary height to draw in - * @param cb (optional) a callback for when complete that ahs the end co-ordinates of the text * @example * ```ts * import { Jimp } from "jimp"; @@ -109,40 +133,22 @@ export const methods = { */ print( image: I, - font: BmFont, - x: number, - y: number, - text: - | string - | number - | { - text: string | number; - alignmentX?: HorizontalAlign; - alignmentY?: VerticalAlign; - }, - maxWidth: number = Infinity, - maxHeight: number = Infinity, - cb: (options: { x: number; y: number }) => void = () => {} - ) { - if (typeof font !== "object") { - throw new Error("font must be a font loaded with loadFont"); - } - - if ( - typeof x !== "number" || - typeof y !== "number" || - typeof maxWidth !== "number" - ) { - throw new Error("x, y and maxWidth must be numbers"); - } - - if (typeof maxWidth !== "number") { - throw new Error("maxWidth must be a number"); - } - - if (typeof maxHeight !== "number") { - throw new Error("maxHeight must be a number"); + { + font, + ...options + }: PrintOptions & { + /** the BMFont instance */ + font: BmFont; } + ) { + let { + x, + y, + text, + maxWidth = Infinity, + maxHeight = Infinity, + cb = () => {}, + } = PrintOptionsSchema.parse(options); let alignmentX: HorizontalAlign; let alignmentY: VerticalAlign; diff --git a/plugins/plugin-resize/package.json b/plugins/plugin-resize/package.json index 1e640f52..96630f20 100644 --- a/plugins/plugin-resize/package.json +++ b/plugins/plugin-resize/package.json @@ -13,19 +13,20 @@ "license": "MIT", "dependencies": { "@jimp/core": "workspace:*", - "@jimp/types": "workspace:*" + "@jimp/types": "workspace:*", + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/test-utils": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-resize/src/constants.ts b/plugins/plugin-resize/src/constants.ts index 78dd4bb8..11b9bda2 100644 --- a/plugins/plugin-resize/src/constants.ts +++ b/plugins/plugin-resize/src/constants.ts @@ -1,6 +1,3 @@ -/** Used to automate the resizing of one dimension of an image */ -export const AutoSize = -1; - /** * What resizing algorithm to use. */ diff --git a/plugins/plugin-resize/src/index.test.ts b/plugins/plugin-resize/src/index.test.ts index eadf9807..1090337e 100644 --- a/plugins/plugin-resize/src/index.test.ts +++ b/plugins/plugin-resize/src/index.test.ts @@ -88,7 +88,11 @@ describe("Resize images", () => { const mode = (ResizeStrategy as any)[modeType]; expect( - image.src.clone().resize(size.width, size.height, mode) + image.src.clone().resize({ + w: size.width, + h: size.height, + mode, + }) ).toMatchSnapshot(); }); }); diff --git a/plugins/plugin-resize/src/index.ts b/plugins/plugin-resize/src/index.ts index fc5d417b..4fdaaacd 100644 --- a/plugins/plugin-resize/src/index.ts +++ b/plugins/plugin-resize/src/index.ts @@ -1,50 +1,82 @@ import { JimpClass } from "@jimp/types"; -import { ResizeStrategy, AutoSize } from "./constants.js"; +import { ResizeStrategy } from "./constants.js"; +import { z } from "zod"; import Resize from "./modules/resize.js"; import Resize2 from "./modules/resize2.js"; export * from "./constants.js"; +const ResizeOptionsSchema = z.union([ + z.object({ + /** the width to resize the image to */ + w: z.number().min(0), + /** the height to resize the image to */ + h: z.number().min(0).optional(), + /** a scaling method (e.g. ResizeStrategy.BEZIER) */ + mode: z.nativeEnum(ResizeStrategy).optional(), + }), + z.object({ + /** the width to resize the image to */ + w: z.number().min(0).optional(), + /** the height to resize the image to */ + h: z.number().min(0), + /** a scaling method (e.g. ResizeStrategy.BEZIER) */ + mode: z.nativeEnum(ResizeStrategy).optional(), + }), +]); + +export type ResizeOptions = z.infer; + +const ScaleToFitOptionsSchema = z.object({ + /** the width to resize the image to */ + w: z.number().min(0), + /** the height to resize the image to */ + h: z.number().min(0), + /** a scaling method (e.g. Jimp.RESIZE_BEZIER) */ + mode: z.nativeEnum(ResizeStrategy).optional(), +}); + +export type ScaleToFitOptions = z.infer; + +const ScaleComplexOptionsSchema = z.object({ + /** the width to resize the image to */ + f: z.number().min(0), + /** a scaling method (e.g. Jimp.RESIZE_BEZIER) */ + mode: z.nativeEnum(ResizeStrategy).optional(), +}); + +export type ScaleComplexOptions = z.infer; + +const ScaleOptionsSchema = z.union([z.number(), ScaleComplexOptionsSchema]); +export type ScaleOptions = z.infer; + export const methods = { /** * Resizes the image to a set width and height using a 2-pass bilinear algorithm - * @param w the width to resize the image to (or AutoSize) - * @param h the height to resize the image to (or AutoSize) - * @param mode a scaling method (e.g. Jimp.RESIZE_BEZIER) * @example * ```ts - * import { Jimp, AutoSize } from "jimp"; + * import { Jimp } from "jimp"; * * const image = await Jimp.read("test/image.png"); * - * image.resize(150, AutoSize); + * image.resize({ w: 150 }); * ``` */ - resize( - image: I, - w: number, - h: number, - mode?: ResizeStrategy - ) { - if (typeof w !== "number" || typeof h !== "number") { - throw new Error("w and h must be numbers"); - } - - if (w === AutoSize && h === AutoSize) { - throw new Error("w and h cannot both be set to auto"); - } - - if (w === AutoSize) { - w = image.bitmap.width * (h / image.bitmap.height); - } - - if (h === AutoSize) { - h = image.bitmap.height * (w / image.bitmap.width); - } - - if (w < 0 || h < 0) { - throw new Error("w and h must be positive numbers"); + resize(image: I, options: ResizeOptions) { + const { mode } = ResizeOptionsSchema.parse(options); + + let w: number; + let h: number; + + if (typeof options.w === "number") { + w = options.w; + h = options.h ?? image.bitmap.height * (w / image.bitmap.width); + } else if (typeof options.h === "number") { + h = options.h; + w = options.w ?? image.bitmap.width * (h / image.bitmap.height); + } else { + throw new Error("w must be a number"); } // round inputs @@ -93,20 +125,15 @@ export const methods = { * image.scale(0.5); * ``` */ - scale(image: I, f: number, mode?: ResizeStrategy) { - if (typeof f !== "number") { - throw new Error("f must be a number"); - } - - if (f < 0) { - throw new Error("f must be a positive number"); - } - + scale(image: I, options: ScaleOptions) { + const { f, mode } = + typeof options === "number" + ? ({ f: options } as ScaleComplexOptions) + : ScaleComplexOptionsSchema.parse(options); const w = image.bitmap.width * f; const h = image.bitmap.height * f; - this.resize(image, w, h, mode); - return image; + return this.resize(image, { w, h, mode: mode }); }, /** @@ -123,23 +150,13 @@ export const methods = { * image.scaleToFit(100, 100); * ``` */ - scaleToFit( - image: I, - w: number, - h: number, - mode?: ResizeStrategy - ) { - if (typeof w !== "number" || typeof h !== "number") { - throw new Error("w and h must be numbers"); - } - + scaleToFit(image: I, options: ScaleToFitOptions) { + const { h, w, mode } = ScaleToFitOptionsSchema.parse(options); const f = w / h > image.bitmap.width / image.bitmap.height ? h / image.bitmap.height : w / image.bitmap.width; - this.scale(image, f, mode); - - return image; + return this.scale(image, { f, mode: mode }); }, }; diff --git a/plugins/plugin-rotate/package.json b/plugins/plugin-rotate/package.json index 4f96012e..0f71a1ba 100644 --- a/plugins/plugin-rotate/package.json +++ b/plugins/plugin-rotate/package.json @@ -12,24 +12,25 @@ "author": "Andrew Lisowski ", "license": "MIT", "dependencies": { + "@jimp/core": "workspace:*", "@jimp/plugin-crop": "workspace:*", "@jimp/plugin-resize": "workspace:*", + "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "@jimp/core": "workspace:*", - "@jimp/types": "workspace:*" + "zod": "^3.22.4" }, "devDependencies": { - "@jimp/test-utils": "workspace:*", - "@jimp/core": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/core": "workspace:*", + "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-rotate/src/index.test.ts b/plugins/plugin-rotate/src/index.test.ts index 8e4df621..312d4cd5 100644 --- a/plugins/plugin-rotate/src/index.test.ts +++ b/plugins/plugin-rotate/src/index.test.ts @@ -76,7 +76,9 @@ describe("Rotate a non-square image without resizing", () => { angles.forEach((angle) => { test(`${angle} degrees`, () => { - expect(imgSrc.clone().rotate(angle, false)).toMatchSnapshot(); + expect( + imgSrc.clone().rotate({ deg: angle, mode: false }) + ).toMatchSnapshot(); }); }); }); diff --git a/plugins/plugin-rotate/src/index.ts b/plugins/plugin-rotate/src/index.ts index d35263e5..7484d8a2 100644 --- a/plugins/plugin-rotate/src/index.ts +++ b/plugins/plugin-rotate/src/index.ts @@ -3,6 +3,19 @@ import { JimpClass } from "@jimp/types"; import { clone } from "@jimp/utils"; import { composite } from "@jimp/core"; import { methods as cropMethods } from "@jimp/plugin-crop"; +import { z } from "zod"; + +const RotateOptionsSchema = z.union([ + z.number(), + z.object({ + /** the number of degrees to rotate the image by */ + deg: z.number(), + /** resize mode or a boolean, if false then the width and height of the image will not be changed */ + mode: z.union([z.boolean(), z.nativeEnum(ResizeStrategy)]).optional(), + }), +]); + +export type RotateOptions = z.infer; /** function to translate the x, y coordinate to the index of the pixel in the buffer */ function createIdxTranslationFunction(w: number, h: number) { @@ -107,7 +120,6 @@ function createTranslationFunction(deltaX: number, deltaY: number) { /** * Rotates an image counter-clockwise by an arbitrary number of degrees. NB: 'this' must be a Jimp object. * @param {number} deg the number of degrees to rotate the image by - * @param {string|boolean} mode (optional) resize mode or a boolean, if false then the width and height of the image will not be changed */ function advancedRotate( image: I, @@ -155,12 +167,11 @@ function advancedRotate( }); const max = Math.max(w, h, image.bitmap.width, image.bitmap.height); - image = resizeMethods.resize( - image, - max, - max, - mode === true ? undefined : mode - ); + image = resizeMethods.resize(image, { + h: max, + w: max, + mode: mode === true ? undefined : mode, + }); image = composite( image, @@ -206,14 +217,13 @@ function advancedRotate( // now crop the image to the final size const x = Math.max(bW / 2 - w / 2, 0); const y = Math.max(bH / 2 - h / 2, 0); - image = cropMethods.crop(image, x, y, w, h); + image = cropMethods.crop(image, { x, y, w, h }); } } export const methods = { /** * Rotates the image counter-clockwise by a number of degrees. By default the width and height of the image will be resized appropriately. - * @param deg the number of degrees to rotate the image by * @example * ```ts * import { Jimp } from "jimp"; @@ -223,19 +233,10 @@ export const methods = { * image.rotate(90); * ``` */ - rotate( - image: I, - deg: number, - mode: boolean | ResizeStrategy = true - ) { - if (typeof deg !== "number") { - throw new Error("deg must be a number"); - } - - if (typeof mode !== "boolean" && typeof mode !== "string") { - throw new Error("mode must be a boolean or a string"); - } - + rotate(image: I, options: RotateOptions) { + const parsed = RotateOptionsSchema.parse(options); + const { deg, mode = true } = + typeof parsed === "number" ? { deg: parsed } : parsed; // use matrixRotate if the angle is a multiple of 90 degrees (eg: 180 or -90) and resize is allowed or not needed. const matrixRotateAllowed = deg % 90 === 0 && diff --git a/plugins/plugin-shadow/package.json b/plugins/plugin-shadow/package.json index 583cda2a..319e6fe4 100644 --- a/plugins/plugin-shadow/package.json +++ b/plugins/plugin-shadow/package.json @@ -12,24 +12,25 @@ "author": "Andrew Lisowski ", "license": "MIT", "devDependencies": { - "@jimp/test-utils": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "dependencies": { + "@jimp/core": "workspace:*", "@jimp/plugin-blur": "workspace:*", "@jimp/plugin-color": "workspace:*", "@jimp/plugin-resize": "workspace:*", - "@jimp/core": "workspace:*", "@jimp/types": "workspace:*", - "@jimp/utils": "workspace:*" + "@jimp/utils": "workspace:*", + "zod": "^3.22.4" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-shadow/src/index.ts b/plugins/plugin-shadow/src/index.ts index 6ebff482..6e203a9a 100644 --- a/plugins/plugin-shadow/src/index.ts +++ b/plugins/plugin-shadow/src/index.ts @@ -4,19 +4,22 @@ import { clone } from "@jimp/utils"; import { methods as resizeMethods } from "@jimp/plugin-resize"; import { methods as blurMethods } from "@jimp/plugin-blur"; import { methods as colorMethods } from "@jimp/plugin-color"; +import { z } from "zod"; -interface ShadowOptions { +const ShadowOptionsSchema = z.object({ /** opacity of the shadow between 0 and 1 */ - opacity?: number; + opacity: z.number().min(0).max(1).optional(), /** size of the shadow */ - size?: number; + size: z.number().optional(), /** how blurry the shadow is */ - blur?: number; + blur: z.number().optional(), /** x position of shadow */ - x?: number; + x: z.number().optional(), /** y position of shadow */ - y?: number; -} + y: z.number().optional(), +}); + +export type ShadowOptions = z.infer; export const methods = { /** @@ -29,17 +32,16 @@ export const methods = { x = -25, y = 25, blur: blurAmount = 5, - } = options; + } = ShadowOptionsSchema.parse(options); // clone the image const orig = clone(image); let shadow = clone(image); // enlarge it. This creates a "shadow". - shadow = resizeMethods.resize( - shadow, - image.bitmap.width * size, - image.bitmap.height * size - ); + shadow = resizeMethods.resize(shadow, { + w: image.bitmap.width * size, + h: image.bitmap.height * size, + }); shadow = blurMethods.blur(shadow, blurAmount); shadow = colorMethods.opacity(shadow, opacityAmount); diff --git a/plugins/plugin-threshold/package.json b/plugins/plugin-threshold/package.json index 83a42437..868f7cd2 100644 --- a/plugins/plugin-threshold/package.json +++ b/plugins/plugin-threshold/package.json @@ -12,24 +12,25 @@ "author": "Andrew Lisowski ", "license": "MIT", "devDependencies": { - "@jimp/test-utils": "workspace:*", - "@jimp/js-jpeg": "workspace:*", "@jimp/config-eslint": "workspace:*", "@jimp/config-typescript": "workspace:*", "@jimp/config-vitest": "workspace:*", + "@jimp/js-jpeg": "workspace:*", + "@jimp/test-utils": "workspace:*", + "@vitest/browser": "^1.4.0", "eslint": "^8.57.0", "tshy": "^1.12.0", "typescript": "^5.4.2", - "@vitest/browser": "^1.4.0", - "vitest": "^1.4.0", - "vite-plugin-node-polyfills": "^0.21.0" + "vite-plugin-node-polyfills": "^0.21.0", + "vitest": "^1.4.0" }, "dependencies": { "@jimp/core": "workspace:*", "@jimp/plugin-color": "workspace:*", "@jimp/plugin-hash": "workspace:*", + "@jimp/types": "workspace:*", "@jimp/utils": "workspace:*", - "@jimp/types": "workspace:*" + "zod": "^3.22.4" }, "tshy": { "exclude": [ diff --git a/plugins/plugin-threshold/src/index.ts b/plugins/plugin-threshold/src/index.ts index 4f5f8f4c..38e9972d 100644 --- a/plugins/plugin-threshold/src/index.ts +++ b/plugins/plugin-threshold/src/index.ts @@ -1,15 +1,18 @@ import { JimpClass } from "@jimp/types"; import { limit255 } from "@jimp/utils"; import { methods as color } from "@jimp/plugin-color"; +import { z } from "zod"; -export interface ThresholdOptions { +const ThresholdOptionsSchema = z.object({ /** A number auto limited between 0 - 255 */ - max: number; + max: z.number().min(0).max(255), /** A number auto limited between 0 - 255 (default 255) */ - replace?: number; + replace: z.number().min(0).max(255).optional(), /** A boolean whether to apply greyscale beforehand (default true) */ - autoGreyscale?: boolean; -} + autoGreyscale: z.boolean().optional(), +}); + +export type ThresholdOptions = z.infer; export const methods = { /** @@ -25,19 +28,11 @@ export const methods = { * ``` */ threshold(image: I, options: ThresholdOptions) { - let { max, replace = 255, autoGreyscale = true } = options; - - if (typeof max !== "number") { - throw new Error("max must be a number"); - } - - if (typeof replace !== "number") { - throw new Error("replace must be a number"); - } - - if (typeof autoGreyscale !== "boolean") { - throw new Error("autoGreyscale must be a boolean"); - } + let { + max, + replace = 255, + autoGreyscale = true, + } = ThresholdOptionsSchema.parse(options); max = limit255(max); replace = limit255(replace); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0646670a..78ff91b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -399,6 +399,10 @@ importers: version: 1.4.0(@types/node@20.11.28)(@vitest/browser@1.4.0) packages/types: + dependencies: + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -688,6 +692,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -768,6 +775,9 @@ importers: '@jimp/types': specifier: workspace:* version: link:../../packages/types + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -817,6 +827,9 @@ importers: tinycolor2: specifier: ^1.6.0 version: 1.6.0 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -875,6 +888,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -924,6 +940,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -967,6 +986,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1007,6 +1029,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1063,6 +1088,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1103,6 +1131,9 @@ importers: '@jimp/types': specifier: workspace:* version: link:../../packages/types + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1210,6 +1241,9 @@ importers: '@jimp/types': specifier: workspace:* version: link:../../packages/types + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1262,6 +1296,9 @@ importers: '@jimp/types': specifier: workspace:* version: link:../../packages/types + '@jimp/utils': + specifier: workspace:* + version: link:../../packages/utils parse-bmfont-ascii: specifier: ^1.0.6 version: 1.0.6 @@ -1271,6 +1308,9 @@ importers: parse-bmfont-xml: specifier: ^1.1.6 version: 1.1.6 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1302,6 +1342,9 @@ importers: '@jimp/types': specifier: workspace:* version: link:../../packages/types + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1351,6 +1394,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1403,6 +1449,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:* @@ -1452,6 +1501,9 @@ importers: '@jimp/utils': specifier: workspace:* version: link:../../packages/utils + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@jimp/config-eslint': specifier: workspace:*