Orient every frame of an animated Imagick image - #1519
Merged
olivervogel merged 2 commits intoAug 23, 2026
Conversation
rotateImage() and flopImage() act on the frame the iterator is currently pointing at, so an animated source came back with one frame turned and the rest untouched, a sequence of mixed geometries. The TOPLEFT marker was written on that one frame too, so the others stayed flagged with the original orientation. Move the transform into the frame loop the other modifiers already use.
getImageOrientation() is a current image call like the transforms below it, so the value depended on where the iterator had been left. Now that the result is applied to the whole sequence, a stale read turns every frame the wrong way instead of just one. Also assert the mirrored pixel on both sides and the alignment marker in the flop test, the single pixel it checked would pass on an image that was filled rather than mirrored.
Member
|
Thank you. |
Contributor
Author
|
Thanks Oliver! |
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.
Drivers\Imagick\Modifiers\OrientModifier::apply()callsrotateImage()andflopImage()straight on$image->core()->native()with no iteration. Those act on the frame the iterator is currently pointing at, so an animated source comes back with one frame turned and every other frame as it was.Repro, a 2 frame 40x20 GIF with orientation 6 set on both frames:
So a mixed geometry sequence, and the frames that were skipped keep their original orientation flag because the
setImageOrientation(TOPLEFT)at the end lands on the same single frame. Encoded back to GIF it composes wrong, frame 1 came back showing frame 0's background instead of its own color.The fix moves the transform into the
foreach ($image as $frame)loop the other modifiers already use, and marks each frame aligned instead of just one. After the patch both frames are 20x40 with orientation 1, and the re-encoded GIF reads blue over the whole of frame 1.getImageOrientation()is a current image call too, so I made it read the first frame explicitly. It was already positional before, but it only mis-turned one frame back then. Now that the value drives the whole sequence a stale read turns everything the wrong way. Measured on a source with mixed per frame flags,orient()gave 4x2 or 2x4 for the same input depending on where the iterator sat, and it is stable at 2x4 after. That path is not reachable through the decoders as far as I can tell,coalesceImages()leaves the iterator at 0, so this is defensive.I looked at whether this needs
RotateModifier'ssetImagePage(0, 0, 0, 0)and it does not. That guard is for the negative page offsets of non right angles (measured6x6+-1+-2at 45 degrees), and orient only ever uses 90, 180 and 270, which all come out+0+0. Encoding an oriented animation with and without the reset gave byte identical GIF and animated AVIF.Worth saying that the input is awkward to build, GIF carries no EXIF so the orientation has to be set in memory on each frame. That is probably why this went unnoticed.
Two tests added, one for a rotating orientation (6) checking the geometry and the flag on every frame, one for the mirror only orientation (2) on frames that are red on the left and blue on the right.
AutoOrientationTestcovers the single frame cases for all eight orientations on both drivers and still passes.One note on writing the second test. I first read the mirrored pixel through
$frame->toImage($driver)->colorAt(0, 0)and it went green ondevelopwith the bug in place, because on ImagicktoImage()hands back the whole sequence andcolorAt()then answers frame 0 (that is #1518). Reading throughcolorAt($x, $y, $frame)on the original image works, it goes throughframe($i)->native()and reads before anything moves the wand.Same shape as #1517, which was the colorspace modifier missing the same loop.