|
Tech Tips Archive
June 18, 2002
WELCOME to the Java Developer Connection (JDC) Tech Tips, June 18, 2002. This issue covers:
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.
This issue of the JDC Tech Tips is written by John Zukowski,
president of JZ Ventures, Inc.
READING FROM OUTPUT STREAMS
I/O stands for input and output. It represents how programs
interact with the outside world. You read input and you write
output. In the Java platform, I/O relies on a streams-based model. This model allows you to read from files, network
connections, or consoles in the same way. You don't have to
change your code based on the type of input device. Similarly,
for output you don't have to change your code for each type of
output device.
The streams-based model works well when you want to read from
an input stream or write to an output stream. However, there are
added considerations when you need to get data that was written
to an output stream, or put data into an input stream to be read.
This tip examines some of these considerations.
Input streams are for reading. A program opens an input stream
for reading information from a source. Output streams are for
writing. To write to a destination, a program opens an output
stream to that destination, and then writes to that output stream.
The java.io package contains a variety of stream classes for
reading from and writing to a stream. These classes are broadly
divided into two categories: character streams and byte streams.
Character stream classes are for reading and writing character
data. Byte stream classes are for reading and writing binary
data (that is, bytes). Reader and Writer are abstract
superclasses for character streams. InputStream and OutStream are abstract superclasses for byte streams. Subclasses of
these superclasses implement streams for specific sources and
destinations.
A question that is sometimes asked is how do you read what was
just written to a stream? For example, suppose you write to an
output stream. To do this, you pass to some library method an
OutputStream or Writer. How do you then read what you just wrote from the OutputStream or Writer?
One way to read what you wrote is to pass to the library method
one of the file-related output streams: FileOutputStream or
FileWriter. This allows you to reread what you wrote to the file
system. Here's an example:
import java.io.*;
public class FileRead {
public static void main(String args[])
throws IOException {
// Get temp output file
File file = File.createTempFile(
"zuk", ".tmp");
// Remove when program ends
file.deleteOnExit();
// Create output stream for file
Writer writer = new FileWriter(file);
// Send data to output
save(writer);
// Close output
writer.close();
// Open output as input
Reader reader = new FileReader(file);
BufferedReader bufferedReader =
new BufferedReader(reader);
// Read input
String line;
while ((line = bufferedReader.readLine())
!= null) {
System.out.println(line);
}
bufferedReader.close();
}
private static void save(Writer generic)
throws IOException {
PrintWriter out = new PrintWriter(generic);
out.println("Line One");
out.println("Line Two");
}
}
|
The FileRead program creates a file-related output stream (a
FileWriter), writes two lines of output to the stream, opens the
output as an input stream, and then reads from the input
stream. The program should display the reread lines:
Line One
Line Two
This approach is especially appropriate if the output is larger
than available memory. That's because you will have to save the
output to an external source anyway.
In cases where the intermediate information is relatively small,
you can use memory-based stream classes. Memory-based stream
classes read from and write to memory (as opposed to streams
such as FileWriter which are used to read from an external
source). You can have the library send intermediate information
to the appropriate memory-based stream class: for output, that
would be either ByteArrayOutputStream or StringWriter. Then, to
read the in-memory information, you can convert it to a
memory-based input stream: ByteArrayInputStream or StringReader.
The output streams, ByteArrayOutputStream and StringWriter, rely
on an internal byte array and StringBuffer to store the
intermediate information. At input time, the ByteArrayInputStream
and StringReader work with a byte array and String for input.
That means that when you need to change from output to input
mode, you get the current contents of the output destination to
create an input source.
In the case of the byte-based streams, the ByteArrayOutputStream
and ByteArrayInputStream work together. When you finish writing,
you get the bytes with the toByteArray() method. Passing the
bytes to the ByteArrayInputStream provides the input stream:
// Create output stream
ByteArrayOutputStream outputStream =
new ByteArrayOutputStream(initialSize);
// Write to stream
...
// When done, get bytes
byte bytes[] = outputStream.toByteArray();
// Create intput stream
ByteArrayInputStream inputStream =
new ByteArrayInputStream(bytes);
// Read from stream
...
|
There are some things to consider when you use
ByteArrayOutputStream. First, it helps if you can estimate the
size of the output. If you don't provide an initial size, the
byte array starts at 32 bytes and increases by a factor of two
each time the internal buffer fills up (the growth is even faster
if you write an array instead of a character). If you know you're
going to have at least 2000 characters, start at that size and
avoid the resizings at 64, 128, 256, 512, and 1024, just to get
to 2048 characters. Another consideration is that the
toByteArray() method doesn't return a reference to the internal
byte array. Instead, it returns a copy. This can be both a good
or bad thing. While copying does prevent the buffer from changing,
(or you changing the buffer), it does mean that there are two sets
of data, requiring twice as much memory.
For character-based streams, there are StringWriter and
StringReader. StringWriter uses an internal StringBuffer for
managing the characters read. The code to use for
character-based streams is similar to the code for byte-array
streams:
// Create output stream
StringWriter writer =
new StringWriter(initialSize);
// Write to stream
...
// When done, get characters
String string = writer.toString();
// Create intput stream
StringReader reader =
new StringReader(string);
// Read from stream
...
|
StringWriter uses a character array for the internal storage.
The character array is in a StringBuffer. As the array fills up,
it increases in size with the same doubling effect as described
previously for ByteArrayOutputStream. If you don't provide an
initial size, the StringBuffer array starts at only an initial
size of 16. As always, try to size the array to a more realistic
initial size. Note that it's possible to get the contents of the
StringBuffer used by the StringWriter without allocating more memory. For more information about this, see the documentation of the toString method of StringBuffer.
Using the StringWriter-StringReader pair, you can now change the earlier example to keep all accesses in memory:
import java.io.*;
public class MemRead {
public static void main(String args[])
throws IOException {
// Create memory output stream
StringWriter writer =
new StringWriter(128);
// Send data to output
save(writer);
// Close output
writer.close();
// Open output as input
Reader reader =
new StringReader(writer.toString());
BufferedReader bufferedReader =
new BufferedReader(reader);
// Read input
String line;
while ((line = bufferedReader.readLine())
!= null) {
System.out.println(line);
}
bufferedReader.close();
}
private static void save(Writer generic)
throws IOException {
PrintWriter out = new PrintWriter(generic);
out.println("Line One");
out.println("Line Two");
}
}
|
There's at least one other way to read from an output stream.
This approach uses filters. Both the byte and character-based
streaming classes provide for the installation of filters into
the I/O streams. While keeping the basic reading and writing
operations the same, filters enhance streams by adding
capabilities. The BufferedReader class used in the previous
examples is an example of a filter. It maintains input from
the source in an internal buffer and feeds the characters to the
reader as requested. Instead of having to go to the input source
for each request, the BufferedReader fetches input in bulk,
usually for quicker performance. Because the MemRead program uses
an in-memory buffer, there is no real performance difference. For
the FileRead program, there is a performance difference, albeit
minimal for input this small.
Filters typically fit into the processing sequence as follows:
you pass in the original source or destination to a constructor,
then the filter performs its processing before (or after) passing
the bytes or characters to the original source or destination.
Filters subclass either an existing class or one of the filtering
stream. The filtering stream classes depend on the type of data
to be filtered: FilterInputStream, FilterOutputStream, FilterReader, or FilterWriter.
You use filters in a program in the same way as shown earlier for
BufferedReader earlier, that is:
SourceStream source = new SourceStream(...);
AFilterStream filter = new AFilterStream(source);
// use filter
...
// close filter, not source
filter.close();
|
Here's a program that uses a filter to count characters, numbers,
and white space. When the filter is closed, it writes the counts
to the stream sent to the constructor:
import java.io.*;
public class CountWriter
extends FilterWriter {
PrintStream out;
int chars, nums, whites;
public CountWriter(Writer destination,
PrintStream out) {
super(destination);
this.out = out;
}
public void write(int c)
throws IOException {
super.write(c);
check((char)c);
}
public void write(char cbuf[], int off,
int len) throws IOException {
super.write(cbuf, off, len);
for (int i=off; end=off+len; i<end; i++) {
check(cbuf[i]);
}
}
public void write(String str, int off,
int len) throws IOException {
super.write(str, off, len);
for (int i=off; end=off+len; i<end; i++) {
check(str.charAt(i));
}
}
private void check(char ch) {
if (Character.isLetter(ch)) {
chars++;
} else if (Character.isDigit(ch)) {
nums++;
} else if (Character.isWhitespace(ch)) {
whites++;
}
}
public void close() throws IOException {
super.close();
out.println("Chars: " + chars);
out.println("Nums: " + nums);
out.println("Whitespace: " + whites);
}
}
|
Let's update the earlier MemRead program to use the filter.
Notice that you don't need to reread the data to produce the
necessary output:
import java.io.*;
public class MemRead2 {
public static void main(String args[])
throws IOException {
// Create memory output stream
StringWriter writer =
new StringWriter(128);
CountWriter counter =
new CountWriter(writer, System.err);
// Send data to output
save(counter);
// Close output
counter.close();
}
private static void save(Writer generic)
throws IOException {
PrintWriter out = new PrintWriter(generic);
out.println("Line One");
out.println("Line Two");
}
}
|
When you run the program in UNIX, you should see the following output:
Chars: 14
Nums: 0
Whitespace: 4
If you run the program in Windows, the whitespace count should
be 6. That's because Windows ends lines in a text file
with two Unicode escape sequences ('\u000D' and '\u000A').
That's really all there is to reading from output streams. You can
either take the brute force approach of reading the completely
written output, or intercept the output as it is being written to
perform your own read operation.
The New I/O libraries of Java 1.4 provide additional mechanisms
to create read-write buffers. See the article "New I/0
Functionality for Java 2 Standard Edition 1.4" for
information on working with the newer buffering capabilities.
BLENDING IMAGES
The Java 2D API provides support for the blending of multiple
drawn images through what are known as Porter-Duff rules.
Originally described by a SIGGRAPH paper from 1984, Compositing
Digital Images, by Thomas Porter and Tom Duff, the rules describe
how to combine the contents of multiple images when one image is
drawn on top of the other.
There are twelve such rules. These include rules such as "draw
only the source image" and "draw the part of the destination
image that doesn't overlap the source." At first glance, some of
these rules might seem complex. However they aren't as complex as
they seem. If you see a picture, things get much clearer.
Within the Java 2D API, the blending rules are supported by the
AlphaComposite class. The class provides twelve constants, one
for each rule. To change the setting, you pass the specific
constant to the setComposite method of the Graphics2D class. Then, when an image is drawn, the rule associated with the
constant is used to describe how the new image is blended with
the existing content. The Java 2D API supports AlphaComposite
objects with transparency percentages. If you want to slowly blend
one image into another, you can alter the percentages such that
more of the new image can appear or disappear based on the
blending rule used.
Here are the twelve constants and their associated rules:
CLEAR | Draw nothing. Creates empty output. |
DST | Draw only the destination image. |
SRC | Draw only the source image. |
DST_ATOP | Draw the source image. Where the two images overlap,
draw the destination image. |
SRC_ATOP | Draw the destination image. Where the two images
overlap, draw the source image. |
DST_IN | Draw the part of the destination image that overlaps
the source. |
SRC_IN | Draw the part of the source image that overlaps the
destination. |
DST_OUT | Draw the part of the destination image that doesn't
overlap the source. |
SRC_OUT | Draw the part of the source image that doesn't overlap
the destination. |
DST_OVER | Draw the destination image over the source image. |
SRC_OVER | Draw the source image over the destination image. |
XOR | Draw the part of the destination and source images that
don't overlap. |
To help you visualize the twelve rules, this tip uses a program
that blends images. The program comes from the book Mastering
Java 2, J2SE 1.4 by John Zukowski, published by Sybex.
The program produces a screen using all
twelve rules. In the program, each rule is used with three
different percentages for transparency settings of the source and
destination images. The source image is a green triangle on the
left. The destination image is a magenta triangle on the right.
The two triangles overlap in the middle.
In the screen for the program are two sets of drawings. The first
set on top uses the composite settings of CLEAR, DST, DST_ATOP,
DST_IN, DST_OUT, and DST_OVER. The bottom set uses SRC, SRC_ATOP,
SRC_IN, SRC_OUT, SRC_OVER, and XOR.
The program is only meant to show the different rules. How to
actually blend images will be explained shortly.
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;
public class CompositeIt extends JFrame {
int rules[] = {
AlphaComposite.CLEAR,
AlphaComposite.DST,
AlphaComposite.DST_ATOP,
AlphaComposite.DST_IN,
AlphaComposite.DST_OUT,
AlphaComposite.DST_OVER,
AlphaComposite.SRC,
AlphaComposite.SRC_ATOP,
AlphaComposite.SRC_IN,
AlphaComposite.SRC_OUT,
AlphaComposite.SRC_OVER,
AlphaComposite.XOR};
float percents[] = {.33f, .67f, 1.0f};
BufferedImage source, dest;
GeneralPath sourcePath, destPath;
public CompositeIt() {
sourcePath = new GeneralPath();
sourcePath.moveTo(0, 0);
sourcePath.lineTo(50, 0);
sourcePath.lineTo(50, 25);
sourcePath.closePath();
source = new BufferedImage(80, 30,
BufferedImage.TYPE_INT_ARGB);
destPath = new GeneralPath();
destPath.moveTo(25, 0);
destPath.lineTo(75, 0);
destPath.lineTo(25, 25);
destPath.closePath();
dest = new BufferedImage(80, 30,
BufferedImage.TYPE_INT_ARGB);
}
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
Graphics2D sourceG =
source.createGraphics();
Graphics2D destG =
dest.createGraphics();
AffineTransform at =
new AffineTransform();
Composite originalComposite =
g2d.getComposite();
for(int i=0; i<3; i++) {
for(int j=0, n=rules.length; j<n;
j++) {
at = AffineTransform.
getTranslateInstance(j*80+10,
i*30+30);
if (j >= rules.length/2) {
at.translate(-rules.length/2*80,
120);
}
g2d.setTransform(at);
g.drawRect(0, 0, 80, 30);
destG.setComposite(
AlphaComposite.Clear);
destG.fillRect(0, 0, 80, 30);
destG.setComposite(
AlphaComposite.getInstance(
AlphaComposite.XOR, percents[i]));
destG.setPaint(Color.MAGENTA);
destG.fill(destPath);
sourceG.setComposite(
AlphaComposite.Clear);
sourceG.fillRect(0, 0, 80, 30);
sourceG.setComposite(
AlphaComposite.getInstance(
AlphaComposite.XOR, percents[i]));
sourceG.setPaint(Color.GREEN);
sourceG.fill(sourcePath);
destG.setComposite(
AlphaComposite.getInstance(rules[j]));
destG.drawImage(source, 0, 0, null);
g2d.drawImage(dest, 0, 0, this);
}
}
}
public static void main(String args[]) {
JFrame f = new CompositeIt();
f.setDefaultCloseOperation(
JFrame.EXIT_ON_CLOSE);
f.setTitle("CompositeIt");
f.setSize(525, 275);
f.show();
}
}
|
Here's the blended image the program displays:
You can combine the images in memory, as opposed to using the
current Graphics context for the screen. Using a BufferedImage
object for double buffering, you draw the one image to the
buffer. Then you draw the second image on the first using the
desired rule. Finally, you draw the combined image to the screen.
Here's the approach:
// Create in-memory image buffer
BufferedImage dest = new BufferedImage(
width, height,
BufferedImage.TYPE_INT_ARGB);
// Get the Graphics Context
Graphics2D destG = dest.createGraphics();
// Draw first image on it
destG.drawImage(image1, 0, 0, this);
// Combine them
destG.setComposite(mode);
destG.drawImage(source, 0, 0, this);
// Draw image on screen
g2d.drawImage(dest, 0, 0, this);
|
There is more to these capabilities though. The example didn't
show how to blend images. More specifically, you haven't seen how
to have one image fade, such that a second image is progressively
favored. With properly sized images, you can watch as a baby
picture morphs into an adult, or a puppy into a full grown dog.
To have images fade, it becomes necessary to draw the source and
destination images with varying transparency percentages. The
AlphaComposite class provides a getInstance method that allows you to provide a Porter-Duff rule, and to specify a transparency
percentage for a drawing operation. By having that drawing
operation be the initial drawing of the image, you effectively
make the image transparent to varying degrees. Then, when the two
images are drawn, one on top of the other, you can get a fading
effect by altering the degree of transparency.
The following program demonstrates this capability by fading the
image of a stage coach:
into that of a saloon:
You can grab the
images from the Cowboy Clip Art site or you can provide your own images. The saloon image is saloon.gif, the
stagecoach image is oldstage.gif. The program uses a TimerTask
and Timer to loop through a fixed set of steps between images.
Feel free to adjust the STEPS setting to alter this counter, or
the SLEEP_DELAY setting to change the speed of the changes.
A higher STEPS setting means there are more intermediate steps
between images. A higher SLEEP_DELAY setting makes the time
between each step longer.
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;
import java.util.Timer;
import java.util.TimerTask;
public class Converge extends JFrame {
ImageIcon saloonIcon =
new ImageIcon("saloon.gif");
ImageIcon coachIcon =
new ImageIcon("oldstage.gif");
Image saloon = saloonIcon.getImage();
Image coach = coachIcon.getImage();
BufferedImage dest;
float sourcePercentage = 1,
destinationPercentage = 0;
private static int STEPS = 100;
private static float STEP_CHANGE =
1.0f/STEPS;
private static int SLEEP_DELAY = 100;
Insets insets;
public Converge() {
super("Image Blending");
setDefaultCloseOperation(
JFrame.EXIT_ON_CLOSE);
dest = new BufferedImage(200, 200,
BufferedImage.TYPE_INT_ARGB);
setSize(200, 200);
TimerTask task = new TimerTask() {
public void run() {
repaint();
sourcePercentage -= STEP_CHANGE;
destinationPercentage +=
STEP_CHANGE;
if (sourcePercentage < 0) {
sourcePercentage = 0;
destinationPercentage = 1;
cancel();
}
}
};
Timer timer = new Timer();
timer.schedule(task, 0, SLEEP_DELAY);
}
public void paint(Graphics g) {
if (insets == null) {
insets = getInsets();
}
g.translate(insets.left, insets.top);
Graphics2D g2d = (Graphics2D)g;
Graphics2D destG = dest.createGraphics();
destG.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC, sourcePercentage));
destG.drawImage(coach, 0, 0, this);
destG.setComposite(AlphaComposite.getInstance(
AlphaComposite.XOR, destinationPercentage));
destG.drawImage(saloon, 0, 0, this);
g2d.drawImage(dest, 0, 0, this);
}
public static void main(String args[]) {
new Converge().show();
}
}
|
Here's a snapshot of the blended image the
program displays:
The Java 1.4 platform includes a number of graphics improvements
related to the Java 2D API. For a description of these
improvements see the article "Graphics Performance Improvements in the Java 2 SDK, version 1.4".
Also see the presentation "Translucency, Alpha Compositing and
Animation: Taking Advantage of Java 2D Technology in Your Rich
Client Application".
IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html
FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com
SUBSCRIBE/UNSUBSCRIBE
- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
- COPYRIGHT
Copyright 2002 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
JDC Tech Tips
June 18, 2002
Sun, Sun Microsystems, Java, Java Developer Connection, and
Java 2D are trademarks or registered trademarks of Sun Microsystems,
Inc. in the United States and other countries.
|