fix(modifiers): strip meta data at save time - #123
Merged
olivervogel merged 1 commit intoAug 28, 2026
Conversation
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.
deluxetom
approved these changes
Aug 27, 2026
Member
|
Thanks. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
StripMetaModifierstrips by saving the image to a TIFF buffer withkeep => ForeignKeep::ICCand 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:
I think libvips rebuilds it from
xres,yresandinterpretationat save time, and only thekeepflag 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-pagesstays 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 defaultmemory_limitof 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 passkeep => ForeignKeep::ICCto the save, which is the only place the block can be kept out. The mark survivessetNative(), so it still reaches the encoder after any later modifier.The
copy()plussetNative()is deliberate. A clone shares its vips image, so removing the fields in place would empty both. AndsetNative()clears the stashed source, otherwise a later resize reloads the original file throughthumbnail()and puts the fields back.The
keepandstripblock was the same in seven encoders, it moves tosrc/Traits/CanStripMeta.php.PngEncoderandGifEncoderuse it too, they passed nokeepat 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::$stripnow 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 literalstrip => trueinstead 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.PngEncodernow reads the driver config, so it needssetDriver()when it is instantiated directly. Nothing changes throughImage::encode().Frame::toImage()builds a new core and loses the mark, so the synthesised block comes back there. Documented onCore::setMetaStripped(), not fixed.