Return-Path: Received: from pacific-carrier-annex.mit.edu by po10.mit.edu (8.9.2/4.7) id TAA20881; Tue, 18 Jun 2002 19:08:34 -0400 (EDT) Received: from hermes.sun.com (hermes.sun.com [64.124.140.169]) by pacific-carrier-annex.mit.edu (8.9.2/8.9.2) with SMTP id TAA14815 for ; Tue, 18 Jun 2002 19:08:33 -0400 (EDT) Date: Tue, 18 Jun 2002 12:57:03 GMT-08:00 From: "JDC Tech Tips" To: alexp@mit.edu Message-Id: <170147211578454271@hermes.sun.com> Subject: JDC Tech Tips, June 18, 2002 (Reading From Output Streams, Blending Images) Mime-Version: 1.0 Content-Type: text/html; charset=us-ascii Content-Transfer-Encoding: 7bit X-Mailer: SunMail 1.0 Technical Tips
   View this issue as simple text June 18, 2002        

Reading from Output Streams
Blending Images

These tips were developed using javaTM 2 SDK, Standard Edition, v 1.4.

This issue of the JDC Tech Tips is written by John Zukowski, president of JZ Ventures, Inc.

Pixel

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 JavaTM 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; i<len; i++) {
      check(cbuf[i]);
    }
  }

  public void write(String str, int off, 
      int len) throws IOException {
    super.write(str, off, len);
    for (int i=off; i<len; 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() {
    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, you should see the following output:

  Chars:      14
  Nums:       0
  Whitespace: 4 

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.

Pixel
Pixel

Blending Images

The Java 2DTM 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:

CLEARDraw nothing. Creates empty output.
DSTDraw only the destination image.
SRCDraw only the source image.
DST_ATOPDraw the source image. Where the two images overlap, draw the destination image.
SRC_ATOPDraw the destination image. Where the two images overlap, draw the source image.
DST_INDraw the part of the destination image that overlaps the source.
SRC_INDraw the part of the source image that overlaps the destination.
DST_OUTDraw the part of the destination image that doesn't overlap the source.
SRC_OUTDraw the part of the source image that doesn't overlap the destination.
DST_OVERDraw the destination image over the source image.
SRC_OVERDraw the source image over the destination image.
XORDraw 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:

CompositeIt

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:

stage coach

into that of a saloon:

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:

Image Blending

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".

Pixel
Pixel

IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html

Comments? Send your feedback on the JavaTM Developer Technical Tips to: jdc-webmaster@sun.com

Go to the subscriptions page to subscribe or unsubscribe to this newsletter.

ARCHIVES: You'll find the Java Developer Connection Technical Tips archives at:
http://developer.java.sun.com/developer/JDCTechTips/

Copyright 2002 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

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.

Sun Microsystems, Inc.
Please send me newsletters in text.
Please unsubscribe me from this newsletter.