feat: add "Crop to Content" mode to smart crop tool

Adds a new mode that trims uniform-color borders around the subject,
like GIMP's "Crop to Content." Includes configurable tolerance threshold
and optional pad-to-square with target size for e-commerce workflows.

The original attention-based crop is preserved as "Focus Crop" mode.

Closes #7
This commit is contained in:
Siddharth Kumar Sah
2026-04-04 19:49:49 +08:00
parent dba3f65d8c
commit 93ad7cdfc1
2 changed files with 295 additions and 112 deletions
+52 -12
View File
@@ -4,27 +4,67 @@ import { z } from "zod";
import { createToolRoute } from "../tool-factory.js";
const settingsSchema = z.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
mode: z.enum(["attention", "content"]).default("attention"),
// Attention mode: resize to target dimensions using subject detection
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
// Content mode: trim uniform borders, optionally pad to square
threshold: z.number().int().min(0).max(255).default(30),
padToSquare: z.boolean().default(false),
padColor: z.string().default("#ffffff"),
targetSize: z.number().int().positive().optional(),
});
/**
* Smart crop using Sharp's attention-based strategy.
* Uses entropy/saliency detection to find the most interesting region.
* No Python needed.
* Smart crop with two modes:
* - "attention": Sharp's entropy/saliency detection to crop to the most interesting region
* - "content": Trims uniform-color borders (like GIMP's "Crop to Content"),
* optionally pads to a square at a target size
*/
export function registerSmartCrop(app: FastifyInstance) {
createToolRoute(app, {
toolId: "smart-crop",
settingsSchema,
process: async (inputBuffer, settings, filename) => {
const result = await sharp(inputBuffer)
.resize(settings.width, settings.height, {
fit: "cover",
position: sharp.strategy.attention,
})
.png()
.toBuffer();
let result: Buffer;
if (settings.mode === "content") {
// Crop to content: trim uniform borders
const pipeline = sharp(inputBuffer).trim({ threshold: settings.threshold });
let trimmed = await pipeline.toBuffer({ resolveWithObject: true });
if (settings.padToSquare || settings.targetSize) {
const meta = await sharp(trimmed.data).metadata();
const w = meta.width ?? 1;
const h = meta.height ?? 1;
const target = settings.targetSize || Math.max(w, h);
const padR = Math.round(parseInt(settings.padColor.slice(1, 3), 16));
const padG = Math.round(parseInt(settings.padColor.slice(3, 5), 16));
const padB = Math.round(parseInt(settings.padColor.slice(5, 7), 16));
trimmed = await sharp(trimmed.data)
.resize({
width: target,
height: target,
fit: "contain",
background: { r: padR, g: padG, b: padB, alpha: 1 },
})
.toBuffer({ resolveWithObject: true });
}
result = trimmed.data;
} else {
// Attention mode: resize to target using subject detection
const w = settings.width ?? 1080;
const h = settings.height ?? 1080;
result = await sharp(inputBuffer)
.resize(w, h, {
fit: "cover",
position: sharp.strategy.attention,
})
.png()
.toBuffer();
}
const outputFilename = `${filename.replace(/\.[^.]+$/, "")}_smartcrop.png`;
return { buffer: result, filename: outputFilename, contentType: "image/png" };