/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.image.libs;

import java.awt.*;
import java.awt.color.ICC_Profile;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Iterator;
import java.util.Map;

import org.w3c.dom.Document;

import sun.awt.image.ByteInterleavedRaster;
import sun.awt.image.ShortInterleavedRaster;

import com.lightcrafts.image.metadata.*;
import com.lightcrafts.image.metadata.values.ImageMetaValue;
import com.lightcrafts.image.types.TIFFImageType;
import com.lightcrafts.mediax.jai.PlanarImage;
import com.lightcrafts.utils.file.OrderableRandomAccessFile;
import com.lightcrafts.utils.ProgressIndicator;
import com.lightcrafts.utils.thread.ProgressThread;
import com.lightcrafts.image.export.ResolutionOption;
import com.lightcrafts.image.export.ResolutionUnitOption;
import com.lightcrafts.image.types.TIFFConstants;
import com.lightcrafts.utils.Version;
import com.lightcrafts.utils.xml.XMLUtil;

import static com.lightcrafts.image.metadata.EXIFConstants.*;
import static com.lightcrafts.image.metadata.EXIFTags.*;
import static com.lightcrafts.image.metadata.TIFFTags.*;
import static com.lightcrafts.image.types.TIFFConstants.*;

/**
 * An <code>LCTIFFWriter</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 LCTIFFWriter extends LCTIFFCommon {

    ////////// public /////////////////////////////////////////////////////////

    /**
     * Construct an <code>LCTIFFWriter</code> and open a TIFF file.
     *
     * @param fileName The name of the TIFF file to open.
     * @param width The width of the image in pixels.
     * @param height The height of the image in pixels.
     */
    public LCTIFFWriter( String fileName, int width, int height )
        throws LCImageLibException
    {
        this( fileName, null, width, height );
    }

    /**
     * Construct an <code>LCTIFFWriter</code> and open a TIFF file.
     *
     * @param fileName The name of the TIFF file to open.
     * @param width The width of the image in pixels.
     * @param height The height of the image in pixels.
     * @param resolution The resolution (in pixels per unit).
     * @param resolutionUnit The resolution unit; must be either
     * {@link TIFFConstants#TIFF_RESOLUTION_UNIT_CM} or
     * {@link TIFFConstants#TIFF_RESOLUTION_UNIT_INCH}.
     */
    public LCTIFFWriter( String fileName, int width, int height,
                         int resolution, int resolutionUnit )
        throws LCImageLibException
    {
        this( fileName, null, width, height, resolution, resolutionUnit );
    }

    /**
     * Construct an <code>LCTIFFWriter</code> and open a TIFF file.
     *
     * @param fileName The name of the TIFF file to open.
     * @param appendFileName The name of the TIFF file to append as the second
     * page in a 2-page TIFF file.
     * @param width The width of the image in pixels.
     * @param height The height of the image in pixels.
     */
    public LCTIFFWriter( String fileName, String appendFileName, int width,
                         int height )
        throws LCImageLibException
    {
        this(
            fileName, appendFileName, width, height,
            ResolutionOption.DEFAULT_VALUE, ResolutionUnitOption.DEFAULT_VALUE
        );
    }

    /**
     * Construct an <code>LCTIFFWriter</code> and open a TIFF file.
     *
     * @param fileName The name of the TIFF file to open.
     * @param appendFileName The name of the TIFF file to append as the second
     * page in a 2-page TIFF file.
     * @param width The width of the image in pixels.
     * @param height The height of the image in pixels.
     * @param resolution The resolution (in pixels per unit).
     * @param resolutionUnit The resolution unit; must be either
     * {@link TIFFConstants#TIFF_RESOLUTION_UNIT_CM} or
     * {@link TIFFConstants#TIFF_RESOLUTION_UNIT_INCH}.
     */
    public LCTIFFWriter( String fileName, String appendFileName, int width,
                         int height, int resolution, int resolutionUnit )
        throws LCImageLibException
    {
        m_fileName = fileName;
        m_appendFileName = appendFileName;
        m_exportWidth = width;
        m_exportHeight = height;
        m_resolution = resolution;
        m_resolutionUnit = resolutionUnit;
        openForWriting( fileName );
        //
        // If openForWriting() fails, it will store 0 in the native pointer.
        //
        if ( m_nativePtr == 0 )
            throw new LCImageLibException( "Could not open " + fileName );
    }

    /**
     * Puts a TIFF image as tiles.
     *
     * @param image The image to put.
     * @param thread The thread that's doing the putting.
     */
    public void putImageTiled( RenderedImage image, ProgressThread thread )
        throws IOException, LCImageLibException
    {
        try {
            writeImageTiled( image, thread );
            if ( m_appendFileName != null )
                append( m_appendFileName );
            dispose();
            if ( m_hasExifMetadata )
                fixEXIFMetadata( m_fileName );
        }
        finally {
            dispose();
        }
    }

    /**
     * Puts a TIFF image as strips.
     *
     * @param image The image to put.
     * @param thread The thread that's doing the putting.
     */
    public void putImageStriped( RenderedImage image, ProgressThread thread )
        throws IOException, LCImageLibException
    {
        try {
            writeImageStriped( image, thread );
            if ( m_appendFileName != null )
                append( m_appendFileName );
            dispose();
            if ( m_hasExifMetadata )
                fixEXIFMetadata( m_fileName );
        }
        finally {
            dispose();
        }
    }

    /**
     * Puts the given {@link ImageMetadata} into the TIFF file.
     * This <i>must</i> be called only once and prior
     * to {@link #putImageStriped(RenderedImage,ProgressThread)}.
     *
     * @param metadata The {@link ImageMetadata} to put.
     */
    public void putMetadata( ImageMetadata metadata )
        throws IOException, LCImageLibException
    {
        metadata = metadata.prepForExport(
            TIFFImageType.INSTANCE, m_exportWidth, m_exportHeight,
            m_resolution, m_resolutionUnit, false
        );

        ////////// Put TIFF metadata //////////////////////////////////////////

        final ImageMetadataDirectory tiffDir =
            metadata.getDirectoryFor( TIFFDirectory.class );
        if ( tiffDir != null ) {
            for ( Iterator<Map.Entry<Integer,ImageMetaValue>>
                  i = tiffDir.iterator(); i.hasNext(); ) {
                final Map.Entry<Integer,ImageMetaValue> me = i.next();
                final int tagID = me.getKey();
                final ImageMetaValue value = me.getValue();
                switch ( tagID ) {
                    case TIFF_ARTIST:
                    case TIFF_COPYRIGHT:
                    case TIFF_DATE_TIME:
                    case TIFF_DOCUMENT_NAME:
                    case TIFF_HOST_COMPUTER:
                    case TIFF_IMAGE_DESCRIPTION:
                    case TIFF_INK_NAMES:
                    case TIFF_MAKE:
                    case TIFF_MODEL:
                    case TIFF_PAGE_NAME:
                    case TIFF_SOFTWARE:
                    case TIFF_TARGET_PRINTER:
                        setStringField( tagID, value.getStringValue() );
                        break;
                    case TIFF_MS_RATING:
                    case TIFF_RESOLUTION_UNIT:
                        setIntField( tagID, value.getIntValue() );
                        break;
                    case TIFF_X_RESOLUTION:
                    case TIFF_Y_RESOLUTION:
                        setFloatField( tagID, value.getFloatValue() );
                        break;
                }
            }
        }

        ////////// Put EXIF metadata //////////////////////////////////////////

        final ImageMetadataDirectory exifDir =
            metadata.getDirectoryFor( EXIFDirectory.class );
        if ( exifDir != null ) {
            final ByteBuffer exifBuf = EXIFEncoder.encode( metadata, false );
            //ByteBufferUtil.dumpToFile( exifBuf, "/tmp/tiff.exif");
            //
            // Libtiff doesn't support writing EXIF metadata so we have to do
            // an annoying work-around.  We temporarily store the encoded EXIF
            // metadata as belonging to the PHOTOSHOP tag.  Later, after the
            // TIFF file has been completely written, we go back and patch the
            // file in-place by changing the tag ID to EXIF_IFD_POINTER and
            // adjusting the EXIF metadata offsets.
            //
            // The reason the PHOTOSHOP tag is used is because: (1) we don't
            // use it for anything else, (2) its field type is unsigned byte
            // so we can set its value to the encoded binary EXIF metadata, and
            // (3) its tag ID (0x8649) is fairly close to that of the real tag
            // ID of EXIF_IFD_POINTER (0x8769).  Point #3 is important because
            // the tag IDs in a TIFF file must be in ascending sorted order so
            // even after the tag ID is changed, the set of tags is still in
            // ascending sorted order.
            //
            setByteField( TIFF_PHOTOSHOP_IMAGE_RESOURCES, exifBuf.array() );
            m_hasExifMetadata = true;
        }

        ////////// Put IPTC metadata //////////////////////////////////////////

        final ImageMetadataDirectory iptcDir =
            metadata.getDirectoryFor( IPTCDirectory.class );
        if ( iptcDir != null ) {
            //
            // Write both the binary and XMP forms of IPTC metadata: the binary
            // form to enable non-XMP-aware applications to read it and the
            // XMP form to write all the metadata, i.e., the additional IPTC
            // tags present in XMP.
            //
            final byte[] iptcBuf = ((IPTCDirectory)iptcDir).encode( false );
            if ( iptcBuf != null )
                setByteField( TIFF_RICH_TIFF_IPTC, iptcBuf );

            final Document xmpDoc = metadata.toXMP( false, true, IPTCDirectory.class );
            final byte[] xmpBuf = XMLUtil.encodeDocument( xmpDoc, false );
            setByteField( TIFF_XMP_PACKET, xmpBuf );
        }
    }

    /**
     * Sets the value of the given TIFF byte field.
     *
     * @param tagID The tag ID of the metadata field to set.  The ID should be
     * that of a tag whose value is a byte array
     * ({@link TIFFConstants#TIFF_FIELD_TYPE_UBYTE}.
     * @param value The value for the given tag.
     * @return Returns <code>true</code> only if the value was set.
     * @throws IllegalArgumentException if <code>tagID</code> isn't that of an
     * byte metadata field or is otherwise unsupported.
     * @see #setFloatField(int,float)
     * @see #setIntField(int,int)
     * @see #setStringField(int,String)
     */
    public native boolean setByteField( int tagID, byte[] value )
        throws LCImageLibException;

    /**
     * Sets the value of the given TIFF integer metadata field.
     *
     * @param tagID The tag ID of the metadata field to set.  The ID should be
     * that of a tag whose value is an integer
     * ({@link TIFFConstants#TIFF_FIELD_TYPE_USHORT} or
     * {@link TIFFConstants#TIFF_FIELD_TYPE_ULONG} and not a string.
     * @param value The value for the given tag.
     * @return Returns <code>true</code> only if the value was set.
     * @throws IllegalArgumentException if <code>tagID</code> isn't that of an
     * integer metadata field or is otherwise unsupported.
     * @see #setByteField(int,byte[])
     * @see #setIntField(int,int)
     * @see #setStringField(int,String)
     */
    public native boolean setFloatField( int tagID, float value )
        throws LCImageLibException;

    /**
     * Sets the ICC profile of the TIFF image.  This <i>must</i> be called only
     * once and prior to {@link #putImageStriped(RenderedImage,ProgressThread)}
     * or {@link #putImageTiled(RenderedImage,ProgressThread)}.
     *
     * @param iccProfile The {@link ICC_Profile} to set.
     */
    public void setICCProfile( ICC_Profile iccProfile )
        throws LCImageLibException
    {
        setByteField( TIFF_ICC_PROFILE, iccProfile.getData() );
    }

    /**
     * Sets the value of the given TIFF integer metadata field.
     *
     * @param tagID The tag ID of the metadata field to set.  The ID should be
     * that of a tag whose value is an integer
     * ({@link TIFFConstants#TIFF_FIELD_TYPE_USHORT} or
     * {@link TIFFConstants#TIFF_FIELD_TYPE_ULONG} and not a string.
     * @param value The value for the given tag.
     * @return Returns <code>true</code> only if the value was set.
     * @throws IllegalArgumentException if <code>tagID</code> isn't that of an
     * integer metadata field or is otherwise unsupported.
     * @see #setByteField(int,byte[])
     * @see #setFloatField(int,float)
     * @see #setStringField(int,String)
     */
    public native boolean setIntField( int tagID, int value )
        throws LCImageLibException;

    /**
     * Sets 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 TIFFConstants#TIFF_FIELD_TYPE_ASCII}.
     * @param value The value for the given tag.
     * @return Returns <code>true</code> only if the value was set.
     * @throws IllegalArgumentException if <code>tagID</code> isn't that of an
     * string metadata field or is otherwise unsupported.
     * @see #setByteField(int,byte[])
     * @see #setFloatField(int,float)
     * @see #setIntField(int,int)
     */
    public native boolean setStringField( int tagID, String value )
        throws LCImageLibException;

    ////////// private ////////////////////////////////////////////////////////

    /**
     * Append the TIFF image in the given file creating a multi-page TIFF file.
     *
     * @param fileName The name of the TIFF file to append.
     * @return Returns <code>true</code> only if the append succeeded.
     */
    private native boolean append( String fileName );

    /**
     * Computes which tile a given point is in.
     *
     * @param x The X coordinate.
     * @param y The Y coordinate.
     * @param z The Z coordinate.
     * @param sample TODO
     * @return Returns the tile index.
     */
    private native int computeTile( int x, int y, int z, int sample );

    /**
     * Fix the EXIF metadata in a TIFF file.
     *
     * @param fileName The full path of the TIFF file.
     */
    private static void fixEXIFMetadata( String fileName ) throws IOException {
        //
        // This code is based on the code in TIFFMetadataReader but it's been
        // simplified and does much less error-checking because we just wrote
        // the TIFF file ourselves so we know it's valid.
        //
        final OrderableRandomAccessFile file =
            new OrderableRandomAccessFile( fileName, "rw" );
        try {
            if ( file.readShort() == TIFF_LITTLE_ENDIAN )
                file.order( ByteOrder.LITTLE_ENDIAN );
            file.seek( TIFF_HEADER_SIZE - TIFF_INT_SIZE );
            int ifdOffset = file.readInt();
            while ( ifdOffset > 0 ) {
                file.seek( ifdOffset );
                final int entryCount = file.readUnsignedShort();
                for ( int entry = 0; entry < entryCount; ++entry ) {
                    final int entryOffset =
                        TIFFMetadataReader.calcIFDEntryOffset( ifdOffset, entry );
                    file.seek( entryOffset );
                    final int tagID = file.readUnsignedShort();
                    if ( tagID == TIFF_PHOTOSHOP_IMAGE_RESOURCES ) {
                        file.seek( file.getFilePointer() - TIFF_SHORT_SIZE );
                        file.writeShort( TIFF_EXIF_IFD_POINTER );
                        file.writeShort( TIFF_FIELD_TYPE_ULONG );
                        file.writeInt( 1 );
                        final int subdirOffset = file.readInt();
                        fixEXIFDirectory( file, subdirOffset, 0 );
                        return;
                    }
                }
                ifdOffset = file.readInt();
            }
        }
        finally {
            file.close();
        }
    }

    /**
     * Fix an EXIF directory in a TIFF file.  Specifically, this means to
     * adjust all value offsets so that they are relative to the beginning of
     * the TIFF file (as is required by the TIFF specification) rather than
     * relative to the start of the EXIF directory (as is the case when in a
     * JPEG file).
     *
     * @param file The TIFF file containing an EXIF directory to fix.
     * @param dirOffset The offset to the start of the EXIF directory.
     * @param parentDirSize The size of the parent EXIF directory, if any.
     */
    private static void fixEXIFDirectory( OrderableRandomAccessFile file,
                                          long dirOffset, int parentDirSize )
        throws IOException
    {
        file.seek( dirOffset );
        final int entryCount = file.readUnsignedShort();
        for ( int entry = 0; entry < entryCount; ++entry ) {
            final int entryOffset =
                TIFFMetadataReader.calcIFDEntryOffset( (int)dirOffset, entry );
            file.seek( entryOffset );
            final int tagID = file.readUnsignedShort();
            final int fieldType = file.readUnsignedShort();
            final int numValues = file.readInt();
            final int byteCount = numValues * EXIF_FIELD_SIZE[ fieldType ];
            if ( byteCount > TIFF_INLINE_VALUE_MAX_SIZE ||
                 tagID == EXIF_IFD_POINTER ) {
                int valueOffset = file.readInt();
                valueOffset += (int)dirOffset + parentDirSize;
                file.seek( file.getFilePointer() - TIFF_INT_SIZE );
                file.writeInt( valueOffset );
                switch ( tagID ) {
                    case EXIF_IFD_POINTER:
                        final int exifIFDSize =
                            EXIF_SHORT_SIZE
                            + entryCount * EXIF_IFD_ENTRY_SIZE
                            + EXIF_INT_SIZE;
                        fixEXIFDirectory( file, valueOffset, -exifIFDSize );
                        break;
                }
            }
        }
    }

    /**
     * Opens a TIFF file.
     *
     * @param fileName The name of the TIFF file to open.
     */
    private native void openForWriting( String fileName )
        throws LCImageLibException;

    /**
     * Writes a TIFF image as strips.
     *
     * @param image The image to put.
     * @param thread The thread that's doing the writing.
     */
    private void writeImageStriped( RenderedImage image,
                                    ProgressThread thread )
        throws LCImageLibException
    {
        final int dataType = image.getSampleModel().getDataType();
        final int bands = image.getSampleModel().getNumBands();
        final int imageWidth = image.getWidth();
        final int imageHeight = image.getHeight();
        final int stripHeight = 32;

        setIntField(
            TIFF_BITS_PER_SAMPLE, dataType == DataBuffer.TYPE_BYTE ? 8 : 16
        );
        setIntField( TIFF_IMAGE_WIDTH, imageWidth );
        setIntField( TIFF_IMAGE_LENGTH, imageHeight );
        setIntField(
            TIFF_PHOTOMETRIC_INTERPRETATION,
            bands == 4 ? TIFF_PHOTOMETRIC_SEPARATED :
            bands == 3 ? TIFF_PHOTOMETRIC_RGB :
                         TIFF_PHOTOMETRIC_BLACK_IS_ZERO
        );
        setIntField(
            TIFF_PLANAR_CONFIGURATION, TIFF_PLANAR_CONFIGURATION_CHUNKY
        );
        setIntField( TIFF_ROWS_PER_STRIP, stripHeight );
        setIntField( TIFF_SAMPLES_PER_PIXEL, bands );

        final ProgressIndicator indicator;
        if ( thread != null ) {
            indicator = thread.getProgressIndicator();
            if ( indicator != null )
                indicator.setMaximum( imageHeight );
        } else
            indicator = null;

        // Allocate the output buffer only once
        final SampleModel sm =
            image.getSampleModel().createCompatibleSampleModel(
                imageWidth, stripHeight
            );
        final WritableRaster outBuffer;
        if (dataType == DataBuffer.TYPE_BYTE)
            outBuffer = new ByteInterleavedRaster(sm, new Point(0, 0));
        else
            outBuffer = new ShortInterleavedRaster(sm, new Point(0, 0));

        int stripIndex = 0;
        for ( int y = 0; y < imageHeight; y += stripHeight ) {
            if ( thread != null && thread.isCanceled() )
                return;

            final int currentStripHeight = Math.min( stripHeight, imageHeight - y );

            // Create a child raster of the out buffer for the current strip
            final WritableRaster raster = outBuffer.createWritableChild(0, 0, imageWidth, currentStripHeight, 0, y, null);

            // Prefetch tiles, uses all CPUs
            if (image instanceof PlanarImage)
                ((PlanarImage) image).getTiles(((PlanarImage) image).getTileIndices(raster.getBounds()));

            image.copyData(raster);

            int offset;
            if (raster instanceof ByteInterleavedRaster) {
                final ByteInterleavedRaster interleaved =
                    (ByteInterleavedRaster) raster;

                final int[] offsets = interleaved.getDataOffsets();
                offset = offsets[0];
                for (int i = 1; i < offsets.length; i++)
                    offset = Math.min(offset, offsets[i]);

                final DataBufferByte db = (DataBufferByte)raster.getDataBuffer();

                final int written = writeStripByte( stripIndex, db.getData(), offset, bands * imageWidth * currentStripHeight );

                if ( written != bands * imageWidth * currentStripHeight )
                    throw new LCImageLibException(
                        "something is wrong: " + written + " != " +
                        (bands * imageWidth * currentStripHeight)
                    );
            } else {
                final ShortInterleavedRaster interleaved =
                    (ShortInterleavedRaster) raster;

                final int[] offsets = interleaved.getDataOffsets();
                offset = offsets[0];
                for (int i = 1; i < offsets.length; i++)
                    offset = Math.min(offset, offsets[i]);

                final DataBufferUShort db = (DataBufferUShort) raster.getDataBuffer();

                final int written = writeStripShort( stripIndex, db.getData(), offset, 2 * bands * imageWidth * currentStripHeight );

                if ( written != 2 * bands * imageWidth * currentStripHeight )
                    throw new LCImageLibException(
                        "something is wrong: " + written + " != " + (2 * bands * imageWidth * currentStripHeight)
                    );
            }
            stripIndex++;
            if ( indicator != null )
                indicator.incrementBy( currentStripHeight );
        }

        if ( indicator != null )
            indicator.setIndeterminate( true );
    }

    /**
     * Writes a TIFF image as tiles.
     *
     * @param image The image to put.
     * @param thread
     */
    private void writeImageTiled( RenderedImage image,
                                  ProgressThread thread )
        throws LCImageLibException
    {
        final int dataType = image.getSampleModel().getDataType();

        setIntField(TIFF_IMAGE_WIDTH, image.getWidth());
        setIntField(TIFF_IMAGE_LENGTH, image.getHeight());
        setIntField(TIFF_BITS_PER_SAMPLE, dataType == DataBuffer.TYPE_BYTE ? 8 : 16);
        setIntField(TIFF_SAMPLES_PER_PIXEL, image.getSampleModel().getNumBands());

        setIntField(TIFF_PLANAR_CONFIGURATION, TIFF_PLANAR_CONFIGURATION_CHUNKY);
        setIntField(TIFF_PHOTOMETRIC_INTERPRETATION, TIFF_PHOTOMETRIC_RGB );

        setIntField(TIFF_TILE_WIDTH, image.getTileWidth());
        setIntField(TIFF_TILE_LENGTH, image.getTileHeight());

        final ProgressIndicator indicator;
        if (thread != null) {
            indicator = thread.getProgressIndicator();
            if ( indicator != null )
                indicator.setMaximum( image.getNumXTiles() * image.getNumYTiles() );
        } else
            indicator = null;

        for ( int tileX = 0; tileX < image.getNumXTiles(); tileX++ )
            for ( int tileY = 0; tileY < image.getNumYTiles(); tileY++ ) {
                if ( thread != null && thread.isCanceled() )
                    return;
                final int tileIndex = computeTile(tileX * image.getTileWidth(), tileY * image.getTileHeight(), 0, 0);
                final Raster tile = image.getTile(tileX, tileY);

                if (dataType == DataBuffer.TYPE_BYTE) {
                    final byte[] buffer = ((DataBufferByte) tile.getDataBuffer()).getData();

                    final int bytesWritten =  writeTileByte(
                        tileIndex, buffer, 0, buffer.length
                    );
                    if ( bytesWritten != buffer.length )
                        throw new LCImageLibException(
                            "something is wrong: " + bytesWritten + " != " + buffer.length
                        );
                } else {
                    final short[] buffer = ((DataBufferUShort) tile.getDataBuffer()).getData();

                    final int bytesWritten = writeTileShort(
                        tileIndex, buffer, 0, buffer.length * 2
                    );
                    if ( bytesWritten != buffer.length * 2 )
                        throw new LCImageLibException(
                            "something is wrong: " + bytesWritten + " != " + buffer.length * 2
                        );
                }
                if ( indicator != null )
                    indicator.incrementBy( 1 );
            }
        if ( indicator != null )
            indicator.setIndeterminate( true );
    }

    /**
     * Encodes and writes a strip to the TIFF image.
     *
     * @param stripIndex The index of the strip to write.
     * @param buf The buffer into which to write 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 written or -1 if there was an error.
     */
    private native int writeStripByte( int stripIndex, byte[] buf, long offset,
                                       int stripSize )
        throws LCImageLibException;

    /**
     * Encodes and writes a strip to the TIFF image.
     *
     * @param stripIndex The index of the strip to write.
     * @param buf The buffer into which to write 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 written or -1 if there was an error.
     */
    private native int writeStripShort( int stripIndex, short[] buf, long offset,
                                        int stripSize )
        throws LCImageLibException;

    /**
     * Encodes and writes a tile to the TIFF image.
     *
     * @param tileIndex The index of the tile to write.
     * @param buf The buffer into which to write 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 written or -1 if there was an error.
     */
    private native int writeTileByte( int tileIndex, byte[] buf, long offset,
                                      int tileSize )
        throws LCImageLibException;

    /**
     * Encodes and writes a tile to the TIFF image.
     *
     * @param tileIndex The index of the tile to write.
     * @param buf The buffer into which to write 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 written or -1 if there was an error.
     */
    private native int writeTileShort( int tileIndex, short[] buf, long offset,
                                       int tileSize )
        throws LCImageLibException;

    /**
     * The name of the TIFF file to append, if any.
     */
    private final String m_appendFileName;

    /**
     * The height of the image as exported.
     */
    private final int m_exportHeight;

    /**
     * The width of the image as exported.
     */
    private final int m_exportWidth;

    /**
     * The name of the TIFF file.
     */
    private final String m_fileName;

    /**
     * Flag used to remember whether the image has EXIF metadata.
     */
    private boolean m_hasExifMetadata;

    /**
     * The resolution (in pixels per unit) of the image as exported.
     */
    private final int m_resolution;

    /**
     * The resolution unit of the image as exported.
     */
    private final int m_resolutionUnit;

    ////////// main() /////////////////////////////////////////////////////////

    public static void main(String[] args) throws Exception {
        try {
            final LCTIFFReader tiff = new LCTIFFReader( args[0] );
            final PlanarImage image = tiff.getImage( null );

            final LCTIFFWriter writer = new LCTIFFWriter(
                "/Users/pjl/Desktop/out.tiff", args[1],
                image.getWidth(), image.getHeight()
            );
            writer.setStringField( TIFF_SOFTWARE, Version.getApplicationName() );
            writer.putImageStriped( image, null );
        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }
}
/* vim:set et sw=4 ts=4: */
