Skill: fuyunzhishang/pixmind-image-compress (version 1.0.1)

--- name: pixmind-image-compress description: "Cloud-powered image compression and resize — compress JPG/PNG/WebP/HEIC to WebP/AVIF, resize images, batch process folders. No local dependencies, uses Tencent Cloud COS imageMogr2 via Pixmind API. Save 60-90% file size with scene presets (web, wechat, email, thumbnail). Supports format conversion, quality control, and dimension adjustment." homepage: https://www.pixmind.io metadata: {"openclaw": {"requires": {"env": ["PIXMIND_API_KEY"]}, "primaryEnv": "PIXMIND_API_KEY"}} --- # Pixmind Image Compress Cloud-powered image compression and resize. Compress JPG, PNG, WebP, HEIC photos to WebP, AVIF or smaller JPG — save 60-90% file size with zero local dependencies. Powered by [Pixmind](https://www.pixmind.io) + Tencent Cloud COS [imageMogr2](https://cloud.tencent.com/document/product/460/36540). ## Why this skill? - **Zero install** — No sharp, ImageMagick, or native modules. Runs on any platform with just an API key. - **Cloud-processed** — Handles HEIC, AVIF, TPG formats that local tools struggle with. - **Scene presets** — One command for web, WeChat, email, or archival use. - **Batch ready** — Compress entire folders recursively. - **Safe by default** — Never overwrites originals, auto-numbered output. ## When to use **Use this skill when the user mentions:** 1. **Compress** — "图片太大"、"压缩一下"、"缩小文件"、"reduce image size"、"compress photo"、"file too large" 2. **Format conversion** — "转成 JPG"、"PNG 转 WebP"、"HEIC 转 JPG"、"convert to webp"、"change format" 3. **Resize** — "缩小到 1920px"、"调整尺寸"、"resize image"、"reduce dimensions"、"scale down" 4. **Batch** — "批量压缩"、"处理这个文件夹"、"compress all images"、"batch resize" 5. **Scene optimization** — "用于邮件附件"、"上传网站"、"微信发送"、"optimize for web"、"shrink for email" 6. **Typical triggers**: - "帮我压缩这张图片，文件太大了发不了微信" - "Screenshot.png is 10MB, make it smaller" - "Convert all PNGs in this folder to WebP" - "Resize these photos to max 1920px width" **Do NOT trigger for:** - Image generation (use `pixmind-image` skill) - Image editing (crop, rotate, watermark, filters) - Image analysis (OCR, content recognition, EXIF reading) ## Prerequisites 1. Register at [pixmind.io](https://www.pixmind.io/) — **200 bonus points on signup** (free trial) 2. Create an API key at [pixmind.io/api-keys](https://www.pixmind.io/api-keys) 3. Set env `PIXMIND_API_KEY` with your key ## Quick Start ```bash # Compress a single image (local file) node {baseDir}/compress.js --file ~/Desktop/photo.png --preset web # Compress from URL node {baseDir}/compress.js --url https://example.com/image.jpg --quality 80 --format webp # Resize to max 1920px width node {baseDir}/compress.js --file photo.jpg --width 1920 # Batch compress a folder node {baseDir}/compress.js --dir ~/Pictures/ --preset web --recursive ``` ## Presets Quick scene-based compression — no need to remember quality numbers: | Preset | Quality | Format | Use Case | Savings | |--------|---------|--------|----------|---------| | `web` | 75 | WebP | Website, blog, CMS upload | 60-70% | | `wechat` | 65 | JPG | WeChat, social media sharing | 70-80% | | `email` | 55 | JPG | Email attachments, fast transfer | 80-90% | | `quality` | 95 | original | High-quality archiving, print | 30-40% | | `thumbnail` | 60 | WebP | Thumbnails, previews, avatars | 80-90% | ## API Details **Endpoint**: `POST https://aihub-admin.aimix.pro/open/cos/image/compress` **Auth**: Header `X-[REDACTED_SECRET]` and `X-App-Key: {APP_KEY}` ### Request Body (JSON) | Parameter | Required | Type | Description | |-----------|----------|------|-------------| | `imageUrl` | One of | string | Image URL (COS or external link) | | `imageBase64` | One of | string | Base64 image data (supports `data:image/xxx;base64,` prefix) | | `format` | No | string | Output format: `webp`, `jpg`, `png`, `avif`, `heif`, `tpg` | | `quality` | No | number | Compression quality 1-100 (default: 75) | | `width` | No | number | Target width in pixels (maintains aspect ratio) | | `height` | No | number | Target height in pixels (maintains aspect ratio) | | `mode` | No | string | Response type: `url` (default) or `base64` | ### Response Format ```json { "code": 1000, "message": "success", "data": { "originalUrl": "https://bucket.cos.region.myqcloud.com/image.jpg", "compressedUrl": "https://bucket.cos.region.myqcloud.com/image.jpg?imageMogr2/format/webp/quality/75", "originalSize": 1024000, "compressedSize": 256000, "format": "webp", "quality": 75, "width": 800, "height": 600 } } ``` ## Supported Formats **Input**: JPG, JPEG, PNG, WebP, GIF, BMP, HEIC, HEIF **Output**: `webp`, `jpg`, `png`, `avif`, `heif`, `tpg` ## CLI Options | Option | Short | Type | Default | Description | |--------|-------|------|---------|-------------| | `--url` | `-u` | string | — | Image URL to compress | | `--file` | `-f` | string | — | Local image file path | | `--dir` | `-d` | string | — | Directory for batch processing | | `--format` | | string | original | Output: webp, jpg, png, avif, heif, tpg | | `--quality` | `-q` | number | 75 | Quality 1-100 | | `--width` | `-w` | number | — | Target width (maintain ratio) | | `--height` | | number | — | Target height (maintain ratio) | | `--preset` | `-p` | string | — | web, wechat, email, quality, thumbnail | | `--output` | `-o` | string | auto | Output directory | | `--recursive` | `-r` | boolean | false | Process subdirectories | | `--mode` | | string | url | Response: url or base64 | ## Output Rules - Compressed files saved to `~/Downloads/compressed-images/{date}/` by default - Original files are **never** modified or overwritten - Duplicate filenames auto-numbered: `photo_001.jpg`, `photo_002.jpg` - Batch mode preserves original directory structure ## Guidelines 1. For local files, the script auto-reads and converts to Base64 for upload 2. Default to quality 75 — good balance of quality and size 3. Match presets to use cases: `web` for sites, `wechat` for sharing, `email` for attachments 4. When resizing, specify only one dimension to maintain aspect ratio 5. For batch processing, show progress and total savings summary 6. After compression, always show before/after size and savings percentage 7. Prefer `webp` for web — best compression with good quality 8. For photos: recommend `jpg` or `webp`; for screenshots/diagrams: `png` or `webp` 9. HEIC (iPhone) photos are natively supported — no extra dependencies needed

