# @macrulez/mediatoolz — AI Reference A toolbox of command-line commands (binary `mediatoolz`) for images: resize, convert and recompress raster images in bulk by rules (`image-batch`), and generate hazehash / blurhash / thumbhash placeholders, a dominant color and a tiny preview for them (`image-hash`), plus a local web interface for both (`ui`). It started inside `@macrulez/devtoolz` and has been a separate package since devtoolz 0.5.0 (this package starts at 0.1.0 with the same commands, flags and behavior). Version 0.1.1. This document is hand-written for AI agents and other tools that drive this CLI: every flag, default, exit code and behavior note below is verified against the TypeScript source. It covers what `--help` does not make obvious: which command to pick, whether a command can change files, exit codes, the JSON shapes and the behavior to expect. For narrative docs, see the site: - Full docs (EN): https://npm.vuecraft.ru/en/packages/mediatoolz/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/mediatoolz/guide/overview - GitHub: https://github.com/macrulezru/mediatoolz - npm: https://www.npmjs.com/package/@macrulez/mediatoolz Links starting with "/" are relative to https://npm.vuecraft.ru. Install: `npm install -g @macrulez/mediatoolz`, or run without installing: `npx @macrulez/mediatoolz ...`. Node >= 20. The native `sharp` library is needed by both commands and comes with the package (a normal dependency, prebuilt binary picked by npm); only if its binary is missing does a command offer to install it into `~/.mediatoolz/deps` (`-y/--yes` agrees up front). Migrating from devtoolz <= 0.4.3: the commands and flags are unchanged; the config folder `.devtoolz/image-batch/` (project or home) becomes `.mediatoolz/image-batch/`, the managed `sharp` lives in `~/.mediatoolz/deps`, and the default `image-hash` cache file is `.mediatoolz-image-hash-cache.json`. In devtoolz >= 0.5.0 `image-hash` and `image-batch` only print where they moved and exit 2. --- ## 1. Read this first — rules that prevent most mistakes 1. **Only these can change files: `image-hash` (only when you name an output: `-o`, `--per-file`, `--out-dir`; never with `--dry-run` or `--check`) and `image-batch` (into `--out`, next to the sources with `--beside`, over them with `--replace`).** 2. **Exit code 1 means "found something / a file failed", not "the tool broke".** Bad flags, a bad config, colliding outputs, or `sharp` missing and not installable exit 2. See section 3 for the exact rules. 3. **Add `--json` whenever you will parse the result.** It prints the full report as one JSON document on stdout, with no banner and no color. The human text output is for people; do not parse it. 4. **Output is colored only in a real terminal.** Piped, `CI`, `NO_COLOR`, `TERM=dumb` turn color off; `--color` or `FORCE_COLOR=1` force it on; `--plain` removes color, the banner and the joke lines; `--quiet` prints nothing when there is nothing to report. Text is identical with and without color. 5. **Nothing asks a question without a terminal.** With a terminal there are: the one-time `sharp` install question, `image-batch`'s overwrite question, the `--replace` and `restore` confirmations, `--select`, `init`, `config`. All are avoidable: `--yes`, `--overwrite`/`--skip-existing`/`--no-overwrite`, `--include`/`--files-from`, an explicit place. Without a terminal `--replace` and `restore` REQUIRE `--yes`. 6. **`[paths...]`** are files, directories and (for `image-hash`) http(s) URLs; both commands walk subdirectories only with `-r`. An explicitly named file is always included. 7. **Preview before applying**: `--dry-run` encodes in memory and shows the real sizes (`image-batch`) or the hashes as a table (`image-hash`) and writes nothing. --- ## 2. Choosing a command | Task | Command | Changes files? | |---|---|---| | hazehash / blurhash / thumbhash / dominant color / tiny preview of images | `image-hash` | only with an output option | | Resize / convert / recompress images in bulk by rules (sizes, formats, codec options, names) | `image-batch` | always: `--out`, `--beside` or `--replace` | | A web interface for both | `ui` | only on explicit choice | Responsive set for a site: `image-batch` with `-w 400,800,1200 -f avif,webp,jpg`. Convert only the format, same size: `image-batch -f webp` (no size flag = no resize). Shrink oversized originals in place: `image-batch --replace -w 1600` (preview with `--dry-run`). Placeholders for lazy loading: `image-hash`. --- ## 3. Exit codes and `--json` report shapes Every report has `exitCode`. | Command | Exit 1 when | |---|---| | `image-batch` | any `error`, unreadable source or conflict nobody could be asked about → 1 (`--skip-existing` skips without failing); also a declined or `q`-stopped run. Bad flags/config/template, colliding outputs, a codec the sharp build lacks, `--select` without a terminal, `--replace` without `--yes` and without a terminal → 2. `restore`: a file left alone because it changed, or a missing/damaged backup copy → 1. | | `image-hash` | any problem in `errors`, or (with `--check`) an output out of date → 1. Bad options, or `sharp` missing and not installable → 2. | Common flags on both commands: `--json`, `--quiet`, `--plain`, `--color`, `--cwd ` (paths are resolved against it, default the current directory), `-r/--recursive`, `--ext `, `--ignore ` (repeatable), `--no-respect-gitignore`. - `image-hash` (`--json`): see 4.1. - `image-batch` (`--json`): see 4.2. File paths in reports are relative to `--cwd` with `/` separators. --- ## 4. Commands ### 4.1 `image-hash [paths...]` Generates placeholders for raster images (jpg, jpeg, png, webp, gif, avif, tif, tiff). Needs the native `sharp` library, which comes with mediatoolz. It is found next to mediatoolz, in the project (`--cwd`), or in `~/.mediatoolz/deps`; if absent, a terminal gets a y/n question and `npm install`s it into `~/.mediatoolz/deps`; without a terminal the command exits 2 unless `-y/--yes` was given. A fully cached run does not load sharp at all. Input: files, directories (top level only; `-r` recurses), `http(s)` URLs (30 s timeout, 100 MB cap; keyed by URL), `--files-from ` (one path/URL per line, `#` comments, `-` = stdin), comma-separated lists in one argument. `--ext` picks extensions from directories (upper-case variants match). Animated GIF/WebP use the first frame; CMYK is converted to sRGB; a truncated or non-image file is reported as a problem, never hashed from partial data; `--max-pixels ` (0 = no limit; default sharp's ~268 million). What to compute: `-t/--type` comma list of `hazehash` (base64url string of 7..48 bytes, keeps aspect ratio, alpha only when the image has transparency), `blurhash`, `thumbhash` (base64), `color` (dominant `#rrggbb`, transparent pixels ignored), `preview` (tiny PNG `data:image/png;base64,...` decoded from the thumbhash); `both` = blurhash+thumbhash, `all` = all five (hazehash included); default `both` (hazehash is NOT in the default — ask for it with `-t hazehash` or `-t all`). Output order is always hazehash, blurhash, thumbhash, color, preview. `--budget ` (hazehash only, 7-48, default 28, a CEILING: flat images give shorter hashes; below 9 an image with transparency fails with a per-image problem; non-integer or out of range exits 2). `--components ` (blurhash, 1-9 each, default `4x3`; `auto` picks by aspect ratio: 16:9 -> 4x3, portrait -> 3x4, square -> 4x4), `--size ` (longest side before hashing, 1-100, default 100; every hash is computed from that downscaled copy, so a hazehash here can differ slightly from `hazehash encode` of the original file). Every entry also carries real `width`/`height` (EXIF orientation applied). Where it goes (mutually exclusive groups): - no option: formatted data on stdout, problems on stderr, nothing written; - `-o `: one file (directories created); `--update` merges into the existing file instead of replacing it (json/ts/js/csv, on a file this command generated — a Prettier-reformatted file cannot be read back; not plain), `--prune` (with `--update`) drops entries whose image no longer exists; - `--per-file`: a file beside each image; `--out-dir `: into that directory mirroring the folder structure (implies `--per-file`; required for URL inputs). Name = full image file name + `--suffix` (default `.{type}`; `{type}` = `hazehash`/`blurhash`/`thumbhash`/ `color`/`preview`, or `hash` when a structured format holds several types) + extension (`--out-ext`, default by format: `.json`, `.txt` for plain, `.csv`, `.ts`, `.js`); - `--dry-run`: write nothing, print a table (file, size, one column per type) and the files that would be written; `--check`: write nothing, exit 1 if an output file is missing or differs (needs `-o`/`--per-file`/`--out-dir`; not with `--dry-run`/ `--update`). `--out` cannot be combined with `--per-file`/`--out-dir`. Formats `-f/--format`: `json` (default; object keyed by file with `width`, `height` and the types), `plain` (just the hash text; one file per type per image; aggregated: tab separated lines; the per-image file has NO trailing newline), `csv`, `ts` (`export const imageHashes = {...} as const`, per-image `export default`), `js`; `--name ` renames the constant. Plain with several types needs `{type}` in `--suffix`. Keys are the path relative to `--cwd`; `--key-base ` makes them relative to another directory and `--key-prefix ` prepends text (e.g. `/` for URL-style keys). Speed: `--cache [file]` (default `.mediatoolz-image-hash-cache.json` in `--cwd`) skips images unchanged in mtime and size and with the same `--size`/`--components`/`--budget`; a changed setting or a newly requested type recomputes. `--concurrency ` (default 4). A progress counter goes to stderr only in a terminal. `--format` is the format of the hashes; `--json` is the full REPORT: `{filesScanned, entries:[{file, width, height, hazehash?, blurhash?, thumbhash?, color?, preview?}], written:[...], errors:[{file, message}], stdout, dryRun, check, checked, outdated:[{file, reason:"missing"|"changed"}], cached, pruned, exitCode}`. `written` lists planned files in `--dry-run`. Bad option combinations (`--check` without an output, `--update` without `-o`, `--prune` without `--update`, bad `--type`) exit 2 with `error: ...` on stderr. ### 4.2 `image-batch [paths...] (-o | --beside | --replace)` Resizes, converts and recompresses raster images in bulk by RULES. Exactly ONE place for the results is required (none: a terminal is asked for `-o`, otherwise exit 2; two: exit 2): `-o ` a separate folder repeating the source structure (`--flat` drops it; sources untouched), `--beside` the folder of each source (sources untouched), `--replace` over the source files in their own format (see below). Needs the native `sharp` library, loaded exactly like `image-hash` (4.1, `-y/--yes` installs it). Quick start: `mediatoolz image-batch ./src/images -r -o ./public/images -w 400,800,1200 -f avif,webp,jpg -q high`. A CONFIG holds only the rules, never input/output paths: a JSON file (or `.js`/`.mjs`/`.ts` exporting an object or a function; a `.ts` file is transpiled alone, no imports) named by its file name, found in `.mediatoolz/image-batch/` (project, searched upward to the repository root = the folder with `.git`) or `~/.mediatoolz/image-batch/` (global; the project one wins). `-c, --config ` selects one; with no `-c` a SINGLE config found is used, unless flags such as `-w`/`-f` already describe the job; several configs: a terminal asks, otherwise exit 2 with the list. Flags override the config for that run. Keys: `name`, `description` (labels), `defaults`, `presets` (`extends`), `outputs[]` (recipes), `match[]` (`{glob, outputs}`: first matching glob replaces `outputs` for that source; a source matching nothing is left alone and listed in `unmatched`), `placeholders`. Unknown keys, bad values and bad options are exit 2 with a suggestion. Format-only conversion: with no size key and no size flag a recipe does NOT resize - each result keeps its source dimensions (default name `{dir}/{name}.{format}`), e.g. `mediatoolz image-batch ./photos -r -o ./out -f webp`. `--no-resize` forces that for one run: it drops every size of the config (widths heights size longEdge shortEdge megapixels percent scale matchOrientation) and is exit 2 together with -w --heights --long --short --megapixels --percent --match-orientation. In the web interface (`mediatoolz ui`, Convert) the sizes sit behind a "Resize the images" switch that is off by default. A recipe (every key optional; an empty one copies at the source size and format): `widths[]`, `heights[]`, `size:"WxH"`, `longEdge[]` / `shortEdge[]` (by the long / short side, the same for landscape and portrait — use these for mixed sets and for `--replace`), `megapixels[]` (area, e.g. `[2, 0.5]`), `percent[]` (share of the source), `matchOrientation` (turns a `size` box around for an image of the other orientation), `maxBytes` (`"200KB"`/`"1.5MB"`/bytes >= 1 KB: each file is re-encoded at the highest quality that fits; only formats with a quality — jpg webp avif heif jp2, png with `palette`, else exit 2 before writing; unreachable = that file is an `error` with the smallest size, nothing written; part of the settings), `scale[]` (multiplies those sizes; area by the square), `formats` (list or map format -> options; `jpg png webp avif gif tiff jp2 heif original`), `quality`, `codecs`, `fit` (`inside` default, `outside`, `cover`, `contain`, `fill`), `position`, `background`, `withoutEnlargement` (default true: sizes above the source clamp to it; outputs that become the same file merge, count in `deduped`; SVG is always drawn at the asked size), `name` (template), `autoOrient` (default true), `rotate` (90/180/270), `flip`, `flop`, `grayscale`, `sharpen` (a saved preset name, or an object `{preset?, for: screen|matte|glossy, amount: low|standard|high, radius 0.000001-10, flat 0-1000000, jagged 0-1000000, threshold 0-1000000}`, applied after the resize; naming any key switches it on, `for` / `amount` default to screen / standard, `radius` `flat` `jagged` `threshold` replace the table values and go to sharp as they are; unset = no sharpening; fields next to `preset` replace the preset's; merged field by field across defaults, recipe preset, recipe and flags, and a layer that names another preset starts afresh), `blur`, `flatten` (JPEG is flattened onto white by default), `metadata` (`strip` default, `keep`, `keep-icc`), `id`, `preset`. Sizes are computed BEFORE writing, so `{width}`/`{height}` are the real result dimensions. File name template (relative to `--out`; default `{dir}/{name}-{width}w.{format}` with widths, `-{height}h` heights, `-{size}` size, `-{long}l` longEdge, `-{short}s` shortEdge, `-{mp}mp` megapixels, `-{percent}pct` percent, `@{scale}x` bare scale, else `{dir}/{name}.{format}`; a recipe mixing methods names each result by its own method): `{name} {ext} {orig} {dir} {format} {width} {height} {size} {long} {short} {mp} {percent} {scale} {index} {index:3} {hash} {hash:6} {date}`. `{dir}` = source folder relative to its input folder (empty for the root; the slash disappears; `--flat` forces empty). Absolute path, `..`, unknown variable or an illegal character = exit 2 before writing. Two outputs with one name = exit 2 listing them (sources that differ only by extension: add `{ext}`). Codec options (set in `defaults.codecs`/preset/recipe or `--codec fmt.option=value`, repeatable; checked before running): jpg `quality mozjpeg progressive chromaSubsampling`; png `compressionLevel palette colors quality dither effort progressive` (quantized ONLY with `palette`); webp `quality lossless nearLossless alphaQuality effort smartSubsample preset minSize`; avif `quality lossless effort chromaSubsampling bitdepth tune`; gif `colors effort dither loop delay reuse` (animation kept); tiff `compression quality predictor bitdepth xres yres`; jp2 `quality lossless chromaSubsampling` (needs a libvips with OpenJPEG; the prebuilt one lacks it -> exit 2 before writing); heif `quality lossless effort compression chromaSubsampling`. `quality` is a number 1-100, a level `low|medium|high|best` (per format: jpg 65/78/85/92, webp 60/75/82/90, avif 40/50/60/75) or a map `{jpg:82, webp:"high"}`; CLI `-q 82`, `-q high`, `-q jpg=82,webp=high`. Layering, weak to strong: built-ins (jpg 82+mozjpeg+progressive, webp 80 effort 4, avif 55 effort 4, png level 9, heif av1) -> `defaults` -> preset -> recipe -> flags. Wrong-format option, unknown name or out-of-range value = exit 2 with `Did you mean`. Choosing files: `-r`, `--ext` (default adds `.svg` to image-hash's list), `--ignore`, `--no-respect-gitignore`, `--include`/`--exclude ` (path inside the input folder, repeatable), `--files-from `, globs in `[paths...]`. An output folder inside an input folder is skipped when reading. `--list` prints the files and writes nothing. `-i, --select` opens an interactive checklist (all ticked; arrows/jk move, space tick, a/n/i all/none/invert, `/` filter, Enter confirm, q cancel) and needs a REAL terminal (otherwise exit 2 pointing to `--list`); do not use it from an agent, use `--include` or `--files-from`. `--beside`: a template must give a name different from the source's (else exit 2 pointing at `--replace`). Results of an earlier run are NOT read as sources: the record of made files (`derived` in the report) and, without it, files that are the planned result of another source in the same run are left alone. `--flat` is rejected. `--replace` (use for shrinking oversized originals: `-w 1600` = "no wider than 1600"; or recompressing: `-q high`, png `palette`, `mozjpeg`). Rewrites each source in ITS OWN format and name. One result per image: several sizes or a different format is exit 2 before writing (leave `formats` out or `original`); a name template is ignored, `--name` is exit 2; SVG files are left alone (`vectors` in the report, not an error). A file is replaced only if the result is SMALLER (`--no-only-if-smaller` replaces anyway); otherwise status `kept`. A file already rewritten or kept with the same settings is `already-processed` and not touched, so repeated runs do not degrade it (settings change or `--force` re-process). Before overwriting, the original is copied to `.image-batch-backup//` in `--cwd` with a `journal.json` (`--backup ` another base, `--no-backup` none). Confirmation: a terminal is asked; without one `--yes` is REQUIRED (exit 2). `--dry-run` encodes in memory and reports `would-replace` / `would-keep` with before/after bytes, writes nothing. Writes are atomic. `image-batch restore [--dry-run] [--force] [-y] [--json]` copies the saved originals back (needs `--yes` without a terminal); a file changed since it was replaced is left alone (`modified`, `--force` overrides), a missing/damaged copy is reported; report `{dir, dryRun, results:[{path,status,bytes?,message?}], counts:{restored,"would-restore",modified, "missing-backup","damaged-backup",error}, cancelled, exitCode}`. After a restore the next `--replace` run processes those files again. Do not use `--replace` without a `--dry-run` first. Existing outputs (`--out`/`--beside`): one made by an earlier run from the same source and settings is "up to date" and skipped (record in `~/.mediatoolz/cache/image-batch/.json`; `--cache ` or `--no-cache`). Anything else already present is a CONFLICT. In a terminal the FIRST conflict asks y (this) / a (this and all next) / s (skip this) / n (skip all next) / q (quit). Without a terminal nothing is asked: conflicts are skipped and listed, exit 1. `--overwrite` replaces them, `--skip-existing` skips them with exit 0, `--no-overwrite` fails on any conflict (exit 1), `--force` rebuilds everything ignoring the record. A source file is never overwritten. `--dry-run` writes nothing (no cache, no `--emit`) and reports `would-write`/`would-overwrite`. Hashes: `--hazehash`, `--blurhash`, `--thumbhash`, `--dominant-color` (or config `placeholders`), `--budget ` (hazehash 7-48, default 28; needs `--hazehash`). They are computed per SOURCE and written only into `--emit `: `{version:1, sources:{:{width, height, format, bytes, , outputs:[{path,width,height,format,bytes}]}}, placeholders:{ <--key-prefix>:{,width,height}}}` — the placeholders map is the shape of a vue-image-kit placeholders manifest. Hash flags without `--emit` = exit 2. Report (`--json`): `{placement:"out"|"beside"|"replace", out, backupDir?, backupEnabled, configLabel?, dryRun, listOnly, sources:[{file,width,height,format,bytes}], failures:[{file,message}], unmatched:[...], derived:[...], vectors:[...], results:[{source,output,recipe,format,width,height,status,bytes?,before?,message?}], counts:{written, "up-to-date", skipped, conflict, "would-write", "would-overwrite", replaced, kept, "already-processed", "would-replace", "would-keep", error}, deduped, bytesIn, bytesOut, emitPath?, aborted, cancelled, nothingSelected, exitCode}`. Exit: 0 all written / replaced / kept / already-processed / up to date / skipped by `--skip-existing`; 1 any `error`, unreadable source (`failures`), conflict nobody could be asked about, `--no-overwrite` conflict, `q`, or a declined `--replace` confirmation (`cancelled`); 2 usage errors (including `--replace` without `--yes` and without a terminal). `--concurrency` (default 4), `--max-pixels`, `--verbose` (human output lists only problems for runs above 40 files), `--quiet`. Managing configs: `image-batch init` (wizard in a terminal; `--name ` plus `-w -f -q --file-name