Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(modifiers): strip meta data at save time - #123

Merged
olivervogel merged 1 commit into
Intervention:developfrom
nlemoine:fix/strip-meta-modifier
Aug 28, 2026
Merged

fix(modifiers): strip meta data at save time#123
olivervogel merged 1 commit into
Intervention:developfrom
nlemoine:fix/strip-meta-modifier

Conversation

@nlemoine

Copy link
Copy Markdown
Contributor

StripMetaModifier strips by saving the image to a TIFF buffer with keep => ForeignKeep::ICC and reading it back. That removes the fields from the image, but the next save writes a 186 byte EXIF block anyway.

A brand new image carrying no metadata shows the same block, so it is not coming from the source:

VipsImage::black(10, 10) -> .jpg   exif-data 186 bytes
VipsImage::black(10, 10) -> .png   exif-data 180 bytes

I think libvips rebuilds it from xres, yres and interpretation at save time, and only the keep flag on the writer suppresses it. Removing fields from the image cannot reach that.

The round trip has two other effects. It reads back only the first TIFF page while n-pages stays at 8, so an animation loses its frames and still claims to have them (count() returns 8, frame(3) throws). And it puts the whole uncompressed TIFF into a PHP string, 207 MB on a 4000x2667 JPEG, which dies at the default memory_limit of 128M.

The modifier no longer round trips. It copies the image, removes the metadata fields it can, and marks the core with Core::setMetaStripped(). The encoders read that mark and pass keep => ForeignKeep::ICC to the save, which is the only place the block can be kept out. The mark survives setNative(), so it still reaches the encoder after any later modifier.

The copy() plus setNative() is deliberate. A clone shares its vips image, so removing the fields in place would empty both. And setNative() clears the stashed source, otherwise a later resize reloads the original file through thumbnail() and puts the fields back.

The keep and strip block was the same in seven encoders, it moves to src/Traits/CanStripMeta.php. PngEncoder and GifEncoder use it too, they passed no keep at all before.

After a strip, JPEG, PNG, WEBP and AVIF come out with no profile at all. ICC is kept, animations keep their frames, delays and loop count, and without a strip nothing changes.

Worth flagging:

  • Config::$strip now applies to PNG and GIF, those two ignored it before. Tell me if you would rather keep them out of it.
  • TiffEncoder's pre-8.15 branch passed a literal strip => true instead of the resolved value, the shared helper settles that. I noticed it in fix(encoders): version-gate ForeignKeep::ALL for GAINMAP (libvips 8.18) #103 and left it then.
  • PngEncoder now reads the driver config, so it needs setDriver() when it is instantiated directly. Nothing changes through Image::encode().
  • Frame::toImage() builds a new core and loses the mark, so the synthesised block comes back there. Documented on Core::setMetaStripped(), not fixed.

StripMetaModifier stripped by saving the image to a TIFF buffer with
`keep => ForeignKeep::ICC` and reading it back. That does remove the
fields from the image, but libvips builds an EXIF block from the image's
core fields at save time whether or not the image carries one, so the
next save wrote a 186 byte block anyway. A brand new `VipsImage::black()`
carrying no metadata at all shows the same block, which is what makes it
clear the block is synthesised and not copied. Only the `keep` flag on
the save itself keeps it out.

The round trip had two more consequences. `tiffsave_buffer` wrote every
page but `newFromBuffer` read back only the first, leaving `n-pages` at 8
on an image holding one frame, so `count()` reported 8 frames while
`frame(3)` threw. And it materialised the whole uncompressed TIFF into a
PHP string, 207 MB on a 4000x2667 JPEG, which is fatal at the default
128M `memory_limit`.

So drop the round trip. The modifier now copies the image, removes the
metadata fields it can, and marks the core. The encoders read that mark
and pass the matching `keep` option to the save, which is the only place
the synthesised block can be suppressed. The copy plus `setNative()` is
deliberate, it leaves a clone's fields intact and it clears the stashed
source, so a later resize cannot reload the original file and put the
fields back.

The `keep` and `strip` block was duplicated across seven encoders. It
moves to `CanStripMeta`, which `PngEncoder` and `GifEncoder` now use as
well, so `Config::$strip` reaches PNG and GIF for the first time.
`TiffEncoder`'s pre-8.15 branch passed a literal `strip => true` where
its siblings passed the resolved value, the shared helper settles that
too.

Measured on a 4000x2667 JPEG: 122 ms and 207 MB before, 36 ms and 6 MB
after.
@olivervogel

Copy link
Copy Markdown
Member

Thanks.

@olivervogel
olivervogel merged commit ee86c2a into Intervention:develop Aug 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants