如何在Java应用中将图像存储到图像文件中?

本文主要讨论以下内容:

1.  Java 2D API介绍

2.加载图像:如何使用Image I/O API从外部图像加载到Java应用程序中。

3.存储图像:如何以适当的格式存储创建的图像。



1. Java 2D API  概念介绍

Java 2D API 是增强了AWT的图形、文本、图像处理的能力,使富客户端用户界面和新型Java应用成为了可能。

除了丰富的图形、文本和图像API ,Java API也提供了增强的颜色定义和混合,对突现的任意几何图形及文本的发现,并且为打印和输出设备提供了统一的说明方式。 

Java 2D API同时增加了高级图形库的开发,比如CAD-CAM库和图形或图像特效库以及对图形或图像文件的读/写过滤器。

当和Java Media Framework以其其它的Java Media APIs协同开发时,Java 2D APIs同样能创建和演示动画和其它的多媒体效果。Java动画和Java Media Framework APIs的渲染也依赖于Java 2D API。



1.1 增强的图形、文本和图像处理功能 

AWT的早期版本为渲染普通的HTML页面一种简单的渲染包,但对复杂的图形、文本和图像的处理上却显不足。作为一种简化的渲染包,早期的AWT包含了具有更多通用渲染理念的特定案例。Java 2D API通过对AWT的增强提供了一种更灵活、全面的渲染包,用于支持更通用的图像和渲染操作。

例如,通过Graphics 类,你可以绘制矩形、椭圆和任意的多边形。Graphics2D 类为渲染实际的任意多边形而丰富了几何图形处理的概念。类似的,利用API 2D API 可以绘制各种风格的具有任意宽度的直线和利用任意的质地填充几何图形。 

几何图形实现Shape 接口,例如Rectangle2D 和Ellipse2D。同样Curves 和arcs 等也是对Shape接口的特定实现。 

填充和画笔的风格通过实现接口Paint 和Stroke 而提供,例如BasicStroke、GradientPaint、TextruePaint以及Color。

AffineTransform 定义了2D 坐标的一次转换,包括缩放、移动、旋转和切变。 

剪辑区域同样实现了Shape接口,它用来定义通用的剪辑区域,例如Rectange2D 和 GeneralPath。 

颜色混合机制实现Composite接口而提供,例如AlphaComposite。 

字体类由字形的集合来定义这些字体依次被独特的Shapes来定义。



1.2 渲染模式 

基本的图形渲染模式并没有随着Java 2D APIs 的增加而改变。为渲染一个图形,你需要创建一个图形文本并调用Graphics 对象上的渲染方法。

Java 2D API 中的Graphics2D 类扩展了Graphics 类,支持更多的图形属性并且提供了新的渲染方法。

Java 2D API 自动补偿了各种渲染设备之间的差别并为不同设备提供了一种统一的渲染模式。在应用程序层次上,不管目标设备是显示器还是打印机,它们的渲染过程是相同的。



1.2.1 坐标系 

Java 2D API 包含了两种坐标系: 

(1)用户空间坐标系——一种设备无关的、独立的坐标系。应用程序使用这种坐标系;所有引入到Java 2D  的几何图形的渲染规例也是指定用这种用户空间。 

(2)设备空间坐标系——一种设备相关坐标系。它根据不同的目标设备而发生变化。 

在有一个实际桌面的多屏幕环境下,一个窗口可以跨越多于一个的物理屏幕设备,这种情况下的设备坐标系是实际桌面的坐标系,它包含了各个屏幕。Java 2D 系统自动完成从用户空间到各种渲染设备的设备空间的必要的转换。虽然显示器和打印机的坐标系大不相同,但这些区别对于应用程序来说是透明的(不需要关注的)。



1.2.1.1 用户空间 

如下图所示,用户空间坐标原点位于空间的左上角,x 值向右增大,y 值向下增大。 


用户空间坐标系 

用户空间代表了所有可能设备的坐标系的一种统一的抽象。对于一个特定的设备来说,设备空间可能和用户空间有着相同的坐标原点和方向,或者相反它们是不同的。不管哪种情况,当一个图形对象被渲染时,用户空间坐标都会自动转换成恰当的设备空间坐标。通常,内在的设备驱动完成这种转换。



1.2.1.2 设备空间 

为了支持用户空间到设备空间的转换,Java 2D API 定义了三个层次的构造信息。这些信息被包含在收下三个类中: 

GraphicsEnvironment 
GraphicsDevice 
GrapihcsConfiguration

这三个类描绘了的所有的信息在以下情况下是必要的:在Java 平台上定位一个渲染设备或者字体;从用户空间到设备空间的坐标转换。应用程序可以访问这些信息,但并不需要执行从用户空间到设备空间的任何的转换。 

GraphicsEnvironment 描述了在一个特定的Java 平台上对于Java 应用可见的渲染设备的集合。渲染设备包括屏幕、打印机和图像缓存。 GraphicsEnvironment 同样包含了该平台上可用的字体的列表。 

GraphicsDevice 描述了应用程序可见的渲染设备,像屏幕或者打印机等。每个可能的设备的构造用GraphicsConfiguration  来描述。例如SVGA 显示设备能以以下几种方式操作:640x480x16 色、640x480x256 色、 和 800x600x256  色。这个SVGA 屏幕这可以描述为一个GraphicsDevice 对象而每种模式则被描述为GraphicsConfiguration 对象。 

一个 GraphicsEnviroment能包含一到多个 GraphicsDevice 对象;类似的,每个GraphicsDevice 对象又能包含一到多个GraphicsConfiguraitons对象。



1.2.2 转换 

Java 2D API 使用一种统一的坐标转换模式。包括从用户空间到设备空间的转换在内的所有坐标转换被AffineTransform  对象所描述。AffineTransform  定义了使用模型操作坐标的规则。

你可以把一个AffineTransform  对象增加到图形上下文中,用它来旋转、缩放、移动和切变一个几何图形、文本和图像在这些图元被渲染时。这种增加的转换被应用到任何图形对象的渲染的上下文中。当从用户空间到设备空间的坐标发生转变时,这种转换也被执行。



1.2.3 字体 

字符串通常被认为是从字符的方面来说的,这些字符构成了字符串。当字符串被绘制时,它的外观由被选择的字体来定义。尽管如此,用来显示这些字符串的字体的状态(Shapes)并不始终和特定的字符的状态(Shapes)等价。例如,在专业的出版业上,两个或多个字符的组合经常被一个称为连字符的单独的图形(Shape)所代替。 

用于表示字符串中每个字符的字体的形状称为字形。一种字体可以用多种方式来表示字符,例如小写字母;或者某种确定的字符组合,例如在单个字形后面跟着一个“fi”。在Java 2D API 中,一种字形就是一个简单的Shape 对象,它能像其它的Shape 对象一样被操作和渲染。

一种字体可被认为是字形的集合。一种字体可以有多种版本,例如加重的、适中的、斜体、哥特风格以及普通体。这些不同的版本叫做字面(faces )。字体中所有的字面都有一种相似的印刷术上的设计,并认为是相同的家庭(family)的不同成员。换句话说,特定风格的字形的集合构成字面,字面的集合构成家的家族,字体家庭的集合构成了字体的集合,而这些字体才是能在特定的GraphicsEnvironment中使用的。 

在Java 2D API  中,字体用描述特定字面的名字来表示——例如,Helvetica Bold 。这与JDK1.1  中的不同,在JDK1.1  中,字体用逻辑名称来描述,这些逻辑名称与不同的字面相一致,而这些字面是在特定的平台上可用的。为向后兼容,Java 2D API 支持像支持字面名称一样,也支持逻辑名称的描述。 

利用Java 2D API,你可以构成和渲染由多种不同家庭(families )、字面、大小甚至语言组成的多种字体。文本的显示和文本的布局在逻辑上是分离的。Font 对象用来定义显示,布局信息则保存在TextLayout  和TextAttributeSet  对象中。保持字体和布局信息的分离使得使得在不同布局环境中使用相同字体变得容易。



1.2.4 图像 

图像是空间上有条理的组织的像素的集合。一个像素定义了图像在单个显示位置上的显示。一个二维像素的矩阵称为光栅(raster )。像素的显示可以直接定义,也可以作为图像颜色表的索引。 图像可以包含很多种颜色(多于256 ),像素通常直接反应了每个屏幕位置上的color、alpha以及其它的显示特性。这些图像趋向于比indexed-color  图像更大,但看起来更真实。

在indexed-color 图像中,颜色被局限于特定的颜色表中,所以经常导致了更少的可用的颜色。尽量这样,一个index类型的图像比真实的颜色值需要更少的存储空间,作为indexed-color 而存储的图像通常更小。这种像素格式对于通常只包含16 种或者256 种颜色的图像来说是受欢迎的。 

Java 2D API  中的图像有两种首选的组件: 

(1)原始的图像数据(像素) 

(2)对这些像素解释的必须的信息 

这些解释像素的规则包含在ColorModel对象中——不管这些值应该直接的或者按照indexed方式来解释。为了显示一个像素,它必须和一种颜色模式相配对。 

色带是图像在颜色空间中的一个组件。例如,Red、Green、Blue 组件是RGB 图像中的色带。在直接的颜色模式下,像素被认为是一种单个屏幕位置上的色带值的集合。 

java.awt.image 包含了几种ColorModel 实现,包括了那些用于压缩图像和组件像素阐述。 

ColorSpace 对象包括了一些规则,这些规则控制着一组相当于特定颜色的数字度量尺度。 

java.awt.color  中的ColorSpace 的实现代表了包括RGB和亮度色标在内的最通用的颜色空间。 

值得注意的是颜色空间并不是一组颜色的集合,定定义了怎样解释单个颜色值的规则。从颜色模型中分离,颜色空间为怎样从一种颜色表示转换成另外一种方式提供了更灵活的处理能力。



1.2.5 填充和描边 

通过Java 2D API,你可以使用不同的画笔风格和填充模式来渲染图形(Shape)。因为最终也被表示为字形的集合,文本也可以描边和填充。 

画笔样式被定义为实现了Stroke 接口的对象。描边(Strokes)使你能够为直线和曲线指定不同的宽度了漂亮的模式。 

填充模式被定义为实现了Paint 接口的对象。在早期的AWT 版本中可以使用的Color 类就是一个实现了Paint  的对象,用来定义纯色(solid-color )填充。Java   2D   API 提供了两种额外的Paint  的实现:TexturePaint 和GradientPaint。TexturePaint 定义了使用简单的图形片段来统一的、重复的填充图形的填充模式。GradientPaint 定义了两种颜色之间的渐变的填充方式。 

在Java 2D  中,渲染图形的轮廓和以某种模式来填充图形是两种不同的操作: 

(1)使用draw 方法渲染图形的轮廓是指使用Stroke 属性指定的画笔样式和用Paint属性指定的填充模式来渲染图形的轮廓。 

(2)使用fill 方法填充图形的内部是指使用Paint 属性指定的模式来渲染图形。 

当文本被渲染时,当前的Paint 属性应用到构成字符串的字形上。尽量这样,值得注意的是drawString 实际上填充了那些被渲染的字形。为了能描绘文本字符串中的轮廓,你需要像对图形使用draw 方法一样先获得字形的轮廓,然后渲染它们。



1.2.6 混合 

当你在一个已存在的对象上渲染另一个对象时(即重叠的部分),你需要判断怎样来合并这些新的颜色和已经占据了这些你想要绘制的区域的原有对象的颜色。Java 2D API 把这些组合规则定义在了Composite 对象中。 

早先的渲染系统为颜色的混合只提供了基本的Boolean 操作。例如,一种Boolean 混合规则可能允许源和目标颜色的混合模式为:ANDed、ORed 或者XORed 。但这种方式带来了以下几种问题: 

(1)这样并不太友好——就红色和蓝色之间的ANDed  来说,很难想到目标颜色是什么,因为它们并不是简单的颜色值相加。 

(2)Boolean 混合不支持在不同的颜色空间上的精确的颜色混合。 

(3)直接的Boolean 混合并不考虑颜色的颜色模式。例如,在indexed 颜色模式中,一幅图像的两个像素上的Boolean 操作的结果是两个索引(indices )并不是两种颜色。 

Java 2D API 通过实现alpha-blending 规则,避免了这些问题的发生。当颜色混合发生时,Java 2D 把颜色模式的信息也考虑进来了。AlphaComposite 对象包含了源颜色和目标颜色的颜色模式。



1.3 向后兼容和平台无关性 

Java 2D API向后兼容了JDK1.1。同样它的设计也保留了应用程序的平台无关性。



1.3.1 向后兼容 

为保证向后兼容,原有的JDK 中图形和图像相关的类、接口及其功能都得以保留。已有的特点并没有被移除,也没有对已存在的类的包名进行重新指定。Java 2D API 通过在已有类中增加新的方法、扩展已有类以其添加新的并不影响原有API的类和接口使AWT的功能得到了增强。



1.3.2 平台无关性 

为了使应用程序的平台无关性变为可能,Java 2D API 并没有为目标渲染设备的解决方案、颜色空间、颜色模式等做出假设。Java 2D API 也没有为特定的图像文件格式做出假设。 

真正的字体的平台无关性只能在内置(由JDK 提供)的字体或者它们由数字的或程序的产生中才有可能。Java 2D API  当前并不支持内置或者数学产生字体,但是它能以程序定义的方式,通过字形的集合来定义所有的字体。每个字形都被一个由线段和弧线组成的Shape 来定义。很多字体——特别是字形和大小,只能从单一的字形集合中产生。



2.开始使用JAVA 2D API处理图像



2.1 显示和存储图像

有两个非常主要的,必须学习处理图像的类:

java.awt.Image类的超类,代表为矩形像素阵列图形图像。

java.awt.image.BufferedImage类,它扩展Image类,允许应用程序直接操作图像数据(例如,检索或设立的像素颜色)。 应用程序可以直接构建这个类的实例。

BufferedImage类的Java 2D即时模式成像API的基石。管理在存储器中的图像,并提供了用于存储,解释,并获得像素数据。 BufferedImage Image的一个子类,它可以呈现的Graphics和接受Graphics2D Image参数Graphics2D方法。

BufferedImage本质上是一个Image访问的数据缓冲区。 因此,它是更有效率的工作直接与BufferedImage 。 BufferedImage有一个的ColorModel和一个光栅图像数据。 此ColorModel提供了彩色图像的像素数据的解释。



2.1.1  读/加载图像:

当你想到数字图像,你可能会想到采样的图像格式,如JPEG图像格式应用于数码摄影,常用的网页或GIF图像。 所有程序可以使用这些图像必须首先将其转换成内部格式,外部格式。

Java 2D的支持加载这些外部的图像格式转换成BufferedImage格式,利用其图像I/O API,javax.imageio包。目前的image I/O,支持GIF,PNG,JPEG,BMP,WBMP。 image I/O也是可扩展的,使开发人员或管理员可以“外挂”其他格式的支持。 例如,TIFF和JPEG 2000插件是单独提供的。

从一个图像文件中加载图像,使用下面的代码:

BufferedImage img = null;
try {
    img = ImageIO.read(new File("qie.jpg"));
} catch (IOException e) {
…
}

图像I/O识别为JPEG格式的图像文件的内容,并由Java 2D将其解码成BufferedImage可直接使用。



2.1.2  写入/保存图像

使用javax.imageio包从外部图像格式转换成2D的Java的内部BufferedImage格式,加载图像。

Image I/O类提供了一种简单的方式来保存各种格式的图像:

static boolean ImageIO.write(RenderedImage im, 
                             String formatName,
                             File output)  throws IOException

注: BufferedImage类实现RenderedImage接口。

可以选择保存的图像格式:BufferedImage的formatName参数。

try {
    // retrieve image
    BufferedImage bi = getMyImage();
    File outputfile = new File("saved.png");
    ImageIO.write(bi, "png", outputfile);
} catch (IOException e) {
    ...
}

ImageIO.write方法调用的实现代码PNG插件:“PNG writer plug-in”。因为Image I/O的可扩展性,可以支持非常广泛的格式。但是,以下的标准图像格式的插件:JPEG,PNG,GIF,BMP和WBMP始终存在。

每种图像格式都有其优点和缺点:

格式

优点

缺点

GIF

支持动画和透明的像素

只支持256个的颜色,没有半透明

PNG

高显色性的无损图像比GIF或JPG更好的选择,支持半透明

不支持动画

JPG

伟大的摄影图片

压缩亏损,不擅长文字,截图,或任何应用程序,必须保留原始图像完全相同

这些标准的插件,对于大多数应用程序是足够的。Image I/O类提供了一种方式可以使用其他格式,有很多这样的插件。如果你有兴趣在什么文件格式是可加载或保存在您的系统中,你可以使用getReaderFormatNames和getWriterFormatNames方法ImageIO类。 这些方法返回一个数组,列出所有在此JRE支持的格式的字符串。

String writerNames[] = ImageIO.getWriterFormatNames();

返回的数组的名字将包括任何额外的插件安装和任何这些名称可能被用来作为一个格式的名称,选择一个image writer。

下面提供的示例代码是一个图像加载过滤保存的简单程序,它包括以下功能:

1.加载图像

2.图像过滤器

3.选择新的图像保存格式

4.选择图像保存路径和文件名

示例代码:

import java.io.*;
import java.util.TreeSet;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import javax.imageio.*;
import javax.swing.*;
public class SaveImage extends Component implements ActionListener {
    String descs[] = {
    "Original", 
        "Convolve : LowPass",
        "Convolve : Sharpen", 
        "LookupOp",
    };
    int opIndex;
    private BufferedImage bi, biFiltered;
    int w, h;
    
    public static final float[] SHARPEN3x3 = { // sharpening filter kernel
        0.f, -1.f,  0.f,
       -1.f,  5.f, -1.f,
        0.f, -1.f,  0.f
    };
    public static final float[] BLUR3x3 = {
        0.1f, 0.1f, 0.1f,    // low-pass filter kernel
        0.1f, 0.2f, 0.1f,
        0.1f, 0.1f, 0.1f
    };
    public SaveImage() {
        try {
            bi = ImageIO.read(new File("images/qie.jpg"));
            w = bi.getWidth(null);
            h = bi.getHeight(null);
            if (bi.getType() != BufferedImage.TYPE_INT_RGB) {
                BufferedImage bi2 =
                    new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
                Graphics big = bi2.getGraphics();
                big.drawImage(bi, 0, 0, null);
                biFiltered = bi = bi2;
            }
        } catch (IOException e) {
            System.out.println("Image could not be read");
            System.exit(1);
        }
    }
    public Dimension getPreferredSize() {
        return new Dimension(w, h);
    }
    String[] getDescriptions() {
        return descs;
    }
    void setOpIndex(int i) {
        opIndex = i;
    }
    public void paint(Graphics g) {
        filterImage();
        g.drawImage(biFiltered, 0, 0, null);
    }
    int lastOp;
    public void filterImage() {
        BufferedImageOp op = null;
        if (opIndex == lastOp) {
            return;
        }
        lastOp = opIndex;
        switch (opIndex) {
        case 0: biFiltered = bi; /* original */
                return; 
        case 1:  /* low pass filter */
        case 2:  /* sharpen */
            float[] data = (opIndex == 1) ? BLUR3x3 : SHARPEN3x3;
            op = new ConvolveOp(new Kernel(3, 3, data),
                                ConvolveOp.EDGE_NO_OP,
                                null);
  
            break;
        case 3 : /* lookup */
            byte lut[] = new byte[256];
            for (int j=0; j<256; j++) {
                lut[j] = (byte)(256-j); 
            }
            ByteLookupTable blut = new ByteLookupTable(0, lut); 
            op = new LookupOp(blut, null);
            break;
        }
        /* Rather than directly drawing the filtered image to the
         * destination, filter it into a new image first, then that
         * filtered image is ready for writing out or painting. 
         */
        biFiltered = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        op.filter(bi, biFiltered);
    }
    /* Return the formats sorted alphabetically and in lower case */
    public String[] getFormats() {
        String[] formats = ImageIO.getWriterFormatNames();
        TreeSet<String> formatSet = new TreeSet<String>();
        for (String s : formats) {
            formatSet.add(s.toLowerCase());
        }
        return formatSet.toArray(new String[0]);
    }
     public void actionPerformed(ActionEvent e) {
         JComboBox cb = (JComboBox)e.getSource();
         if (cb.getActionCommand().equals("SetFilter")) {
             setOpIndex(cb.getSelectedIndex());
             repaint();
         } else if (cb.getActionCommand().equals("Formats")) {
             /* Save the filtered image in the selected format.
              * The selected item will be the name of the format to use
              */
             String format = (String)cb.getSelectedItem();
             /* Use the format name to initialise the file suffix.
              * Format names typically correspond to suffixes
              */
             File saveFile = new File("savedimage."+format);
             JFileChooser chooser = new JFileChooser();
             chooser.setSelectedFile(saveFile);
             int rval = chooser.showSaveDialog(cb);
             if (rval == JFileChooser.APPROVE_OPTION) {
                 saveFile = chooser.getSelectedFile();
                 /* Write the filtered image in the selected format,
                  * to the file chosen by the user.
                  */
                 try {
                     ImageIO.write(biFiltered, format, saveFile);
                 } catch (IOException ex) {
                 }
             }
         }
    };
    public static void main(String s[]) {
        JFrame f = new JFrame("Save Image Sample");
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {System.exit(0);}
        });
        SaveImage si = new SaveImage();
        f.add("Center", si);
        JComboBox choices = new JComboBox(si.getDescriptions());
        choices.setActionCommand("SetFilter");
        choices.addActionListener(si);
        JComboBox formats = new JComboBox(si.getFormats());
        formats.setActionCommand("Formats");
        formats.addActionListener(si);
        JPanel panel = new JPanel();
        panel.add(choices);
        panel.add(new JLabel("Save As"));
        panel.add(formats);
        f.add("South", panel);
        f.pack();
        f.setVisible(true);
    }
}