使用 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 报告后一种选择对他不起作用,第一种选择可能更安全。 :-)
我正在创建一个显示动画 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 报告后一种选择对他不起作用,第一种选择可能更安全。 :-)