variants
Multi-width responsive image variant engine with caching, HEIC handling, and OGP dispatch.
What it does
@takazudo/ is the package's batch engine: given a directory (or list) of source images, it emits a ladder of resized WebP (or other format) variants per image, an optional Open Graph card, and a blurhash placeholder — all behind a content-hash cache so a rerun with unchanged sources is nearly free. It also handles HEIC/HEIF sources transparently (via /) and attempts corruption repair via optional external binaries before giving up on a file.
Exports
function processImages(config: ProcessImagesConfig): Promise<ProcessSummary>;
function processOne(entry: ImageEntry, config: ProcessOneConfig): Promise<ItemResult>;
function selectVariantWidths(sourceWidth: number, widths: number[]): number[];
class VariantProcessingError extends Error {
readonly stage:
| 'slug'
| 'heic'
| 'probe'
| 'variants'
| 'ogp'
| 'passthrough'
| 'cache'
| 'unknown';
}
const defaultTagParser: TagParser;
const noTagParser: TagParser;
const PHOTO_VARIANT_WIDTHS: number[]; // [400, 800, 1600]
const photoVariantsPreset: Partial<ProcessOneConfig>;
const DEFAULT_WIDTHS: number[]; // [600, 900, 1200, 1600, 2000]
const DEFAULT_QUALITY: number; // 85
const DEFAULT_FORMATS: string[]; // ['webp']
const DEFAULT_CONCURRENCY: number; // 4
const DEFAULT_OGP_FILENAME: string; // 'ogp.jpg'
const DEFAULT_CACHE_FILENAME: string; // '.cache.json'processImages scans inputDir and/or an explicit files list, processes entries with bounded concurrency, and never throws for a single bad file — each failure lands in ProcessSummary.failed (tagged with the pipeline stage it failed at) while the run continues. processOne is the single-image primitive processImages calls internally; use it directly for custom scanning/queueing. It throws VariantProcessingError on a genuine failure and returns a status: 'skipped' result for a deliberate skip (cache hit, or the non-image guard tripping).
Options
ProcessImagesConfig extends ProcessOneConfig with four batch-only fields (inputDir, files, concurrency, cleanupOrphans). processOne accepts just the ProcessOneConfig shape.
| Name | Type | Default | Effect |
|---|---|---|---|
outputDir | string | required | Root directory the per-image slug directories are created under. |
inputDir | string | — | Directory scanned for source images (by supported extension). Usable together with files. |
files | string[] | — | Explicit list of source file paths, added to whatever inputDir finds. |
concurrency | number | 4 | Max images processed at once. |
widths | number[] | [600, 900, 1200, 1600, 2000] | Responsive widths to emit; a source never gets upscaled past its own width (see selectVariantWidths). |
formats | string[] | ['webp'] | Output formats per width — any format sharp().toFormat() accepts. |
quality | number | 85 | Encode quality (1-100) for variants and the OGP card. |
outputName | (width: number, format: string) => string | `${width}w.${format}` | Names each variant file. |
ogpFileName | string | 'ogp.jpg' | OGP output filename within the slug directory. |
ogpOptions | SmartOgpOptions | {} | Extra options forwarded to /'s generateSmartOgp. |
cacheFileName | string | '.cache.json' | Cache sidecar filename within the slug directory. |
fallbackBlurhash | string | — | blurhash to record when encoding fails. Omitted means the record's blurhash is null on failure. |
tagParser | TagParser | defaultTagParser | Derives { mode, slug } from a filename; see Filename-tag dispatch. |
autoRepair | boolean | true | Attempt corruption repair via magick/ffmpeg when available (see Caveats). |
backupCorrupted | boolean | false | When repair runs, first copy the corrupt source to a sibling <name>.corrupted.bak. The source itself is never modified either way. |
bakeExifOrientation | boolean | false | Bake EXIF orientation into pixels (and drop metadata) before encoding, via /. |
stripMetadata | boolean | false | Strip all metadata before encoding. Subsumed by bakeExifOrientation — stripping without baking would leave a sideways image once the variant pipeline auto-orients. |
cleanupOrphans | boolean | false | Remove output slug directories no longer backed by any input. Only ever touches direct children of outputDir. |
onMetadata | (record: VariantMetadata) => void | Promise<void> | — | Invoked (and awaited) once per produced or cache-hit image. |
onError | (report: ProcessFailure) => void | Promise<void> | — | Invoked (and awaited) once per failing file. |
Example
import { processImages, photoVariantsPreset } from '@takazudo/zudo-image-tweaker/variants';
const summary = await processImages({
...photoVariantsPreset,
inputDir: './photos',
outputDir: './public/photos',
onMetadata: async (record) => {
console.log(record.slug, record.width, record.height, record.blurhash);
},
});
console.log(`processed ${summary.results.length}, failed ${summary.failed.length}`);photoVariantsPreset reproduces a plain-photo pipeline on top of the same engine: the [400, 800, 1600] width ladder (PHOTO_VARIANT_WIDTHS), EXIF orientation baked in with metadata stripped, and noTagParser so every file is treated as a plain full-variant image (no OGP dispatch).
Filename-tag dispatch
defaultTagParser (the default tagParser) reads a suffix on the file's base name to choose which pipeline a source runs through, then strips the suffix from the returned output slug:
| Suffix | Mode | Effect |
|---|---|---|
| (none) | 'full' | Width variants + blurhash. No OGP card. |
__og | 'og' | Width variants + blurhash, plus an OGP card. |
__ogonly | 'ogonly' | Only an OGP card — no width variants, no blurhash. |
noTagParser disables this entirely: every file is 'full', keyed by its bare base name. Pass a custom TagParser to define your own vocabulary.
Caveats
HEIC sources are trusted-input only
A .heic/.heif source (or a .jpg that file sniffs as HEIF) is routed through /'s convertHeifToJpeg, which carries the same trusted-input-only decoder warning documented there.
External-binary repair is best-effort.
autoRepair(defaulttrue) triesmagick(a strip/normalise pass, then a format-swap re-decode through the sibling raster format) thenffmpegon sharp decode errors that look like bitstream corruption; on a host with neither installed (e.g. plain Linux without ImageMagick) it warns to the console and leaves the file inProcessSummary.failedrather than failing the whole run. SetbackupCorrupted: trueto copy each corrupt source to a sibling<name>.corrupted.bakbefore the attempt (the source itself is never modified).Duplicate slugs are rejected up front. Two inputs that normalize to the same output slug (e.g.
photo.jpgandphoto__og.jpg) would race on one output directory;processImagesreports both as failures at the'slug'stage instead of processing either.The cache key covers config, not just file bytes. A rerun with the same source but a different
quality,widths,ogpOptions, or orientation/strip flag invalidates the cache even though the file's content hash is unchanged.cleanupOrphansis a hard-scoped delete. It only ever removes directories that are direct children ofoutputDir— it will not walk into or above it, even given a maliciously constructed slug.Animated GIFs pass through, unencoded. No width variants are generated for them, but a blurhash placeholder still is; a
__og-tagged animated GIF also gets an OGP card generated from its first frame.onMetadata/onErrorare awaited. A slow or throwing callback delays (or, if it throws, aborts) the item it's attached to — keep them fast and non-throwing, or wrap your own error handling inside them.
Supported extensions
inputDir scanning matches jpg, jpeg, png, webp, heic, heif, gif, and avif (case-insensitive), exported as SUPPORTED_IMAGE_EXTENSIONS and IMAGE_EXTENSION_REGEX. Files passed explicitly via files are not filtered by extension.