Files
tools/image-scale/main.ts
2026-03-08 17:28:22 +08:00

143 lines
4.1 KiB
TypeScript
Executable File

#!/usr/bin/env -S deno run -A
import sharp from "sharp";
import {parseArgs} from "@std/cli/parse-args";
/**
* Resize an image so that its smallest dimension equals the specified minimum pixels.
* Output format is always JPG.
*
* @param inputPath - Path to the input image (PNG or JPG)
* @param outputPath - Path to the output JPG image
* @param minPixels - The minimum dimension (width or height) in pixels
*/
export async function scaleImage(
inputPath: string,
outputPath: string,
minPixels: number,
quality?: number,
): Promise<void> {
if (minPixels <= 0) {
throw new Error("minPixels must be a positive number");
}
const metadata = await sharp(inputPath).metadata();
const originalWidth = metadata.width;
const originalHeight = metadata.height;
if (!originalWidth || !originalHeight) {
throw new Error("Could not determine image dimensions");
}
if (originalWidth < minPixels || originalHeight < minPixels) {
// original width or height is less than min pixels, should not scale image
throw new Error(
`Image width ${originalWidth} and/or height ${originalHeight} less then ${minPixels}, cannot scale image`,
);
}
// Calculate the scale factor based on the smaller dimension
const minDimension = Math.min(originalWidth, originalHeight);
const scaleFactor = minPixels / minDimension;
const newWidth = Math.round(originalWidth * scaleFactor);
const newHeight = Math.round(originalHeight * scaleFactor);
await sharp(inputPath)
.resize(newWidth, newHeight)
.jpeg({ quality: quality ?? 90 })
.toFile(outputPath);
console.log(
`Resized: ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight} (min dimension: ${minPixels}px)`,
);
}
/**
* Generate output path for the scaled image.
* Changes the extension to .jpg
*/
export function getOutputPath(
inputPath: string,
unsafeReplaceFile: boolean,
): string {
if (unsafeReplaceFile) {
const lowerInputPath = inputPath.toLowerCase();
if (lowerInputPath.endsWith(".jpg") || lowerInputPath.endsWith(".jpeg")) {
return inputPath;
}
}
const lastDot = inputPath.lastIndexOf(".");
return lastDot !== -1
? `${inputPath.slice(0, lastDot)}${unsafeReplaceFile ? "" : "_scaled"}.jpg`
: `${inputPath}${unsafeReplaceFile ? "" : "_scaled"}.jpg`;
}
function printUsage() {
console.log(`
Usage: deno run --allow-read --allow-write main.ts <input-image> [options]
Arguments:
input-image Path to the input image (PNG or JPG)
Options:
--min-pixels, -m Minimum dimension in pixels (default: 1200)
--output, -o Output directory or file path
--help, -h Show this help message
Examples:
deno run --allow-read --allow-write main.ts input.png -m 1200
deno run --allow-read --allow-write main.ts input.jpg -m 800 -o ./output
deno run --allow-read --allow-write main.ts input.png -m 1920 -o ./resized.jpg
`);
}
if (import.meta.main) {
const flags = parseArgs(Deno.args, {
string: ["output", "o", "min-pixels", "m", "quality", "q"],
boolean: ["help", "h", "unsafe-replace-file"],
alias: {
"min-pixels": "m",
output: "o",
help: "h",
quality: "q",
},
default: {
"min-pixels": "1200",
"quality": "90",
},
});
if (flags.help) {
printUsage();
Deno.exit(0);
}
const imagePaths = flags._;
if (imagePaths.length == 0) {
console.error("No image paths found.");
printUsage();
Deno.exit(1);
}
const minPixels = parseInt(flags["min-pixels"] as string, 10);
const quality = parseInt(flags["quality"] as string, 10);
const unsafeReplaceFile = flags["unsafe-replace-file"];
console.log(
`Image scale config, min-pixels: ${minPixels}, quality: ${quality}, unsafe-replace-file: ${unsafeReplaceFile}`,
);
for (const imagePath of imagePaths) {
const imagePathStr = imagePath as string;
const outputPath = getOutputPath(imagePathStr, unsafeReplaceFile);
try {
await scaleImage(imagePathStr, outputPath, minPixels, quality);
console.log(`Output saved to: ${outputPath}`);
} catch (error) {
console.error("Error:", error instanceof Error ? error.message : error);
}
}
}