path: compress.js
content: import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
import { join, dirname, basename, extname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { homedir } from 'os';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..', '..');

// Presets
const PRESETS = {
  web: { quality: 75, format: 'webp' },
  wechat: { quality: 65, format: 'jpg' },
  email: { quality: 55, format: 'jpg' },
  quality: { quality: 95 },
  thumbnail: { quality: 60, format: 'webp' },
};

const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.heic', '.heif', '.avif', '.tpg']);

// Load .env
function loadEnv() {
  try {
    const content = readFileSync(join(ROOT, '.env'), 'utf8');
    for (const line of content.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const eq = trimmed.indexOf('=');
      if (eq === -1) continue;
      const key = trimmed.slice(0, eq).trim();
      const val = trimmed.slice(eq + 1).trim();
      if (!process.env[key]) process.env[key] = val;
    }
  } catch {}
}
loadEnv();

const APP_KEY = process.env.PIXMIND_APP_KEY || 'app_7867c26ab713fb03aec0774ed28f5802';
const [REDACTED_SECRET];
const API_BASE = (process.env.PIXMIND_API_BASE || 'https://aihub-admin.aimix.pro').replace(/\/+$/, '');

// Parse CLI args
function parseArgs(args) {
  const opts = {};
  for (let i = 0; i < args.length; i++) {
    if (args[i].startsWith('--')) {
      const key = args[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
      if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
        opts[key] = args[++i];
      } else {
        opts[key] = true;
      }
    }
  }
  return opts;
}

function printHelp() {
  console.log(`
Usage: node compress.js <options>

Input (one of):
  --url <url>            Image URL to compress
  --file <path>          Local image file path
  --dir <path>           Directory for batch processing

Options:
  --format <fmt>         Output format: webp, jpg, png, avif, heif, tpg
  --quality <1-100>      Compression quality (default: 75)
  --width <px>           Target width (maintain aspect ratio)
  --height <px>          Target height (maintain aspect ratio)
  --preset <name>        Preset: web, wechat, email, quality, thumbnail
  --output <dir>         Output directory (default: ~/Downloads/compressed-images/<date>)
  --recursive            Process subdirectories
  --mode <type>          Response type: url or base64 (default: url)

Presets:
  web        quality=75, format=webp    — Web display
  wechat     quality=65, format=jpg     — WeChat/social media
  email      quality=55, format=jpg     — Email attachments
  quality    quality=95                 — High-quality archiving
  thumbnail  quality=60, format=webp    — Thumbnails/previews

Examples:
  node compress.js --file photo.png --preset web
  node compress.js --url https://example.com/img.jpg --quality 80 --format webp
  node compress.js --file photo.jpg --width 1920 --quality 75
  node compress.js --dir ~/Pictures/ --preset web --recursive
`);
}

function fileToBase64(filePath) {
  const ext = extname(filePath).toLowerCase().replace('.jpeg', '.jpg').slice(1);
  const mimeMap = { jpg: 'jpeg', png: 'png', webp: 'webp', gif: 'gif', bmp: 'bmp', heic: 'heic', heif: 'heif', avif: 'avif', tpg: 'tpg' };
  const mime = `image/${mimeMap[ext] || 'jpeg'}`;
  const data = readFileSync(filePath);
  return `data:${mime};base64,${data.toString('base64')}`;
}

function getOutputDir(customDir) {
  if (customDir) return resolve(customDir);
  const date = new Date().toISOString().slice(0, 10);
  return join(homedir(), 'Downloads', 'compressed-images', date);
}

function getOutputPath(outputDir, originalName, format) {
  const base = basename(originalName, extname(originalName));
  const ext = format ? `.${format}` : extname(originalName);
  let candidate = join(outputDir, `${base}${ext}`);
  let i = 1;
  while (existsSync(candidate)) {
    candidate = join(outputDir, `${base}_${String(i).padStart(3, '0')}${ext}`);
    i++;
  }
  return candidate;
}

function formatBytes(bytes) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}

