Info | Value |
-------------------------|-------------------------------------|
Platform Name | ios
Platform Version | 10+
SDWebImage Version | 4.4.2
Integration Method | cocoapods
Xcode Version | Xcode 9.4
Repro rate | 100% given the steps
Repro with our demo prj | - -
Demo project link | - -
When I load a Gif into the same FLAnimatedImageView that was previously used to load a JPG(which was taken with the camera), the gif appears to be rotated. But if I load that same cached Gif without it previously loading the JPG, the Gif isn't rotated.
This is happening in a tableview which has tableviewcells that have FLAnimatedImageView's on them.
Similar Issue: https://github.com/rs/SDWebImage/issues/1317
From what I've found in my searches this has something to with the JPG having EXIF metadata and the Gif not, but I'm unsure what I can do to fix the issue.
One of the JPG's causing the issue: https://skrumble-dev-assets.s3.amazonaws.com/chats/5b4cadeaf1ad970f5063416b/4744214435480065278.jpg
Even easier way to reproduce it is,
I select a JPG from my Photo library & load it into an image view, then I select a gif from my photo library and load it into the same image view, the displayed Gif is rotated.
It seems a FLAnimatedImageView bug. UIImageView and our decoder always process the EXIF to image orientation correctly.
Could you please help to provide a demo that can trigger this issue ? Maybe we can figure it out quickly.
Here's a test repo link:
https://github.com/ericeddy/FLAnimatedImage-RotationIssue
I've noticed that using just any jpg doesn't cause it, like random jpgs from the internet cause it to occur sporadically,
But using images taken with the phone's camera will cause it 100% of the time, and using a landscape photo vs using a portrait photo will add an extra 90 degrees to the rotation
@ericeddy After some test and my experiment, I can confirm it's a BUG in FLAnimatedImage third party library itself. I've create a MR to the FLAnimatedImage repo. You can push them to merge it and solve this problem.
Now I can talk about the reason that cause this problem. At first, you should have basic knowledge of UIView-CALayer, if you don't know them, you should watch Understanding UIKit Rendering before reading the below.
FLAnimatedImageView, it's a UIImageView subclass, which manually specify CALayer.contents with the current rendering frame's CGImage. And they use a CADisplayLink as a timer, to refresh current frame, finally produce the visual animation.
However, CALayer itself, contains a private property, called contentsTransform. You can check the private header of CALayer. CALayer will calculate current UIImage.imageOrientation and translate to CGAffineTransform to render the bitmap. Why they to do this it's because it does not actually take rotation on CGImage's bitmap when you create UIImage with -[UIImage imageWithCGImage:scale:orientation:], The both scale and imageOrientation are just hint to the CALayer's property contentsScale and contentsTransform. So that CALayer use the contentsTransform to actually do rotation about the bitmap.
Which means, actually, CALayer draw its contents, based on the both public CALayer.transform and private CALayer.contentsTransform two properties.
See the screenshot during View Debug and print out the CALayer information. The actually CALayer.contents bitmap show the correct orientation, but the rendering result cause a rotation because the CALayer contains a contentsTransform = CGAffineTransform (0 1.06667; -0.9375 0; 375 0), which is a right-90 degress rotation.

After I use KVO to observe this contentsTransform changes, I found that the only write on this property was happend on -[UIImageView setImage:] function call with a non-nil UIImage. The non-nil is really important, when you sepcify nil, the contentsTransform property leave there without reset to identity transform. (Because UIKit assume you don't touch layer.contents and so they don't need to reset the state)

