
Canghe Compress Image
- 464 installs
- 424 repo stars
- Updated June 8, 2026
- freestylefly/canghe-skills
canghe-compress-image is an agent skill that compresses and optimizes images for developers who need faster-loading web, mobile, and content-pipeline assets without visible quality degradation.
About
canghe-compress-image from freestylefly/canghe-skills is an agent skill focused on compressing and optimizing raster images before they ship to web pages, mobile applications, or automated content pipelines. The workflow targets smaller file sizes and improved load metrics while preserving perceptual quality for hero banners, thumbnails, icons, and editorial imagery. Developers invoke it when build artifacts or CMS uploads are too heavy, Lighthouse flags oversized images, or CDN bandwidth needs trimming ahead of a release. The skill fits into frontend and mobile delivery paths where manual batch tools are slow or inconsistently applied across a repo. Expect guidance and execution around lossy and lossless optimization choices, format selection, and batch processing of image directories rather than vector or video transcoding.
- Lossy and lossless compression workflows
- Batch resize and format conversion
- Preserves visual quality targets
- Cuts page weight and CDN costs
- Ready for web and mobile asset pipelines
Canghe Compress Image by the numbers
- 464 all-time installs (skills.sh)
- Ranked #413 of 1,337 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/freestylefly/canghe-skills --skill canghe-compress-imageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 464 |
|---|---|
| repo stars | ★ 424 |
| Last updated | June 8, 2026 |
| Repository | freestylefly/canghe-skills ↗ |
How do you compress images for faster web loading?
Compress and optimize images before publishing to web pages, mobile apps, or content pipelines so assets load faster without visible quality loss.
Who is it for?
Developers preparing image-heavy frontends or mobile builds who need agent-driven compression before publish or CI asset upload.
Skip if: Teams optimizing video streams, vector SVG workflows only, or backend-only services with no static image assets.
When should I use this skill?
User asks to compress images, optimize PNG or JPEG assets, reduce image payload, or improve page load from heavy media files.
What you get
Optimized image files with reduced byte size suitable for web, mobile, and content CDN deployment.
- Compressed image files
- Reduced static asset payloads
Files
Image Compressor
Compresses images using best available tool (sips → cwebp → ImageMagick → Sharp).
Script Directory
Scripts in scripts/ subdirectory. Replace ${SKILL_DIR} with this SKILL.md's directory path.
| Script | Purpose |
|---|---|
scripts/main.ts | Image compression CLI |
Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
# Check project-level first
test -f .canghe-skills/canghe-compress-image/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.canghe-skills/canghe-compress-image/EXTEND.md" && echo "user"┌────────────────────────────────────────────────────────┬───────────────────┐ │ Path │ Location │ ├────────────────────────────────────────────────────────┼───────────────────┤ │ .canghe-skills/canghe-compress-image/EXTEND.md │ Project directory │ ├────────────────────────────────────────────────────────┼───────────────────┤ │ $HOME/.canghe-skills/canghe-compress-image/EXTEND.md │ User home │ └────────────────────────────────────────────────────────┴───────────────────┘
┌───────────┬───────────────────────────────────────────────────────────────────────────┐ │ Result │ Action │ ├───────────┼───────────────────────────────────────────────────────────────────────────┤ │ Found │ Read, parse, apply settings │ ├───────────┼───────────────────────────────────────────────────────────────────────────┤ │ Not found │ Use defaults │ └───────────┴───────────────────────────────────────────────────────────────────────────┘
EXTEND.md Supports: Default format | Default quality | Keep original preference
Usage
npx -y bun ${SKILL_DIR}/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)
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png
# Keep PNG format
npx -y bun ${SKILL_DIR}/scripts/main.ts image.png -f png --keep
# Directory recursive
npx -y bun ${SKILL_DIR}/scripts/main.ts ./images/ -r -q 75
# JSON output
npx -y bun ${SKILL_DIR}/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
How it compares
Use canghe-compress-image for pre-publish raster tuning; pair with build-time bundler plugins when optimization must be fully automated in CI.
FAQ
What assets does canghe-compress-image optimize?
canghe-compress-image optimizes raster images destined for web pages, mobile apps, and content pipelines—reducing byte size while aiming to avoid visible quality loss before publish.
When should developers run canghe-compress-image?
canghe-compress-image belongs in pre-ship workflows when Lighthouse, bundle size, or CDN cost flags oversized PNG, JPEG, or similar static images across a frontend or mobile release.