Describe the Bug
A WebP, GIF or TIFF uploaded to a collection with upload: true and nothing else is decoded and written again by sharp with its default settings. The collection sets no resizeOptions, formatOptions, trimOptions, imageSizes or crop. A JPEG or PNG uploaded to the same collection is stored as uploaded.
What happens to a quality-95 WebP of 541,550 B with a Display P3 colour profile:
- The stored file is 268,646 B. sharp writes it at its default quality of 80.
- The colour profile is gone, because
withMetadata is off by default.
- A duplicate of the document re-encodes the stored file once more: 252,154 B. Each duplicate loses quality again.
The same happens to an animated WebP and to a GIF. The GIF keeps its size here, but its bytes change.
The cause is in packages/payload/src/uploads/generateFileData.ts. A file whose type can be animated always goes through sharp, whether or not it has more than one frame, and whether or not anything has to change:
if (sharp && (fileIsAnimatedType || fileHasAdjustments)) {
Expected: when the collection asks for no change, the file is stored as uploaded, as a JPEG is.
The condition dates from #6708, which creates the sharp file for every animated type so that the crop code can read its metadata. #11612 later said "It should be possible to upload files without compression" and removed imageSizes from the adjustments, but left this branch as it was.
#18346 stores single-frame WebP, GIF and TIFF files as uploaded. It is open and has no linked issue. With it, a file with more than one frame is still re-encoded, on upload and on each duplicate. The same condition is on main.
Link to the code that reproduces this issue
https://github.com/franknoel/payload-webp-gif-recompressed
Reproduction Steps
- Clone the repository.
cp .env.example .env, then fill in DATABASE_URL and PAYLOAD_SECRET.
pnpm install
pnpm check
The script empties the Media collection, uploads each file of fixtures/ with the local API, duplicates it, and compares every stored file with the fixture:
same bytes photo.jpg upload fixture 557406 B stored 557406 B colour profile yes -> yes
same bytes photo.jpg duplicate fixture 557406 B stored 557406 B colour profile yes -> yes
RE-ENCODED photo.webp upload fixture 541550 B stored 268646 B colour profile yes -> no
RE-ENCODED photo.webp duplicate fixture 541550 B stored 252154 B colour profile yes -> no
RE-ENCODED animation.gif upload fixture 4155 B stored 4155 B colour profile no -> no
RE-ENCODED animation.gif duplicate fixture 4155 B stored 4155 B colour profile no -> no
RE-ENCODED animation.webp upload fixture 5014 B stored 3040 B colour profile no -> no
RE-ENCODED animation.webp duplicate fixture 5014 B stored 2904 B colour profile no -> no
The same happens over REST, the way the admin uploads: with pnpm dev running, a POST /api/media with fixtures/photo.webp stores a file of 268,646 B in media/.
Which area(s) are affected?
area: core
Environment Info
Binaries:
Node: 24.21.0
npm: 11.19.0
Yarn: 1.22.22
pnpm: 9.15.9
Relevant Packages:
payload: 3.90.2
next: 16.3.3
@payloadcms/db-mongodb: 3.90.2
@payloadcms/graphql: 3.90.2
@payloadcms/next/utilities: 3.90.2
@payloadcms/richtext-lexical: 3.90.2
@payloadcms/translations: 3.90.2
@payloadcms/ui/shared: 3.90.2
react: 19.2.6
react-dom: 19.2.6
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0
Available memory (MB): 32768
Available CPU cores: 10
Describe the Bug
A WebP, GIF or TIFF uploaded to a collection with
upload: trueand nothing else is decoded and written again by sharp with its default settings. The collection sets noresizeOptions,formatOptions,trimOptions,imageSizesor crop. A JPEG or PNG uploaded to the same collection is stored as uploaded.What happens to a quality-95 WebP of 541,550 B with a Display P3 colour profile:
withMetadatais off by default.The same happens to an animated WebP and to a GIF. The GIF keeps its size here, but its bytes change.
The cause is in
packages/payload/src/uploads/generateFileData.ts. A file whose type can be animated always goes through sharp, whether or not it has more than one frame, and whether or not anything has to change:Expected: when the collection asks for no change, the file is stored as uploaded, as a JPEG is.
The condition dates from #6708, which creates the sharp file for every animated type so that the crop code can read its metadata. #11612 later said "It should be possible to upload files without compression" and removed
imageSizesfrom the adjustments, but left this branch as it was.#18346 stores single-frame WebP, GIF and TIFF files as uploaded. It is open and has no linked issue. With it, a file with more than one frame is still re-encoded, on upload and on each duplicate. The same condition is on
main.Link to the code that reproduces this issue
https://github.com/franknoel/payload-webp-gif-recompressed
Reproduction Steps
cp .env.example .env, then fill inDATABASE_URLandPAYLOAD_SECRET.pnpm installpnpm checkThe script empties the Media collection, uploads each file of
fixtures/with the local API, duplicates it, and compares every stored file with the fixture:The same happens over REST, the way the admin uploads: with
pnpm devrunning, aPOST /api/mediawithfixtures/photo.webpstores a file of 268,646 B inmedia/.Which area(s) are affected?
area: core
Environment Info