Detach Imagick frame when converting to image - #1518
Open
nlemoine wants to merge 2 commits into
Open
Conversation
Imagick::current() returns the wand itself, so a frame taken from an animation carries the whole sequence. toImage() wrapped that same wand, which made the resulting image report every frame and read whichever one the shared wand happened to be seeked to, usually frame 0. Copy the current frame out with getImage(). It keeps the delay, the disposal method and the page geometry, so nothing the frame API exposes is lost.
Copying the current image out only lands on the right frame while the shared wand still points at it. Taking any other frame in between moved it, so a deferred toImage() returned whatever frame was touched last. Keep the position the frame was taken from and seek back before copying.
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.
On the Imagick driver, taking a frame off an animation and calling
toImage()gives back an image that still holds every frame. Reading a pixel off it then answers frame 0, whatever index you asked for.Repro on a 2 frame GIF that is red then blue (
magick -delay 50 -loop 0 -size 100x100 xc:red xc:blue animated.gif):Imagick:
GD, same bytes:
GD matches how the file was made, so the two drivers disagree.
Imagick::current()returns the wand itself, not a copy, so theFramethatCore::frame()builds wraps the multi frame wand the core holds.toImage()then didnew Core($this->native())on that same wand, which is where both thecount=2and the frame 0 pixels come from.The fix copies the current frame out with
getImage(). Copying alone is not quite enough, it only lands on the right frame while the shared wand still points at it, and taking any other frame in between moves it:So the frame also keeps the position it was taken from and seeks back before copying. I checked that the copy keeps the delay and the disposal method, and the page geometry too, so nothing the frame API exposes gets lost.
toImage()has one caller in the library, the GDGifEncoder, and that one is untouched.Three tests added to
FrameTest, one on the colors and the frame count, one asserting the result is detached from the core and does not change when the derived image is modified, one on the deferred case above. About the detach test: I first wrote it as "read frame 0's image after touching frame 1" and it passed ondevelopeven with the bug there, becausePixelColorAnalyzerdefaults to$frame = 0and seeks the shared wand back before reading. So that phrasing tests nothing, worth knowing if you write your own.One thing I left out. Two frame handles taken from the same core still alias each other for everything other than
toImage():My guess is this one should stay as it is. Modifiers mutate through
foreach ($image as $frame) { $frame->native()->... }, soframe()andcurrent()have to hand back a live handle on the wand, a copy there would silently drop every modifier. It would also copy a frame on everycolorAt()call. Happy to look at it separately if you disagree.