226 lines
9.1 KiB
JavaScript
226 lines
9.1 KiB
JavaScript
import { colorDiff, intToRGBA, scan } from "@jimp/utils";
|
|
import { z } from "zod";
|
|
export const CropOptionsSchema = z.object({
|
|
/** the x position to crop form */
|
|
x: z.number(),
|
|
/** the y position to crop form */
|
|
y: z.number(),
|
|
/** the width to crop form */
|
|
w: z.number(),
|
|
/** the height to crop form */
|
|
h: z.number(),
|
|
});
|
|
const AutocropComplexOptionsSchema = z.object({
|
|
/** percent of color difference tolerance (default value) */
|
|
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: z.boolean().optional(),
|
|
/** force cropping top be symmetric */
|
|
cropSymmetric: z.boolean().optional(),
|
|
/** Amount of pixels in border to leave */
|
|
leaveBorder: z.number().optional(),
|
|
ignoreSides: z
|
|
.object({
|
|
north: z.boolean().optional(),
|
|
south: z.boolean().optional(),
|
|
east: z.boolean().optional(),
|
|
west: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
});
|
|
export const methods = {
|
|
/**
|
|
* Crops the image at a given point to a give size.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* import { Jimp } from "jimp";
|
|
*
|
|
* const image = await Jimp.read("test/image.png");
|
|
* const cropped = image.crop(150, 100);
|
|
* ```
|
|
*/
|
|
crop(image, options) {
|
|
let { x, y, w, h } = CropOptionsSchema.parse(options);
|
|
// round input
|
|
x = Math.round(x);
|
|
y = Math.round(y);
|
|
w = Math.round(w);
|
|
h = Math.round(h);
|
|
if (x === 0 && w === image.bitmap.width) {
|
|
// shortcut
|
|
const start = (w * y + x) << 2;
|
|
const end = start + ((h * w) << 2);
|
|
image.bitmap.data = image.bitmap.data.slice(start, end);
|
|
}
|
|
else {
|
|
const bitmap = Buffer.allocUnsafe(w * h * 4);
|
|
let offset = 0;
|
|
scan(image, x, y, w, h, function (_, __, idx) {
|
|
const data = image.bitmap.data.readUInt32BE(idx);
|
|
bitmap.writeUInt32BE(data, offset);
|
|
offset += 4;
|
|
});
|
|
image.bitmap.data = bitmap;
|
|
}
|
|
image.bitmap.width = w;
|
|
image.bitmap.height = h;
|
|
return image;
|
|
},
|
|
/**
|
|
* Autocrop same color borders from this image.
|
|
* This function will attempt to crop out transparent pixels from the image.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* import { Jimp } from "jimp";
|
|
*
|
|
* const image = await Jimp.read("test/image.png");
|
|
* const cropped = image.autocrop();
|
|
* ```
|
|
*/
|
|
autocrop(image, options = {}) {
|
|
const { tolerance = 0.0002, cropOnlyFrames = true, cropSymmetric = false, leaveBorder = 0, ignoreSides: ignoreSidesArg, } = typeof options === "number"
|
|
? { tolerance: options }
|
|
: AutocropComplexOptionsSchema.parse(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
|
|
// i.e. north and south / east and west are cropped by the same value
|
|
const ignoreSides = {
|
|
north: false,
|
|
south: false,
|
|
east: false,
|
|
west: false,
|
|
...ignoreSidesArg,
|
|
};
|
|
/**
|
|
* All borders must be of the same color as the top left pixel, to be cropped.
|
|
* It should be possible to crop borders each with a different color,
|
|
* but since there are many ways for corners to intersect, it would
|
|
* introduce unnecessary complexity to the algorithm.
|
|
*/
|
|
// scan each side for same color borders
|
|
let colorTarget = image.getPixelColor(0, 0); // top left pixel color is the target color
|
|
const rgba1 = intToRGBA(colorTarget);
|
|
// for north and east sides
|
|
let northPixelsToCrop = 0;
|
|
let eastPixelsToCrop = 0;
|
|
let southPixelsToCrop = 0;
|
|
let westPixelsToCrop = 0;
|
|
// north side (scan rows from north to south)
|
|
colorTarget = image.getPixelColor(0, 0);
|
|
if (!ignoreSides.north) {
|
|
north: for (let y = 0; y < h - minPixelsPerSide; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const colorXY = image.getPixelColor(x, y);
|
|
const rgba2 = intToRGBA(colorXY);
|
|
if (colorDiff(rgba1, rgba2) > tolerance) {
|
|
// this pixel is too distant from the first one: abort this side scan
|
|
break north;
|
|
}
|
|
}
|
|
// this row contains all pixels with the same color: increment this side pixels to crop
|
|
northPixelsToCrop++;
|
|
}
|
|
}
|
|
// west side (scan columns from west to east)
|
|
colorTarget = image.getPixelColor(w, 0);
|
|
if (!ignoreSides.west) {
|
|
west: for (let x = 0; x < w - minPixelsPerSide; x++) {
|
|
for (let y = 0 + northPixelsToCrop; y < h; y++) {
|
|
const colorXY = image.getPixelColor(x, y);
|
|
const rgba2 = intToRGBA(colorXY);
|
|
if (colorDiff(rgba1, rgba2) > tolerance) {
|
|
// this pixel is too distant from the first one: abort this side scan
|
|
break west;
|
|
}
|
|
}
|
|
// this column contains all pixels with the same color: increment this side pixels to crop
|
|
westPixelsToCrop++;
|
|
}
|
|
}
|
|
// south side (scan rows from south to north)
|
|
colorTarget = image.getPixelColor(0, h);
|
|
if (!ignoreSides.south) {
|
|
south: for (let y = h - 1; y >= northPixelsToCrop + minPixelsPerSide; y--) {
|
|
for (let x = w - eastPixelsToCrop - 1; x >= 0; x--) {
|
|
const colorXY = image.getPixelColor(x, y);
|
|
const rgba2 = intToRGBA(colorXY);
|
|
if (colorDiff(rgba1, rgba2) > tolerance) {
|
|
// this pixel is too distant from the first one: abort this side scan
|
|
break south;
|
|
}
|
|
}
|
|
// this row contains all pixels with the same color: increment this side pixels to crop
|
|
southPixelsToCrop++;
|
|
}
|
|
}
|
|
// east side (scan columns from east to west)
|
|
colorTarget = image.getPixelColor(w, h);
|
|
if (!ignoreSides.east) {
|
|
east: for (let x = w - 1; x >= 0 + westPixelsToCrop + minPixelsPerSide; x--) {
|
|
for (let y = h - 1; y >= 0 + northPixelsToCrop; y--) {
|
|
const colorXY = image.getPixelColor(x, y);
|
|
const rgba2 = intToRGBA(colorXY);
|
|
if (colorDiff(rgba1, rgba2) > tolerance) {
|
|
// this pixel is too distant from the first one: abort this side scan
|
|
break east;
|
|
}
|
|
}
|
|
// this column contains all pixels with the same color: increment this side pixels to crop
|
|
eastPixelsToCrop++;
|
|
}
|
|
}
|
|
// decide if a crop is needed
|
|
let doCrop = false;
|
|
// apply leaveBorder
|
|
westPixelsToCrop -= leaveBorder;
|
|
eastPixelsToCrop -= leaveBorder;
|
|
northPixelsToCrop -= leaveBorder;
|
|
southPixelsToCrop -= leaveBorder;
|
|
if (cropSymmetric) {
|
|
const horizontal = Math.min(eastPixelsToCrop, westPixelsToCrop);
|
|
const vertical = Math.min(northPixelsToCrop, southPixelsToCrop);
|
|
westPixelsToCrop = horizontal;
|
|
eastPixelsToCrop = horizontal;
|
|
northPixelsToCrop = vertical;
|
|
southPixelsToCrop = vertical;
|
|
}
|
|
// make sure that crops are >= 0
|
|
westPixelsToCrop = westPixelsToCrop >= 0 ? westPixelsToCrop : 0;
|
|
eastPixelsToCrop = eastPixelsToCrop >= 0 ? eastPixelsToCrop : 0;
|
|
northPixelsToCrop = northPixelsToCrop >= 0 ? northPixelsToCrop : 0;
|
|
southPixelsToCrop = southPixelsToCrop >= 0 ? southPixelsToCrop : 0;
|
|
// safety checks
|
|
const widthOfRemainingPixels = w - (westPixelsToCrop + eastPixelsToCrop);
|
|
const heightOfRemainingPixels = h - (southPixelsToCrop + northPixelsToCrop);
|
|
if (cropOnlyFrames) {
|
|
// crop image if all sides should be cropped
|
|
doCrop =
|
|
eastPixelsToCrop !== 0 &&
|
|
northPixelsToCrop !== 0 &&
|
|
westPixelsToCrop !== 0 &&
|
|
southPixelsToCrop !== 0;
|
|
}
|
|
else {
|
|
// crop image if at least one side should be cropped
|
|
doCrop =
|
|
eastPixelsToCrop !== 0 ||
|
|
northPixelsToCrop !== 0 ||
|
|
westPixelsToCrop !== 0 ||
|
|
southPixelsToCrop !== 0;
|
|
}
|
|
if (doCrop) {
|
|
// do the real crop
|
|
this.crop(image, {
|
|
x: westPixelsToCrop,
|
|
y: northPixelsToCrop,
|
|
w: widthOfRemainingPixels,
|
|
h: heightOfRemainingPixels,
|
|
});
|
|
}
|
|
return image;
|
|
},
|
|
};
|
|
//# sourceMappingURL=index.js.map
|