#!/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 { 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 [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); } } }