directus-extension-transform-image-on-upload
v1.0.1
Published
Directus hook extension for image optimization using directus's AssetsService
Downloads
178
Maintainers
Readme
Transform Directus Images on Upload
A Directus hook extension that automatically re-encodes and resizes images the moment they are uploaded — saving disk space and bandwidth without requiring any change to your Directus collections, app code, or upload flow.
When a file is uploaded through any Directus interface (Admin app, REST/GraphQL API, SDK, etc.), the hook intercepts it, converts it to a configurable target format (default: AVIF), optionally shrinks oversized images down to a maximum dimension, and replaces the stored file in place.
How it works
The extension listens for the files.upload action. For each upload it:
- Checks the file's MIME type. Only
image/*files in a Sharp-supported format (jpg,jpeg,png,webp,tiff,avif) are processed — everything else passes through untouched. - Generates an optimized variant via Directus' built-in
AssetsService, applying the configured format and quality. IfEXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTHand/orEXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHTis set, it also resizes per the configured sharpfitmode; otherwise the original dimensions are kept. - Compares the new file size against the original. If the optimization would produce a larger file (common for already-compressed assets), the original is kept.
- Otherwise the file on disk is overwritten with the optimized stream, the file extension and MIME type in the database row are updated to the new format, and stale
width/heightmetadata is cleared so Directus re-reads them.
The hook runs as an internal process (no user accountability) and disables event emission on the rewrite to prevent infinite recursion. It also retries reading the freshly uploaded asset with linear backoff, since some storage adapters need a moment to flush the bytes before they are readable.
Installation
Refer to the Official Guide for details on installing the extension from the Marketplace or manually.
Configuration
All settings are optional and read from environment variables on Directus startup. Set them in your Directus .env (or wherever you manage env vars):
| Variable | Default | Description |
| ---------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_QUALITY | 50 | Encoder quality, 1–100. Lower = smaller files, more visible artifacts. AVIF and WebP look good at much lower numbers than JPEG. |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_TARGET_FORMAT | avif | Output format. One of: avif, webp, jpg, jpeg, png, tiff. Invalid values disable the hook. |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH | unset | Maximum output width in pixels. If only this is set, the height auto-scales to preserve the original aspect ratio. See sharp resize docs. |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT | unset | Maximum output height in pixels. If only this is set, the width auto-scales to preserve the original aspect ratio. See sharp resize docs |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT | inside | How width and height combine when both are set. One of: inside, outside, cover, contain, fill. inside (default) preserves aspect ratio and fits within the box; fill ignores aspect ratio and stretches to exact dimensions. Invalid values fall back to inside. See sharp resize docs |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_WITHOUT_ENLARGEMENT | true | When true, images smaller than the target dimensions are not upscaled. Set to false to allow upscaling — usually paired with EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT=fill to force exact output sizes. |
| EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_ATTEMPTS | 10 | How many times to retry reading the freshly uploaded asset before giving up. Each retry adds a linear backoff (1 s, 2 s, 3 s, …). Raise this on slow storage backends if you see "asset not found" errors. |
Recipes
The width/height/fit triplet maps directly to sharp's resize options, so any combination sharp supports works here. A few common shapes:
1. Compress only — no resize
Omit both EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH and EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT. Original dimensions are kept; only the format/quality re-encode runs.
EXTENSIONS_TRANSFORM_ON_UPLOAD_QUALITY=70
EXTENSIONS_TRANSFORM_ON_UPLOAD_TARGET_FORMAT=avif2. Cap longest side (typical "shrink huge uploads")
Set both EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH and EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT to the same number with the default EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT=inside. Aspect ratio is preserved; landscape images cap on width, portrait on height. Smaller images are untouched.
EXTENSIONS_TRANSFORM_ON_UPLOAD_QUALITY=70
EXTENSIONS_TRANSFORM_ON_UPLOAD_TARGET_FORMAT=avif
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH=2560
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT=25603. Constrain only one axis — auto-scale the other
Set just one. sharp auto-scales the other to preserve aspect ratio. Good for "all images max 1920 px wide" feeds.
EXTENSIONS_TRANSFORM_ON_UPLOAD_QUALITY=70
EXTENSIONS_TRANSFORM_ON_UPLOAD_TARGET_FORMAT=avif
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH=19204. Force exact dimensions (stretch, ignore aspect ratio)
Set both, switch EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT to fill, and set EXTENSIONS_TRANSFORM_ON_UPLOAD_WITHOUT_ENLARGEMENT=false so even small images are upscaled. Output will be exactly EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH × EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT. Distorts non-matching aspect ratios.
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH=1024
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT=1024
EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT=fill
EXTENSIONS_TRANSFORM_ON_UPLOAD_WITHOUT_ENLARGEMENT=false5. Square thumbnails (crop to fill)
Set both, use EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT=cover. sharp scales the image to cover the box and crops the overflow — output is exactly EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH × EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT with no distortion.
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTH=512
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHT=512
EXTENSIONS_TRANSFORM_ON_UPLOAD_FIT=coverFor the full behaviour table of every fit mode (inside, outside, cover, contain, fill), see sharp's resize parameters reference.
Usage
There is nothing to invoke. Once installed and Directus is running:
- Upload an image through the Admin app, the
/filesREST endpoint, the GraphQLupload_filesmutation, or any SDK call — it is processed automatically. - The file in the database (
directus_filesrow) and on the storage adapter is replaced with the optimized version.filename_download,filename_disk, andtypeare updated to reflect the new extension and MIME type. - The file's UUID/key is preserved, so any existing references (relations, M2A, embedded URLs) keep working.
What gets processed
- Format must be one of
jpg,jpeg,png,webp,tiff,avif. - Anything else (SVG, GIF, HEIC, PDF, video, audio, raw files…) is left untouched.
- If the optimized result would be larger than the original, the original is kept and the file is left as-is.
Verifying it works
After uploading a large JPEG:
- Check the file in the Directus Admin — the extension and MIME type should match your
EXTENSIONS_TRANSFORM_ON_UPLOAD_TARGET_FORMAT. - The reported file size should be smaller than the source.
- If
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_WIDTHand/orEXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_HEIGHTis set, width/height should reflect the resized dimensions according to the configuredEXTENSIONS_TRANSFORM_ON_UPLOAD_FITmode.
Caveats
- Lossy by default. The defaults (AVIF, q=50) target storage savings, not archival fidelity. If you serve the originals to designers or printers, raise the quality or change the target format.
- No EXIF preservation guarantee. Re-encoding through Sharp drops most metadata. If you rely on EXIF (camera info, GPS, color profiles), test before deploying.
- Synchronous to the upload. Optimization happens in-line with the upload request; very large images add latency to the response.
- Storage flush retry. The hook retries reading the freshly written asset with linear backoff (1 s, 2 s, 3 s, …) up to
EXTENSIONS_TRANSFORM_ON_UPLOAD_MAX_ATTEMPTStimes (default10). Raise the env var on unusually slow storage backends if you see "asset not found" failures. - One-way. The original bytes are replaced. Keep your own archive if you need the source.
Development
npm install
npm run dev # watch + rebuild, no minification
npm run build # production build to dist/
npm run validate # validate the extension manifestThe source lives in src/index.ts and is built to dist/index.js per the directus:extension block in package.json.
License
See LICENSE.