async function compressImage(body) {
  if (!API_KEY) {
    console.error('Error: PIXMIND_API_KEY not set. Create .env file with PIXMIND_API_KEY=xxx');
    process.exit(1);
  }

  const url = `${API_BASE}/open/cos/image/compress`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-App-Key': APP_KEY,
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify(body),
  });

  const data = await res.json();
  if (data.code !== 1000) {
    throw new Error(`API Error: ${data.message || JSON.stringify(data)}`);
  }
  return data.data;
}

async function processOne(input, opts, outputDir) {
  // Build request body
  const body = {};

  // Input source
  if (input.startsWith('http://') || input.startsWith('https://')) {
    body.imageUrl = input;
  } else {
    body.imageBase64 = fileToBase64(input);
  }

  // Apply preset
  if (opts.preset && PRESETS[opts.preset]) {
    const preset = PRESETS[opts.preset];
    body.quality = preset.quality;
    if (preset.format) body.format = preset.format;
  }

  // Override with explicit params
  if (opts.quality) body.quality = parseInt(opts.quality);
  if (opts.format) body.format = opts.format;
  if (opts.width) body.width = parseInt(opts.width);
  if (opts.height) body.height = parseInt(opts.height);
  if (opts.mode) body.mode = opts.mode;

  const result = await compressImage(body);

  // Determine output
  const originalName = input.startsWith('http') ? basename(new URL(input).pathname) : basename(input);
  const outputFormat = body.format || extname(originalName).slice(1) || 'jpg';
  const outputPath = getOutputPath(outputDir, originalName, outputFormat);

  // Save result
  if (result.compressedUrl && !result.base64) {
    // Download from URL
    const imgRes = await fetch(result.compressedUrl);
    const buffer = Buffer.from(await imgRes.arrayBuffer());
    writeFileSync(outputPath, buffer);
  } else if (result.base64) {
    const base64Data = result.base64.replace(/^data:image\/\w+;base64,/, '');
    writeFileSync(outputPath, Buffer.from(base64Data, 'base64'));
  }

  // Print result
  const savings = result.originalSize && result.compressedSize
    ? ((1 - result.compressedSize / result.originalSize) * 100).toFixed(1)
    : '?';
  const formatInfo = outputFormat !== extname(originalName).slice(1) ? ` → ${outputFormat}` : '';
  console.log(`  ${originalName}${formatInfo}: ${formatBytes(result.originalSize)} → ${formatBytes(result.compressedSize)} (-${savings}%)`);
  console.log(`  Saved to: ${outputPath}`);

  return { originalName, outputPath, originalSize: result.originalSize, compressedSize: result.compressedSize };
}

