Keep the ICC profile of a stripped Imagick image in PNG output - #1524
Merged
olivervogel merged 1 commit intoAug 28, 2026
Merged
olivervogel merged 1 commit into
olivervogel merged 1 commit into
Conversation
Imagick::stripImage() does not remove the meta data from the property cache. It sets a "png:exclude-chunk" artifact instead, which tells the PNG encoder to skip the chunks that would carry it. That artifact stays on the wand and the PNG encoder skips every profile as soon as the text chunks are excluded (coders/png.c, the profile loop is guarded by "exclude_tEXt == false || exclude_zTXt == false"). So the profile that StripMetaModifier reads back and re-applies never reaches an encoded PNG, and neither does a resolution set afterwards. The artifact outlives the modifier, so this is not limited to calling the modifier directly. Encoding an image as JPEG with strip enabled and then as PNG drops the profile of the PNG as well. Drop the artifact and clear the property cache instead, so the meta data is really gone for every encoder. The sweep keeps "png:" and "jpeg:" properties, which are encoding hints that the PNG encoder skips anyway. Imagick::getImageProperties() cannot be the only source for that sweep, because it silently skips every property whose name starts with "[", which a PNG text chunk is able to produce. The PNG encoder has no such restriction and would write those back out. The property names reported by the "%[*]" format complete the list. That format builds a string of every property and its value, so it runs last, once the properties that can be enumerated are already gone. A stripped PNG now carries the cHRM, bKGD and pHYs chunks that the artifact used to suppress, and is a little larger for it.
Member
|
Thanks. |
Contributor
Author
|
Thanks @olivervogel |
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.
StripMetaModifierreads the icc profile, callsImagick::stripImage(), then puts the profile back. That works for JPEG, WebP and the other formats that reach the modifier. Not for PNG: the profile is on the wand afterwards, but the encoded PNG carries none.stripImage()sets apng:exclude-chunkartifact and that artifact stays on the wand, so it also hits any later PNG encode of the same image:Encoding the PNG first keeps the profile, so the result depends on the order. Same with
strip: truein the config.The cause lives in
coders/png.cthe loop that writes profiles is guarded byThe list
stripImage()sets excludes bothtEXtandzTXt, so no profile is written at all. Removing onlyiCCPfrom that list does not help, I checked, andpng:include-chunk=iCCPdoes not either. The list also excludespHYs, so a resolution set after the strip is lost in the PNG too.Fix: delete the artifact and clear the property cache instead. The sweep is needed because the JPEG reader materialises
exif:*into the property cache andstripImage()leaves them there, so once the artifact is gone the PNG writer would serialise them back into text chunks.png:andjpeg:properties are kept, the writer skips those prefixes anyway.One caveat on the sweep, and the reason I am flagging it rather than deciding it myself.
Imagick::getImageProperties()does not report properties whose name starts with[, and a PNGtEXtkeyword can produce one (0x5Bis a legal keyword byte). The PNG writer has no such restriction, so a sweep based only ongetImageProperties()lets those through, which is worse than today where the artifact suppresses every text chunk. I complete the list with the names fromidentifyFormat('%[*]'), which walks the same property iterator as the writer.That call builds a string of every property with its value. On normal images it is nothing, on a crafted PNG it is not:
exif.jpgzTXtchunks x 20 KB[The sweep runs
%[*]last, once everything it can enumerate is already deleted, which is why the middle row is cheap. The last row I could not avoid: to delete those properties I have to read them, and reading them is the cost. Timings grow faster than the payload, my guess is the repeated realloc inAppendKeyValue2Text. For reference that same file takes about 100 ms to decode, and 72 ms to decode and re-encode as PNG without stripping.I tried
identifyImage(true)(9.5 s on a 4000x3000 image),identifyImage(false)(no properties),%[property:*](returns nothing) and a glob restricted to[names to keep the output small (could not express one, the%[...]parser counts bracket depth). None of them work.So if the last row is not acceptable, the alternative is to drop the
identifyFormat('%[*]')part and accept that[prefixed text chunks survive a strip in PNG output. Happy to do that, just tell me which you prefer. Both underlying problems look like ImageMagick ones anyway, the profile loop guarded by the text chunk flags andgetImageProperties()hiding names the writer will write. Not reported upstream yet.Side effect: a stripped PNG now carries the
cHRM,bKGDandpHYschunks the artifact used to suppress, so it is slightly larger.test.jpgstripped and encoded as PNG goes from 2356 to 3034 bytes, 536 of which are the icc profile that was missing.