Record a GIF Problem Investigation and Exploration

Foreword

One day, we received feedback from the operator that a GIF banner could not be played on the website, and it was normal on the APP side. The operator indicated that the last configuration of GIF banner was normal, and gave the picture links of the two configurations.

Analysis

From the link address of the image, the two banner images are all suffixed with jpg, but the GIF judgment is not determined by its suffix, so let’s first look at the headers of the two images:

As we can see, although the image suffix is jpg, both figures are typical GIF diagrams. The Header part of GIF is most commonly known as 47 49 46 38 38 61, ie GIF89a and also GIF87a. The former is an enhanced version of the latter. The transparent channels and animations we are familiar with are the former. The ability to have it, and another important point, GIF89a has added Application Extension, which will be used in later analysis.

Since they are all GIF diagrams, let’s try opening the Console to replace the new image with the web page and find that it is not impossible to play the image, but the animation is short and only played once, so it is mistaken for no animation at all.

If you have experience in editing GIF diagrams, you must know that you can select the number of GIF loops when exporting, but since my computer does not have Photoshop installed, I still check it with the old method:

NETSCAPE is the famous Netscape company. The most well-known one is probably the story that its Netscape browser was defeated by IE in the browser war of the year. In the 1990s, Netscape introduced the Netscape Looping Application Extension, an unofficial implementation of the Application Extension defined in GIF89a mentioned above, and is the most common implementation ever passed. It adds a declaration of the number of animation loops, and I refer to a diagram here to introduce its structure:


(Image source see here)

The picture is very clear, so we can see that the loop Count value of the infinite loop image is 0, and the image that only loops once has no associated Block.

At this point, the solution came out, telling the operator to re-export the GIF image, and set the number of loops to “unlimited”. The problem solved.

Explore

However, I am still confused. Why is there no problem on the APP?

Let’s look at the source code.

Here is the explore about our Android Project. Glide is wildly used around the world. We know that Glide supports GIF display by default. When Glide is used for image loading, the basic version can be quickly applied in just a few lines, as shown below.

1
2
3
Glide.with(context)
.load(url)
.into(mImageView)

Generally speaking, a more appropriate way to look at the source code is to look at the call chain from the externally exposed method. However, the previous part of this article has briefly introduced the structure of GIF, and we can follow this line of thought to find it.

As you can see from the above introduction, the latest configuration of the GIF image is missing the Block describing the number of loops, so it is reasonable to doubt that the Glide framework does not read the relevant information?

Directly search for the keyword GifDecoder and find it is an interface. Observe that there are several related methods as follows.

1
2
3
4
5
6
@Deprecated
int getLoopCount();

int getNetscapeLoopCount();

int getTotalIterationCount();

Then search for its interface implementation, and found that there is only one implementation class StandardGifDecoder, the method is implemented as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Deprecated
@Override
public int getLoopCount() {
if (header.loopCount == GifHeader.NETSCAPE_LOOP_COUNT_DOES_NOT_EXIST) {
return 1;
}
return header.loopCount;
}

@Override
public int getNetscapeLoopCount() {
return header.loopCount;
}

@Override
public int getTotalIterationCount() {
if (header.loopCount == GifHeader.NETSCAPE_LOOP_COUNT_DOES_NOT_EXIST) {
return 1;
}
if (header.loopCount == GifHeader.NETSCAPE_LOOP_COUNT_FOREVER) {
return TOTAL_ITERATION_COUNT_FOREVER;
}
return header.loopCount + 1;
}

Then look for the assignment of header.loopCount, we will find the following function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Reads Netscape extension to obtain iteration count.
*/
private void readNetscapeExt() {
do {
readBlock();
if (block[0] == 1) {
// Loop count sub-block.
int b1 = ((int) block[1]) & MASK_INT_LOWEST_BYTE;
int b2 = ((int) block[2]) & MASK_INT_LOWEST_BYTE;
header.loopCount = (b2 << 8) | b1;
}
} while ((blockSize > 0) && !err());
}

