Repository navigation
feat: add sharp file transformer with dynamic image resizing #18411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
paulpopus
merged 10 commits into
feat/file-transformers-core
from
feat/file-transformers-sharp
Oct 5, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
aa85e9e
feat(transformer-sharp): add sharp file transformer package
r1tsuu d820483
Merge remote-tracking branch 'origin/feat/file-transformers-core' int…
r1tsuu c835a39
chore(transformer-sharp): write sanitized upload.variants instead of …
r1tsuu d41d543
chore(transformer-sharp): address review feedback
r1tsuu 33a22a1
Merge branch 'feat/file-transformers-core' into feat/file-transformer…
r1tsuu a4087a5
chore(transformer-sharp): collapse near-identical spec cases into tables
r1tsuu f0c43a5
Merge branch 'feat/file-transformers-core' into feat/file-transformer…
r1tsuu 83a58f2
Merge branch 'feat/file-transformers-core' into feat/file-transformer…
r1tsuu 585dd96
Merge branch 'feat/file-transformers-core' into feat/file-transformer…
r1tsuu 8563a75
chore: publish transformer-sharp and drop its unused dependencies
r1tsuu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "$schema": "https://json.schemastore.org/swcrc", | ||
| "sourceMaps": true, | ||
| "jsc": { | ||
| "target": "esnext", | ||
| "parser": { | ||
| "syntax": "typescript", | ||
| "tsx": true, | ||
| "dts": true | ||
| } | ||
| }, | ||
| "module": { | ||
| "type": "es6" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Sharp Transformer for Payload | ||
|
|
||
| Payload's official [file transformer](https://payloadcms.com/docs/upload/transformers) for Sharp-based image processing: upload-time resizing/format conversion (the same behavior Payload has always provided) plus opt-in request-time dynamic resizing. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```sh | ||
| pnpm add @payloadcms/transformer-sharp | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| Register `sharpTransformer()` under `upload.transformers` in your Payload Config: | ||
|
|
||
| ```ts | ||
| import { buildConfig } from 'payload' | ||
| import { sharpTransformer } from '@payloadcms/transformer-sharp' | ||
|
|
||
| export default buildConfig({ | ||
| upload: { | ||
| transformers: [sharpTransformer()], | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| ### Upload-time image processing | ||
|
|
||
| Configure per-collection Sharp settings through `sharpTransformer({ collections })`: | ||
|
|
||
| ```ts | ||
| sharpTransformer({ | ||
| collections: { | ||
| media: { | ||
| variants: [ | ||
| { name: 'thumbnail', width: 400, height: 300 }, | ||
| { name: 'card', width: 768, height: 1024 }, | ||
| ], | ||
| resizeOptions: { width: 2048, height: 2048, withoutEnlargement: true }, | ||
| formatOptions: { format: 'webp' }, | ||
| trimOptions: undefined, | ||
| constructorOptions: undefined, | ||
| withMetadata: false, | ||
| crop: true, | ||
| focalPoint: true, | ||
| }, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| `variants`, `crop`, and `focalPoint` are also written back onto the collection's own sanitized `upload` config at startup (name/`admin`/`generateImageName` only, for `variants`), so the Admin Panel, generated types, and the `sizes` shape on your documents behave exactly as before. | ||
|
|
||
| ### Dynamic (request-time) resizing | ||
|
|
||
| Dynamic resizing is **disabled by default** — resize query parameters are ignored and the original file is served. Enable it for every upload collection, or only the ones that need it: | ||
|
|
||
| ```ts | ||
| sharpTransformer({ dynamic: true }) | ||
| sharpTransformer({ dynamic: { collections: ['media'] } }) | ||
| ``` | ||
|
|
||
| Once enabled, requesting a stored image with recognized query parameters resizes it on the fly, without storing the result: | ||
|
|
||
| ``` | ||
| GET /api/media/file/photo.png?width=400 | ||
| GET /api/media/file/photo.png?width=400&height=300 | ||
| GET /api/media/file/photo.png?width=400&withoutEnlargement=true | ||
| ``` | ||
|
|
||
| Configure limits and defaults for these requests: | ||
|
|
||
| ```ts | ||
| sharpTransformer({ | ||
| dynamic: { | ||
| collections: ['media'], // omit to allow every upload collection | ||
| fit: 'cover', // default | ||
| position: 'center', // default | ||
| maxWidth: 4096, // default | ||
| maxHeight: 4096, // default | ||
| maxPixels: 16_777_216, // default | ||
| withoutEnlargement: false, // default | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| Resized output is not cached, and anyone who can read a file can request it at any size within these limits. Before enabling dynamic resizing on a public collection, see [Securing dynamic resizing](https://payloadcms.com/docs/upload/transformers#securing-dynamic-resizing) for how to restrict it with access control, tighten the limits, and cache the output. | ||
|
|
||
| ### Injecting a custom Sharp build | ||
|
|
||
| ```ts | ||
| import customSharp from 'sharp' | ||
|
|
||
| sharpTransformer({ sharp: customSharp }) | ||
| ``` | ||
|
|
||
| See the [File Transformers](https://payloadcms.com/docs/upload/transformers) docs for the full transformer contract, access-control lifecycle, and non-goals (no caching, no CDN integration, no jobs queue). | ||
|
|
||
| ## Migrating from Payload 3.x | ||
|
|
||
| If you previously configured `sharp`/`resizeOptions`/`imageSizes`/etc. directly on your Payload Config or collections, run: | ||
|
|
||
| ```sh | ||
| npx @payloadcms/codemod --transform migrate-sharp-to-transformer | ||
| ``` | ||
|
|
||
| See the [v4 migration guide](https://payloadcms.com/docs/migration-guide/v4#sharp-moved-out-of-core-into-payloadcmstransformer-sharp) for details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| { | ||
| "name": "@payloadcms/transformer-sharp", | ||
| "version": "4.0.0-canary.37", | ||
| "description": "Official Sharp-based file transformer for Payload: request-time image resizing and upload-time image processing", | ||
| "homepage": "https://payloadcms.com", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/payloadcms/payload.git", | ||
| "directory": "packages/transformer-sharp" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "Payload <dev@payloadcms.com> (https://payloadcms.com)", | ||
| "maintainers": [ | ||
| { | ||
| "name": "Payload", | ||
| "email": "info@payloadcms.com", | ||
| "url": "https://payloadcms.com" | ||
| } | ||
| ], | ||
| "sideEffects": false, | ||
| "type": "module", | ||
| "exports": { | ||
| ".": { | ||
| "import": "./src/index.ts", | ||
| "types": "./src/index.ts", | ||
| "default": "./src/index.ts" | ||
| } | ||
| }, | ||
| "main": "./src/index.ts", | ||
| "types": "./src/index.ts", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "pnpm build:types && pnpm build:swc", | ||
| "build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build", | ||
| "build:debug": "pnpm build", | ||
| "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths", | ||
| "build:types": "tsc --emitDeclarationOnly --outDir dist", | ||
| "clean": "rimraf -g {dist,*.tsbuildinfo}", | ||
| "lint": "eslint .", | ||
| "lint:fix": "eslint . --fix" | ||
| }, | ||
| "dependencies": { | ||
| "sharp": "0.35.4" | ||
| }, | ||
| "devDependencies": { | ||
| "payload": "workspace:*" | ||
| }, | ||
| "peerDependencies": { | ||
| "payload": "workspace:*" | ||
| }, | ||
| "engines": { | ||
| "node": ">=24.15.0" | ||
| }, | ||
| "publishConfig": { | ||
| "exports": { | ||
| ".": { | ||
| "import": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| } | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "types": "./dist/index.d.ts" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export function canResizeImage(mimeType: string): boolean { | ||
| return ( | ||
| ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/tiff', 'image/avif'].indexOf( | ||
| mimeType, | ||
| ) > -1 | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import type { ImageSize, ProbedImageSize } from 'payload' | ||
|
|
||
| import { isNumber } from 'payload/shared' | ||
|
|
||
| /** | ||
| * Determine whether or not to resize the image. | ||
| * - resize using image config | ||
| * - resize using image config with focal adjustments | ||
| * - do not resize at all | ||
| * | ||
| * `imageResizeConfig.withoutEnlargement`: | ||
| * - undefined [default]: uploading images with smaller width AND height than the image size will return null | ||
| * - false: always enlarge images to the image size | ||
| * - true: if the image is smaller than the image size, return the original image | ||
| * | ||
| * `imageResizeConfig.withoutReduction`: | ||
| * - false [default]: always enlarge images to the image size | ||
| * - true: if the image is smaller than the image size, return the original image | ||
| * | ||
| * @return 'omit' | 'resize' | 'resizeWithFocalPoint' | ||
| */ | ||
| export const getImageResizeAction = ({ | ||
| dimensions: originalImage, | ||
| hasFocalPoint, | ||
| imageResizeConfig, | ||
| }: { | ||
| dimensions: ProbedImageSize | ||
| hasFocalPoint?: boolean | ||
| imageResizeConfig: ImageSize | ||
| }): 'omit' | 'resize' | 'resizeWithFocalPoint' => { | ||
| const { fit, withoutEnlargement, withoutReduction } = imageResizeConfig | ||
| const targetWidth = imageResizeConfig.width! | ||
| const targetHeight = imageResizeConfig.height! | ||
|
|
||
| // prevent upscaling by default when x and y are both smaller than target image size | ||
| if (targetHeight && targetWidth) { | ||
| const originalImageIsSmallerXAndY = | ||
| originalImage.width < targetWidth && originalImage.height < targetHeight | ||
| if (withoutEnlargement === undefined && originalImageIsSmallerXAndY) { | ||
| return 'omit' // prevent image size from being enlarged | ||
| } | ||
| } | ||
|
|
||
| if (withoutEnlargement === undefined && (!targetWidth || !targetHeight)) { | ||
| if ( | ||
| (targetWidth && originalImage.width < targetWidth) || | ||
| (targetHeight && originalImage.height < targetHeight) | ||
| ) { | ||
| return 'omit' | ||
| } | ||
| } | ||
|
|
||
| const originalImageIsSmallerXOrY = | ||
| originalImage.width < targetWidth || originalImage.height < targetHeight | ||
| if (fit === 'contain' || fit === 'inside') { | ||
| return 'resize' | ||
| } | ||
| if (!isNumber(targetHeight) && !isNumber(targetWidth)) { | ||
| return 'resize' | ||
| } | ||
|
|
||
| const targetAspectRatio = targetWidth / targetHeight | ||
| const originalAspectRatio = originalImage.width / originalImage.height | ||
| if (originalAspectRatio === targetAspectRatio) { | ||
| return 'resize' | ||
| } | ||
|
|
||
| if (withoutEnlargement && originalImageIsSmallerXOrY) { | ||
| return 'resize' | ||
| } | ||
| if (withoutReduction && !originalImageIsSmallerXOrY) { | ||
| return 'resize' | ||
| } | ||
|
|
||
| return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize' | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@payloadcms/transformer-sharpis absent frompackagePublishList. The release tool will not update its version or publish it. Addtransformer-sharpafterpayloadand add a completeness test for public packages.