function collectImages(dir, recursive) {
  const images = [];
  const entries = readdirSync(dir);
  for (const entry of entries) {
    const full = join(dir, entry);
    const stat = statSync(full);
    if (stat.isDirectory() && recursive) {
      images.push(...collectImages(full, recursive));
    } else if (stat.isFile() && IMAGE_EXTS.has(extname(entry).toLowerCase())) {
      images.push(full);
    }
  }
  return images;
}

async function main() {
  const args = process.argv.slice(2);
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
    printHelp();
    process.exit(0);
  }

  const opts = parseArgs(args);

  // Validate input
  if (!opts.url && !opts.file && !opts.dir) {
    console.error('Error: specify --url, --file, or --dir');
    process.exit(1);
  }

  const outputDir = getOutputDir(opts.output);
  mkdirSync(outputDir, { recursive: true });

  // Single image via URL
  if (opts.url) {
    console.log('Compressing image from URL...\n');
    await processOne(opts.url, opts, outputDir);
    return;
  }

  // Single local file
  if (opts.file) {
    const filePath = resolve(opts.file);
    if (!existsSync(filePath)) {
      console.error(`Error: file not found: ${filePath}`);
      process.exit(1);
    }
    console.log('Compressing image...\n');
    await processOne(filePath, opts, outputDir);
    return;
  }

  // Batch directory
  if (opts.dir) {
    const dirPath = resolve(opts.dir);
    if (!existsSync(dirPath)) {
      console.error(`Error: directory not found: ${dirPath}`);
      process.exit(1);
    }

    const images = collectImages(dirPath, opts.recursive);
    if (images.length === 0) {
      console.error('No images found in directory');
      process.exit(1);
    }

    console.log(`Found ${images.length} image(s) to compress\n`);

    let totalOriginal = 0;
    let totalCompressed = 0;
    let errors = 0;

    for (let i = 0; i < images.length; i++) {
      const img = images[i];
      console.log(`[${i + 1}/${images.length}] ${basename(img)}`);
      try {
        const result = await processOne(img, opts, outputDir);
        totalOriginal += result.originalSize || 0;
        totalCompressed += result.compressedSize || 0;
      } catch (err) {
        console.error(`  Error: ${err.message}`);
        errors++;
      }
      console.log('');
    }

    // Summary
    console.log('--- Summary ---');
    console.log(`Processed: ${images.length - errors}/${images.length}`);
    if (totalOriginal > 0) {
      const saved = ((1 - totalCompressed / totalOriginal) * 100).toFixed(1);
      console.log(`Total: ${formatBytes(totalOriginal)} → ${formatBytes(totalCompressed)} (-${saved}%)`);
    }
    if (errors > 0) console.log(`Failed: ${errors}`);
    console.log(`Output: ${outputDir}`);
  }
}

main().catch(err => {
  console.error('Fatal error:', err.message);
  process.exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)