Matthew Flynn Matthew Flynn - 1 year ago 80
Java Question

Image Quality Loss When Drawing One BufferedImage to Another Using Graphics2D

I'm creating a program that displays animated gifs. Because some animated gif files only store the pixels that changed from the previous frame, before each frame is displayed, it's being drawn to a master

object, named
, then that
is being drawn. The problem is that drawing the frames (stored as
objects themselves) to the
reduces their quality.

I know it's not a problem with the frames themselves, if I just draw the frames individually without drawing them to
then they look fine. It's also not a problem with the fact that there's lots of frames being layered on top of each other, even the first frame shows quality reduction. I've tried setting every
to every possible value, but it changes nothing.

Below is my code, with unnecessary parts for solving this problem omitted:

import java.awt.image.BufferedImage;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import javax.activation.MimetypesFileTypeMap;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

class A extends javax.swing.JPanel{

public static final String PATH = "C:/Users/Owner/Desktop/test.gif";
public B i;

public A() throws{
i = new B(new;

public java.awt.Dimension preferredSize(){
return i.getSize();

public void paintComponent(java.awt.Graphics g){

public static void main(String[] args){
javax.swing.SwingUtilities.invokeLater(new Runnable(){
public void run(){
javax.swing.JFrame f = new javax.swing.JFrame();
f.add(new A());
}catch(Exception e){


class B{

private final static String META_FORMAT = "javax_imageio_gif_image_1.0";
// instance variables
private final BufferedImage[] frames;
private BufferedImage master;// Because Gif images can store only the changing
// pixels, the first frame is drawn to this image, then the next one *on top of it*, etc.
private final short[] frameDurations; // in 100ths of a second
private final short[] xOffsets;
private final short[] yOffsets;
private int frame = 0;
private final Dimension size;// the size of the gif (calculated in findSize)
private final Timer animationTimer;

// constructor from a File (checked to be a gif)
public B(File src) throws IOException{
if (!(new MimetypesFileTypeMap().getContentType(src.getPath()).equals("image/gif"))){
throw new IOException("File is not a gif. It's Mime Type is: " +
new MimetypesFileTypeMap().getContentType(src.getAbsolutePath()));
FileImageInputStream stream = new FileImageInputStream(src);
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
ImageReader reader = null;
// loop through the availible ImageReaders, find one for .gif
while (readers.hasNext()){
reader =;
String metaFormat = reader.getOriginatingProvider().getNativeImageMetadataFormatName();
// if it's a gif
if ("gif".equalsIgnoreCase(reader.getFormatName()) && META_FORMAT.equals(metaFormat)){
reader = null;
}// while (readers.hasNext())
// if no reader for gifs was found
if (reader == null){
throw new IOException("File could not be read as a gif");
reader.setInput(stream, false, false);
// Lists to be converted to arrays and set as the instance variables
ArrayList<BufferedImage> listFrames = new ArrayList<BufferedImage>();
ArrayList<Short> listFrameDurs = new ArrayList<Short>();
ArrayList<Short> listXs = new ArrayList<Short>();
ArrayList<Short> listYs = new ArrayList<Short>();
boolean unknownMeta = false;// asume that the metadata can be read until proven otherwise
// loop until there are no more frames (since that isn't known, break needs to be used)
for (int i = 0;true;i++){// equivalent of while(true) with a counter
IIOImage frame = null;
frame = reader.readAll(i, null);
}catch(IndexOutOfBoundsException e){
break;// this means theres no more frames
if (unknownMeta){// if the metadata has already proven to be unreadable
IIOMetadata metadata = frame.getMetadata();
IIOMetadataNode rootNode = null;
rootNode = (IIOMetadataNode) metadata.getAsTree(META_FORMAT);
}catch(IllegalArgumentException e){
// means that the metadata can't be read, it's in an unknown format
unknownMeta = true;
// get the duration of the current frame
IIOMetadataNode graphicControlExt = (IIOMetadataNode)rootNode.getElementsByTagName("GraphicControlExtension").item(0);
// get the x and y offsets
IIOMetadataNode imageDescrip = (IIOMetadataNode)rootNode.getElementsByTagName("ImageDescriptor").item(0);
}catch(IndexOutOfBoundsException e){
listXs.add((short) 0);
listYs.add((short) 0);
}// for loop
// put the values in the lists into the instance variable arrays
frames = listFrames.toArray(new BufferedImage[0]);
// looping must be used because the ArrayList can't contian primitives
frameDurations = new short[listFrameDurs.size()];
for (int i = 0;i < frameDurations.length;i++){
frameDurations[i] = (short)(listFrameDurs.get(i) * 10);
xOffsets = new short[listXs.size()];
for (int i = 0;i < xOffsets.length;i++){
xOffsets[i] = listXs.get(i);
yOffsets = new short[listYs.size()];
for (int i = 0;i < yOffsets.length;i++){
yOffsets[i] = listYs.get(i);
size = findSize();
animationTimer = new Timer(frameDurations[0], null);

// finds the size of the image in constructors
private final Dimension findSize(){
int greatestX = -1;
int greatestY = -1;
// loop through the frames and offsets, finding the greatest combination of the two
for (int i = 0;i < frames.length;i++){
if (greatestX < frames[i].getWidth() + xOffsets[i]){
greatestX = frames[i].getWidth() + xOffsets[i];
if (greatestY < frames[i].getHeight() + yOffsets[i]){
greatestY = frames[i].getHeight() + yOffsets[i];
}// loop
return new Dimension(greatestX, greatestY);
}// findSize

private BufferedImage getFrame(){
/* returning frames[frame] gives a perfect rendering of each frame (but only changed
* pixels), but when master is returned, even the first frame shows quality reduction
* (seen by slowing down the framerate). The issue is with drawing images to master
Graphics2D g2d = master.createGraphics();
g2d.drawImage(frames[frame], xOffsets[frame], yOffsets[frame], null);
return master;

public Dimension getSize(){
return size;

// adds a FrameChangeListener associated with a component to the Timer
public void registerComponent(Component c){
FrameChangeListener l = new FrameChangeListener(c);
if (!animationTimer.isRunning()){

// draws the image to the given Graphics context (registerComponent must be used for the image
// to animate properly)
public void draw(Graphics g){
g.drawImage(getFrame(), 0, 0, null);

// resets master
private void clearLayers(){
master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), frames[0].getType());

// class that listens for the Swing Timer.
private class FrameChangeListener implements ActionListener{

private final Component repaintComponent;

// the Components repaint method will be invoked whenever the animation changes frame
protected FrameChangeListener(Component c){
repaintComponent = c;

public void actionPerformed(ActionEvent e){
int delay;
delay = frameDurations[frame] * 10;
}catch(ArrayIndexOutOfBoundsException x){
frame = 0;
delay = frameDurations[frame] * 10;
}// actionPerformed

}// FrameChangeListener


And here is the image file I've been using to test: enter image description here

And here is how it displays:
enter image description here

It would be much appreciated if anyone could help me solve this issue

Answer Source

The problem is this line from the clearLayers() method:

master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), frames[0].getType());

As the GIF uses a palette, the BufferedImage type will be TYPE_BYTE_INDEXED. However, if you pass this parameter to the BufferedImage constructor, it will use a default IndexColorModel (a built-in, fixed 256 color palette), not the palette from your GIF. Thus, the frames from the GIF will have to be dithered into the destination, as the colors doesn't match.

Instead, use TYPE_INT_RGB/TYPE_INT_ARGB for type, or use the constructor that also takes an IndexColorModel parameter and pass the IndexColorModel from the frames of the GIF.

In code:

master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), BufferedImage.TYPE_INT_ARGB);

Alternatively, the following should also work if all frames of the GIF uses the same palette (not necessarily the case):

master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), frames[0].getType(), (IndexColorModel) frames[0].getColorModel());

However, as the OP reports back the latter option doesn't work for him, the first option is probably safer. :-)

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download