使用 Graphics2D 将一个缓冲图像绘制到另一个缓冲图像时的图像质量损失

Image Quality Loss When Drawing One BufferedImage to Another Using Graphics2D

我正在创建一个显示动画 gif 的程序。因为一些动画gif文件只存储了与前一帧相比变化的像素,所以在每一帧显示之前,它被绘制到一个主BufferedImage对象,名为master,那么BufferedImage是被绘制。问题是将帧(存储为 BufferedImage 对象本身)绘制到 master 会降低它们的质量。

我知道这不是框架本身的问题,如果我只是单独绘制框架而不将它们绘制到 master 那么它们看起来很好。很多帧彼此叠加也不是问题,即使是第一帧也显示质量下降。我已经尝试将每个 RenderingHint 设置为每个可能的值,但它没有任何改变。

下面是我的代码,省略了解决这个问题不需要的部分:

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<ImageReader> 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()) && 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<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;
            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 < 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);
        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 < 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);
        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

}

这是我用来测试的图像文件:

这是它的显示方式:

如果有人能帮我解决这个问题,我将不胜感激

问题是来自 clearLayers() 方法的这一行:

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

由于 GIF 使用调色板,BufferedImage 类型将为 TYPE_BYTE_INDEXED。但是,如果将此参数传递给 BufferedImage 构造函数,它将使用 default IndexColorModel(内置的固定 256 调色板),不是 GIF 中的调色板。因此,GIF 中的帧必须抖动到目标中,因为颜色不匹配。

相反,使用 TYPE_INT_RGB/TYPE_INT_ARGB 作为类型, 使用 the constructor that also takes an IndexColorModel parameter 并传递 GIF 帧中的 IndexColorModel

在代码中:

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

或者,如果 GIF 的所有帧都使用相同的调色板(不一定如此),以下应该也有效:

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

但是,由于 OP 报告后一种选择对他不起作用,第一种选择可能更安全。 :-)