Materializr
오픈소스 파라메트릭 3D CAD로 제약 스케치·솔리드 모델·나사·SVG·텍스트 조각·STL/STEP 내보내기를 지원합니다
사진을 넣으면 crustyimg가 웹용으로 크기 조정하고 AVIF로 최신화하며 파일을 크게 반환하지 않는다
Drop a photo: crustyimg resizes it for the web, modernizes it to AVIF, and never hands back a bigger file — entirely in your browser. Then run the same thing on whole folders from the CLI.
디자인Rust · ShellGitHub @jysf온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
crustyimg is an open-source, single-binary command-line tool — pure Rust, permissively licensed, with zero system dependencies — for preparing images for the web. It is outcome-driven compression, instead of guessing a quality number, you declare the result you want — a perceptual quality target
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Make an image web-ready in one command. crustyimg web photo.jpg -o out.avif downscales,
re-encodes to the smallest modern format that beats the downscaled image, and prints a real
quality score. One Rust binary, no system libraries.
Beyond web it's a general image CLI: view, resize, convert, optimize, inspect, strip
metadata, watermark, diff, and build responsive sets. Tell it the visual outcome you want
(a quality target or a size budget) and it finds the smallest file that hits it. Set the
look, not the number.
jysf.github.io/crustyimg: drop in a photo, watch
it become AVIF, and read an SSIMULACRA2 quality score. It runs entirely in the browser (the
Rust engine compiled to WebAssembly), so nothing is uploaded and there's nothing to install.
It's the fastest way to see what crustyimg web does.
web, optimize, and diff score the output with
SSIMULACRA2, a perceptual metric, so "high quality" is a number you can gate on in CI.web is one command to a web-ready file: downscale, strip metadata, re-encode to the
smallest modern format that beats the downscaled image (AVIF for photos, lossless WebP or
PNG for graphics), and report the score. For a keep-the-dimensions pass that is guaranteed
never larger than the source, use optimize..dng, .cr2, .nef, .arw, and more — by extracting the
camera's embedded full-res JPEG preview. That's not a RAW develop (no demosaic, no white
balance), but it's enough to get a RAW straight into web or optimize; sharp and squoosh
can't open these at all.edit --save-recipe, or start from a
bundled web/gallery/product, then replay it in parallel across a batch with
apply --recipe. The same recipe TOML runs in the browser demo too, via the wasm
transform() binding.Over a corpus of 8 real photos (0.7 to 47 MP), crustyimg web produced files a median 97%
smaller in about one to five seconds each. Runtime barely changes across that size range
because web downscales before it encodes. Reproduce it on your own photos with just bench --corpus /path/to/photos.
How does that stack up against sharp, ImageMagick, and squoosh at matched quality — where crustyimg wins and where it doesn't? See BENCHMARKS.md: an equal-quality, reproducible comparison (crustyimg isn't the smallest or the fastest, and the doc says so).
cargo install crustyimg # crates.io
brew install jysf/tap/crustyimg # Homebrew
cargo install --git https://github.com/jysf/crustyimg # latest from git
Prebuilt binaries are on the Releases page.
Building from a clone works too: cargo build --release puts the binary at
./target/release/crustyimg. To use crustyimg as a Rust library, cargo add crustyimg.
See RELEASING.md for the versioning policy and release channels.
The view command (terminal image preview), AVIF output and SVG input are
all on by default — a plain cargo install or release binary includes them. For a
headless, smaller binary (CI / server / container), build without them:
cargo install --git https://github.com/jysf/crustyimg --no-default-features
Add back what you need with --features, e.g. --features avif,svg.
Two additional codecs are opt-in (compile-time features):
| Feature | What it adds |
|---|---|
webp-lossy | Lossy WebP encode (libwebp, C dep) — by default WebP is lossless only |
heic | HEIC/HEIF input via system libheif — local builds only, see below |
Enable with --features webp-lossy at build/install time.
SVG input is rasterized at its own size. Its text always draws in the bundled Go font,
whatever font-family it names: crustyimg never reads your system's fonts, and bold or italic
text draws upright in Go Regular.
crustyimg reads AVIF, SVG, and RAW out of the box, but not HEIC. HEVC — the
codec inside a .heic — is covered by the Access Advance patent pool, and a
copyright license of any kind grants zero patent rights; separately, the mature
pure-Rust HEIC decoders are AGPL. Either blocker alone keeps HEIC off the default
path, so no release binary, Homebrew bottle, or cargo install default can decode
one. Running those on a .heic prints HEIC decoding isn't built into this crustyimg; convert the file to a supported format (JPEG, PNG, or WebP) first, or rebuild with --features heic and exits 4.
Easiest fix — pre-convert. crustyimg works in web/standard formats, so turn a
.heic into one first, then run crustyimg on the result:
sips -s format jpeg photo.heic --out photo.jpg # macOS, built in
magick photo.heic photo.jpg # ImageMagick, any OS
Building --features heic (below) is only needed if you want crustyimg to read
.heic directly — for a one-off, pre-converting is faster.
If you accept those terms, build it yourself against a system libheif (≥ 1.17):
# macOS — Homebrew's libheif bundles its codec backends
brew install libheif
# Debian/Ubuntu — the HEVC decoder is a SEPARATE plugin package. Without it,
# libheif parses a .heic but fails to decode it ("Unsupported codec").
sudo apt-get install libheif-dev libheif-plugin-libde265
cargo build --release --features heic
Decode only — crustyimg never encodes HEIC. See
docs/licensing.md for the LGPL attribution that a
redistributed --features heic build carries, and decisions/DEC-052 for the
full rationale.
Handy examples below; the complete reference (every command, flag, and exit code)
is in docs/cli-reference.md. Or run crustyimg --help /
crustyimg <cmd> --help. Every transform accepts a single file, a glob, a directory, or
- (stdin); see Batch and Recipes below to run
over many.
crustyimg view photo.jpg # display in the terminal (viuer)
crustyimg info photo.jpg # dimensions, format, size, color, EXIF/ICC presence
crustyimg info photo.jpg --exif # dump EXIF tags
crustyimg info photo.jpg --json # machine-readable JSON to stdout
crustyimg resize photo.jpg --max 1200 -o out.jpg # bound the long edge (never upscales)
crustyimg resize photo.jpg --exact 800x600 -o out.jpg
crustyimg resize photo.jpg --percent 50 -o out.jpg
crustyimg resize photo.jpg --fit 800x800 -o out.jpg # fit inside, keep aspect
crustyimg resize photo.jpg --fill 800x800 -o out.jpg # scale + crop to exactly 800x800
crustyimg thumbnail photo.jpg --size 200 --square -o thumb.jpg
# The flagship: downscale (long edge ≤2048), pick the smallest modern format that
# beats the DOWNSCALED image (AVIF for photos, lossless WebP/PNG for graphics), strip
# metadata, and report the SSIMULACRA2 score. Size-insensitive — a 24 MP photo is as
# fast as a small one. The downscale is the contract, so an already-small source above
# 2048px can come back larger than the original (reported honestly as "N% larger"); for
# an unconditional never-bigger guarantee that keeps dimensions, use `optimize` below.
crustyimg web photo.jpg -o out.avif
crustyimg web photo.jpg --max 1200 -o out.avif # override the downscale bound
# Keep the original dimensions (the byte-primitive): fast fixed-quality, never bigger.
# Off by default the score is skipped; add --verify to report it for this run.
crustyimg optimize photo.jpg -o out.avif
crustyimg optimize photo.jpg --verify -o out.avif
# Perceptual auto-quality: smallest file that still clears a visual target (JPEG/WebP)
crustyimg optimize photo.jpg --target high -o out.jpg
crustyimg optimize photo.jpg --ssim 85 -o out.jpg
# Byte budget: fit under a size, lowering quality then dimensions as needed
crustyimg optimize photo.jpg --max-size 200KB -o out.jpg
crustyimg convert photo.png --format webp -o out.webp # PNG/JPEG/GIF/BMP/TIFF/ICO/WebP
crustyimg convert photo.jpg --format webp --max-size 150KB -o out.webp
crustyimg convert photo.jpg --format avif -o out.avif # AVIF: built in by default
crustyimg auto-orient photo.jpg -o fixed.jpg # bake EXIF orientation into pixels, clear the tag
crustyimg meta strip photo.jpg -o clean.jpg # remove ALL metadata (EXIF/IPTC/XMP/ICC)
crustyimg meta clean photo.jpg --gps -o nogeo.jpg # remove only GPS/location, keep the rest
crustyimg meta set photo.jpg --artist "Jane Doe" --copyright "© 2026" -o tagged.jpg
crustyimg meta copy --from original.jpg --to edited.jpg # copy EXIF+ICC between images
Pixel-lane encodes drop GPS by default; pass
--keep-gpsto retain it.
crustyimg watermark photo.jpg --image logo.png --gravity southeast --opacity 0.6 -o out.jpg
crustyimg watermark photo.jpg --image logo.png --tile --scale 0.1 -o out.jpg
crustyimg watermark photo.jpg --text "© crustyimg" --size 32 --color FFFFFF -o out.jpg
# SSIMULACRA2 score of b vs a; --fail-under makes it a CI visual-regression gate (exit 7 if below)
crustyimg diff original.jpg compressed.jpg --fail-under 70
crustyimg diff a.png b.png --json
# Width × format variants + a paste-ready <picture>/srcset snippet on stdout
crustyimg responsive hero.jpg --widths 320,640,1280 --formats webp,jpeg --out-dir web/
Pass many inputs (a list, a glob, or a directory) to any transform — multi-input runs
require --out-dir, and --name-template controls output names ({stem}, {ext}). A
literal extension in the template ('{stem}.png') also sets the output format, as an -o
extension does:
crustyimg web *.jpg --out-dir web/ # a glob
crustyimg convert photos/ --format webp --out-dir out/ # a whole directory
crustyimg thumbnail *.png --size 200 --square --out-dir thumbs/
crustyimg meta strip *.jpg --out-dir clean/ --name-template "{stem}_clean.{ext}"
Per-command fan-outs run sequentially;
apply --recipe(below) is the parallel path (-j N, default = CPU count). A failed input in a batch doesn't abort the rest — the run exits6(partial batch) with a stderr summary.
A recipe is an ordered pipeline of operations saved as TOML — resize, watermark and more, optionally with the output format and quality to write. Tune it once, replay it anywhere. Three ship inside the binary, so there's no file to write for the common jobs:
| Recipe | Pipeline |
|---|---|
web | auto-orient → downscale long edge to 2048px → smallest modern format |
gallery | auto-orient → downscale to 2560px → smallest modern format (full-bleed, lightbox) |
product | auto-orient → downscale to 1600px → smallest modern format (cards, catalogue) |
# Replay a bundled recipe across a batch, in parallel (-j workers, progress bar)
crustyimg apply --recipe web *.jpg --out-dir out/ --name-template "{stem}_web.{ext}" -j 8
Or tune your own on one image, save it, and replay that:
crustyimg edit hero.jpg --auto-orient --resize-max 1600 --save-recipe mine.toml
crustyimg apply --recipe mine.toml *.jpg --out-dir out/ -j 8
--recipe takes a path or a bundled name, and a real file always wins — a local
web.toml unambiguously shadows the bundled web. The round trip is byte-stable:
apply-ing a saved recipe reproduces the edit that wrote it byte for byte, so a recipe
reviewed in a PR is exactly what runs in CI. The same TOML runs in the browser demo, via
the wasm transform() binding, unless a step needs a file (a watermark image or font):
the browser has no files to read, so transform() refuses that recipe and names the step.
To declare many source → output pairs in one file instead of one command per batch, use
crustyimg build with a crustyimg.build.toml manifest — it adds
a content-addressed cache, a lockfile, and --watch.
Going further: docs/recipes.md is the recipe cookbook — a
copy-paste catalog of standard workflows (web prep, responsive, privacy, CI gates, bulk
photography). recipes/ holds the bundled TOMLs and the recipe file format.
# `-` reads stdin / writes stdout; all diagnostics stay on stderr so pipes are clean
crustyimg resize - --max 800 -o - < in.jpg > out.jpg
cat in.jpg | crustyimg convert - --format webp -o - > out.webp
-o/--output (- = stdout) · --out-dir · --name-template · -q/--quality ·
--format · -j/--jobs · -y/--yes (assume yes to overwrite) · -Q/--quiet ·
-v/--verbose · --keep-gps. Exit codes: 0 ok, 1 runtime error, 2 usage, 3
input not found, 4 unsupported format, 5 output refused, 6 partial batch, 7
check failed (e.g. diff --fail-under).
The engine compiles to WebAssembly, and that's what runs the
browser demo: the real web pipeline, client-side,
with nothing uploaded. The same build ships as an npm package —
crustyimg-wasm, no native addon, no
postinstall, no dependencies:
npm install crustyimg-wasm
It runs in the browser and in Node. Note the edges: --target web needs an explicit
init(), calls are single-threaded and blocking, and AVIF is encode-only (a .avif
you pass in is decoded by the browser). You can also build the bundle from this repo
with just wasm-build.
crustyimg can generate completion scripts for bash, zsh, fish, powershell, and
elvish. Pipe the output into your shell's completion directory:
# zsh — add to your $fpath
crustyimg completions zsh > "${fpath[1]}/_crustyimg"
# bash — append to your completions file
crustyimg completions bash >> ~/.bash_completion
# fish
crustyimg completions fish > ~/.config/fish/completions/crustyimg.fish
# powershell
crustyimg completions powershell >> $PROFILE
# elvish
crustyimg completions elvish >> ~/.config/elvish/lib/completions.elv
Where to install the script is your (or your package manager's) step — the command writes the script to stdout only.
skills/crustyimg/SKILL.md teaches a coding agent when to
reach for crustyimg and how to call it: which command fits the job, the output rules
that avoid a failed run, recipes, and CI checks. It's a standard SKILL.md, so it works
with any agent that reads skills.
In Claude Code, install it as a plugin:
/plugin install crustyimg --marketplace jysf/crustyimg
Older Claude Code versions use two steps: /plugin marketplace add jysf/crustyimg, then
/plugin install crustyimg@crustyimg. For other agents, copy the skills/crustyimg/
folder into the agent's skills directory (for example ~/.claude/skills/).
The skill needs the crustyimg binary installed (see Install).
crustyimg lint is a format-aware, no-URL, per-file image-asset linter — the pre-deploy
check you run on assets//content/ in CI before anything is Lighthouse-able. It flags GPS/EXIF
privacy leaks, over-budget and wrong-format assets, non-baked orientation, corrupt files and more,
naming a runnable crustyimg fix for each and exiting 7 on any error-severity finding (a
CI-native gate). Drop it into any CI in three lines:
Setting this up on a repo? Follow the GitHub Actions guide, which
covers budgets, required checks, pull requests from forks and fixing findings. A working example
lives at jysf/crustyimg-example.
The lint wrapper — installs crustyimg and annotates findings inline in the PR:
name: images
on: [pull_request]
jobs:
lint-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jysf/crustyimg-action@v1
with:
paths: assets/**/* content/**/*
A directory in paths covers only the images directly inside it. Use a recursive glob such as
assets/**/* to include subfolders. The same holds when you call crustyimg lint directly: quote
the glob ('assets/**/*') so crustyimg expands it rather than the shell.
Other inputs: mode: optimize re-encodes the tree instead of linting it, args passes extra
flags straight to crustyimg (--select privacy, --config path), fail-level: warn|never
controls what fails the job, and version pins a release tag instead of latest.
Or install the binary yourself with setup-crustyimg
(generic — it enables optimize/convert/lint alike) and call it directly:
- uses: jysf/setup-crustyimg@v1
- run: crustyimg lint assets content # exit 7 fails the job on an error finding
- run: crustyimg optimize assets --out-dir dist # (any crustyimg command works)
jysf/setup-crustyimg — installs the CLI (via the
checksum-verifying cargo-dist installer) on Linux/macOS/Windows.jysf/crustyimg-action — the lint/optimize wrapper
with native PR annotations + a job-summary table.The format-aware upgrade from check-added-large-files — lint image assets before they land, via
.pre-commit-hooks.yaml:
repos:
- repo: https://github.com/jysf/crustyimg
rev: v0.8.0 # a crustyimg release that ships `lint` (0.4.0+)
hooks:
- id: crustyimg-lint
--format sarif emits SARIF 2.1.0 — upload it to put
findings in the repo's Security tab (and inline in PRs) via GitHub code-scanning:
- uses: jysf/setup-crustyimg@v1
- run: crustyimg lint assets --format sarif > crustyimg.sarif
continue-on-error: true # let the upload run even when findings fail the gate
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: crustyimg.sarif
The binary + its exit code is the whole contract — no Action required:
crustyimg lint assets content # 0 clean · 7 error finding · 2 usage · 3 no inputs
crustyimg lint assets --format json # machine-readable report for tooling
crustyimg lint assets --format sarif # SARIF 2.1.0 for GitHub code-scanning
just lint-images assets content # the same, via the repo's justfile recipe
Tune rules with a .crustyimg-lint.toml (select/ignore, per-rule severity, per-glob byte
budgets, per-file-ignores); zero-config works out of the box.
[Unreleased]
section tracks work merged since the last release.0.x minor
bumps may carry breaking CLI changes), the vX.Y.Z annotated-tag convention,
and the release-cut checklist a maintainer follows to publish a new version.crustyimg is dual-licensed under MIT OR Apache-2.0 — use whichever suits
you. See LICENSE-MIT and LICENSE-APACHE.
crustyimg is built with a spec-driven workflow (Claude plays every role — architect, implementer, reviewer — across separate sessions). The full contributor guide lives in docs/development.md; the agent conventions are in AGENTS.md.