add image-convert-scale-bun

This commit is contained in:
2026-05-18 01:23:53 +08:00
parent 7bb3313b79
commit 013da7eb01
6 changed files with 258 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+21
View File
@@ -0,0 +1,21 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "image-convert-scale",
"devDependencies": {
"@types/bun": "latest"
}
}
},
"packages": {
"@types/bun": ["@types/bun@1.3.14", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.14.tgz", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/node": ["@types/node@25.8.0", "https://registry.npmmirror.com/@types/node/-/node-25.8.0.tgz", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="],
"bun-types": ["bun-types@1.3.14", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"undici-types": ["undici-types@7.24.6", "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="]
}
}
+203
View File
@@ -0,0 +1,203 @@
import { parseArgs } from "util";
const SUPPORTED_INPUT_FORMATS = [".heic", ".jpeg", ".jpg", ".png", ".webp", ".gif", ".bmp", ".avif", ".tiff", ".tif"];
const OUTPUT_FORMATS = ["jpeg", "png"] as const;
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
function printUsage() {
console.log(`Usage: bun run index.ts [options] <image1> [image2] ...
Convert and scale images to JPEG or PNG format.
Arguments:
<image> ... Input image file(s) (heic, jpeg, png, webp, gif, bmp, avif, tiff)
Options:
-f, --format <format> Output format: jpeg or png (default: jpeg)
-w, --width <number> Scale image to this width (height auto-calculated)
-h, --height <number> Scale image to this height (width auto-calculated)
-s, --size <number> Scale image so the larger dimension matches this size
-o, --output <dir> Output directory (default: same as input)
--help Show this help message
Examples:
bun run index.ts photo.heic
bun run index.ts photo.jpg --format png
bun run index.ts photo.jpg --width 800
bun run index.ts photo.jpg --height 600
bun run index.ts a.jpg b.heic c.png --format png --width 1920
bun run index.ts photo.jpg --output ./converted --format jpeg --width 1280`);
}
function parseCliArgs(args: string[]) {
const { values, positionals } = parseArgs({
args,
options: {
format: { type: "string", short: "f", default: "jpeg" },
width: { type: "string", short: "w" },
height: { type: "string", short: "h" },
size: { type: "string", short: "s" },
output: { type: "string", short: "o" },
help: { type: "boolean" },
},
strict: false,
allowPositionals: true,
});
return { values, positionals };
}
function getOutputPath(inputPath: string, format: OutputFormat, outputDir?: string): string {
const base = inputPath.split("/").pop()!;
const nameWithoutExt = base.replace(/\.[^.]+$/, "");
const ext = format === "jpeg" ? "jpg" : "png";
const outputName = `${nameWithoutExt}_converted.${ext}`;
if (outputDir) {
return `${outputDir.replace(/\/$/, "")}/${outputName}`;
}
const dir = inputPath.includes("/") ? inputPath.substring(0, inputPath.lastIndexOf("/")) : ".";
return `${dir}/${outputName}`;
}
async function processImage(
inputPath: string,
outputPath: string,
format: OutputFormat,
targetWidth?: number,
targetHeight?: number,
targetSize?: number,
) {
const inputFile = Bun.file(inputPath);
if (!(await inputFile.exists())) {
console.error(`❌ File not found: ${inputPath}`);
return false;
}
const ext = "." + inputFile.name.split(".").pop()?.toLowerCase();
if (!SUPPORTED_INPUT_FORMATS.includes(ext)) {
console.error(`❌ Unsupported format: ${ext} (${inputPath})`);
return false;
}
const image = await inputFile.image();
const meta = await image.metadata();
const originalWidth = meta.width;
const originalHeight = meta.height;
// Resolve --size into width/height (scale by larger dimension, no upscaling)
if (targetSize) {
const largerDim = Math.max(originalWidth, originalHeight);
if (largerDim <= targetSize) {
console.warn(`⚠️ ${inputPath} already smaller than ${targetSize}px (${originalWidth}x${originalHeight}), skipping scale`);
targetSize = undefined;
} else if (originalWidth >= originalHeight) {
targetWidth = targetSize;
} else {
targetHeight = targetSize;
}
}
let processed: Image;
if (targetWidth && targetHeight) {
processed = await image.resize(targetWidth, targetHeight, { fit: "fill" });
} else if (targetWidth) {
processed = await image.resize(targetWidth);
} else if (targetHeight) {
const scale = targetHeight / originalHeight;
const scaledWidth = Math.round(originalWidth * scale);
processed = await image.resize(scaledWidth, targetHeight, { fit: "fill" });
} else {
processed = image;
}
let outputImg: Image;
if (format === "jpeg") {
outputImg = await processed.jpeg({ quality: 90 });
} else {
outputImg = await processed.png();
}
await Bun.write(outputPath, await outputImg.buffer());
const outMeta = await (await Bun.file(outputPath).image()).metadata();
const outputSize = (await Bun.file(outputPath).size).toLocaleString();
const scaled = outMeta.width !== originalWidth || outMeta.height !== originalHeight;
const sizeInfo = scaled
? `${originalWidth}x${originalHeight} -> ${outMeta.width}x${outMeta.height}`
: `${originalWidth}x${originalHeight}`;
console.log(`${inputPath} -> ${outputPath} (${sizeInfo}, ${outputSize} bytes)`);
return true;
}
async function main() {
const args = Bun.argv.slice(2);
if (args.length === 0 || args.includes("--help")) {
printUsage();
process.exit(0);
}
const { values, positionals } = parseCliArgs(args);
if (positionals.length === 0) {
console.error("❌ Error: No input files specified.\n");
printUsage();
process.exit(1);
}
const format = String(values.format).toLowerCase() as OutputFormat;
if (!OUTPUT_FORMATS.includes(format)) {
console.error(`❌ Error: Invalid format "${format}". Must be one of: ${OUTPUT_FORMATS.join(", ")}\n`);
process.exit(1);
}
const targetWidth = values.width ? parseInt(String(values.width), 10) : undefined;
const targetHeight = values.height ? parseInt(String(values.height), 10) : undefined;
const targetSize = values.size ? parseInt(String(values.size), 10) : undefined;
if (targetWidth !== undefined && (isNaN(targetWidth) || targetWidth <= 0)) {
console.error(`❌ Error: Invalid width "${values.width}". Must be a positive integer.\n`);
process.exit(1);
}
if (targetHeight !== undefined && (isNaN(targetHeight) || targetHeight <= 0)) {
console.error(`❌ Error: Invalid height "${values.height}". Must be a positive integer.\n`);
process.exit(1);
}
if (targetSize !== undefined && (isNaN(targetSize) || targetSize <= 0)) {
console.error(`❌ Error: Invalid size "${values.size}". Must be a positive integer.\n`);
process.exit(1);
}
if (targetWidth && targetHeight) {
console.log(`⚠️ Both width and height specified — using explicit resize (${targetWidth}x${targetHeight})`);
} else if (targetWidth) {
console.log(`📐 Scaling to width: ${targetWidth}px (height auto-calculated)`);
} else if (targetHeight) {
console.log(`📐 Scaling to height: ${targetHeight}px (width auto-calculated)`);
} else if (targetSize) {
console.log(`📐 Scaling by size: ${targetSize}px (larger dimension)`);
}
console.log(`🔄 Converting ${positionals.length} image(s) to ${format.toUpperCase()}...\n`);
let successCount = 0;
for (const inputPath of positionals) {
const outputDir = values.output ? String(values.output) : undefined;
const outputPath = getOutputPath(inputPath, format, outputDir);
const ok = await processImage(inputPath, outputPath, format, targetWidth, targetHeight, targetSize);
if (ok) successCount++;
}
console.log(`\n📊 Done: ${successCount}/${positionals.length} image(s) processed.`);
if (successCount === 0) {
process.exit(1);
}
}
main();
+12
View File
@@ -0,0 +1,12 @@
{
"name": "image-convert-scale",
"module": "index.ts",
"type": "module",
"private": true,
"scripts": {
"convert": "bun run index.ts"
},
"devDependencies": {
"@types/bun": "latest"
}
}
+7
View File
@@ -0,0 +1,7 @@
1. this is a bun project
2. read document https://bun.com/docs/runtime/image
3. load images including (heic,jpeg, png and other formats)
4. convert to jpeg or png file(user can assign the format, default jpeg)
5. user can 等比例 scale image, user can assign width or height
6. user can assign one or multiple images one time
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "."
},
"include": ["**/*.ts"]
}