
Baoyu Compress Image
- 25.9k installs
- 24.2k repo stars
- Updated July 4, 2026
- jimliu/baoyu-skills
baoyu-compress-image is a Claude Code skill that compresses images to WebP, PNG, or JPEG from the terminal using the best available local compressor.
About
baoyu-compress-image is a terminal skill that compresses images in a file or folder to WebP, PNG, or JPEG using whichever local compressor is available. It auto-selects among sips, cwebp, ImageMagick, and Sharp, and supports quality control, recursive traversal, keep-original, and JSON output. A developer runs it before deploy to shrink public/ or asset directories. It reports input size, output size, and reduction ratio per file.
- Auto-selects sips, cwebp, ImageMagick, or Sharp
- Outputs WebP (default), PNG, or JPEG
- Recursive directory traversal with --recursive
- JSON output mode for CI logs
- Requires bun or npx to run the CLI
Baoyu Compress Image by the numbers
- 25,878 all-time installs (skills.sh)
- +386 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #20 of 560 CLI & Terminal skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
baoyu-compress-image capabilities & compatibility
Free, no API key; runs against local image compressors.
- Capabilities
- image compression · webp conversion · asset optimization
- Use cases
- image generation
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What baoyu-compress-image says it does
Compresses images using best available tool (sips → cwebp → ImageMagick → Sharp).
description: Compresses images to WebP (default) or PNG with automatic tool selection. Use when user asks to "compress image", "optimize image", "convert to webp", or reduce image file size.
npx skills add https://github.com/jimliu/baoyu-skills --skill baoyu-compress-imageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25.9k |
|---|---|
| repo stars | ★ 24.2k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 4, 2026 |
| Repository | jimliu/baoyu-skills ↗ |
How do I shrink images in my project folder without leaving the terminal or wiring up an image service?
Compress and convert images to WebP/PNG/JPEG from the terminal before deploy to cut page weight and bundle size.
Who is it for?
Developers shrinking image assets before deploy to reduce page weight or app bundle size without a hosted image service.
Skip if: Users who need cloud image transforms, on-the-fly resizing, or a CDN-backed image pipeline rather than a one-shot local compression pass.
When should I use this skill?
When the user asks to compress an image, optimize an image, convert to webp, or reduce image file size.
What you get
Images are re-encoded to a smaller format at a chosen quality, with per-file size and reduction ratio reported.
- compressed image files
- per-file size and reduction report
By the numbers
- auto-selects among 4 compressors
- supports 6 input extensions
- default quality 80
Files
Image Compressor
Compresses images using best available tool (sips → cwebp → ImageMagick → Sharp).
Script Directory
Scripts in scripts/ subdirectory. {baseDir} = this SKILL.md's directory path. Resolve ${BUN_X} runtime: if bun installed → bun; if npx available → npx -y bun; else suggest installing bun. Replace {baseDir} and ${BUN_X} with actual values.
| Script | Purpose |
|---|---|
scripts/main.ts | Image compression CLI |
Preferences (EXTEND.md)
Check EXTEND.md in priority order — the first one found wins:
| Priority | Path | Scope |
|---|---|---|
| 1 | .baoyu-skills/baoyu-compress-image/EXTEND.md | Project |
| 2 | ${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-compress-image/EXTEND.md | XDG |
| 3 | $HOME/.baoyu-skills/baoyu-compress-image/EXTEND.md | User home |
If none found, use defaults.
EXTEND.md supports: Default format, default quality, keep-original preference.
Usage
${BUN_X} {baseDir}/scripts/main.ts <input> [options]Options
| Option | Short | Description | Default |
|---|---|---|---|
<input> | File or directory | Required | |
--output | -o | Output path | Same path, new ext |
--format | -f | webp, png, jpeg | webp |
--quality | -q | Quality 0-100 | 80 |
--keep | -k | Keep original | false |
--recursive | -r | Process subdirs | false |
--json | JSON output | false |
Examples
# Single file → WebP (replaces original)
${BUN_X} {baseDir}/scripts/main.ts image.png
# Keep PNG format
${BUN_X} {baseDir}/scripts/main.ts image.png -f png --keep
# Directory recursive
${BUN_X} {baseDir}/scripts/main.ts ./images/ -r -q 75
# JSON output
${BUN_X} {baseDir}/scripts/main.ts image.png --jsonOutput:
image.png → image.webp (245KB → 89KB, 64% reduction)Extension Support
Custom configurations via EXTEND.md. See Preferences section for paths and supported options.
#!/usr/bin/env bun
import { existsSync, statSync, readdirSync, unlinkSync, renameSync } from "fs";
import { basename, dirname, extname, join, resolve } from "path";
import { spawn } from "child_process";
type Compressor = "sips" | "cwebp" | "imagemagick" | "sharp";
type Format = "webp" | "png" | "jpeg";
interface Options {
input: string;
output?: string;
format: Format;
quality: number;
keep: boolean;
recursive: boolean;
json: boolean;
}
interface Result {
input: string;
output: string;
inputSize: number;
outputSize: number;
ratio: number;
compressor: Compressor;
}
const SUPPORTED_EXTS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".tiff"];
async function commandExists(cmd: string): Promise<boolean> {
try {
const proc = spawn("which", [cmd], { stdio: "pipe" });
return new Promise((res) => {
proc.on("close", (code) => res(code === 0));
proc.on("error", () => res(false));
});
} catch {
return false;
}
}
async function detectCompressor(format: Format): Promise<Compressor> {
if (format === "webp") {
if (await commandExists("cwebp")) return "cwebp";
if (await commandExists("convert")) return "imagemagick";
return "sharp";
}
if (process.platform === "darwin") return "sips";
if (await commandExists("convert")) return "imagemagick";
return "sharp";
}
function runCmd(cmd: string, args: string[]): Promise<{ code: number; stderr: string }> {
return new Promise((res) => {
const proc = spawn(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
let stderr = "";
proc.stderr?.on("data", (d) => (stderr += d.toString()));
proc.on("close", (code) => res({ code: code ?? 1, stderr }));
proc.on("error", (e) => res({ code: 1, stderr: e.message }));
});
}
async function compressWithSips(input: string, output: string, format: Format, quality: number): Promise<void> {
const fmt = format === "jpeg" ? "jpeg" : format;
const args = ["-s", "format", fmt, "-s", "formatOptions", String(quality), input, "--out", output];
const { code, stderr } = await runCmd("sips", args);
if (code !== 0) throw new Error(`sips failed: ${stderr}`);
}
async function compressWithCwebp(input: string, output: string, quality: number): Promise<void> {
const args = ["-q", String(quality), input, "-o", output];
const { code, stderr } = await runCmd("cwebp", args);
if (code !== 0) throw new Error(`cwebp failed: ${stderr}`);
}
async function compressWithImagemagick(input: string, output: string, quality: number): Promise<void> {
const args = [input, "-quality", String(quality), output];
const { code, stderr } = await runCmd("convert", args);
if (code !== 0) throw new Error(`convert failed: ${stderr}`);
}
async function compressWithSharp(input: string, output: string, format: Format, quality: number): Promise<void> {
const sharp = (await import("sharp")).default;
let pipeline = sharp(input);
if (format === "webp") pipeline = pipeline.webp({ quality });
else if (format === "png") pipeline = pipeline.png({ quality });
else if (format === "jpeg") pipeline = pipeline.jpeg({ quality });
await pipeline.toFile(output);
}
async function compress(
compressor: Compressor,
input: string,
output: string,
format: Format,
quality: number
): Promise<void> {
switch (compressor) {
case "sips":
await compressWithSips(input, output, format, quality);
break;
case "cwebp":
if (format !== "webp") {
await compressWithSharp(input, output, format, quality);
} else {
await compressWithCwebp(input, output, quality);
}
break;
case "imagemagick":
await compressWithImagemagick(input, output, quality);
break;
case "sharp":
await compressWithSharp(input, output, format, quality);
break;
}
}
function getOutputPath(input: string, format: Format, keep: boolean, customOutput?: string): string {
if (customOutput) return resolve(customOutput);
const dir = dirname(input);
const base = basename(input, extname(input));
const ext = format === "jpeg" ? ".jpg" : `.${format}`;
if (keep && extname(input).toLowerCase() === ext) {
return join(dir, `${base}-compressed${ext}`);
}
return join(dir, `${base}${ext}`);
}
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
async function processFile(
compressor: Compressor,
input: string,
opts: Options
): Promise<Result> {
const absInput = resolve(input);
const inputSize = statSync(absInput).size;
const output = getOutputPath(absInput, opts.format, opts.keep, opts.output);
const tempOutput = output + ".tmp";
await compress(compressor, absInput, tempOutput, opts.format, opts.quality);
const outputSize = statSync(tempOutput).size;
if (!opts.keep && absInput !== output) {
const ext = extname(absInput);
const base = absInput.slice(0, -ext.length);
renameSync(absInput, `${base}_original${ext}`);
}
renameSync(tempOutput, output);
return {
input: absInput,
output,
inputSize,
outputSize,
ratio: outputSize / inputSize,
compressor,
};
}
function collectFiles(dir: string, recursive: boolean): string[] {
const files: string[] = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory() && recursive) {
files.push(...collectFiles(full, recursive));
} else if (entry.isFile() && SUPPORTED_EXTS.includes(extname(entry.name).toLowerCase())) {
files.push(full);
}
}
return files;
}
function printHelp() {
console.log(`Usage: bun main.ts <input> [options]
Options:
-o, --output <path> Output path
-f, --format <fmt> Output format: webp, png, jpeg (default: webp)
-q, --quality <n> Quality 0-100 (default: 80)
-k, --keep Keep original file
-r, --recursive Process directories recursively
--json JSON output
-h, --help Show help`);
}
function parseArgs(args: string[]): Options | null {
const opts: Options = {
input: "",
format: "webp",
quality: 80,
keep: false,
recursive: false,
json: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "-h" || arg === "--help") {
printHelp();
process.exit(0);
} else if (arg === "-o" || arg === "--output") {
opts.output = args[++i];
} else if (arg === "-f" || arg === "--format") {
const fmt = args[++i]?.toLowerCase();
if (fmt === "webp" || fmt === "png" || fmt === "jpeg" || fmt === "jpg") {
opts.format = fmt === "jpg" ? "jpeg" : (fmt as Format);
} else {
console.error(`Invalid format: ${fmt}`);
return null;
}
} else if (arg === "-q" || arg === "--quality") {
const q = parseInt(args[++i], 10);
if (isNaN(q) || q < 0 || q > 100) {
console.error(`Invalid quality: ${args[i]}`);
return null;
}
opts.quality = q;
} else if (arg === "-k" || arg === "--keep") {
opts.keep = true;
} else if (arg === "-r" || arg === "--recursive") {
opts.recursive = true;
} else if (arg === "--json") {
opts.json = true;
} else if (!arg.startsWith("-") && !opts.input) {
opts.input = arg;
}
}
if (!opts.input) {
console.error("Error: Input file or directory required");
printHelp();
return null;
}
return opts;
}
async function main() {
const args = process.argv.slice(2);
const opts = parseArgs(args);
if (!opts) process.exit(1);
const input = resolve(opts.input);
if (!existsSync(input)) {
console.error(`Error: ${input} not found`);
process.exit(1);
}
const compressor = await detectCompressor(opts.format);
const isDir = statSync(input).isDirectory();
if (isDir) {
const files = collectFiles(input, opts.recursive);
if (files.length === 0) {
console.error("No supported images found");
process.exit(1);
}
const results: Result[] = [];
for (const file of files) {
try {
const r = await processFile(compressor, file, { ...opts, output: undefined });
results.push(r);
if (!opts.json) {
const reduction = Math.round((1 - r.ratio) * 100);
console.log(`${r.input} → ${r.output} (${formatSize(r.inputSize)} → ${formatSize(r.outputSize)}, ${reduction}% reduction)`);
}
} catch (e) {
if (!opts.json) console.error(`Error processing ${file}: ${(e as Error).message}`);
}
}
if (opts.json) {
const totalInput = results.reduce((s, r) => s + r.inputSize, 0);
const totalOutput = results.reduce((s, r) => s + r.outputSize, 0);
console.log(
JSON.stringify({
files: results,
summary: {
totalFiles: results.length,
totalInputSize: totalInput,
totalOutputSize: totalOutput,
ratio: totalInput > 0 ? totalOutput / totalInput : 0,
compressor,
},
}, null, 2)
);
} else {
const totalInput = results.reduce((s, r) => s + r.inputSize, 0);
const totalOutput = results.reduce((s, r) => s + r.outputSize, 0);
const reduction = Math.round((1 - totalOutput / totalInput) * 100);
console.log(`\nProcessed ${results.length} files: ${formatSize(totalInput)} → ${formatSize(totalOutput)} (${reduction}% reduction)`);
}
} else {
try {
const r = await processFile(compressor, input, opts);
if (opts.json) {
console.log(JSON.stringify(r, null, 2));
} else {
const reduction = Math.round((1 - r.ratio) * 100);
console.log(`${r.input} → ${r.output} (${formatSize(r.inputSize)} → ${formatSize(r.outputSize)}, ${reduction}% reduction)`);
}
} catch (e) {
console.error(`Error: ${(e as Error).message}`);
process.exit(1);
}
}
}
main();
Related skills
Forks & variants (1)
Baoyu Compress Image has 1 known copy in the catalog totaling 25 installs. They canonicalize to this original listing.
- guanyang - 25 installs
How it compares
Use baoyu-compress-image for local batch asset shrinking; CDN image APIs suit runtime transforms instead.
FAQ
What image formats can it output?
WebP (the default), PNG, or JPEG, selected with the --format flag. It accepts .png, .jpg, .jpeg, .webp, .gif, and .tiff inputs.
What does it need installed?
It needs bun or npx to run the CLI script, and picks whichever image compressor is present locally: sips, cwebp, ImageMagick, or Sharp.
Is Baoyu Compress Image safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.