-
Bug
-
Resolution: Unresolved
-
P4
-
17, 21, 25
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
Observed on Mac 15.4.1 using Java 25
A DESCRIPTION OF THE PROBLEM :
When gif frames are set to disposal method 1 (DISPOSAL_SAVE), and the transparent index changes across frames: sometimes the GifImageDecoder mistakenly replaces opaque pixels with transparent pixels.
(This error is only triggered when two consecutive frames use identical color palettes, and the transparent pixel index in that palette is a pixel color a previous frame used as an opaque color.)
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached test file with "ukraine-flag.gif".
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The test should pass. Although the gif has a color palette with only 2 colors, the resulting BufferedImages should actually use 3 colors (blue, yellow, and transparent)
ACTUAL -
The test fails.
The color palette has 2 colors:
0 = blue
1 = yellow
The last two frames use color index 0 as the transparent pixel index. When GifImageDecoder processes the last (third) frame: it overwrites blue pixels as transparent pixels and it makes the transparent border opaque yellow.
So the core problem here is some clever gif encoders have figured out how to squeeze (n+1)-many colors into an n-many color palette (if you include the transparent pixel as a color). In this image, for ex, the BufferedImage should have 3 colors: blue, yellow, transparent.
(This is of course a simplified version of the problem; the original gif that made me notice this issue used all 256-colors, which means the solution may (?) require changing byte-based data to int (or short?) based data.)
---------- BEGIN SOURCE ----------
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.IndexColorModel;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.concurrent.Semaphore;
public class GifReuseFormerlyTransPixelTest {
public static void main(String[] args) throws Exception {
URL srcURL = GifReuseFormerlyTransPixelTest.class.getResource("ukraine-flag.gif");
BufferedImage[] frames = getFrames(srcURL, 3);
boolean pass = true;
// these first two conditions have never failed. The second frame is always fine
if ( !Integer.toUnsignedString(frames[1].getRGB(100, 50), 16).equals("ff005bbb") ) {
System.err.println("On frame #2, the top stripe should be opaque blue");
pass = false;
}
if ( !Integer.toUnsignedString(frames[1].getRGB(5, 5), 16).equals("0") ) {
System.err.println("On frame #2, the top-left corner should be transparent");
pass = false;
}
// these two conditions failed when we start looking at frame 3
if ( !Integer.toUnsignedString(frames[2].getRGB(100, 50), 16).equals("ff005bbb") ) {
System.err.println("On frame #3, the top stripe should be opaque blue");
pass = false;
}
if ( !Integer.toUnsignedString(frames[2].getRGB(5, 5), 16).equals("0") ) {
System.err.println("On frame #3, the top-left corner should be transparent");
pass = false;
}
if (!pass) {
throw new Error("See log for details");
}
}
private static BufferedImage[] getFrames(URL gifURL, int numberOfFrames) {
Image image = Toolkit.getDefaultToolkit().createImage(gifURL);
ArrayList<BufferedImage> returnValue = new ArrayList<>(numberOfFrames);
Semaphore semaphore = new Semaphore(1);
semaphore.acquireUninterruptibly();
image.getSource().startProduction(new ImageConsumer() {
BufferedImage bi;
int frameCtr = 0;
@Override
public void setDimensions(int width, int height) {
bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
@Override
public void setProperties(Hashtable<?, ?> props) {}
@Override
public void setColorModel(ColorModel model) {}
@Override
public void setHints(int hintflags) {}
@Override
public void setPixels(int x, int y, int w, int h, ColorModel model, byte[] pixels, int off, int scansize) {
try {
final int yMax = y + h;
final int xMax = x + w;
IndexColorModel icm = (IndexColorModel) model;
int[] colorModelRGBs = new int[icm.getMapSize()];
icm.getRGBs(colorModelRGBs);
int[] argbRow = new int[bi.getWidth()];
for (int y_ = y; y_ < yMax; y_++) {
int i = y_ * scansize + off;
for (int x_ = x; x_ < xMax; x_++, i++) {
int pixel = pixels[i] & 0xff;
argbRow[x_ - x] = colorModelRGBs[pixel];
}
bi.getRaster().setDataElements(x, y_, w, 1, argbRow);
}
} 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();
throw e;
}
}
@Override
public void setPixels(int x, int y, int w, int h, ColorModel model, int[] pixels, int off, int scansize) {}
@Override
public void imageComplete(int status) {
try {
frameCtr++;
BufferedImage copy = new BufferedImage(bi.getWidth(),
bi.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = copy.createGraphics();
g.drawImage(bi, 0, 0, null);
g.dispose();
returnValue.add(copy);
if (frameCtr == numberOfFrames) {
semaphore.release();
// if we don't detach this consumer the producer will
// loop forever
image.getSource().removeConsumer(this);
image.flush();
}
} catch(Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
});
semaphore.acquireUninterruptibly();
return returnValue.toArray(new BufferedImage[0]);
}
}
---------- END SOURCE ----------
Observed on Mac 15.4.1 using Java 25
A DESCRIPTION OF THE PROBLEM :
When gif frames are set to disposal method 1 (DISPOSAL_SAVE), and the transparent index changes across frames: sometimes the GifImageDecoder mistakenly replaces opaque pixels with transparent pixels.
(This error is only triggered when two consecutive frames use identical color palettes, and the transparent pixel index in that palette is a pixel color a previous frame used as an opaque color.)
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached test file with "ukraine-flag.gif".
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The test should pass. Although the gif has a color palette with only 2 colors, the resulting BufferedImages should actually use 3 colors (blue, yellow, and transparent)
ACTUAL -
The test fails.
The color palette has 2 colors:
0 = blue
1 = yellow
The last two frames use color index 0 as the transparent pixel index. When GifImageDecoder processes the last (third) frame: it overwrites blue pixels as transparent pixels and it makes the transparent border opaque yellow.
So the core problem here is some clever gif encoders have figured out how to squeeze (n+1)-many colors into an n-many color palette (if you include the transparent pixel as a color). In this image, for ex, the BufferedImage should have 3 colors: blue, yellow, transparent.
(This is of course a simplified version of the problem; the original gif that made me notice this issue used all 256-colors, which means the solution may (?) require changing byte-based data to int (or short?) based data.)
---------- BEGIN SOURCE ----------
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.IndexColorModel;
import java.net.URL;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.concurrent.Semaphore;
public class GifReuseFormerlyTransPixelTest {
public static void main(String[] args) throws Exception {
URL srcURL = GifReuseFormerlyTransPixelTest.class.getResource("ukraine-flag.gif");
BufferedImage[] frames = getFrames(srcURL, 3);
boolean pass = true;
// these first two conditions have never failed. The second frame is always fine
if ( !Integer.toUnsignedString(frames[1].getRGB(100, 50), 16).equals("ff005bbb") ) {
System.err.println("On frame #2, the top stripe should be opaque blue");
pass = false;
}
if ( !Integer.toUnsignedString(frames[1].getRGB(5, 5), 16).equals("0") ) {
System.err.println("On frame #2, the top-left corner should be transparent");
pass = false;
}
// these two conditions failed when we start looking at frame 3
if ( !Integer.toUnsignedString(frames[2].getRGB(100, 50), 16).equals("ff005bbb") ) {
System.err.println("On frame #3, the top stripe should be opaque blue");
pass = false;
}
if ( !Integer.toUnsignedString(frames[2].getRGB(5, 5), 16).equals("0") ) {
System.err.println("On frame #3, the top-left corner should be transparent");
pass = false;
}
if (!pass) {
throw new Error("See log for details");
}
}
private static BufferedImage[] getFrames(URL gifURL, int numberOfFrames) {
Image image = Toolkit.getDefaultToolkit().createImage(gifURL);
ArrayList<BufferedImage> returnValue = new ArrayList<>(numberOfFrames);
Semaphore semaphore = new Semaphore(1);
semaphore.acquireUninterruptibly();
image.getSource().startProduction(new ImageConsumer() {
BufferedImage bi;
int frameCtr = 0;
@Override
public void setDimensions(int width, int height) {
bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
@Override
public void setProperties(Hashtable<?, ?> props) {}
@Override
public void setColorModel(ColorModel model) {}
@Override
public void setHints(int hintflags) {}
@Override
public void setPixels(int x, int y, int w, int h, ColorModel model, byte[] pixels, int off, int scansize) {
try {
final int yMax = y + h;
final int xMax = x + w;
IndexColorModel icm = (IndexColorModel) model;
int[] colorModelRGBs = new int[icm.getMapSize()];
icm.getRGBs(colorModelRGBs);
int[] argbRow = new int[bi.getWidth()];
for (int y_ = y; y_ < yMax; y_++) {
int i = y_ * scansize + off;
for (int x_ = x; x_ < xMax; x_++, i++) {
int pixel = pixels[i] & 0xff;
argbRow[x_ - x] = colorModelRGBs[pixel];
}
bi.getRaster().setDataElements(x, y_, w, 1, argbRow);
}
} 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();
throw e;
}
}
@Override
public void setPixels(int x, int y, int w, int h, ColorModel model, int[] pixels, int off, int scansize) {}
@Override
public void imageComplete(int status) {
try {
frameCtr++;
BufferedImage copy = new BufferedImage(bi.getWidth(),
bi.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = copy.createGraphics();
g.drawImage(bi, 0, 0, null);
g.dispose();
returnValue.add(copy);
if (frameCtr == numberOfFrames) {
semaphore.release();
// if we don't detach this consumer the producer will
// loop forever
image.getSource().removeConsumer(this);
image.flush();
}
} catch(Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
});
semaphore.acquireUninterruptibly();
return returnValue.toArray(new BufferedImage[0]);
}
}
---------- END SOURCE ----------