|
Welcome to the Core Java Technologies Tech Tips for November 16, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).
This issue covers:
Converting Images to BMP/WMBP
Pooling Threads to Execute Short Tasks
These tips were developed using the Java 2 Platform Standard Edition Development Kit 5.0 (JDK 5.0). Download JDK 5.0.
This issue of the Core Java Technologies Tech Tips is written by John Zukowski, president of JZ Ventures, Inc.
See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
For more Java technology content, visit these sites:
java.sun.com - The latest Java platform releases, tutorials, and newsletters.
java.net - A web forum for collaborating and building solutions together.
java.com - The marketplace for Java technology, applications and services.
CONVERTING IMAGES TO BMP/WBMP
With the growing importance of wireless technologies, J2SE 5.0 now includes support for reading and writing bitmap (BMP) and wireless bitmap (WBMP) image formats. In this tip, you'll learn how to work with these image formats. You'll also create a tool to convert other image formats to BMP, with different compression types.
Internally, images are stored as a series of pixels. For a two-color, black and white picture, a 100 by 100 pixel picture requires 100 x 100 or 10,000 bits. For a full-color picture, each pixel of the image requires 24 bits, 8 each for red, green, and blue. The 100 by 100 pixel picture now jumps in size to 240,000 bits.
But there are different options than storing individual colors for each pixel. These options help reduce image size. Formats like GIF rely on identifying repeating patterns for compression. However, there were patent issues about using this compression format. Formats like BMP (and GIF) rely on using an indexed color table approach. Instead of storing the specific color for each pixel with each pixel, the BMP format relies on an indexing approach. All the necessary colors are placed in a table, and an index into the table is stored for each pixel.
The indexed lookup table size determines how many bits are needed for each pixel. For a 16 color table, you need 4 bits. For 256 colors, you need 8 bits. This means that a 100 by 100 pixel image requires 40,000 or 80,000 bits with the indexed approach, instead of the 240,000 bits with the true color approach.
As previously mentioned, the J2SE 5.0 platform supports images of type BMP and WBMP. These options are now available as part of the Image I/O framework, which means you can read and write these images by simply using the methods in the ImageIO class. The BMPImageWriteParam class of the javax.imageio.plugins.bmp package provides options for controlling how BMP Images are written out. However the actual image readers and writers are available through the service provider interface (SPI). As defined by the SPI, you don't call the constructor to get the reader/writer. Instead, you request the reader/writer by format name, mime type, or suffix. For example, to get the writer for the image/bmp type, you make the following request:
Iterator<ImageWriter> iter =
ImageIO.getImageWritersByMIMEType("image/bmp");
This returns an Iterator of all the writers (just one by default). Then you find the right one to write your image.
The BMP Image format also supports a series of compression types that are specified in the BMPImageWriteParam class by the type strings: BI_RGB, BI_RLE8, BI_RLE4, and BI_BITFIELDS. The compression type determines how the image is stored. The BI_RGB format is uncompressed. When compressed, the format is run length encoding (or RLE). This compression scheme provides compression by encoding successive sequences of the same color, with the count of the sequences and index of the color in the colormap. BI_RLE8 and BI_RLE4 specify the bits-per-sample to use. BI_RLE8 should be used when the image bit depth is 8. BI_RLE4 should be used when the image bit depth is 4. The BI_BITFIELDS type acts by packing the data. It essentially does away with the index, using three bit masks for red, green, and blue colors. Each pixel in the image is represented by the data element (16 or 32 bits, depending on the biBitCount). You get the color components by applying corresponding masks to the data element related to the pixel. The benefit of all these formats is that unlike JPEG, they are lossless. (The JAI-Image I/O Tools supports lossless JPEG and the newer JPEG-LS, in addition to the usual DCT-based lossy JPEG.)
You can write with default settings simply by calling to ImageIO.write(image, "BMP", object) with a final argument of a File, ImageOutputStream, or OutputStream. However, if you want to manually set the compression type, you have to get the ImageWriteParam object from the writer for the image type:
// one of "BI_RGB", "BI_RLE8", "BI_RLE4", or "BI_BITFIELDS"
String type = ...;
ImageWriteParam iwp = writer.getDefaultWriteParam();
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionType(type);
Then you explicitly call write on the writer (rather than the ImageIO object):
writer.setOutput(obj);
IIOImage image = new IIOImage(inputImage, null, null);
writer.write(null, image, iwp);
This creates an image of the requested type.
The following program demonstrates the use of the BMP image writer.
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.swing.*;
public class Converter extends JFrame {
BufferedImage currentImage;
public Converter() {
super("Converter");
setDefaultCloseOperation(EXIT_ON_CLOSE);
final ButtonGroup compressionGroup = new ButtonGroup();
JPanel compressionPanel = new JPanel();
String types[] =
{"BI_RGB", "BI_RLE8", "BI_RLE4", "BI_BITFIELDS"};
JRadioButton compressionType = null;
for (String type: types) {
compressionType = new JRadioButton(type);
compressionType.setActionCommand(type);
compressionGroup.add(compressionType);
compressionPanel.add(compressionType);
}
// Select last button by default
compressionType.setSelected(true);
add(compressionPanel, BorderLayout.NORTH);
final JLabel imageLabel = new JLabel();
JScrollPane scrollPane = new JScrollPane(imageLabel);
add(scrollPane, BorderLayout.CENTER);
JPanel actions = new JPanel();
JButton load = new JButton("Load");
ActionListener loadListener = new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
loadImage(imageLabel);
}
};
load.addActionListener(loadListener);
actions.add(load);
JButton save = new JButton("Save");
ActionListener saveListener = new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
saveImage(compressionGroup);
}
};
save.addActionListener(saveListener);
actions.add(save);
add(actions, BorderLayout.SOUTH);
setSize(400, 300);
setVisible(true);
}
private void saveImage(final ButtonGroup compressionGroup) {
if (currentImage != null) {
// Get model for selected button
ButtonModel model =
compressionGroup.getSelection();
String type = model.getActionCommand();
System.out.println("Compression Type: " + type);
JFileChooser fileChooser = new JFileChooser(".");
int status = fileChooser.showSaveDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
try {
Iterator<ImageWriter> iter =
ImageIO.getImageWritersByMIMEType("image/bmp");
// Get first writer
if (iter.hasNext()) {
ImageWriter writer = iter.next();
ImageWriteParam iwp =
writer.getDefaultWriteParam();
iwp.setCompressionMode
(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionType(type);
File outputFile =
fileChooser.getSelectedFile();
FileImageOutputStream fios = new
FileImageOutputStream(outputFile);
writer.setOutput(fios);
IIOImage image =
new IIOImage(currentImage, null, null);
writer.write(null, image, iwp);
System.err.println("Image saved");
}
} catch (IOException e) {
System.err.println("Unable to save: " + e);
}
}
}
}
private void loadImage(final JLabel imageLabel) {
JFileChooser fileChooser = new JFileChooser(".");
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
currentImage = null;
File selectedFile = fileChooser.getSelectedFile();
// Try to load selected file
if (selectedFile != null) {
try {
currentImage = ImageIO.read(selectedFile);
} catch (IOException e) {
}
}
// If selection loaded, use it
if (currentImage == null) {
imageLabel.setIcon(null);
imageLabel.setText("Not image: " +
selectedFile.getName());
} else {
imageLabel.setIcon(new ImageIcon(currentImage));
imageLabel.setText(null);
}
}
}
public static void main(String args[]) {
Runnable runner = new Runnable() {
public void run() {
new Converter();
}
};
EventQueue.invokeLater(runner);
}
}
Compile and run the Converter program.
Click the Load button to load an image. Then click the compression type from the set across the top of the screen.
Then click Save to save the image. If the image cannot be saved with the selected compression type, an exception will be thrown. For example:
Unable to save: java.io.IOException: Image can not be encoded
with compression type BI_RLE4
It is possible that an image with too large a color map for the compression type selected cannot be saved.
This program can be easily converted to one that supports WBMP instead of BMP. However, WBMP only supports single-band, bilevel images and no compression. This means that the program will work with black and white without gray scales. Otherwise, you need to convert the image to that format before saving.
For additional information on using the Image I/O libraries, see the February 16, 2004 Tech Tip Loading and Saving Images
with the Image I/O Library.
POOLING THREADS TO EXECUTE SHORT TASKS
If you develop programs that execute many short-lived tasks, it's wise to take advantage of a technique called thread pooling. Instead of creating a thread for each new task and discarding the thread when the task is done, you can create a pool of threads and give the pool each task to execute. If a thread in the pool is available, the task executes immediately. The thread returns to the pool when the task is done. Otherwise, the task waits for a thread to become available from the pool before executing.
J2SE 5.0 offers a new java.util.concurrent package, and in that package there are concurrency utilities that provide a pre-built thread pooling framework. The Executor interface in java.util.concurrent provides a single method, execute, that accepts a Runnable object as follows:
public interface Executor {
public void execute(Runnable command);
}
To use the thread pooling framework, you create an Executor instance, then you pass it some runnable tasks:
Executor executor = ...;
executor.execute(aRunnable1);
executor.execute(aRunnable2);
Then you create or find an implementation of the Executor interface. The implementation could run the task immediately, in a new thread, or serially. For example, here is an implementation that spawns a new thread for each task:
class MyExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}
The concurrency utilities also include a ThreadPoolExecutor class that offers support for many common pooling operations. With one of the four ThreadPoolExecutor constructors, you can specify options such as pool size, keep alive time, a thread factory, and a handler for rejected threads:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
But you really don't need to call a constructor. Instead, the Executors class of the java.util.concurrent package creates the thread pool for you. In the simplest case, you call the newFixedThreadPool method in the Executors class and pass in the number of threads you want in the pool. You then use ExecutorService, an interface that extends Executor, to either execute Runnable tasks or submit them. Calling the submit method of ExecutorService allows you to get a result back. The submit method also returns a Future object that you can use to check if the task is done.
Let's run a test program to demonstrate the use of thread pools. First, here's a program, NamePrinter, that notifies you when it starts, pauses for some amount of time, and then notifies you when it's done.
public class NamePrinter implements Runnable {
private final String name;
private final int delay;
public NamePrinter(String name, int delay) {
this.name = name;
this.delay = delay;
}
public void run() {
System.out.println("Starting: " + name);
try {
Thread.sleep(delay);
} catch (InterruptedException ignored) {
}
System.out.println("Done with: " + name);
}
}
Here is the test program, UsePool. It creates a thread pool of size 3, and adds 10 tasks to it (that is, 10 runs of NamePrinter). The UsePool program then waits for the tasks to finish before calling shutdown and awaitTermination. An ExecutorService should be shutdown before being terminated. There is also a shutdownNow method which attempts an immediate shutdown. Termination here is even faster than through the shutdown method. The shutdownNow method returns a List of any remaining Runnable tasks.
import java.util.concurrent.*;
import java.util.Random;
public class UsePool {
public static void main(String args[]) {
Random random = new Random();
ExecutorService executor =
Executors.newFixedThreadPool(3);
// Sum up wait times to know when to shutdown
int waitTime = 500;
for (int i=0; i<10; i++) {
String name = "NamePrinter " + i;
int time = random.nextInt(1000);
waitTime += time;
Runnable runner = new NamePrinter(name, time);
System.out.println("Adding: " + name + " / " + time);
executor.execute(runner);
}
try {
Thread.sleep(waitTime);
executor.shutdown();
executor.awaitTermination
(waitTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
System.exit(0);
}
}
Compile NamePrinter and UsePool, then run UsePool. Here's a sample output run -- note that each run will be unique with the random sleeps present:
Adding: NamePrinter 0 / 30
Adding: NamePrinter 1 / 727
Adding: NamePrinter 2 / 980
Starting: NamePrinter 0
Starting: NamePrinter 1
Starting: NamePrinter 2
Adding: NamePrinter 3 / 409
Adding: NamePrinter 4 / 49
Adding: NamePrinter 5 / 802
Adding: NamePrinter 6 / 211
Adding: NamePrinter 7 / 459
Adding: NamePrinter 8 / 994
Adding: NamePrinter 9 / 459
Done with: NamePrinter 0
Starting: NamePrinter 3
Done with: NamePrinter 3
Starting: NamePrinter 4
Done with: NamePrinter 4
Starting: NamePrinter 5
Done with: NamePrinter 1
Starting: NamePrinter 6
Done with: NamePrinter 6
Starting: NamePrinter 7
Done with: NamePrinter 2
Starting: NamePrinter 8
Done with: NamePrinter 5
Starting: NamePrinter 9
Done with: NamePrinter 7
Done with: NamePrinter 9
Done with: NamePrinter 8
Notice that the first three NamePrinter objects started quickly. Later NamePrinter objects started as each executing NamePrinter object finished.
There is much more to the thread pooling framework available in J2SE 5.0. For example, you can create scheduled thread pools, where you can schedule tasks to run "later".
For more information on thread pooling and the new concurrency utilities, see Concurrency Utilities.
|
|
 |
 |
|
|
 |
 |
IMPORTANT: Please read our Licensing, Terms of Use, and Privacy policies:
http://developer.java.sun.com/berkeley_license.html
http://www.sun.com/share/text/termsofuse.html
Privacy Statement: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your email preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe.
Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet
Subscribe to other Java developer Tech Tips:
- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).
To subscribe to these and other Sun Developer Network publications:
- Go to the Sun Developer Network - Subscriptions page choose the newsletters you want to subscribe to and click "Submit".
- To unsubscribe, go to the Subscriptions page uncheck the appropriate checkbox, and click "Submit".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html
Copyright 2004 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html
Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.
|