-
Bug
-
Resolution: Unresolved
-
P4
-
25
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
Observed using a local build of OpenJDK 25 on Mac 15.3.1
A DESCRIPTION OF THE PROBLEM :
Sometimes Toolkit.createImage() creates Images that do not render correctly. I think (?) I see two separate issues:
A. Sometimes -- usually towards the bottom of frames -- the scanline appears messed up. For example: a vertical line may appear OK at y=400, but offset at y=401.
B. Sometimes the background color appears wrong.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached program with the this folder of images:
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
All tests should pass
ACTUAL -
Several tests fail
---------- BEGIN SOURCE ----------
package com.hippogif;
import javax.imageio.ImageIO;
import javax.tools.Tool;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
public class GifImageTest {
/**
* This scans a folder named 'gif-files'. This folder contains a set of gifs, and some png images that
* represent the *expected* frames. For example the file "abc[3].png" refers to the 4th frame of "abc.gif".
* <p>
* The initial set of files provided for this ticket include a few passing examples just to prove that
* the renderer in this test is working correctly.
* <p>
* You can also observe these discrepancies visually if you call <code>new JLabel(new ImageIcon(toolkitImage))</code>,
* however that will be animating so it's hard to do a frame-by-frame comparison.
* <p>
* This test may takes about 20s to complete because the ToolkitImage only reveals new frames after calling
* Thread.sleep(..)
*/
public static void main(String[] args) throws IOException {
File dir = new File("gif-files");
Map<String, File> gifsByFilename = new HashMap<>();
for (File gifFile : dir.listFiles((dir1, name) -> name.endsWith(".gif"))) {
String name = gifFile.getName();
name = name.substring(0, name.length() - ".gif".length());
gifsByFilename.put(name, gifFile);
}
boolean allTestsPassed = true;
for (File pngFile : dir.listFiles((dir1, name) -> name.endsWith(".png"))) {
String name = pngFile.getName();
int i = name.indexOf('[');
String gifName = name.substring(0,i);
int frameIndex = Integer.parseInt(name.substring(i + 1, name.length() - "].png".length()));
System.out.println("Testing " + pngFile.getPath());
BufferedImage expectedFrame = ImageIO.read(pngFile);
File gifFile = gifsByFilename.get(gifName);
BufferedImage actualFrame = getFrame(gifFile, frameIndex);
// this diff image is not officially part of this test, but it's helpful to visually
// compare the two images in debugger mode:
BufferedImage diff = new BufferedImage(actualFrame.getWidth(), actualFrame.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = diff.createGraphics();
g.drawImage(expectedFrame, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
g.drawImage(actualFrame, 0, 0, null);
g.dispose();
boolean passed = testEquals(expectedFrame, actualFrame);
if (!passed) {
allTestsPassed = false;
System.out.println("\tfailed");
} else {
System.out.println("\tpassed");
}
}
if (!allTestsPassed)
throw new Error("One or more tests failed.");
}
private static boolean testEquals(BufferedImage expectedImage, BufferedImage actualImage) {
if (expectedImage.getWidth() != actualImage.getWidth())
return false;
if (expectedImage.getHeight() != actualImage.getHeight())
return false;
int tolerance = 50;
for (int y = 0; y < expectedImage.getHeight(); y++) {
for (int x = 0; x < expectedImage.getWidth(); x++) {
int argb1 = expectedImage.getRGB(x, y);
int argb2 = actualImage.getRGB(x, y);
int a1 = (argb1 >> 24) & 0xff;
int r1 = (argb1 >> 16) & 0xff;
int g1 = (argb1 >> 8) & 0xff;
int b1 = (argb1 >> 0) & 0xff;
int a2 = (argb2 >> 24) & 0xff;
int r2 = (argb2 >> 16) & 0xff;
int g2 = (argb2 >> 8) & 0xff;
int b2 = (argb2 >> 0) & 0xff;
// transparency should be 0% or 100%
if (a1 != a2)
return false;
if (a1 == 255) {
if (Math.abs(r1 - r2) > tolerance)
return false;
if (Math.abs(g1 - g2) > tolerance)
return false;
if (Math.abs(b1 - b2) > tolerance)
return false;
}
}
}
return true;
}
/**
* @param gifFile
* @param frameIndex
* @return
* @throws IOException
*/
private static BufferedImage getFrame(File gifFile, int frameIndex) throws IOException {
Image image = Toolkit.getDefaultToolkit().createImage(gifFile.toURI().toURL());
AtomicReference<BufferedImage> returnValue = new AtomicReference<>();
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);
returnValue.set(bi);
}
@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 {
if (frameCtr++ == frameIndex) {
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);
}
}
});
// wait for producer thread to finish:
semaphore.acquireUninterruptibly();
return returnValue.get();
}
}
---------- END SOURCE ----------
Observed using a local build of OpenJDK 25 on Mac 15.3.1
A DESCRIPTION OF THE PROBLEM :
Sometimes Toolkit.createImage() creates Images that do not render correctly. I think (?) I see two separate issues:
A. Sometimes -- usually towards the bottom of frames -- the scanline appears messed up. For example: a vertical line may appear OK at y=400, but offset at y=401.
B. Sometimes the background color appears wrong.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached program with the this folder of images:
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
All tests should pass
ACTUAL -
Several tests fail
---------- BEGIN SOURCE ----------
package com.hippogif;
import javax.imageio.ImageIO;
import javax.tools.Tool;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ImageConsumer;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
public class GifImageTest {
/**
* This scans a folder named 'gif-files'. This folder contains a set of gifs, and some png images that
* represent the *expected* frames. For example the file "abc[3].png" refers to the 4th frame of "abc.gif".
* <p>
* The initial set of files provided for this ticket include a few passing examples just to prove that
* the renderer in this test is working correctly.
* <p>
* You can also observe these discrepancies visually if you call <code>new JLabel(new ImageIcon(toolkitImage))</code>,
* however that will be animating so it's hard to do a frame-by-frame comparison.
* <p>
* This test may takes about 20s to complete because the ToolkitImage only reveals new frames after calling
* Thread.sleep(..)
*/
public static void main(String[] args) throws IOException {
File dir = new File("gif-files");
Map<String, File> gifsByFilename = new HashMap<>();
for (File gifFile : dir.listFiles((dir1, name) -> name.endsWith(".gif"))) {
String name = gifFile.getName();
name = name.substring(0, name.length() - ".gif".length());
gifsByFilename.put(name, gifFile);
}
boolean allTestsPassed = true;
for (File pngFile : dir.listFiles((dir1, name) -> name.endsWith(".png"))) {
String name = pngFile.getName();
int i = name.indexOf('[');
String gifName = name.substring(0,i);
int frameIndex = Integer.parseInt(name.substring(i + 1, name.length() - "].png".length()));
System.out.println("Testing " + pngFile.getPath());
BufferedImage expectedFrame = ImageIO.read(pngFile);
File gifFile = gifsByFilename.get(gifName);
BufferedImage actualFrame = getFrame(gifFile, frameIndex);
// this diff image is not officially part of this test, but it's helpful to visually
// compare the two images in debugger mode:
BufferedImage diff = new BufferedImage(actualFrame.getWidth(), actualFrame.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = diff.createGraphics();
g.drawImage(expectedFrame, 0, 0, null);
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
g.drawImage(actualFrame, 0, 0, null);
g.dispose();
boolean passed = testEquals(expectedFrame, actualFrame);
if (!passed) {
allTestsPassed = false;
System.out.println("\tfailed");
} else {
System.out.println("\tpassed");
}
}
if (!allTestsPassed)
throw new Error("One or more tests failed.");
}
private static boolean testEquals(BufferedImage expectedImage, BufferedImage actualImage) {
if (expectedImage.getWidth() != actualImage.getWidth())
return false;
if (expectedImage.getHeight() != actualImage.getHeight())
return false;
int tolerance = 50;
for (int y = 0; y < expectedImage.getHeight(); y++) {
for (int x = 0; x < expectedImage.getWidth(); x++) {
int argb1 = expectedImage.getRGB(x, y);
int argb2 = actualImage.getRGB(x, y);
int a1 = (argb1 >> 24) & 0xff;
int r1 = (argb1 >> 16) & 0xff;
int g1 = (argb1 >> 8) & 0xff;
int b1 = (argb1 >> 0) & 0xff;
int a2 = (argb2 >> 24) & 0xff;
int r2 = (argb2 >> 16) & 0xff;
int g2 = (argb2 >> 8) & 0xff;
int b2 = (argb2 >> 0) & 0xff;
// transparency should be 0% or 100%
if (a1 != a2)
return false;
if (a1 == 255) {
if (Math.abs(r1 - r2) > tolerance)
return false;
if (Math.abs(g1 - g2) > tolerance)
return false;
if (Math.abs(b1 - b2) > tolerance)
return false;
}
}
}
return true;
}
/**
* @param gifFile
* @param frameIndex
* @return
* @throws IOException
*/
private static BufferedImage getFrame(File gifFile, int frameIndex) throws IOException {
Image image = Toolkit.getDefaultToolkit().createImage(gifFile.toURI().toURL());
AtomicReference<BufferedImage> returnValue = new AtomicReference<>();
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);
returnValue.set(bi);
}
@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 {
if (frameCtr++ == frameIndex) {
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);
}
}
});
// wait for producer thread to finish:
semaphore.acquireUninterruptibly();
return returnValue.get();
}
}
---------- END SOURCE ----------