Image Quality Loss When Drawing One BufferedImage to Another Using Graphics2D
<p>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 <code>BufferedImage</code> object, named <code>master</code>, then that <code>BufferedImage</code> is being drawn.  The problem is that drawing the frames (stored as <code>BufferedImage</code> objects themselves) to the <code>master</code> reduces their quality. </p>

<p>I know it's not a problem with the frames themselves, if I just draw the frames individually without drawing them to <code>master</code> 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 <code>RenderingHint</code> to every possible value, but it changes nothing. </p>

<p>Below is my code, with unnecessary parts for solving this problem omitted:</p>

<pre><code>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.io.File;
import java.io.IOException;
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.imageio.stream.FileImageInputStream;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

@SuppressWarnings("serial")
class A extends javax.swing.JPanel{

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


    public A() throws java.io.IOException{
        i = new B(new java.io.File(PATH));
        i.registerComponent(this);
    }

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

    @Override
    public void paintComponent(java.awt.Graphics g){
        i.draw(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.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);
                try{
                    f.add(new A());
                }catch(Exception e){

                }
                f.pack();
                f.setVisible(true);
            }
        });
    }
}

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&lt;ImageReader&gt; readers = ImageIO.getImageReaders(stream);
        ImageReader reader = null;
        // loop through the availible ImageReaders, find one for .gif
        while (readers.hasNext()){
            reader = readers.next();
            String metaFormat = reader.getOriginatingProvider().getNativeImageMetadataFormatName();
            // if it's a gif
            if ("gif".equalsIgnoreCase(reader.getFormatName()) &amp;&amp; META_FORMAT.equals(metaFormat)){
                break;
            }else{
                reader = null;
                continue;
            }
        }// 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&lt;BufferedImage&gt; listFrames = new ArrayList&lt;BufferedImage&gt;();
        ArrayList&lt;Short&gt; listFrameDurs = new ArrayList&lt;Short&gt;();
        ArrayList&lt;Short&gt; listXs = new ArrayList&lt;Short&gt;();
        ArrayList&lt;Short&gt; listYs = new ArrayList&lt;Short&gt;();
        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;
            try{
                frame = reader.readAll(i, null);
            }catch(IndexOutOfBoundsException e){
                break;// this means theres no more frames
            }
            listFrames.add((BufferedImage)frame.getRenderedImage());
            if (unknownMeta){// if the metadata has already proven to be unreadable
                continue;
            }  
            IIOMetadata metadata = frame.getMetadata();
            IIOMetadataNode rootNode = null;
            try{
                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;
                continue;
            }
            // get the duration of the current frame
            IIOMetadataNode graphicControlExt = (IIOMetadataNode)rootNode.getElementsByTagName("GraphicControlExtension").item(0);
            listFrameDurs.add(Short.parseShort(graphicControlExt.getAttribute("delayTime")));
            // get the x and y offsets
            try{
                 IIOMetadataNode imageDescrip = (IIOMetadataNode)rootNode.getElementsByTagName("ImageDescriptor").item(0);
                listXs.add(Short.parseShort(imageDescrip.getAttribute("imageLeftPosition")));
                listYs.add(Short.parseShort(imageDescrip.getAttribute("imageTopPosition")));
            }catch(IndexOutOfBoundsException e){
                e.printStackTrace();
                listXs.add((short) 0);
                listYs.add((short) 0);
            }
        }// for loop
        reader.dispose();
        // 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 &lt; frameDurations.length;i++){
            frameDurations[i] = (short)(listFrameDurs.get(i) * 10);
        }
        xOffsets = new short[listXs.size()];
        for (int i = 0;i &lt; xOffsets.length;i++){
            xOffsets[i] = listXs.get(i);
        }
        yOffsets = new short[listYs.size()];
        for (int i = 0;i &lt; yOffsets.length;i++){
            yOffsets[i] = listYs.get(i);
        }
        size = findSize();
        animationTimer = new Timer(frameDurations[0], null);
        clearLayers();
    }

    // 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 &lt; frames.length;i++){
            if (greatestX &lt; frames[i].getWidth() + xOffsets[i]){
                greatestX = frames[i].getWidth() + xOffsets[i];
            }
            if (greatestY &lt; 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);
        g2d.dispose();
        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);
        animationTimer.addActionListener(l);
        if (!animationTimer.isRunning()){
            animationTimer.start();
        }
    }

    // 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){
            frame++;
            int delay;
            try{
                delay = frameDurations[frame] * 10;
            }catch(ArrayIndexOutOfBoundsException x){
                frame = 0;
                clearLayers();
                delay = frameDurations[frame] * 10;
            }
            animationTimer.setDelay(delay);
            repaintComponent.repaint();
        }// actionPerformed

    }// FrameChangeListener

}
</code></pre>

<p>And here is the image file I've been using to test: <a href="https://i.stack.imgur.com/yUzN4.gif" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/yUzN4.gif" alt="enter image description here"></a></p>

<p>And here is how it displays:
<a href="https://i.stack.imgur.com/TyBeu.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/TyBeu.png" alt="enter image description here"></a></p>

<p>It would be much appreciated if anyone could help me solve this issue</p>