/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.image.libs;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.color.ICC_ColorSpace;
import java.awt.image.*;
import java.awt.geom.AffineTransform;
import com.lightcrafts.mediax.jai.PlanarImage;
import com.lightcrafts.mediax.jai.ImageLayout;
import com.lightcrafts.mediax.jai.TileCache;
import javax.swing.*;
import com.lightcrafts.image.ColorProfileException;
import com.lightcrafts.image.BadColorProfileException;
import com.lightcrafts.utils.ProgressIndicator;
import com.lightcrafts.utils.UserCanceledException;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.jai.JAIContext;
import com.lightcrafts.jai.opimage.CachedImage;
import static com.lightcrafts.image.metadata.TIFFTags.*;
import static com.lightcrafts.image.types.TIFFConstants.*;
/**
* An <code>LCTIFFReader</code> is a Java wrapper around the LibTIFF library.
*
* @author Paul J. Lucas [paul@lightcrafts.com]
* @see <a href="http://www.remotesensing.org/libtiff/">LibTIFF</a>
*/
public final class LCTIFFReader extends LCTIFFCommon {
////////// public /////////////////////////////////////////////////////////
/**
* Construct an <code>LCTIFFReader</code> and open a TIFF file.
*
* @param fileName The name of the TIFF file to open.
*/
public LCTIFFReader( String fileName ) throws LCImageLibException {
this( fileName, false );
}
/**
* Construct an <code>LCTIFFReader</code> and open a TIFF file.
*
* @param fileName The name of the TIFF file to open.
* @param read2nd If <code>true</code>, read the second TIFF image (if
* present).
*/
public LCTIFFReader( String fileName, boolean read2nd )
throws LCImageLibException
{
m_read2nd = read2nd;
openForReading( fileName );
//
// If openForReading() fails, it will store 0 in the native pointer.
//
if ( m_nativePtr == 0 )
throw new LCImageLibException( "Could not open " + fileName );
}
/**
* Get the ICC profile of the TIFF image.
*
* @return Returns the {@link ICC_Profile} or <code>null</code> if the
* image doesn't have a color profile.
*/
public ICC_Profile getICCProfile() throws ColorProfileException {
final byte[] iccProfileData = getICCProfileData();
if ( iccProfileData == null )
return null;
try {
return ICC_Profile.getInstance( iccProfileData );
}
catch ( IllegalArgumentException e ) {
throw new BadColorProfileException( null );
}
}
/**
* Gets the TIFF image.
*
* @param thread The thread doing the getting.
* @return Returns said image.
*/
public synchronized PlanarImage getImage( ProgressThread thread )
throws LCImageLibException, UserCanceledException
{
if ( m_image == null ) {
try {
if ( m_read2nd && isLightZoneLayeredTIFF() &&
!nextDirectory() )
return null;
readImage( thread );
}
finally {
dispose();
}
}
return m_image;
}
/**
* Gets the value of the given TIFF integer metadata field.
*
* @param tagID The tag ID of the metadata field to get. The ID should be
* that of a tag whose value is an integer ({@link #TIFF_FIELD_TYPE_USHORT}
* or {@link #TIFF_FIELD_TYPE_ULONG} and not a string.
* @return Returns said value.
* @throws IllegalArgumentException if <code>tagID</code> isn't that of an
* integer metadata field or is otherwise unsupported.
* @see #getStringField(int)
*/
public native int getIntField( int tagID );
/**
* Gets one of the values of the given TIFF two-value integer metadata
* field.
*
* @param tagID The tag ID of the metadata field to get. The ID should be
* that of a tag whose value is an integer ({@link #TIFF_FIELD_TYPE_USHORT}
* or {@link #TIFF_FIELD_TYPE_ULONG} and not a string.
* @param getSecond If <code>true</code>, gets the second value; otherwise
* gets the first.
* @return Returns said value.
* @throws IllegalArgumentException if <code>tagID</code> isn't that of an
* integer metadata field or is otherwise unsupported.
* @see #getStringField(int)
*/
public native int getIntField2( int tagID, boolean getSecond );
/**
* Gets the number of strips in the TIFF image. The image must be stored
* in strips and not tiles.
*
* @return Returns said number of strips.
* @see #isTiled()
*/
public native int getNumberOfStrips();
/**
* Gets the number of tiles in the TIFF image. The image must be stored
* in tiles and not strips.
*
* @return Returns said number of tiles.
* @see #isTiled()
*/
public native int getNumberOfTiles();
/**
* Gets the value of the given TIFF string metadata field.
*
* @param tagID The tag ID of the metadata field to set. The ID should be
* that of a tag whose value is a string ({@link #TIFF_FIELD_TYPE_ASCII}.
* @return Returns the string value.
* @throws IllegalArgumentException if <code>tagID</code> isn't that of an
* string metadata field or is otherwise unsupported.
* @see #getIntField(int)
* @see #getIntField2(int,boolean)
*/
public native String getStringField( int tagID );
/**
* Returns the size of the strips in bytes. The image must be stored in
* strips and not in tiles.
*
* @return Returns said size.
* @see #isTiled()
*/
public native int getStripSize();
/**
* Returns the size of the tiles in bytes. The image must be stored in
* tiles and not in strips.
*
* @return Returns said size.
* @see #isTiled()
*/
public native int getTileSize();
/**
* Returns whether the TIFF image is stored in tiles.
*
* @return Returns <code>true</code> only if the image is stored in tiles.
*/
public native boolean isTiled();
////////// private ////////////////////////////////////////////////////////
/**
* Gets the raw ICC profile data (minus the header) from the TIFF image.
*
* @return Returns said data as a <code>byte</code> array.
*/
private native byte[] getICCProfileData();
/**
* Checks whether the current TIFF file is a 2-page (layered) TIFF file
* created by LightZone.
*
* @return Returns <code>true</code> only if the TIFF file is a LightZone
* layers TIFF file.
*/
private boolean isLightZoneLayeredTIFF() {
final String software = getStringField( TIFF_SOFTWARE );
if ( software == null || !software.startsWith( "LightZone" ) )
return false;
//
// The TIFF has to have exactly 2 pages.
//
final int pages = getIntField2( TIFF_PAGE_NUMBER, true );
return pages == 2;
}
/**
* Reads the next TIFF directory replacing the current one.
*
* @return Returns <code>true</code> if the next directory was read
* successfully.
*/
private native boolean nextDirectory() throws LCImageLibException;
/**
* Opens a TIFF file.
*
* @param fileName The name of the TIFF file to open.
*/
private native void openForReading( String fileName )
throws LCImageLibException;
private class TIFF_Format {
final int imageWidth = getIntField(TIFF_IMAGE_WIDTH);
final int imageHeight = getIntField(TIFF_IMAGE_LENGTH);
final int bitsPerSample = getIntField(TIFF_BITS_PER_SAMPLE);
final int samplesPerPixel = getIntField(TIFF_SAMPLES_PER_PIXEL);
final int sampleFormat = getIntField(TIFF_SAMPLE_FORMAT);
final int extraSamples = getIntField(TIFF_EXTRA_SAMPLES);
final int photometric = getIntField(TIFF_PHOTOMETRIC_INTERPRETATION);
final int planarConfig = getIntField(TIFF_PLANAR_CONFIGURATION);
final int planes = planarConfig == TIFF_PLANAR_CONFIGURATION_CHUNKY ? 1 : samplesPerPixel;
final boolean tiled = isTiled();
final int tiles = tiled ? getNumberOfTiles() / planes : getNumberOfStrips() / planes;
final int tileSize = tiled ? getTileSize() : getStripSize();
final int tiffTileWidth = tiled ? getIntField(TIFF_TILE_WIDTH) : imageWidth;
final int tiffTileHeight = tiled ? getIntField(TIFF_TILE_LENGTH)
: tileSize / (imageWidth * (samplesPerPixel / planes) * bitsPerSample / 8);
final boolean hasAlpha = extraSamples == TIFF_EXTRA_SAMPLES_ASSOC_ALPHA || extraSamples == TIFF_EXTRA_SAMPLES_UNASSOS_ALPHA;
final boolean isAlphaPremultiplied = extraSamples == TIFF_EXTRA_SAMPLES_ASSOC_ALPHA;
final ICC_Profile profile;
final ColorSpace colorSpace;
final ComponentColorModel tiffCcm;
final SampleModel tiffTsm;
TIFF_Format() throws LCImageLibException {
if (bitsPerSample != 8 && bitsPerSample != 16)
throw new LCImageLibException("Unsupported TIFF Bit per Sample Value: " + bitsPerSample);
ICC_Profile iccProfile = null;
try {
iccProfile = getICCProfile();
// TODO: deal with weird photometric interpretations
if ( iccProfile == null && photometric == TIFF_PHOTOMETRIC_CIELAB )
iccProfile = JAIContext.labProfile;
}
catch ( ColorProfileException e ) {
// nothing
}
profile = iccProfile;
if (profile != null)
colorSpace = new ICC_ColorSpace(profile);
else
switch ( samplesPerPixel - (hasAlpha ? 1 : 0) ) {
case 1:
colorSpace = JAIContext.gray22ColorSpace;
break;
case 3:
colorSpace = JAIContext.sRGBColorSpace;
break;
case 4:
colorSpace = JAIContext.CMYKColorSpace;
break;
default:
throw new LCImageLibException(
"Bad image: " + samplesPerPixel + " samples per pixel."
);
}
// Color model for the tiff image, can have an alpha channel
tiffCcm = new ComponentColorModel(
colorSpace,
hasAlpha,
isAlphaPremultiplied,
Transparency.OPAQUE,
bitsPerSample == 8
? DataBuffer.TYPE_BYTE
: sampleFormat == TIFF_SAMPLE_FORMAT_INT ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT
);
// Sample model for the readout buffer, large enough to hold a tile or a strip of the TIFF image, can be banded
if ( planarConfig == TIFF_PLANAR_CONFIGURATION_CHUNKY )
tiffTsm = tiffCcm.createCompatibleSampleModel(tiffTileWidth, tiffTileHeight);
else
tiffTsm = new BandedSampleModel(tiffCcm.getTransferType(), tiffTileWidth, tiffTileHeight, samplesPerPixel);
}
}
private TIFF_Format getFormat() throws LCImageLibException {
return new LCTIFFReader.TIFF_Format();
}
public static class TIFFImage extends PlanarImage {
final LCTIFFReader.TIFF_Format tf;
final LCTIFFReader reader;
public TIFFImage(String path) throws LCImageLibException {
reader = new LCTIFFReader(path);
tf = reader.getFormat();
final ImageLayout layout = new ImageLayout(0, 0, tf.imageWidth, tf.imageHeight,
0, 0,
tf.tiffTileWidth, tf.tiffTileHeight,
tf.tiffTsm, tf.tiffCcm);
this.setImageLayout(layout);
}
public void dispose() {
super.dispose();
reader.dispose();
}
protected transient TileCache cache = JAIContext.defaultTileCache;
protected Object tileCacheMetric;
public TileCache getTileCache() {
return cache;
}
public void setTileCache(TileCache cache) {
if (this.cache != null) {
this.cache.removeTiles(this);
}
this.cache = cache;
}
protected Raster getTileFromCache(int tileX, int tileY) {
return cache != null ? cache.getTile(this, tileX, tileY) : null;
}
protected void addTileToCache(int tileX,
int tileY,
Raster tile) {
if (cache != null) {
cache.add(this, tileX, tileY, tile, null);
}
}
public Raster getTile(int tileX, int tileY) {
Raster tile = null; // the requested tile, to be returned
int tileN = tileX + tileY * this.getNumXTiles();
// Make sure the requested tile is inside this image's boundary.
if (tileX >= getMinTileX() && tileX <= getMaxTileX() &&
tileY >= getMinTileY() && tileY <= getMaxTileY()) {
// Check if tile is available in the cache.
tile = getTileFromCache(tileX, tileY);
if (tile == null) { // tile not in cache
tile = Raster.createWritableRaster(tf.tiffTsm, new Point(tileX * tf.tiffTileWidth, tileY * tf.tiffTileHeight));
try {
for ( int plane = 0; plane < tf.planes; plane++ ) {
if ( tf.tiled ) {
if ( tf.bitsPerSample == 8 ) {
final byte[] buffer = ((DataBufferByte) tile.getDataBuffer()).getData( plane );
reader.readTileByte( tileN + plane * tf.tiles, buffer, 0, tf.tileSize );
} else {
final short[] buffer = ((DataBufferUShort) tile.getDataBuffer()).getData( plane );
reader.readTileShort( tileN + plane * tf.tiles, buffer, 0, tf.tileSize );
}
} else {
if ( tf.bitsPerSample == 8 ) {
final byte[] buffer = ((DataBufferByte) tile.getDataBuffer()).getData( plane );
reader.readStripByte( tileY + plane * tf.tiles, buffer, 0, tf.tileSize );
} else {
final short[] buffer = ((DataBufferUShort) tile.getDataBuffer()).getData( plane );
reader.readStripShort( tileY + plane * tf.tiles, buffer, 0, tf.tileSize );
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
// Cache the result tile.
addTileToCache(tileX, tileY, tile);
}
}
return tile;
}
}
/**
* Reads the TIFF image.
*
* @param thread The thread that is doing the reading.
*/
private void readImage( ProgressThread thread )
throws LCImageLibException, UserCanceledException
{
final TIFF_Format tf = new TIFF_Format();
// Color model for a LightZone Image, no alpha chaannel
final ComponentColorModel imageCcm = new ComponentColorModel(
tf.colorSpace,
false,
false,
Transparency.OPAQUE,
tf.bitsPerSample == 8
? DataBuffer.TYPE_BYTE
: tf.sampleFormat == TIFF_SAMPLE_FORMAT_INT ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT
);
// The readout buffer itself
DataBuffer db = null;
if (!(tf.tiled && !tf.hasAlpha)) {
/* If the TIFF image is tiled and it doesn't have an alpha channel
then we can read directly into the destination image buffer,
don't allocate the buffer */
if (tf.bitsPerSample == 8)
db = tf.planes == 1 ? new DataBufferByte(tf.tileSize) : new DataBufferByte(tf.tileSize, tf.planes);
else
db = tf.planes == 1 ? new DataBufferUShort(tf.tileSize/2) : new DataBufferUShort(tf.tileSize/2, tf.planes);
}
// Sample model for the output image, interleaved
final SampleModel tsm = imageCcm.createCompatibleSampleModel(JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT);
// Layout of the output image
final ImageLayout layout = new ImageLayout(0, 0, tf.imageWidth, tf.imageHeight,
0, 0,
JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT,
tsm, imageCcm);
// The output image itself, directly allocated in the file cache
CachedImage image = new CachedImage(layout, JAIContext.fileCache);
final int maxTileX = image.getNumXTiles();
ProgressIndicator indicator = null;
if ( thread != null )
indicator = thread.getProgressIndicator();
if ( indicator != null )
indicator.setMaximum( tf.tiles );
int bandList[] = new int[tf.samplesPerPixel];
for (int i = 0; i < tf.samplesPerPixel; i++)
bandList[i] = i;
Rectangle imageBounds = new Rectangle(0, 0, tf.imageWidth, tf.imageHeight);
for ( int tile = 0; tile < tf.tiles; tile++ ) {
if ( thread != null && thread.isCanceled() )
throw new UserCanceledException();
final int tileX = tf.tiled ? tile % maxTileX : 0;
final int tileY = tf.tiled ? tile / maxTileX : tile;
// The actual tile bounds, clipping on the image bounds
Rectangle tileBounds = new Rectangle(tileX * tf.tiffTileWidth,
tileY * tf.tiffTileHeight,
tf.tiffTileWidth,
tf.tiffTileHeight).intersection(imageBounds);
// the corresponding tile data
int tileData = (tf.samplesPerPixel / tf.planes) * tileBounds.width * tileBounds.height * (tf.bitsPerSample == 8 ? 1 : 2);
/* If the TIFF image is tiled and it doesn't have an alpha channel
then we can read directly into the destination image buffer,
don't allocate an intermediate raster */
WritableRaster raster =
!tf.tiled || tf.hasAlpha
? Raster.createWritableRaster(tf.tiffTsm, db, new Point(tileBounds.x, tileBounds.y))
: null;
for ( int plane = 0; plane < tf.planes; plane++ ) {
if ( tf.tiled ) {
if (!tf.hasAlpha)
raster = image.getWritableTile( tileX, tileY );
if ( tf.bitsPerSample == 8 ) {
final byte[] buffer =
((DataBufferByte)raster.getDataBuffer()).getData(
plane );
int read = readTileByte( tile + plane * tf.tiles, buffer, 0, tileData );
if (read != tileData)
throw new LCImageLibException("Broken TIFF File");
} else {
final short[] buffer =
((DataBufferUShort)raster.getDataBuffer()).getData(
plane );
int read = readTileShort( tile + plane * tf.tiles, buffer, 0, tileData );
if (read != tileData)
throw new LCImageLibException("Broken TIFF File");
}
} else {
if ( tf.bitsPerSample == 8 ) {
final byte[] buffer =
((DataBufferByte)db).getData( plane );
int read = readStripByte( tileY + plane * tf.tiles, buffer, 0, tileData );
if (read != tileData)
throw new LCImageLibException("Broken TIFF File");
} else {
final short[] buffer =
((DataBufferUShort)db).getData( plane );
int read = readStripShort( tileY + plane * tf.tiles, buffer, 0, tileData );
if (read != tileData)
throw new LCImageLibException("Broken TIFF File");
}
}
}
if (!tf.tiled || tf.hasAlpha)
image.setData(raster.createChild(raster.getMinX(), raster.getMinY(),
raster.getWidth(), raster.getHeight(),
raster.getMinX(), raster.getMinY(),
bandList));
if ( indicator != null )
indicator.incrementBy( 1 );
}
if ( indicator != null )
indicator.setIndeterminate( true );
m_image = image;
}
/**
* NOTE: TIFF read functions can be called in parallel for different tiles, make them synchronized...
*/
/**
* Reads and decodes and encoded strip from the TIFF image. The image must
* be stored in strips and not tiles.
*
* @param stripIndex The index of the strip to read.
* @param buf The buffer into which to read the image data.
* @param offset The offset into the buffer where the image data will begin
* being placed.
* @param stripSize The size of the strip.
* @return Returns the number of bytes read or -1 if there was an error.
* @see #isTiled()
* @see #getStripSize()
*/
private native synchronized int readStripByte( int stripIndex, byte[] buf, long offset,
int stripSize )
throws LCImageLibException;
/**
* Reads and decodes and encoded strip from the TIFF image. The image must
* be stored in strips and not tiles.
*
* @param stripIndex The index of the strip to read.
* @param buf The buffer into which to read the image data.
* @param offset The offset into the buffer where the image data will begin
* being placed.
* @param stripSize The size of the strip.
* @return Returns the number of bytes read or -1 if there was an error.
* @see #isTiled()
* @see #getStripSize()
*/
private native synchronized int readStripShort( int stripIndex, short[] buf, long offset,
int stripSize )
throws LCImageLibException;
/**
* Reads and decodes and encoded tile from the TIFF image. The image must
* be stored in tiles and not strips.
*
* @param tileIndex The index of the tile to read.
* @param buf The buffer into which to read the image data.
* @param offset The offset into the buffer where the image data will begin
* being placed.
* @param tileSize The size of the tile.
* @return Returns the number of bytes read or -1 if there was an error.
* @see #isTiled()
* @see #getTileSize()
*/
private native synchronized int readTileByte( int tileIndex, byte[] buf, long offset,
int tileSize ) throws LCImageLibException;
/**
* Reads and decodes and encoded tile from the TIFF image. The image must
* be stored in tiles and not strips.
*
* @param tileIndex The index of the tile to read.
* @param buf The buffer into which to read the image data.
* @param offset The offset into the buffer where the image data will begin
* being placed.
* @param tileSize The size of the tile.
* @return Returns the number of bytes read or -1 if there was an error.
* @see #isTiled()
* @see #getTileSize()
*/
private native synchronized int readTileShort( int tileIndex, short[] buf, long offset,
int tileSize ) throws LCImageLibException;
/**
* The actual end-result image.
*/
private PlanarImage m_image;
/**
* If <code>true</code>, read the second TIFF image (if present).
*/
private final boolean m_read2nd;
////////// main() /////////////////////////////////////////////////////////
public static void main(String[] args) {
try {
// final LCTIFFReader tiff = new LCTIFFReader(args[0]);
// final PlanarImage image = tiff.getImage( null );
final PlanarImage image = new TIFFImage(args[0]);
final JFrame frame = new JFrame("TIFF Image");
final JPanel imagePanel = new JPanel() {
public void paintComponent(Graphics g) {
((Graphics2D) g).drawRenderedImage(image, new AffineTransform());
}
};
imagePanel.setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
final JScrollPane scrollPane = new JScrollPane(imagePanel);
frame.setContentPane(scrollPane);
frame.pack();
frame.setSize(800, 600);
frame.show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/* vim:set et sw=4 ts=4: */