Then find the location where the function is called, and simply trace back to the slightly outer class, which will be called in the GifHeaderParser class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@NonNull
public GifHeader parseHeader() {
if (rawData == null) {
throw new IllegalStateException("You must call setData() before parseHeader()");
}
if (err()) {
return header;
}

readHeader();
if (!err()) {
readContents(); // <-this called readNetscapeExt()
if (header.frameCount < 0) {
header.status = STATUS_FORMAT_ERROR;
}
}

return header;
}

There are two main classes that call this method, one of which is the StandardGifDecoder mentioned above.

1
2
3
4
5
6
7
8
9
@GifDecodeStatus
public synchronized int read(@Nullable byte[] data) {
this.header = getHeaderParser().setData(data).parseHeader();
if (data != null) {
setData(header, data);
}

return status;
}

At this point, we can draw a phased conclusion that StandardGifDecoder does have the number of loops to get GIF.

Looking back at the GIF structure introduced at the beginning of the article, obviously, our initial guess is wrong, Glide still reads the number of loops in the relevant Block.

That being the case, why use Glide on the app to play GIF infinitely, and guess: Will it get the number of loops, but it doesn’t consume it by default?

We look up the callers of the three functions mentioned above in turn.

1
2
3
4
5
6
@Deprecated
int getLoopCount();

int getNetscapeLoopCount();

int getTotalIterationCount();

After a simple filter, you will find that only the last method will be called in GlideFrameLoader.

1
2
3
int getLoopCount() {
return gifDecoder.getTotalIterationCount();
}

Continue to trace back and you will find this function called in GifDrawable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Public API.
@SuppressWarnings("WeakerAccess")
public void setLoopCount(int loopCount) {
if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) {
throw new IllegalArgumentException("Loop count must be greater than 0, or equal to "
+ "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC");
}

if (loopCount == LOOP_INTRINSIC) {
int intrinsicCount = state.frameLoader.getLoopCount();
maxLoopCount =
(intrinsicCount == TOTAL_ITERATION_COUNT_FOREVER) ? LOOP_FOREVER : intrinsicCount;
} else {
maxLoopCount = loopCount;
}
}

And this method is not called by default. Continuing to check the class, you will find that the maxLoopCount variable defaults to LOOP_FOREVER , which is an infinite loop, and the variable is only reassigned in the method.

At this point, we can basically confirm that Glide reads the number of loops in the picture information but does not consume it by default. The important code for the number of GIF loops in Glide is here. Of course, there are other confirmation steps to be completed from the conclusion, but it is not relevant to the problem described in this article. It is simple here. List it, no longer repeat them.

  1. The parseHeader() function has two classes that call it. Another unmentioned class is ByteBufferGifDecoder, which is also used by the StreamGifDecoder class, which can be searched for in the Glide class.
  2. The handles method in StreamGifDecoder contains two logics about GIF, one is GifOptions, which can be set to prohibit GIF playback by the method of this class, and the second is to determine whether the file is GIF. , the default processing logic in the DefaultImageHeaderParser class, is also handled by determining whether the file header is 0x474946.
  3. For Glide how to convert the image address to Drawable and select the corresponding Decoder, you can search for Glide source code analysis, this article will not be expanded.

Usage

After understanding the principle, if we want Glide to play GIF, set it several times in the picture to play it a few times, how to modify it?

As you can see from the previous section, the easiest way is to call setLoopCount(int loopCount). For example, you can get GifDrawable through listener, which can be selected according to the specific scene.

If you use code to describe it, it is like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Method 1
GlideApp.with(context)
.load(url)
.into(object : DrawableImageViewTarget(mImageView) {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
if (resource is GifDrawable) {
resource.setLoopCount(LOOP_INTRINSIC)
}
super.onResourceReady(resource, transition)
}
})

// Method 2
GlideApp.with(context)
.load(url)
.listener(object : RequestListener<Drawable> {
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
if (resource is GifDrawable) {
resource.setLoopCount(LOOP_INTRINSIC)
}
return false
}

override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
e?.printStackTrace()
return false
}
})
.into(mImageView)

More

In most scenarios, we refer to the image in the GIF format in order to obtain a relatively lightweight animation effect compared to the video. So, in some scenarios, is there any alternative?

Try the SVG Animation, Lottie, WebP, APNG.