Skip to content

8356137: GifImageDecode can produce opaque image when disposal method changes #25044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: master
Choose a base branch
from

Conversation

mickleness
Copy link
Contributor

@mickleness mickleness commented May 5, 2025

This resolves a gif parsing bug where an unwanted opaque rectangle could appear under these conditions:

  1. The disposal method for frames is 1 (meaning "do not dispose", aka "DISPOSAL_SAVE")
  2. The transparent pixel is non-zero
  3. There's more than one such consecutive frame

Previously: the GifImageDecoder would leave the saved_image pixels as zero when they were supposed to be transparent. This works great if the transparent pixel index is zero, but it flood fills the background of your frame with the zeroeth color otherwise.

I wrote four PRs that share the GifComparison class in this PR. Once any of them clear code review the other PRs will be much simpler:

  1. 8357034
  2. 8356137 (this one)
  3. 8356320
  4. 8351913

Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8356137: GifImageDecode can produce opaque image when disposal method changes (Bug - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/25044/head:pull/25044
$ git checkout pull/25044

Update a local copy of the PR:
$ git checkout pull/25044
$ git pull https://git.openjdk.org/jdk.git pull/25044/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 25044

View PR using the GUI difftool:
$ git pr show -t 25044

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/25044.diff

Using Webrev

Link to Webrev Comment

mickleness and others added 12 commits May 22, 2022 04:50
Merge openjdk/jdk into mickleness/jdk
Updating mickleness/jdk from openjdk/jdk
updating to openjdk/jdk
Also adding accompanying unit test.

I think there's been a mix-up regarding the sample image attached to the original ticket. This ticket should refer to `clyde.gif` (originally based on a data structure identified here: https://free-gifs.org/gif/CC0-3D ).

There is another similar looking (but different) ticket I filed last week for which `leo.gif` is the appropriate test case file. (See incident report 9218362. As far as I can see that hasn't made its way to the bug database yet.) I had to email both gifs to someone on the triage team, and I think they got mixed up.
@bridgekeeper
Copy link

bridgekeeper bot commented May 5, 2025

👋 Welcome back mickleness! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented May 5, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk bot added the rfr Pull request is ready for review label May 5, 2025
@openjdk
Copy link

openjdk bot commented May 5, 2025

@mickleness The following label will be automatically applied to this pull request:

  • client

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@mlbridge
Copy link

mlbridge bot commented May 5, 2025

@mickleness
Copy link
Contributor Author

For reference, the commented out lines here are my first approach at resolving this problem.

image

This works, and it may be more readable/intuitive than this current PR. When we initialize saved_image we should flood fill it with transparent pixels.

I chose the approach in this PR though because it's lazier. (That is: it should be a little bit less work, CPU-wise.)

If anyone wants we can switch to this approach.

Copy link
Member

@myankelev myankelev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few questions.

Also, a copyright in GifImageDecoder.java

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: do you need 2002 here? Isn't it a new file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this is removed


boolean pass = true;
if (new Color(frames[3].getRGB(20, 20), true).getAlpha() != 0) {
System.err.println("Sampling at (20,20) failed");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be cleaner if you jsut throw a RuntimeException("Sampling at (20,20) failed"); instead of the whole

 System.err.println("Sampling at (20,20) failed");
  pass = false;
        }

        if (!pass)
            throw new Error("See System.err for details");

?
It should result in the same level of details but with better readability imo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion; this is removed. (I often follow that pattern in case I try to add multiple criteria to pass/fail decisions.)


return returnValue.toArray(new BufferedImage[0]);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: could you please add a new line here for github? 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this is updated

image.flush();
}
} catch(Exception e) {
e.printStackTrace();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to print out the stack trace here when you are throwing it below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is caught in the same place the other one is ( #25044 (comment) )

} catch (RuntimeException e) {
// we don't expect this to happen, but if something goes
// wrong nobody else will print our stacktrace for us:
e.printStackTrace();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this print here? Runtime exception should print it out anyway to the system.error afaik

Copy link
Contributor Author

@mickleness mickleness May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need this print here?

Yes, because this eventually is caught here in GifImageDecoder.java:

                    try {
                        if (!readImage(totalframes == 0,
                                       disposal_method,
                                       delay)) {
                            return;
                        }
                    } catch (Exception e) {
                        if (verbose) {
                            e.printStackTrace();
                        }
                        return;
                    }

In this specific test file: I never expect an exception to be thrown, but one did come up when I was first drafting this test (because of my own error). It was hard to debug because it was unreported. I would prefer to leave these printStackTrace calls in, in case a developer someday makes a change and needs to see potential errors.

(Technically I could try to make verbose true, but that's declared as a final variable and I don't want to modify GifImageDecoder.java just for this.)

* @test
* @bug 8356137
* @summary This test verifies a non-zero transparent pixel in gifs works when
* the disposal method changes from 2 to 1, and when the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Do you need the , and when the ? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is updated

@mickleness

This comment was marked as outdated.

@mickleness mickleness closed this May 14, 2025
@mickleness

This comment was marked as outdated.

@mickleness mickleness reopened this May 15, 2025
@openjdk
Copy link

openjdk bot commented May 26, 2025

⚠️ @mickleness This pull request contains merges that bring in commits not present in the target repository. Since this is not a "merge style" pull request, these changes will be squashed when this pull request in integrated. If this is your intention, then please ignore this message. If you want to preserve the commit structure, you must change the title of this pull request to Merge <project>:<branch> where <project> is the name of another project in the OpenJDK organization (for example Merge jdk:master).

Copy link
Member

@jayathirthrao jayathirthrao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we a need a gif with only 2 frames having transpixel>0 and disposal method save to reproduce this scenario.

Also its better if we can actually create test gif file using ImageIO(There are examples in test/jdk/javax/imageio/plugins/gif/) programatically and use it instead of external GIF files for testing.

// If this is our first time updating it, and if zeroes
// are NOT the transparent pixel: we need to replace it
// with the appropriate transparent pixel.
boolean replaceTransPixelsInSavedImage = save &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this check to the place where we actually create and initialize saved image.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this thread is moot having addressed #25044 (comment) .

@@ -424,6 +433,9 @@ private int sendPixels(int x, int y, int width, int height,
}
}
runstart = -1;
if (replaceTransPixelsInSavedImage) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are hitting this check for each transparent pixel for all frames with transparent pixel index > 0 and not initial frame until saved_image is not null. In the .gif present in this PR we are checking this condition for each transparent pixel of 2nd and 3rd frame. If GIF contains many frames and disposal method save comes in later part of animation we will hit this check many times.

I think its better to just initialize saved_image with proper transparent pixel index when it is getting created. Then we can completely remove state management of replaceTransPixelsInSavedImage and this check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK; I think this thread is moot having addressed #25044 (comment) .

* Unit tests may further inspect this image to make sure certain
* conditions are met.
*/
public static BufferedImage run(URL srcURL) throws Throwable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can actually update this helper to check a specific frame is GIF and not run throw all the frame its better.
In this PR we just need the info of 4th frame.

Is this way of checking all frames useful in any other PR's which you have raised?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this way of checking all frames useful in any other PR's which you have raised?

No, I don't think so. (I liked the assurance that every frame was identical, but technically that is beyond the scope of each individual unit test.)

I changed this method to only inspect the last frame.

This made the GifComparison's main() class much less useful, so I removed it. So now the GifComparison class is more limited in scope to unit tests -- and it's no longer a generic tool to identify new problems.

URL srcURL = GifEmptyBackgroundTest.class.getResource("clyde.gif");
BufferedImage bi = GifComparison.run(srcURL);

if (new Color(bi.getRGB(20, 20), true).getAlpha() != 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the product fix this test fails while checking opacity of (0,0) itself and we don't hit this check.

Exception in thread "main" java.lang.Error: pixels at (0, 0) have different opacities: 0 vs ff000000

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this is removed.

That was a redundant check intended to:
A. Reinforce what inspired this test
B. Double-check that ImageIO also did the right thing. (That is: the new GifComparison makes sure ImageIO and ToolkitImage are identical. But if they happened to both be wrong then GifComparison would be satisfied, so this confirmed the test gif file really was parsed correctly.)

@jayathirthrao
Copy link
Member

For reference, the commented out lines here are my first approach at resolving this problem.
image

This works, and it may be more readable/intuitive than this current PR. When we initialize saved_image we should flood fill it with transparent pixels.

I chose the approach in this PR though because it's lazier. (That is: it should be a little bit less work, CPU-wise.)

If anyone wants we can switch to this approach.

I think this is better then current approach of having check multiple times.

@mickleness
Copy link
Contributor Author

I think this is better then current approach of having check multiple times.

OK. I switched back to this approach.

(The downside being: now we'll invoke Arrays.fill(..) for all gifs with that disposal method. I assume (?) gifs with this architecture/problem are a small subset of that group; but it's impossible to quantify that hunch.)

This makes the main() method much less useful, so I removed it too. (I originally used this class to explore a folder of hundreds of gifs to look for discrepancies. But the discrepancies were rarely only on the last frame.)

This is in response to:
openjdk#25044 (comment)
This can be used by multiple gif tests in this directory.

This is in response to:
openjdk#25044 (review)
@mickleness
Copy link
Contributor Author

Also its better if we can actually create test gif file using ImageIO(There are examples in test/jdk/javax/imageio/plugins/gif/) programatically and use it instead of external GIF files for testing.

I wrote a new class GifBuilder that programmatically creates the test gif file; and it can generate test files for some of the other related pending bugs.

I think we a need a gif with only 2 frames having transpixel>0 and disposal method save to reproduce this scenario.

I think we need 3 frames. The test currently resembles:

    public static void main(String[] args) throws Throwable {
        GifBuilder.test(
                new GifBuilder.FrameDescription(GifBuilder.Disposal.restoreToBackgroundColor, false),
                new GifBuilder.FrameDescription(GifBuilder.Disposal.doNotDispose, false),
                new GifBuilder.FrameDescription(GifBuilder.Disposal.doNotDispose, false) );
    }

If I comment out any one of these three frames then this test passes when I run it against the master branch.

For additional context:
A. This file architecture was reverse engineered from https://free-gifs.org/gif/CC0-3D
B. None of this weekend's changes alter the proposed change in GifImageDecoder ; this is all just refactoring the test

mickleness added a commit to mickleness/jdk that referenced this pull request Jun 1, 2025
This makes the main() method much less useful, so I removed it too. (I originally used this class to explore a folder of hundreds of gifs to look for discrepancies. But the discrepancies were rarely only on the last frame.)

This is in response to:
openjdk#25044 (comment)
mickleness added a commit to mickleness/jdk that referenced this pull request Jun 1, 2025
This can be used by multiple gif tests in this directory.

This is in response to:
openjdk#25044 (review)
mickleness added a commit to mickleness/jdk that referenced this pull request Jun 1, 2025
mickleness added a commit to mickleness/jdk that referenced this pull request Jun 1, 2025
This makes the main() method much less useful, so I removed it too. (I originally used this class to explore a folder of hundreds of gifs to look for discrepancies. But the discrepancies were rarely only on the last frame.)

This is in response to:
openjdk#25044 (comment)
mickleness added a commit to mickleness/jdk that referenced this pull request Jun 1, 2025
This can be used by multiple gif tests in this directory.

This is in response to:
openjdk#25044 (review)
mickleness added a commit to mickleness/jdk that referenced this pull request Jun 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
client [email protected] rfr Pull request is ready for review
Development

Successfully merging this pull request may close these issues.

3 participants