Skip to content

Every WebP and GIF upload is re-encoded, even when the collection sets no image option #18386

Description

@franknoel

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

  1. Clone the repository. cp .env.example .env, then fill in DATABASE_URL and PAYLOAD_SECRET.
  2. pnpm install
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bugarea: coreCore Payload functionalitystatus: needs-triagePossible bug which hasn't been reproduced yetv3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions