- Applying Rendering Hints
- Managing Memory
- Scheduling Tiles
- Reformatting an Image
- Extending the Border
- A Rendering Example
- A Closer Look at the PlanarImage Class
- Using the RenderedOp Class
- Working with Tiles
- A Tiled-Image Viewer
- Writing to Pixels
- Creating an Aggregate Image
- A JAI Image Browser
- The Renderable Layer
- Conclusion
Working with Tiles
As you probably know by now, neither AWT imaging nor Java 2D can handle smooth rendering of large images. The reason is that the underlying imaging models read the entire image in memory. As mentioned in Chapter 10, the JAI pull model overcomes this problem through tiling. Although the tile concept is defined in Java 2D, JAI actually implements it. However, tiling works best when the underlying format supports tiles, and this is not the case with most image formats.
If an image is not tiled, the FileLoad operator treats the entire image as a single tile and loads it in the memory. As mentioned earlier, we use the Format operator to tile such images in memory.
Viewing and Manipulating Large Images
In Chapter 10 we developed the JAIImageCanvas class (see Listing 10.2) to display planar images. If we use this class for manipulating large images, the rendering is slow because JAIImageCanvas does not completely exploit the tiling mechanism.
In the JAIImageCanvas class, the paintComponent() method calls Graphics2D's drawRenderedImage() method to render the image. This means that there is no control over which tiles are computed for displaying the visible part of the image.
One way to speed up rendering is to convert the visible portion of the image to a buffered image and render it using drawImage(). In this approach, the granularity of the tile is important. If the tiles are too small compared to the viewport, there will be too many tile computations, possibly degrading performance. On the other hand, too large a tile requires a large amount of memory because BufferedImage stores all the image data in memory. So designing a tile layout means making a trade-off between speed and memory.
To implement a canvas for rendering large images, let's create a subclass of the com.vistech.jai.render.JAIImageCanvas class and extend its functionality to implement tiling. We'll call this class RenderedImageCanvas.
As we saw in the section titled Using the Format Operator earlier in this chapter, a planar image can be restructured to implement any tile layout through the ImageLayout class and the Format operator. To paint a partial image, let's override the paintComponent() method and implement a tile computation loop based on the coordinates of the image that covers the viewport. These tiles can then be converted into a buffered image and painted immediately.
Listing 11.4 shows the RenderedImageCanvas class.
NOTE
The code for RenderedImageCanvas is available on the book's Web page in the directory src/chapter11/render.
LISTING 11.4 The RenderedImageCanvas class
package com.vistech.jai.render; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import javax.swing.*; import java.awt.geom.*; import java.awt.image.renderable.*; import javax.media.jai.*; public class RenderedImageCanvas extends JAIImageCanvas { protected int viewerWidth = 480, viewerHeight = 400; transient protected PlanarImage displayImage, origImage; protected int tileWidth = 256, tileHeight = 256; transient protected SampleModel sampleModel; protected ColorModel colorModel; protected int maxTileIndexX, maxTileIndexY; protected int maxTileCordX, maxTileCordY; protected int minTileIndexX, minTileIndexY; protected int minTileCordX, minTileCordY; protected int tileGridXOffset, tileGridYOffset; protected int imageWidth =0, imageHeight =0; protected TileCache tc; public RenderedImageCanvas() { } public RenderedImageCanvas(PlanarImage img){ this(); setImage(img); } public void setImage(PlanarImage img){ origImage = img; panX =0; panY =0; atx = AffineTransform.getTranslateInstance(0.0, 0.0); RenderedOp op = makeTiledImage(img); displayImage = op.createInstance(); sampleModel = displayImage.getSampleModel(); colorModel = displayImage.getColorModel(); getTileInfo(displayImage); fireTilePropertyChange(); imageDrawn = false; repaint(); } public PlanarImage getDisplayImage(){return displayImage;} protected RenderedOp makeTiledImage(PlanarImage img) { ImageLayout tileLayout = new ImageLayout(img); tileLayout.setTileWidth(tileWidth); tileLayout.setTileHeight(tileHeight); RenderingHints tileHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, tileLayout); ParameterBlock pb = new ParameterBlock(); pb.addSource(img); return JAI.create("format", pb, tileHints); } protected void getTileInfo(PlanarImage img) { imageWidth = img.getWidth(); imageHeight = img.getHeight(); tileWidth = img.getTileWidth(); tileHeight = img.getTileHeight(); maxTileIndexX = img.getMinTileX()+img.getNumXTiles()-1; maxTileIndexY = img.getMinTileY()+img.getNumYTiles()-1; maxTileCordX = img.getMaxX(); maxTileCordY = img.getMaxY(); minTileIndexX = img.getMinTileX(); minTileIndexY = img.getMinTileY(); minTileCordX = img.getMinX(); minTileCordY = img.getMinY(); tileGridXOffset = img.getTileGridXOffset(); tileGridYOffset = img.getTileGridYOffset(); } public void setTileWidth(int tw){ tileWidth = tw; setImage(displayImage); } public int getTileWidth(){ return tileWidth;} public void setTileHeight(int th){ tileHeight = th; setImage(displayImage); } public int getTileHeight(){ return tileHeight;} public int getMaxTileIndexX(){return maxTileIndexX;} public int getMaxTileIndexY(){return maxTileIndexY;} public int getImageWidth(){return imageWidth;} public int getImageHeight(){return imageHeight;} protected void fireTilePropertyChange() { firePropertyChange("maxTileIndexX", null, new Integer(maxTileIndexX)); firePropertyChange("maxTileIndexY", null, new Integer(maxTileIndexY)); firePropertyChange("tileWidth", null, new Integer(tileWidth)); firePropertyChange("tileHeight", null, new Integer(tileWidth)); firePropertyChange("transform", null, atx); } public void paintComponent(Graphics gc){ Graphics2D g = (Graphics2D)gc; Rectangle rect = this.getBounds(); if((viewerWidth != rect.width) || (viewerHeight != rect.height)){ viewerWidth = rect.width; viewerHeight = rect.height; } g.setColor(Color.black); g.fillRect(0, 0, viewerWidth, viewerHeight); if(displayImage == null) return; int ti =0, tj = 0; Rectangle bounds = new Rectangle(0, 0, rect.width, rect.height); bounds.translate(-panX, -panY); int leftIndex = displayImage.XToTileX(bounds.x); if(leftIndex < minTileIndexX) leftIndex = minTileIndexX; if(leftIndex > maxTileIndexX) leftIndex = maxTileIndexX; int rightIndex = displayImage.XToTileX(bounds.x + bounds.width - 1); if(rightIndex < minTileIndexX) rightIndex = minTileIndexX; if(rightIndex > maxTileIndexX) rightIndex = maxTileIndexX; int topIndex = displayImage.YToTileY(bounds.y); if(topIndex < minTileIndexY) topIndex = minTileIndexY; if(topIndex > maxTileIndexY) topIndex = maxTileIndexY; int bottomIndex = displayImage.YToTileY(bounds.y + bounds.height - 1); if(bottomIndex < minTileIndexY) bottomIndex = minTileIndexY; if(bottomIndex > maxTileIndexY) bottomIndex = maxTileIndexY; for(tj = topIndex; tj <= bottomIndex; tj++) { for(ti = leftIndex; ti <= rightIndex; ti++) { Raster tile = displayImage.getTile(ti, tj); DataBuffer dataBuffer = tile.getDataBuffer(); WritableRaster wr = tile.createWritableRaster(sampleModel, dataBuffer, new Point(0,0)); BufferedImage bi = new BufferedImage(colorModel, wr, colorModel.isAlphaPremultiplied(), null); if(bi == null) continue; int xInTile = displayImage.tileXToX(ti); int yInTile = displayImage.tileYToY(tj); AffineTransform tx = AffineTransform.getTranslateInstance(xInTile+panX, yInTile+panY); g.drawRenderedImage(bi, tx); } } imageDrawn = true; } }
The RenderedImageCanvas class inherits several instance variables and methods from JAIImageCanvas (see Listing 10.2). RenderedImageCanvas adds two more instance variablesorigImage and displayImageto hold the original image and the currently displayed image. The value of the displayImage variable is derived by reformatting of the image with the Format operator. These variables allow users to set the desired tile dimensions. The paintComponent() method draws the displayImage object over the graphical context.
The RenderedImageCanvas class has various properties for describing a tile, including tileWidth, tileHeight, maxTileIndexX, and maxTileIndexY. The maxTile IndexX and maxTileIndexY properties represent the number of tile columns and the number of tile rows, respectively. These two properties are needed to compute the positions of the required tiles.
RenderedImageCanvas overrides the setImage() and paintComponent() methods of its superclass, JAIImageCanvas. The setImage() method uses the current tile size attributes to reformat the input image. To accomplish this reformatting, setImage() calls makeTiledImage(), which returns a reformatted planar image. The setImage() method assigns this image to the displayImage variable. Then setImage() calls the getTileInfo() method to set the tile-related attributes. If the image is smaller than the tile, the image size becomes the current tile size.
Tile Computation
Depending on the size of the viewport, the paintComponent() method obtains the tiles needed to display the image within the viewport. To do so, it uses the current panX and panY variables to compute the position of the tiles within the image. The size of the viewport determines how many tiles are required to display the part of the image that covers the viewport (see Figure 11.5).
FIGURE 11.5 Viewing large images
A tile in a rendered image can be obtained by the getTile(xIndex, yIndex) method. This means that to obtain a tile from the image, we need its position. To get all the tiles that cover the viewport, we need the starting and ending indices of the tiles in both the x and the y directions. For a given point in the image, the PlanarImage methods XtoTileX() and YtoTileY() compute the tile indices in the x and y directions, respectively.
To compute the tile indices, the paintComponent() method first starts with the upper left-hand corner of the viewport, which is at position (panX, panY). The tile indices corresponding to this position are leftIndex and topIndex. To compute the ending index, the paintComponent() method uses the lower right-hand corner coordinates of the viewport with respect to the image. These are given by (panX + viewerWidth) and (panY + viewerHeight), and the corresponding indices are right Index and bottomIndex, respectively.
Once the indices in both the x and the y directions have been computed, paintComponent() executes a loop that does the following:
Retrieves a tile from the planar image.
Converts that tile into a buffered image.
Obtains the position of the tile by calling the tileToX() and tileToY() methods. (The position of the tile in the image is needed to display this image.)
Constructs an AffineTransform object to handle the translation. (Each tile must be translated appropriately for the image to appear continuous.)
Draws the tile by calling the drawImage() method with the BufferedImage object produced in step 2.