- preface
- How to play GIF, how to invert GIF?
- GIF inverted implementation
- Scratch each frame out of the GIF
- GifFrameLoader.loadNextFrame
- Get each frame and save it in the collection
- Generate the image again with a sequence of frames
- GIF run backward
- demo
- Scratch each frame out of the GIF
- conclusion
- The source code
- Reference documentation
preface
I often see some funny GIFs on the Internet, and some of them become even more interesting when inverted. They are a daily source of joy.
Like this one down here
It’s funny when it’s playing, it’s sad; When the upside down is actually very cool, it is better than Duan Yu’s ling Bo micro step also cattle force, there is a kind of peerless magic has been trained into the feeling 😎😎😎😎😎, is it possible to and the Hulk world War ONE yuan.
One more
Small boy’s happiness and sadness is so simple, teh baby 😊😊😊😊
🤣🤣🤣🤣🤣🤣🤣 Here’s how to do GIF inversion.
All the following implementation details have been synchronized to the GitHub repository, please refer to the lastThe source code
How to play GIF, how to invert GIF?
Want to invert GIF image, first understand the principle of GIF; Here suggest to look at this article from Tencent hand Q team is the essence of concentration: analysis of GIF format picture storage and compression. In general, the biggest difference between GIF and photoshop is that it is made up of many frames. In this case, it’s easy to think, wouldn’t you just take all the frames out of a GIF, combine them in reverse order, and create a GIF?
Yes, it’s as simple as that. If you go to Google GIF’s reverse order implementation now, you’ll see a number of Python implementations like this:
import os
import sys
from PIL import Image, ImageSequence
path = sys.path[0] Set path -- the current path of the system
dirs = os.listdir(path) Get the files in this path
for i in dirs: # loop through all files
if os.path.splitext(i)[1] = =".gif": Filter GIF files
print(i) Print all GIF file names
# Save the GIF backwards
with Image.open(i) as im:
if im.is_animated:
frames = [f.copy() for f in ImageSequence.Iterator(im)]
frames.reverse() # Built-in list in reverse order
frames[0].save('./save/reverse_'+i+'.gif',save_all=True, append_images=frames[1:)# save
Copy the code
I have to say that Python’s various tripartite libraries are really powerful, and it takes just a few lines of code to reverse GIF order. But as a slightly aspirational person, does that end there? The next time you have a funny GIF and want to see it in reverse order, do you want to turn on your computer and use the script above?
Especially as an Android developer, isn’t that something you can do on your phone? Even if the sky falls and the earth runs dry and the rocks crumble for a daily source of happiness.
All right, enough boasting. Here’s how to do it.
GIF inverted implementation
As mentioned above, there are three things you need to do to achieve reverse GIF order
- Take each frame from the GIF and sequence it
- Reverse the sequence
- Create a new GIF with each frame in reverse order
The above two steps are not difficult to reverse the set. Let’s see how to implement step 1 and step 3.
Scratch each frame out of the GIF
This sounds complicated, and it’s not easy to do. Android doesn’t have an API to do this, nor does Glide, Fresco, or any other library used to load images. But in fact we have a little in-depth look at the third party library is to achieve GIF playback code, will find a breakthrough, here to Glide as an example, assuming you have studied the source Glide (if not seen, it does not matter, you can skip this section, directly look at the implementation)
GifFrameLoader.loadNextFrame
In GifFrameLoader’s loadNextFrame implementation (we can guess this is how Glide loads each frame)
private void loadNextFrame(a) {
if(! isRunning || isLoadPending) {return;
}
if (startFromFirstFrame) {
Preconditions.checkArgument(
pendingTarget == null."Pending target must be null when starting from the first frame");
gifDecoder.resetFrameIndex();
startFromFirstFrame = false;
}
if(pendingTarget ! =null) {
DelayTarget temp = pendingTarget;
pendingTarget = null;
onFrameReady(temp);
return;
}
isLoadPending = true;
// Get the delay before incrementing the pointer because the delay indicates the amount of time
// we want to spend on the current frame.
int delay = gifDecoder.getNextDelay();
long targetTime = SystemClock.uptimeMillis() + delay;
gifDecoder.advance();
next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
}
Copy the code
You can see that the specific implementation is implemented by the gifDecoder object. The key sentence here is
gifDecoder.advance();
Copy the code
We can look at the definition of this method
/** * Move the animation frame counter forward. */
void advance(a);
Copy the code
Jump to the next frame.
Ok, so now we know that if we can get GifDeCoder and GifFrameLoader instances, then we can manually control and fetch each frame in the GIF. However, if we go back to the API supplied by Glide, we find that there is no way to get GifFrameLoader and GifDeCoder directly because these variables are private in the source code. 🤦🤦🤦, is this a dead end? Otherwise, it has been said that any problem in the programming world can be solved by adding an intermediate layer. Our middle layer here is reflection. GifFrameLoader and GifDeCoder can be accessed using reflection. Then the subsequent implementation becomes simple.
Get each frame and save it in the collection
Glide.with(mContext).asGif().listener(object:RequestListener<GifDrawable>{ ... fail stuff...override fun onResourceReady(resource: GifDrawable, model: Any? , target:Target<GifDrawable>? , dataSource:DataSource? , isFirstResource:Boolean): Boolean {
val frames = ArrayList<ResFrame>()
val decoder = getGifDecoder(resource)
if(decoder ! =null) {
for (i in 0..resource.frameCount) {
val bitmap = decoder.nextFrame
val path = IOTool.saveBitmap2Box(context, bitmap, "pic_$i")
val frame = ResFrame(decoder.getDelay(i), path)
frames.add(frame)
decoder.advance()
}
}
return false
}
}).load(originalUrl).into(original)
Copy the code
The implementation here is very simple, listen to the loading process of GIF, load a successful GifDrawable instance resource, through this instance with reflection (specific implementation can refer to the source code, very simple) to obtain the GifDecode instance, With this instance, we can get each frame, and we need to record the time interval of each frame. Each returned frame is a Bitmap, and we save the Bitmap in the installation directory of the application, and then record all the frames in a list. Contains the delay time of the current frame and the storage path of the Bitmap corresponding to the current frame.
Now that the sequence of each frame is set, the sequence is reversed one line of code, and all that’s left is to generate a new GIF with that sequence.
Generate the image again with a sequence of frames
Creating a new image with an existing image is not that difficult, and there are many online implementations. Even the re-generation of giFs can be done with a third party library like GifMaker.
private fun genGifByFrames(context: Context, frames: List<ResFrame>): String {
val os = ByteArrayOutputStream()
val encoder = AnimatedGifEncoder()
encoder.start(os)
encoder.setRepeat(0)
for (value in frames) {
val bitmap = BitmapFactory.decodeFile(value.path)
encoder.setDelay(value.delay)
encoder.addFrame(bitmap)
}
encoder.finish()
val path = IOTool.saveStreamToSDCard("test", os)
IOTool.notifySystemGallay(context, path)
return path
}
Copy the code
Using the AnimatedGifEncoder, it is very easy to splice previously saved sequences into a new GIF.
GIF run backward
Put these three steps together briefly
private fun reverseRes(context: Context, resource: GifDrawable?).: String {
if (resource == null) {
return ""
}
// Get all frame information set
val frames = getResourceFrames(resource, context)
// reverse the set
reverse(frames)
// Generate a new GIF image
return genGifByFrames(context, frames)
}
Copy the code
It should be noted that these three steps are time-consuming operations involving the UI, so RxJava is simply used to do a wrapper here. Then it can be used happily.
demo
GifFactory.getReverseRes(mContext,source)
.subscribe {
Glide.with(mContext).load(it).into(reversed)
}
Copy the code
GIF image upload limit is 5MB, the image is a bit compressed, thank you can pull source code to try the effect
Yes, it is that simple, provide the path of the original GIF resource, can be returned to achieve the reverse order of the GIF image.
conclusion
It has to be said that Glide’s internal implementation is very powerful, with very complex considerations and designs for mobile image loading scenarios, which makes the source code very difficult to read. However, if you only start from a certain point of view, such as caching, networking, image decoding and encoding, you can still gain from looking at parts of the whole process.
Going back to the GIF reverse order steps above, in general there are several key steps
- Glide loads THE GIF image from the URL while listening for the loading process
- Get the GifDecoder via the GifDrawable reflection
- Retrieve all frames with GifDecoder (including saving bitmaps of those frames)
- Invert the frame sequence frames
- Generate the GIF image again with frame
The execution speed of 1 and 4 in the above steps is basically linear and cannot be interfered with too much. Steps 2, 3, and 5 are also at the heart of the GIF inversion implementation, so the method time is simply noted.
GIF image size = 8.9m E/GifFactory: method: getGifDecoder takes 0.001000 second E/GifFactory: Method: getResourceFrames 1.489000 second E/GifFactory: Method: genGifByFrames 9.397000 second GIF size = 11.9 mbit /GifFactory: method: getGifDecoder time 0.000000 second E/GifFactory: getResourceFrames Takes 1.074000 second E/GifFactory: Method: genGifByFrames 9.559000 Second GIF size = 3.3mbit /GifFactory: method: getGifDecoder takes 0.001000 second E/GifFactory: Method: getResourceFrames Takes 0.437000 second E/GifFactory: Method: genGifByFrames 2.907000 second GIF size = 8.1m E/GifFactory: method: getGifDecoder time 0.000000 second E/GifFactory: Method: getResourceFrames 0.854000 second E/GifFactory: Method: genGifByFrames takes 6.416000 secondsCopy the code
As you can see, the GifDecoder acquisition process uses reflection, but it’s not really a performance bottleneck; The method getResourceFrames takes time to get all the frame information, which also depends on the size of the GIF image and is basically an acceptable value. However, the execution time of regenerating giFs from a sequence of frames was a bit scary, even though my test machine was a Kirin 960 with 6GB of ram 😳😳.
But the same image is basically done in milliseconds using Python scripts on a PC. So there is a performance gap in image secondary encoding purely in Java (AnimatedGifEncoder is written in Java, not kotlin 👀).
Although the conversion was slow, it was a good attempt. If there is a more elegant way to shorten the GIF synthesis time for the last step, you can mention PR to GitHub, all views and discussions are welcome.
The source code
In this paper, all the implementation details for the source code has been synchronized to making AndroidAnimationExercise warehouse, can consult ReverseGifActivity this section entry
Reference documentation
Concentrate is the essence: Brief analysis of GIF format image storage and compression