So now, you may know the reason now. Correct, the previous JPEG image which contains non-Up image orienation will change the conrtentsTransform, but the newly set FLAnimatedImage does not clear that transform, cause the final rotation. I check the current FLAnimatedImage code and finally find the bug. The root case is that -[FLAnimatedImage setAnimatedImage:] does not do the correct thing, to reset the super UIImageView state about image.
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
if (![_animatedImage isEqual:animatedImage]) {
if (animatedImage) {
// Clear out the image.
super.image = nil; // -> This will not reset `contentsTransform`, not actually "Clear out the image."
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
super.highlighted = NO;
//...
}
}
The naive solution, it's quite simple, firstlly call super.image = non-nil image and then call super.image = nil. Only one line code and the issue disappear.
--- a/FLAnimatedImage/FLAnimatedImageView.m
+++ b/FLAnimatedImage/FLAnimatedImageView.m
@@ -101,6 +101,7 @@
{
if (![_animatedImage isEqual:animatedImage]) {
if (animatedImage) {
+ // Reset `contentsTransform` state
+ super.image = animatedImage.posterImage;
// Clear out the image.
super.image = nil;
// Ensure disabled highlighting; it's not supported (see `-setHighlighted:`).
From my personal opinion, I think they should always call [super setImage:] during [FLAnimatedImageView setAnimatedImage:]. If you don't call super, you may loss internal consistent behavior during subclass. But anyway, this fix is much simpler.
Yes. FLAnimatedImage seems no longer maintained, though I fire the PR, it may take long time to merge in (or may be forever :) ). And it's the why we create our Animated Image solution in SDWebImage 5.0.0-beta.
For our FLAnimatedImage+WebCache, the simple way to fix this, it's just use the code above, call super.image = animatedImage.posterImage before super.animatedImage = animatedImage. I will create another PR fix for SDWebImage/GIF subspec.
@ericeddy See #2406. The fix (hack) from ourself to solve the problem :)
And is there anyone who also face this issue ? You can push FLAnimatedImage team (I don't know whether they still maintain that repo or not) to merge the PR. It can solve the bug from the root case, instead of hacking code from our user level.
Thank you for such a thought out and fast response!
I think for the meantime I'm going to go ahead with your suggestion and use 5.0.0-beta, I tried out how it works in the demo and it seems to be working well,
Again, Thank you for all this work, I really appreciate it!
Most helpful comment
@ericeddy After some test and my experiment, I can confirm it's a BUG in
FLAnimatedImagethird party library itself. I've create a MR to the FLAnimatedImage repo. You can push them to merge it and solve this problem.Now I can talk about the reason that cause this problem. At first, you should have basic knowledge of UIView-CALayer, if you don't know them, you should watch Understanding UIKit Rendering before reading the below.
What cause this problem
FLAnimatedImageView, it's a UIImageView subclass, which manually specify CALayer.contents with the current rendering frame'sCGImage. And they use a CADisplayLink as a timer, to refresh current frame, finally produce the visual animation.However,
CALayeritself, contains a private property, calledcontentsTransform. You can check the private header of CALayer.CALayerwill calculate currentUIImage.imageOrientationand translate toCGAffineTransformto render the bitmap. Why they to do this it's because it does not actually take rotation onCGImage's bitmap when you create UIImage with -[UIImage imageWithCGImage:scale:orientation:], The bothscaleandimageOrientationare just hint to the CALayer's propertycontentsScaleandcontentsTransform. So thatCALayeruse thecontentsTransformto actually do rotation about the bitmap.Which means, actually,
CALayerdraw its contents, based on the both publicCALayer.transformand privateCALayer.contentsTransformtwo properties.See the screenshot during View Debug and print out the CALayer information. The actually
CALayer.contentsbitmap show the correct orientation, but the rendering result cause a rotation because the CALayer contains acontentsTransform = CGAffineTransform (0 1.06667; -0.9375 0; 375 0), which is a right-90 degress rotation.After I use KVO to observe this
contentsTransformchanges, I found that the only write on this property was happend on-[UIImageView setImage:]function call with a non-nilUIImage. The non-nil is really important, when you sepcify nil, thecontentsTransformproperty leave there without reset to identity transform. (Because UIKit assume you don't touch layer.contents and so they don't need to reset the state)So now, you may know the reason now. Correct, the previous JPEG image which contains non-Up image orienation will change the
conrtentsTransform, but the newly setFLAnimatedImagedoes not clear that transform, cause the final rotation. I check the current FLAnimatedImage code and finally find the bug. The root case is that-[FLAnimatedImage setAnimatedImage:]does not do the correct thing, to reset the superUIImageViewstate about image.Solution
The naive solution, it's quite simple, firstlly call
super.image = non-nil imageand then callsuper.image = nil. Only one line code and the issue disappear.From my personal opinion, I think they should always call
[super setImage:]during[FLAnimatedImageView setAnimatedImage:]. If you don't call super, you may loss internal consistent behavior during subclass. But anyway, this fix is much simpler.Current workaround ?
Yes. FLAnimatedImage seems no longer maintained, though I fire the PR, it may take long time to merge in (or may be forever :) ). And it's the why we create our Animated Image solution in SDWebImage 5.0.0-beta.
For our
FLAnimatedImage+WebCache, the simple way to fix this, it's just use the code above, callsuper.image = animatedImage.posterImagebeforesuper.animatedImage = animatedImage. I will create another PR fix for SDWebImage/GIF subspec.