-
Bug
-
Resolution: Fixed
-
P4
-
11, 17
-
b11
-
generic
-
generic
A DESCRIPTION OF THE PROBLEM :
Call to Graphics2D.drawRenderedImage(RenderedImage, AffineTransform) fails if the 2 following conditions are true:
* The RenderedImage to draw contains at least one tile having a location different than (0,0). Note that this is always the case with images having more than one tile.
* The tiles are not instances of WritableRaster (they are instances of Raster only).
The problem seems to be located in SunGraphics2D.drawRenderedImage. If the tile to drawn is not an instance of WritableRaster, then the code wraps the tile's DataBuffer into a new WritableRaster but with a location set to (0,0), because the location argument in the createWritableRaster(...) call is null. Then the call to createWritableChild(...) applies a translation for bringing the tile location to (0,0). But in the case where the raster has been converted from Raster to WritableRaster, that translation has already been applied, and the effect of current code is to apply the translation twice.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The attached test case draws the same image twice: one time with WritableRaster tiles (which succeed), then the same operation where the only change is the use of Raster tiles.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No exception thrown, both when drawing with WritableRaster and when repeating the operation with Raster.
ACTUAL -
Drawing with WritableRasters passes. But the same drawing with Rasters causes following exception:
Exception in thread "main" java.awt.image.RasterFormatException: (parentX + width) is outside raster
at java.desktop/java.awt.image.WritableRaster.createWritableChild(WritableRaster.java:228)
at java.desktop/sun.java2d.SunGraphics2D.drawTranslatedRenderedImage(SunGraphics2D.java:2852)
at java.desktop/sun.java2d.SunGraphics2D.drawRenderedImage(SunGraphics2D.java:2711)
---------- BEGIN SOURCE ----------
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.BandedSampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Objects;
import java.util.Vector;
final class TiledImage implements RenderedImage {
public static void main(String[] args) {
draw(true); // Pass.
draw(false); // Fail with exception described in javadoc.
}
private static final int NUM_X_TILES = 2, NUM_Y_TILES = 3;
private static final int TILE_WIDTH = 16, TILE_HEIGHT = 12;
/**
* Tests rendering a tiled image.
*
* @param writable whether the image shall use writable raster.
*/
private static void draw(final boolean writable) {
final BufferedImage target = new BufferedImage(
TILE_WIDTH * NUM_X_TILES,
TILE_HEIGHT * NUM_Y_TILES,
BufferedImage.TYPE_BYTE_GRAY);
final RenderedImage source = new TiledImage(writable,
target.getColorModel());
Graphics2D g = target.createGraphics();
g.drawRenderedImage(source, new AffineTransform());
g.dispose();
}
private final ColorModel colorModel;
private final Raster[] tiles;
/**
* Creates a tiled image. The image is empty,
* but pixel values are not relevant for this test.
*
* @param writable whether the image shall use writable raster.
*/
private TiledImage(boolean writable, ColorModel cm) {
/*
* We need a sample model class for which Raster.createRaster
* do not provide a special case. That method has optimizations
* for most SampleModel sub-types, except BandedSampleModel.
*/
SampleModel sm = new BandedSampleModel(DataBuffer.TYPE_BYTE, TILE_WIDTH, TILE_HEIGHT, 1);
tiles = new Raster[NUM_X_TILES * NUM_Y_TILES];
final Point location = new Point();
for (int tileY = 0; tileY < NUM_Y_TILES; tileY++) {
for (int tileX = 0; tileX < NUM_X_TILES; tileX++) {
location.x = tileX * TILE_WIDTH;
location.y = tileY * TILE_HEIGHT;
DataBufferByte db = new DataBufferByte(TILE_WIDTH * TILE_HEIGHT);
Raster r;
if (writable) {
r = Raster.createWritableRaster(sm, db, location);
} else {
// Case causing RasterFormatException later.
r = Raster.createRaster(sm, db, location);
}
tiles[tileX + tileY * NUM_X_TILES] = r;
}
}
colorModel = cm;
}
@Override
public ColorModel getColorModel() {
return colorModel;
}
@Override
public SampleModel getSampleModel() {
return tiles[0].getSampleModel();
}
@Override
public Vector<RenderedImage> getSources() {
return new Vector();
}
@Override
public Object getProperty(String key) {
return Image.UndefinedProperty;
}
@Override
public String[] getPropertyNames() {
return null;
}
@Override public int getMinX() {return 0;}
@Override public int getMinY() {return 0;}
@Override public int getMinTileX() {return 0;}
@Override public int getMinTileY() {return 0;}
@Override public int getTileGridXOffset() {return 0;}
@Override public int getTileGridYOffset() {return 0;}
@Override public int getNumXTiles() {return NUM_X_TILES;}
@Override public int getNumYTiles() {return NUM_Y_TILES;}
@Override public int getTileWidth() {return TILE_WIDTH;}
@Override public int getTileHeight() {return TILE_HEIGHT;}
@Override public int getWidth() {return TILE_WIDTH * NUM_X_TILES;}
@Override public int getHeight() {return TILE_HEIGHT * NUM_Y_TILES;}
@Override
public Raster getTile(final int tileX, final int tileY) {
Objects.checkIndex(tileX, NUM_X_TILES);
Objects.checkIndex(tileY, NUM_Y_TILES);
return tiles[tileX + tileY * NUM_X_TILES];
}
@Override
public Raster getData() {
throw new UnsupportedOperationException("Not needed for this test.");
}
@Override
public Raster getData(Rectangle rect) {
throw new UnsupportedOperationException("Not needed for this test.");
}
@Override
public WritableRaster copyData(WritableRaster raster) {
throw new UnsupportedOperationException("Not needed for this test.");
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Avoid read-only Raster instances, always use WritableRaster.
FREQUENCY : always
Call to Graphics2D.drawRenderedImage(RenderedImage, AffineTransform) fails if the 2 following conditions are true:
* The RenderedImage to draw contains at least one tile having a location different than (0,0). Note that this is always the case with images having more than one tile.
* The tiles are not instances of WritableRaster (they are instances of Raster only).
The problem seems to be located in SunGraphics2D.drawRenderedImage. If the tile to drawn is not an instance of WritableRaster, then the code wraps the tile's DataBuffer into a new WritableRaster but with a location set to (0,0), because the location argument in the createWritableRaster(...) call is null. Then the call to createWritableChild(...) applies a translation for bringing the tile location to (0,0). But in the case where the raster has been converted from Raster to WritableRaster, that translation has already been applied, and the effect of current code is to apply the translation twice.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The attached test case draws the same image twice: one time with WritableRaster tiles (which succeed), then the same operation where the only change is the use of Raster tiles.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
No exception thrown, both when drawing with WritableRaster and when repeating the operation with Raster.
ACTUAL -
Drawing with WritableRasters passes. But the same drawing with Rasters causes following exception:
Exception in thread "main" java.awt.image.RasterFormatException: (parentX + width) is outside raster
at java.desktop/java.awt.image.WritableRaster.createWritableChild(WritableRaster.java:228)
at java.desktop/sun.java2d.SunGraphics2D.drawTranslatedRenderedImage(SunGraphics2D.java:2852)
at java.desktop/sun.java2d.SunGraphics2D.drawRenderedImage(SunGraphics2D.java:2711)
---------- BEGIN SOURCE ----------
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.BandedSampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Objects;
import java.util.Vector;
final class TiledImage implements RenderedImage {
public static void main(String[] args) {
draw(true); // Pass.
draw(false); // Fail with exception described in javadoc.
}
private static final int NUM_X_TILES = 2, NUM_Y_TILES = 3;
private static final int TILE_WIDTH = 16, TILE_HEIGHT = 12;
/**
* Tests rendering a tiled image.
*
* @param writable whether the image shall use writable raster.
*/
private static void draw(final boolean writable) {
final BufferedImage target = new BufferedImage(
TILE_WIDTH * NUM_X_TILES,
TILE_HEIGHT * NUM_Y_TILES,
BufferedImage.TYPE_BYTE_GRAY);
final RenderedImage source = new TiledImage(writable,
target.getColorModel());
Graphics2D g = target.createGraphics();
g.drawRenderedImage(source, new AffineTransform());
g.dispose();
}
private final ColorModel colorModel;
private final Raster[] tiles;
/**
* Creates a tiled image. The image is empty,
* but pixel values are not relevant for this test.
*
* @param writable whether the image shall use writable raster.
*/
private TiledImage(boolean writable, ColorModel cm) {
/*
* We need a sample model class for which Raster.createRaster
* do not provide a special case. That method has optimizations
* for most SampleModel sub-types, except BandedSampleModel.
*/
SampleModel sm = new BandedSampleModel(DataBuffer.TYPE_BYTE, TILE_WIDTH, TILE_HEIGHT, 1);
tiles = new Raster[NUM_X_TILES * NUM_Y_TILES];
final Point location = new Point();
for (int tileY = 0; tileY < NUM_Y_TILES; tileY++) {
for (int tileX = 0; tileX < NUM_X_TILES; tileX++) {
location.x = tileX * TILE_WIDTH;
location.y = tileY * TILE_HEIGHT;
DataBufferByte db = new DataBufferByte(TILE_WIDTH * TILE_HEIGHT);
Raster r;
if (writable) {
r = Raster.createWritableRaster(sm, db, location);
} else {
// Case causing RasterFormatException later.
r = Raster.createRaster(sm, db, location);
}
tiles[tileX + tileY * NUM_X_TILES] = r;
}
}
colorModel = cm;
}
@Override
public ColorModel getColorModel() {
return colorModel;
}
@Override
public SampleModel getSampleModel() {
return tiles[0].getSampleModel();
}
@Override
public Vector<RenderedImage> getSources() {
return new Vector();
}
@Override
public Object getProperty(String key) {
return Image.UndefinedProperty;
}
@Override
public String[] getPropertyNames() {
return null;
}
@Override public int getMinX() {return 0;}
@Override public int getMinY() {return 0;}
@Override public int getMinTileX() {return 0;}
@Override public int getMinTileY() {return 0;}
@Override public int getTileGridXOffset() {return 0;}
@Override public int getTileGridYOffset() {return 0;}
@Override public int getNumXTiles() {return NUM_X_TILES;}
@Override public int getNumYTiles() {return NUM_Y_TILES;}
@Override public int getTileWidth() {return TILE_WIDTH;}
@Override public int getTileHeight() {return TILE_HEIGHT;}
@Override public int getWidth() {return TILE_WIDTH * NUM_X_TILES;}
@Override public int getHeight() {return TILE_HEIGHT * NUM_Y_TILES;}
@Override
public Raster getTile(final int tileX, final int tileY) {
Objects.checkIndex(tileX, NUM_X_TILES);
Objects.checkIndex(tileY, NUM_Y_TILES);
return tiles[tileX + tileY * NUM_X_TILES];
}
@Override
public Raster getData() {
throw new UnsupportedOperationException("Not needed for this test.");
}
@Override
public Raster getData(Rectangle rect) {
throw new UnsupportedOperationException("Not needed for this test.");
}
@Override
public WritableRaster copyData(WritableRaster raster) {
throw new UnsupportedOperationException("Not needed for this test.");
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Avoid read-only Raster instances, always use WritableRaster.
FREQUENCY : always