src.gov.nasa.worldwind.formats.tiff.GeotiffWriter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwindx Show documentation
Show all versions of worldwindx Show documentation
World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.formats.tiff;
import gov.nasa.worldwind.Version;
import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.data.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.util.*;
import java.awt.color.*;
import java.awt.image.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.*;
/**
* @author Lado Garakanidze
* @version $Id: GeotiffWriter.java 1171 2013-02-11 21:45:02Z dcollins $
*/
public class GeotiffWriter
{
private RandomAccessFile targetFile;
private FileChannel theChannel;
// We need the size in bytes of various primitives...
private static final int INTEGER_SIZEOF = Integer.SIZE / Byte.SIZE;
private static final int BufferedImage_TYPE_ELEVATION_SHORT16 = 9001;
private static final int BufferedImage_TYPE_ELEVATION_FLOAT32 = 9002;
public GeotiffWriter(String filename) throws IOException
{
if (null == filename || 0 == filename.trim().length())
{
String msg = Logging.getMessage("generic.FileNameIsMissing");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
// the initializer does the validity checking...
commonInitializer(new File(filename));
}
public GeotiffWriter(File file) throws IOException
{
if (null == file)
{
String msg = Logging.getMessage("nullValue.FileIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
commonInitializer(file);
}
//
// Merely consolidates the error checking for the ctors in one place.
//
private void commonInitializer(File file) throws IOException
{
File parent = file.getParentFile();
if (parent == null)
parent = new File(System.getProperty("user.dir"));
if (!parent.canWrite())
{
String msg = Logging.getMessage("generic.FolderNoWritePermission", parent.getAbsolutePath());
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.targetFile = new RandomAccessFile(file, "rw");
this.theChannel = this.targetFile.getChannel();
}
public void close()
{
try
{
this.targetFile.close();
}
catch (Exception ex)
{ /* best effort */ }
}
public void write(BufferedImage image) throws IOException
{
this.write(image, null);
}
public void write(DataRaster raster) throws IOException, IllegalArgumentException
{
if (null == raster)
{
String msg = Logging.getMessage("nullValue.RasterIsNull");
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
if (!(raster.getWidth() > 0))
{
String msg = Logging.getMessage("generic.InvalidWidth", raster.getWidth());
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
if (!(raster.getHeight() > 0))
{
String msg = Logging.getMessage("generic.InvalidHeight", raster.getHeight());
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
if (raster instanceof BufferedImageRaster)
{
this.write(((BufferedImageRaster) raster).getBufferedImage(), raster);
}
else if (raster instanceof BufferWrapperRaster)
{
this.writeRaster((BufferWrapperRaster) raster);
}
}
/*
Writes BufferedImage as a Geo-Tiff file
@param image BufferedImage
@param params AVList with Georeferecing information
AVKey.WIDTH (Integer) optional, if specified must match BufferedImage's width;
if missing, will be populated from BufferedImage
AVKey.HEIGHT (Integer) optional, if specified must match BufferedImage's height;
if missing, will be populated from BufferedImage
AVKey.SECTOR (Sector) required parameter; if missing GeoTiff info will not be written
AVKey.PIXEL_WIDTH (Double) pixel size, UTM images usually specify 1 (1 meter);
if missing and Geographic Coordinate System is specified will be calculated as LongitudeDelta/WIDTH
AVKey.PIXEL_HEIGHT (Double) pixel size, UTM images usually specify 1 (1 meter);
if missing and Geographic Coordinate System is specified will be calculated as LatitudeDelta/HEIGHT
AVKey.ORIGIN (LatLon) specifies coordinate of the image's origin (one of the corners, or center)
If missing, upper left corner will be set as origin
AVKey.DATE_TIME (0 terminated String, length == 20)
if missing, current date & time will be used
AVKey.PIXEL_FORMAT required (valid values: AVKey.ELEVATION | AVKey.IMAGE }
specifies weather it is a digital elevation model or image
AVKey.DATA_TYPE required for elevations only; valid values: AVKey.INT16, and AVKey.FLOAT32
AVKey.VERSION optional, if missing a default will be used "NASA World Wind"
AVKey.DISPLAY_NAME, (String) optional, specifies a name of the document/image
AVKey.DESCRIPTION (String) optional, for any kind of descriptions
AVKey.MISSING_DATA_SIGNAL optional, set the AVKey.MISSING_DATA_SIGNAL ONLY if you know for sure that
the specified value actually represents void (NODATA) areas. Elevation data usually has "-32767" (like DTED),
or "-32768" like SRTM, but some has "0" (mostly images) and "-9999" like NED.
Note! Setting "-9999" is very ambiguos because -9999 for elevation is valid value;
AVKey.MISSING_DATA_REPLACEMENT (String type forced by spec)
Most images have "NODATA" as "0", elevations have as "-9999", or "-32768" (sometimes "-32767")
AVKey.COORDINATE_SYSTEM required,
valid values AVKey.COORDINATE_SYSTEM_GEOGRAPHIC or AVKey.COORDINATE_SYSTEM_PROJECTED
AVKey.COORDINATE_SYSTEM_NAME Optional, A name of the Coordinates System as a String
AVKey.PROJECTION_EPSG_CODE Required; Integer; EPSG code or Projection Code
If CS is Geodetic and EPSG code is not specified, a default WGS84 (4326) will be used
AVKey.PROJECTION_DATUM Optional,
AVKey.PROJECTION_DESC Optional,
AVKey.PROJECTION_NAME Optional,
AVKey.PROJECTION_UNITS Optional,
AVKey.ELEVATION_UNIT Required, if AVKey.PIXEL_FORMAT = AVKey.ELEVATION,
value: AVKey.UNIT_FOOT or AVKey.UNIT_METER (default, if not specified)
AVKey.RASTER_PIXEL, optional, values: AVKey.RASTER_PIXEL_IS_AREA or AVKey.RASTER_PIXEL_IS_POINT
if not specified, default for images is RASTER_PIXEL_IS_AREA,
and AVKey.RASTER_PIXEL_IS_POINT for elevations
*/
public void write(BufferedImage image, AVList params) throws IOException
{
if (image == null)
{
String msg = Logging.getMessage("nullValue.ImageSource");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (0 == image.getWidth() || 0 == image.getHeight())
{
String msg = Logging.getMessage("generic.InvalidImageSize", image.getWidth(), image.getHeight());
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (null == params || 0 == params.getValues().size())
{
String reason = Logging.getMessage("nullValue.AVListIsNull");
Logging.logger().finest(Logging.getMessage("GeotiffWriter.GeoKeysMissing", reason));
params = new AVListImpl();
}
else
{
this.validateParameters(params, image.getWidth(), image.getHeight());
}
// how we proceed in part depends upon the image type...
int type = image.getType();
// handle CUSTOM type which comes from our GeoTiffreader (for now)
if (BufferedImage.TYPE_CUSTOM == type)
{
int numColorComponents = 0, numComponents = 0, pixelSize = 0, dataType = 0, csType = 0;
boolean hasAlpha = false;
if (null != image.getColorModel())
{
ColorModel cm = image.getColorModel();
numColorComponents = cm.getNumColorComponents();
numComponents = cm.getNumComponents();
pixelSize = cm.getPixelSize();
hasAlpha = cm.hasAlpha();
ColorSpace cs = cm.getColorSpace();
if (null != cs)
csType = cs.getType();
}
if (null != image.getSampleModel())
{
SampleModel sm = image.getSampleModel();
dataType = sm.getDataType();
}
if (dataType == DataBuffer.TYPE_FLOAT && pixelSize == Float.SIZE && numComponents == 1)
{
type = BufferedImage_TYPE_ELEVATION_FLOAT32;
}
else if (dataType == DataBuffer.TYPE_SHORT && pixelSize == Short.SIZE && numComponents == 1)
{
type = BufferedImage_TYPE_ELEVATION_SHORT16;
}
else if (ColorSpace.CS_GRAY == csType && pixelSize == Byte.SIZE)
{
type = BufferedImage.TYPE_BYTE_GRAY;
}
else if (dataType == DataBuffer.TYPE_USHORT && ColorSpace.CS_GRAY == csType && pixelSize == Short.SIZE)
{
type = BufferedImage.TYPE_USHORT_GRAY;
}
else if (ColorSpace.TYPE_RGB == csType && pixelSize == 3 * Byte.SIZE && numColorComponents == 3)
{
type = BufferedImage.TYPE_3BYTE_BGR;
}
else if (ColorSpace.TYPE_RGB == csType && hasAlpha && pixelSize == 4 * Byte.SIZE && numComponents == 4)
{
type = BufferedImage.TYPE_4BYTE_ABGR;
}
}
switch (type)
{
case BufferedImage.TYPE_3BYTE_BGR:
case BufferedImage.TYPE_4BYTE_ABGR:
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
case BufferedImage.TYPE_INT_RGB:
case BufferedImage.TYPE_INT_BGR:
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
{
this.writeColorImage(image, params);
}
break;
case BufferedImage.TYPE_USHORT_GRAY:
case BufferedImage.TYPE_BYTE_GRAY:
{
this.writeGrayscaleImage(image, params);
}
break;
case BufferedImage_TYPE_ELEVATION_SHORT16:
case BufferedImage_TYPE_ELEVATION_FLOAT32:
{
String msg = Logging.getMessage("GeotiffWriter.FeatureNotImplementedd", type);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
// break;
case BufferedImage.TYPE_CUSTOM:
default:
{
ColorModel cm = image.getColorModel();
SampleModel sm = image.getSampleModel();
StringBuffer sb = new StringBuffer(Logging.getMessage("GeotiffWriter.UnsupportedType", type));
sb.append("\n");
sb.append("NumBands=").append(sm.getNumBands()).append("\n");
sb.append("NumDataElements=").append(sm.getNumDataElements()).append("\n");
sb.append("NumColorComponents=").append(cm.getNumColorComponents()).append("\n");
sb.append("NumComponents=").append(cm.getNumComponents()).append("\n");
sb.append("PixelSize=").append(cm.getPixelSize()).append("\n");
sb.append("hasAlpha=").append(cm.hasAlpha());
String msg = sb.toString();
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
}
private void writeColorImage(BufferedImage image, AVList params) throws IOException
{
int numBands = image.getRaster().getNumBands();
long offset;
this.writeTiffHeader();
// write the image data...
int numRows = image.getHeight();
int numCols = image.getWidth();
int[] stripCounts = new int[numRows];
int[] stripOffsets = new int[numRows];
ByteBuffer dataBuff = ByteBuffer.allocateDirect(numCols * numBands);
Raster rast = image.getRaster();
for (int i = 0; i < numRows; i++)
{
stripOffsets[i] = (int) this.theChannel.position();
stripCounts[i] = numCols * numBands;
int[] rowData = rast.getPixels(0, i, image.getWidth(), 1, (int[]) null);
dataBuff.clear();
for (int j = 0; j < numCols * numBands; j++)
{
putUnsignedByte(dataBuff, rowData[j]);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
}
// write out values for the tiff tags and build up the IFD. These are supposed to be sorted; for now
// do this manually here.
ArrayList ifds = new ArrayList(10);
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_WIDTH, Tiff.Type.LONG, 1, numCols));
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_LENGTH, Tiff.Type.LONG, 1, numRows));
ifds.add(new TiffIFDEntry(Tiff.Tag.PLANAR_CONFIGURATION, Tiff.Type.SHORT, 1, Tiff.PlanarConfiguration.CHUNKY));
ifds.add(new TiffIFDEntry(Tiff.Tag.SAMPLES_PER_PIXEL, Tiff.Type.SHORT, 1, numBands));
ifds.add(new TiffIFDEntry(Tiff.Tag.COMPRESSION, Tiff.Type.LONG, 1, Tiff.Compression.NONE));
ifds.add(new TiffIFDEntry(Tiff.Tag.PHOTO_INTERPRETATION, Tiff.Type.SHORT, 1, Tiff.Photometric.Color_RGB));
ifds.add(new TiffIFDEntry(Tiff.Tag.ORIENTATION, Tiff.Type.SHORT, 1, Tiff.Orientation.DEFAULT));
offset = this.theChannel.position();
short[] bps = new short[numBands];
for (int i = 0; i < numBands; i++)
{
bps[i] = Tiff.BitsPerSample.MONOCHROME_BYTE;
}
this.theChannel.write(ByteBuffer.wrap(this.getBytes(bps)));
ifds.add(new TiffIFDEntry(Tiff.Tag.BITS_PER_SAMPLE, Tiff.Type.SHORT, numBands, offset));
offset = this.theChannel.position();
dataBuff = ByteBuffer.allocateDirect(stripOffsets.length * INTEGER_SIZEOF);
for (int stripOffset : stripOffsets)
{
dataBuff.putInt(stripOffset);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
ifds.add(new TiffIFDEntry(Tiff.Tag.STRIP_OFFSETS, Tiff.Type.LONG, stripOffsets.length, offset));
ifds.add(new TiffIFDEntry(Tiff.Tag.ROWS_PER_STRIP, Tiff.Type.LONG, 1, 1));
offset = this.theChannel.position();
dataBuff.clear();
// stripOffsets and stripCounts are same length by design; can reuse the ByteBuffer...
for (int stripCount : stripCounts)
{
dataBuff.putInt(stripCount);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
ifds.add(new TiffIFDEntry(Tiff.Tag.STRIP_BYTE_COUNTS, Tiff.Type.LONG, stripCounts.length, offset));
this.appendGeoTiff(ifds, params);
this.writeIFDs(ifds);
}
//
// We only support 8-bit and 16-bit currently (Tiff spec allows for 4 bit/sample).
//
private void writeGrayscaleImage(BufferedImage image, AVList params) throws IOException
{
int type = image.getType();
int bitsPerSample = (BufferedImage.TYPE_USHORT_GRAY == type)
? Tiff.BitsPerSample.MONOCHROME_UINT16 : Tiff.BitsPerSample.MONOCHROME_UINT8;
int numBands = image.getSampleModel().getNumBands();
// well, numBands for GrayScale images must be 1
int bytesPerSample = numBands * bitsPerSample / Byte.SIZE;
this.writeTiffHeader();
// write the image data...
int numRows = image.getHeight();
int numCols = image.getWidth();
int[] stripCounts = new int[numRows];
int[] stripOffsets = new int[numRows];
ByteBuffer dataBuff = ByteBuffer.allocateDirect(numCols * bytesPerSample);
Raster rast = image.getRaster();
for (int i = 0; i < numRows; i++)
{
stripOffsets[i] = (int) this.theChannel.position();
stripCounts[i] = numCols * bytesPerSample;
int[] rowData = rast.getPixels(0, i, image.getWidth(), 1, (int[]) null);
dataBuff.clear();
if (BufferedImage.TYPE_USHORT_GRAY == type)
{
for (int j = 0; j < numCols * numBands; j++)
{
this.putUnsignedShort(dataBuff, rowData[j]);
}
}
else if (BufferedImage.TYPE_BYTE_GRAY == type)
{
for (int j = 0; j < numCols * numBands; j++)
{
this.putUnsignedByte(dataBuff, rowData[j]);
}
}
dataBuff.flip();
this.theChannel.write(dataBuff);
}
// write out values for the tiff tags and build up the IFD. These are supposed to be sorted; for now
// do this manually here.
ArrayList ifds = new ArrayList(10);
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_WIDTH, Tiff.Type.LONG, 1, numCols));
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_LENGTH, Tiff.Type.LONG, 1, numRows));
ifds.add(new TiffIFDEntry(Tiff.Tag.BITS_PER_SAMPLE, Tiff.Type.SHORT, 1, bitsPerSample));
ifds.add(new TiffIFDEntry(Tiff.Tag.COMPRESSION, Tiff.Type.LONG, 1, Tiff.Compression.NONE));
ifds.add(new TiffIFDEntry(Tiff.Tag.PHOTO_INTERPRETATION, Tiff.Type.SHORT, 1,
Tiff.Photometric.Grayscale_BlackIsZero));
ifds.add(new TiffIFDEntry(Tiff.Tag.SAMPLE_FORMAT, Tiff.Type.SHORT, 1, Tiff.SampleFormat.UNSIGNED));
long offset = this.theChannel.position();
dataBuff = ByteBuffer.allocateDirect(stripOffsets.length * INTEGER_SIZEOF);
for (int stripOffset : stripOffsets)
{
dataBuff.putInt(stripOffset);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
ifds.add(new TiffIFDEntry(Tiff.Tag.STRIP_OFFSETS, Tiff.Type.LONG, stripOffsets.length, offset));
ifds.add(new TiffIFDEntry(Tiff.Tag.SAMPLES_PER_PIXEL, Tiff.Type.SHORT, 1, numBands));
ifds.add(new TiffIFDEntry(Tiff.Tag.ROWS_PER_STRIP, Tiff.Type.LONG, 1, 1));
offset = this.theChannel.position();
dataBuff.clear(); // stripOffsets and stripCounts are same length by design; can reuse the ByteBuffer...
for (int stripCount : stripCounts)
{
dataBuff.putInt(stripCount);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
ifds.add(new TiffIFDEntry(Tiff.Tag.STRIP_BYTE_COUNTS, Tiff.Type.LONG, stripCounts.length, offset));
this.appendGeoTiff(ifds, params);
this.writeIFDs(ifds);
}
private void writeTiffHeader() throws IOException
{
// A TIFF file begins with an 8-byte image file header, containing the following information:
//
// Bytes 0-1: The byte order used within the file.
// Legal values are:
// �II� (4949.H) a little-endian byte order
// �MM� (4D4D.H) a big-endian byte order (default for Java and for networking
//
// Bytes 2-3 An arbitrary but carefully chosen number (42)
// that further identifies the file as a TIFF file. The byte order depends on the value of Bytes 0-1.
//
// Bytes 4-7 The offset (in bytes) of the first IFD.
byte[] tiffHeader = new byte[] {0x4D, 0x4D, 0, 42, 0, 0, 0, 0};
// we'll patch up int16 (last 4 bytes) later after writing the image...
this.theChannel.write(ByteBuffer.wrap(tiffHeader));
}
private void appendGeoTiff(ArrayList ifds, AVList params) throws IOException, IllegalArgumentException
{
if (null == params || 0 == params.getEntries().size())
{
String reason = Logging.getMessage("nullValue.AVListIsNull");
Logging.logger().finest(Logging.getMessage("GeotiffWriter.GeoKeysMissing", reason));
return;
}
long offset = this.theChannel.position();
if (params.hasKey(AVKey.DISPLAY_NAME))
{
String value = params.getStringValue(AVKey.DISPLAY_NAME);
if (null != value && 0 < value.trim().length())
{
offset = this.theChannel.position();
byte[] bytes = value.trim().getBytes();
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(Tiff.Tag.DOCUMENT_NAME, Tiff.Type.ASCII, bytes.length, offset));
}
}
if (params.hasKey(AVKey.DESCRIPTION))
{
String value = params.getStringValue(AVKey.DESCRIPTION);
if (null != value && 0 < value.trim().length())
{
offset = this.theChannel.position();
byte[] bytes = value.trim().getBytes();
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_DESCRIPTION, Tiff.Type.ASCII, bytes.length, offset));
}
}
if (params.hasKey(AVKey.VERSION))
{
String value = params.getStringValue(AVKey.VERSION);
if (null != value && 0 < value.trim().length())
{
offset = this.theChannel.position();
byte[] bytes = value.trim().getBytes();
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(Tiff.Tag.SOFTWARE_VERSION, Tiff.Type.ASCII, bytes.length, offset));
}
}
if (params.hasKey(AVKey.DATE_TIME))
{
String value = params.getStringValue(AVKey.DATE_TIME);
if (null != value && 0 < value.trim().length())
{
offset = this.theChannel.position();
byte[] bytes = value.getBytes();
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(Tiff.Tag.DATE_TIME, Tiff.Type.ASCII, bytes.length, offset));
}
}
if (params.hasKey(AVKey.SECTOR))
{
if (params.hasKey(AVKey.PIXEL_WIDTH) && params.hasKey(AVKey.PIXEL_HEIGHT))
{
offset = this.theChannel.position();
double[] values = new double[]
{
(Double) params.getValue(AVKey.PIXEL_WIDTH),
(Double) params.getValue(AVKey.PIXEL_HEIGHT),
isElevation(params) ? 1d : 0d
};
byte[] bytes = this.getBytes(values);
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(GeoTiff.Tag.MODEL_PIXELSCALE, Tiff.Type.DOUBLE, values.length, offset));
}
if (params.hasKey(AVKey.WIDTH) && params.hasKey(AVKey.HEIGHT))
{
offset = this.theChannel.position();
double w = (Integer) params.getValue(AVKey.WIDTH);
double h = (Integer) params.getValue(AVKey.HEIGHT);
Sector sec = (Sector) params.getValue(AVKey.SECTOR);
double[] values = new double[]
{ // i , j, k=0, x, y, z=0
0d, 0d, 0d, sec.getMinLongitude().degrees, sec.getMaxLatitude().degrees, 0d,
w - 1, 0d, 0d, sec.getMaxLongitude().degrees, sec.getMaxLatitude().degrees, 0d,
w - 1, h - 1, 0d, sec.getMaxLongitude().degrees, sec.getMinLatitude().degrees, 0d,
0d, h - 1, 0d, sec.getMinLongitude().degrees, sec.getMinLatitude().degrees, 0d,
};
byte[] bytes = this.getBytes(values);
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(GeoTiff.Tag.MODEL_TIEPOINT, Tiff.Type.DOUBLE, values.length, offset));
}
// Tiff.Tag.MODEL_TRANSFORMATION excludes Tiff.Tag.MODEL_TIEPOINT & Tiff.Tag.MODEL_PIXELSCALE
if (params.hasKey(AVKey.MISSING_DATA_SIGNAL) || params.hasKey(AVKey.MISSING_DATA_REPLACEMENT))
{
offset = this.theChannel.position();
Object nodata = params.hasKey(AVKey.MISSING_DATA_SIGNAL)
? params.getValue(AVKey.MISSING_DATA_SIGNAL)
: params.getValue(AVKey.MISSING_DATA_REPLACEMENT);
String value = "" + nodata + "\0";
byte[] bytes = value.getBytes();
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(GeoTiff.Tag.GDAL_NODATA, Tiff.Type.ASCII, bytes.length, offset));
}
if (params.hasKey(AVKey.COORDINATE_SYSTEM))
{
String cs = params.getStringValue(AVKey.COORDINATE_SYSTEM);
if (AVKey.COORDINATE_SYSTEM_GEOGRAPHIC.equals(cs))
{
if (isElevation(params))
this.writeGeographicElevationGeoKeys(ifds, params);
else
this.writeGeographicImageGeoKeys(ifds, params);
}
else if (AVKey.COORDINATE_SYSTEM_PROJECTED.equals(cs))
{
String msg = Logging.getMessage("GeotiffWriter.FeatureNotImplementedd", cs);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
// TODO extract PCS (Projection Coordinate System)
}
else
{
String msg = Logging.getMessage("GeotiffWriter.UnknownCoordinateSystem", cs);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
}
}
protected void validateParameters(AVList list, int srcWidth, int srcHeight) throws IllegalArgumentException
{
if (null == list || 0 == list.getValues().size())
{
String reason = Logging.getMessage("nullValue.AVListIsNull");
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", reason);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
if (!(srcWidth > 0 && srcHeight > 0))
{
String msg = Logging.getMessage("generic.InvalidImageSize", srcWidth, srcHeight);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
if (list.hasKey(AVKey.WIDTH))
{
int width = (Integer) list.getValue(AVKey.WIDTH);
if (width != srcWidth)
{
String msg = Logging.getMessage("GeotiffWriter.ImageWidthMismatch", width, srcWidth);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
else
list.setValue(AVKey.WIDTH, srcWidth);
if (list.hasKey(AVKey.HEIGHT))
{
int height = (Integer) list.getValue(AVKey.HEIGHT);
if (height != srcHeight)
{
String msg = Logging.getMessage("GeotiffWriter.ImageHeightMismatch", height, srcHeight);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
else
list.setValue(AVKey.HEIGHT, srcHeight);
Sector sector = null;
if (list.hasKey(AVKey.SECTOR))
sector = (Sector) list.getValue(AVKey.SECTOR);
if (null == sector)
{
String msg = Logging.getMessage("GeotiffWriter.NoSectorSpecified");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (!list.hasKey(AVKey.COORDINATE_SYSTEM))
{
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", AVKey.COORDINATE_SYSTEM);
Logging.logger().finest(msg);
// throw new IllegalArgumentException(msg);
// assume Geodetic Coordinate System
list.setValue(AVKey.COORDINATE_SYSTEM, AVKey.COORDINATE_SYSTEM_GEOGRAPHIC);
}
if (!list.hasKey(AVKey.PROJECTION_EPSG_CODE))
{
if (isGeographic(list))
{
// assume WGS84
list.setValue(AVKey.PROJECTION_EPSG_CODE, GeoTiff.GCS.WGS_84);
}
else
{
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", AVKey.PROJECTION_EPSG_CODE);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
}
// if PIXEL_WIDTH is specified, we are not overriding it because UTM images
// will have different pixel size
if (!list.hasKey(AVKey.PIXEL_WIDTH))
{
if (isGeographic(list))
{
double pixelWidth = sector.getDeltaLonDegrees() / (double) srcWidth;
list.setValue(AVKey.PIXEL_WIDTH, pixelWidth);
}
else
{
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", AVKey.PIXEL_WIDTH);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
}
// if PIXEL_HEIGHT is specified, we are not overriding it
// because UTM images will have different pixel size
if (!list.hasKey(AVKey.PIXEL_HEIGHT))
{
if (isGeographic(list))
{
double pixelHeight = sector.getDeltaLatDegrees() / (double) srcHeight;
list.setValue(AVKey.PIXEL_HEIGHT, pixelHeight);
}
else
{
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", AVKey.PIXEL_HEIGHT);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
}
if (!list.hasKey(AVKey.PIXEL_FORMAT))
{
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", AVKey.PIXEL_FORMAT);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
else
{
String pixelFormat = list.getStringValue(AVKey.PIXEL_FORMAT);
if (!AVKey.ELEVATION.equals(pixelFormat) && !AVKey.IMAGE.equals(pixelFormat))
{
String msg = Logging.getMessage("Geotiff.UnknownGeoKeyValue", pixelFormat, AVKey.PIXEL_FORMAT);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
// validate elevation parameters
if (AVKey.ELEVATION.equals(list.getValue(AVKey.PIXEL_FORMAT)))
{
if (!list.hasKey(AVKey.DATA_TYPE))
{
String msg = Logging.getMessage("GeotiffWriter.GeoKeysMissing", AVKey.DATA_TYPE);
Logging.logger().finest(msg);
throw new IllegalArgumentException(msg);
}
String type = list.getStringValue(AVKey.DATA_TYPE);
if (!AVKey.FLOAT32.equals(type) && !AVKey.INT16.equals(type))
{
String msg = Logging.getMessage("Geotiff.UnknownGeoKeyValue", type, AVKey.DATA_TYPE);
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
if (!list.hasKey(AVKey.ORIGIN))
{
// set UpperLeft corner as the origin, if not specified
LatLon origin = new LatLon(sector.getMaxLatitude(), sector.getMinLongitude());
list.setValue(AVKey.ORIGIN, origin);
}
if (list.hasKey(AVKey.BYTE_ORDER)
&& !AVKey.BIG_ENDIAN.equals(list.getStringValue(AVKey.BYTE_ORDER))
)
{
String msg = Logging.getMessage("generic.UnrecognizedByteOrder", list.getStringValue(AVKey.BYTE_ORDER));
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (!list.hasKey(AVKey.DATE_TIME))
{
// add NUL (\0) termination as required by TIFF v6 spec (20 bytes length)
String timestamp = String.format("%1$tY:%1$tm:%1$td %tT\0", Calendar.getInstance());
list.setValue(AVKey.DATE_TIME, timestamp);
}
if (!list.hasKey(AVKey.VERSION))
{
list.setValue(AVKey.VERSION, Version.getVersion());
}
}
private static boolean isElevation(AVList params)
{
return (null != params
&& params.hasKey(AVKey.PIXEL_FORMAT)
&& AVKey.ELEVATION.equals(params.getValue(AVKey.PIXEL_FORMAT))
);
}
private static boolean isImage(AVList params)
{
return (null != params
&& params.hasKey(AVKey.PIXEL_FORMAT)
&& AVKey.IMAGE.equals(params.getValue(AVKey.PIXEL_FORMAT))
);
}
private static boolean isGeographic(AVList params)
{
return (null != params
&& params.hasKey(AVKey.COORDINATE_SYSTEM)
&& AVKey.COORDINATE_SYSTEM_GEOGRAPHIC.equals(params.getValue(AVKey.COORDINATE_SYSTEM))
);
}
private static boolean isProjected(AVList params)
{
return (null != params
&& params.hasKey(AVKey.COORDINATE_SYSTEM)
&& AVKey.COORDINATE_SYSTEM_PROJECTED.equals(params.getValue(AVKey.COORDINATE_SYSTEM))
);
}
private void writeGeographicImageGeoKeys(ArrayList ifds, AVList params) throws IOException
{
long offset = this.theChannel.position();
if (isImage(params) && isGeographic(params))
{
int epsg = GeoTiff.GCS.WGS_84;
if (params.hasKey(AVKey.PROJECTION_EPSG_CODE))
epsg = (Integer) params.getValue(AVKey.PROJECTION_EPSG_CODE);
short[] values = new short[]
{
// GeoKeyDirectory header
GeoTiff.GeoKeyHeader.KeyDirectoryVersion,
GeoTiff.GeoKeyHeader.KeyRevision,
GeoTiff.GeoKeyHeader.MinorRevision,
0, // IMPORTANT!! we will update count below, after the array initialization
// end of header -
// geo keys array
/* key 1 */
GeoTiff.GeoKey.ModelType, 0, 1, GeoTiff.ModelType.Geographic,
/* key 2 */
GeoTiff.GeoKey.RasterType, 0, 1, (short) (0xFFFF & GeoTiff.RasterType.RasterPixelIsArea),
/* key 3 */
GeoTiff.GeoKey.GeographicType, 0, 1, (short) (0xFFFF & epsg),
/* key 4 */
GeoTiff.GeoKey.GeogAngularUnits, 0, 1, GeoTiff.Unit.Angular.Angular_Degree
};
// IMPORTANT!! update count - number of geokeys
values[3] = (short) (values.length / 4);
byte[] bytes = this.getBytes(values);
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(GeoTiff.Tag.GEO_KEY_DIRECTORY, Tiff.Type.SHORT, values.length, offset));
}
}
private void writeGeographicElevationGeoKeys(ArrayList ifds, AVList params) throws IOException
{
long offset = this.theChannel.position();
if (isElevation(params) && isGeographic(params))
{
int epsg = GeoTiff.GCS.WGS_84;
if (params.hasKey(AVKey.PROJECTION_EPSG_CODE))
epsg = (Integer) params.getValue(AVKey.PROJECTION_EPSG_CODE);
int elevUnits = GeoTiff.Unit.Linear.Meter;
if (params.hasKey(AVKey.ELEVATION_UNIT))
{
if (AVKey.UNIT_FOOT.equals(params.getValue(AVKey.ELEVATION_UNIT)))
elevUnits = GeoTiff.Unit.Linear.Foot;
}
int rasterType = GeoTiff.RasterType.RasterPixelIsArea;
if (params.hasKey(AVKey.RASTER_PIXEL)
&& AVKey.RASTER_PIXEL_IS_POINT.equals(params.getValue(AVKey.RASTER_PIXEL)))
rasterType = GeoTiff.RasterType.RasterPixelIsPoint;
short[] values = new short[]
{
// GeoKeyDirectory header
GeoTiff.GeoKeyHeader.KeyDirectoryVersion,
GeoTiff.GeoKeyHeader.KeyRevision,
GeoTiff.GeoKeyHeader.MinorRevision,
0, // IMPORTANT!! we will update count below, after the array initialization
// end of header -
// geo keys array
/* key 1 */
GeoTiff.GeoKey.ModelType, 0, 1, GeoTiff.ModelType.Geographic,
/* key 2 */
// TODO: Replace GeoTiff.RasterType.RasterPixelIsPoint
GeoTiff.GeoKey.RasterType, 0, 1, (short) (0xFFFF & rasterType),
/* key 3 */
GeoTiff.GeoKey.GeographicType, 0, 1, (short) (0xFFFF & epsg),
/* key 4 */
GeoTiff.GeoKey.GeogAngularUnits, 0, 1, GeoTiff.Unit.Angular.Angular_Degree,
/* key 5 */
GeoTiff.GeoKey.VerticalCSType, 0, 1, GeoTiff.VCS.WGS_84_ellipsoid,
/* key 6 */
GeoTiff.GeoKey.VerticalUnits, 0, 1, (short) (0xFFFF & elevUnits),
};
// IMPORTANT!! update count - number of geokeys
values[3] = (short) (values.length / 4);
byte[] bytes = this.getBytes(values);
this.theChannel.write(ByteBuffer.wrap(bytes));
ifds.add(new TiffIFDEntry(GeoTiff.Tag.GEO_KEY_DIRECTORY, Tiff.Type.SHORT, values.length, offset));
}
}
private void writeIFDs(List ifds) throws IOException
{
long offset = this.theChannel.position();
// This is supposed to start on a word boundary, via decree of the spec.
long adjust = offset % 4L;
offset += (adjust == 0) ? 0 : (4L - adjust);
this.theChannel.position(offset);
Collections.sort(ifds);
ByteBuffer dataBuff = ByteBuffer.allocateDirect(ifds.size() * 12);
// The IFD directory is preceeded by a SHORT count of the number of entries...
putUnsignedShort(dataBuff, ifds.size());
dataBuff.flip();
this.theChannel.write(dataBuff);
dataBuff.clear();
for (TiffIFDEntry ifd : ifds)
{
putUnsignedShort(dataBuff, ifd.tag);
putUnsignedShort(dataBuff, ifd.type);
putUnsignedInt(dataBuff, ifd.count);
if (ifd.type == Tiff.Type.SHORT && ifd.count == 1)
{
// these get packed in the first few bytes...
putUnsignedShort(dataBuff, (int) ifd.valOffset);
dataBuff.putShort((short) 0);
}
else
putUnsignedInt(dataBuff, ifd.valOffset);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
// The spec requires 4 bytes of zeros at the end...
dataBuff.clear();
dataBuff.putInt(0);
dataBuff.flip();
this.theChannel.write(dataBuff);
// go back and patch up the ifd offset in header...
this.theChannel.position(4);
dataBuff.clear();
putUnsignedInt(dataBuff, offset);
dataBuff.flip();
this.theChannel.write(dataBuff);
}
private void putUnsignedByte(ByteBuffer buff, int value)
{
buff.put((byte) (value & 0xff));
}
private void putUnsignedShort(ByteBuffer buff, int value)
{
buff.putShort((short) (value & 0xffff));
}
private void putUnsignedInt(ByteBuffer buff, long value)
{
buff.putInt((int) (value & 0xffffffffL));
}
private byte[] getBytes(double[] array)
{
try
{
ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
DataOutputStream datastream = new DataOutputStream(bytestream);
for (double n : array)
{
datastream.writeDouble(n);
}
datastream.flush();
return bytestream.toByteArray();
}
catch (IOException ioe)
{
Logging.logger().finest(ioe.getMessage());
}
return null;
}
private byte[] getBytes(short[] array)
{
try
{
ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
DataOutputStream datastream = new DataOutputStream(bytestream);
for (short n : array)
{
datastream.writeShort(n);
}
datastream.flush();
return bytestream.toByteArray();
}
catch (IOException ioe)
{
Logging.logger().finest(ioe.getMessage());
}
return null;
}
public void writeRaster(BufferWrapperRaster raster) throws IOException, IllegalArgumentException
{
if (raster == null)
{
String msg = Logging.getMessage("nullValue.RasterIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
if (0 == raster.getWidth() || 0 == raster.getHeight())
{
String msg = Logging.getMessage("generic.InvalidImageSize", raster.getWidth(), raster.getHeight());
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.validateParameters(raster, raster.getWidth(), raster.getHeight());
int bitsPerSample, samplesPerPixel, sampleFormat, photometric, numBands;
if (AVKey.ELEVATION.equals(raster.getValue(AVKey.PIXEL_FORMAT)))
{
if (AVKey.FLOAT32.equals(raster.getValue(AVKey.DATA_TYPE)))
{
numBands = 1;
samplesPerPixel = Tiff.SamplesPerPixel.MONOCHROME;
sampleFormat = Tiff.SampleFormat.IEEEFLOAT;
photometric = Tiff.Photometric.Grayscale_BlackIsZero;
bitsPerSample = Tiff.BitsPerSample.ELEVATIONS_FLOAT32;
}
else if (AVKey.INT16.equals(raster.getValue(AVKey.DATA_TYPE)))
{
numBands = 1;
samplesPerPixel = Tiff.SamplesPerPixel.MONOCHROME;
sampleFormat = Tiff.SampleFormat.SIGNED;
photometric = Tiff.Photometric.Grayscale_BlackIsZero;
bitsPerSample = Tiff.BitsPerSample.ELEVATIONS_INT16;
}
else
{
String msg = Logging.getMessage("GeotiffWriter.UnsupportedType", raster.getValue(AVKey.DATA_TYPE));
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
else if (AVKey.IMAGE.equals(raster.getValue(AVKey.PIXEL_FORMAT)))
{
if (AVKey.INT8.equals(raster.getValue(AVKey.DATA_TYPE)))
{
numBands = 1;
samplesPerPixel = Tiff.SamplesPerPixel.MONOCHROME;
sampleFormat = Tiff.SampleFormat.UNSIGNED;
photometric = Tiff.Photometric.Grayscale_BlackIsZero;
bitsPerSample = Tiff.BitsPerSample.MONOCHROME_UINT8;
}
else if (AVKey.INT16.equals(raster.getValue(AVKey.DATA_TYPE)))
{
numBands = 1;
samplesPerPixel = Tiff.SamplesPerPixel.MONOCHROME;
sampleFormat = Tiff.SampleFormat.UNSIGNED;
photometric = Tiff.Photometric.Grayscale_BlackIsZero;
bitsPerSample = Tiff.BitsPerSample.MONOCHROME_UINT16;
}
else if (AVKey.INT32.equals(raster.getValue(AVKey.DATA_TYPE)))
{
numBands = 3;
// TODO check ALPHA / Transparency
samplesPerPixel = Tiff.SamplesPerPixel.RGB;
sampleFormat = Tiff.SampleFormat.UNSIGNED;
photometric = Tiff.Photometric.Color_RGB;
bitsPerSample = Tiff.BitsPerSample.RGB;
}
else
{
String msg = Logging.getMessage("GeotiffWriter.UnsupportedType", raster.getValue(AVKey.DATA_TYPE));
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
}
else
{
String msg = Logging.getMessage("GeotiffWriter.UnsupportedType", raster.getValue(AVKey.PIXEL_FORMAT));
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
int bytesPerSample = numBands * bitsPerSample / Byte.SIZE;
this.writeTiffHeader();
// write the image data...
int numRows = raster.getHeight();
int numCols = raster.getWidth();
int[] stripCounts = new int[numRows];
int[] stripOffsets = new int[numRows];
BufferWrapper srcBuffer = raster.getBuffer();
ByteBuffer dataBuff = ByteBuffer.allocateDirect(numCols * bytesPerSample);
switch (bitsPerSample)
{
// case Tiff.BitsPerSample.MONOCHROME_BYTE:
case Tiff.BitsPerSample.MONOCHROME_UINT8:
{
for (int y = 0; y < numRows; y++)
{
stripOffsets[y] = (int) this.theChannel.position();
stripCounts[y] = numCols * bytesPerSample;
dataBuff.clear();
for (int x = 0; x < numCols * numBands; x++)
{
dataBuff.put(srcBuffer.getByte(x + y * numCols));
}
dataBuff.flip();
this.theChannel.write(dataBuff);
}
}
break;
// case Tiff.BitsPerSample.MONOCHROME_UINT16:
case Tiff.BitsPerSample.ELEVATIONS_INT16:
{
for (int y = 0; y < numRows; y++)
{
stripOffsets[y] = (int) this.theChannel.position();
stripCounts[y] = numCols * bytesPerSample;
dataBuff.clear();
for (int x = 0; x < numCols * numBands; x++)
{
dataBuff.putShort(srcBuffer.getShort(x + y * numCols));
}
dataBuff.flip();
this.theChannel.write(dataBuff);
}
}
break;
case Tiff.BitsPerSample.ELEVATIONS_FLOAT32:
{
for (int y = 0; y < numRows; y++)
{
stripOffsets[y] = (int) this.theChannel.position();
stripCounts[y] = numCols * bytesPerSample;
dataBuff.clear();
for (int x = 0; x < numCols * numBands; x++)
{
dataBuff.putFloat(srcBuffer.getFloat(x + y * numCols));
}
dataBuff.flip();
this.theChannel.write(dataBuff);
}
}
break;
case Tiff.BitsPerSample.RGB:
{
for (int y = 0; y < numRows; y++)
{
stripOffsets[y] = (int) this.theChannel.position();
stripCounts[y] = numCols * bytesPerSample;
dataBuff.clear();
for (int x = 0; x < numCols; x++)
{
int color = srcBuffer.getInt(x + y * numCols);
byte red = (byte) (0xFF & (color >> 16));
byte green = (byte) (0xFF & (color >> 8));
byte blue = (byte) (0xFF & color);
// dataBuff.put(0xFF & (color >> 24)); // alpha
dataBuff.put(red).put(green).put(blue);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
}
}
break;
}
// write out values for the tiff tags and build up the IFD. These are supposed to be sorted; for now
// do this manually here.
ArrayList ifds = new ArrayList(10);
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_WIDTH, Tiff.Type.LONG, 1, numCols));
ifds.add(new TiffIFDEntry(Tiff.Tag.IMAGE_LENGTH, Tiff.Type.LONG, 1, numRows));
long offset = this.theChannel.position();
if (Tiff.BitsPerSample.RGB == bitsPerSample)
{
short[] bps = new short[numBands];
for (int i = 0; i < numBands; i++)
{
bps[i] = Tiff.BitsPerSample.MONOCHROME_BYTE;
}
this.theChannel.write(ByteBuffer.wrap(this.getBytes(bps)));
ifds.add(new TiffIFDEntry(Tiff.Tag.BITS_PER_SAMPLE, Tiff.Type.SHORT, numBands, offset));
}
else
ifds.add(new TiffIFDEntry(Tiff.Tag.BITS_PER_SAMPLE, Tiff.Type.SHORT, 1, bitsPerSample));
ifds.add(new TiffIFDEntry(Tiff.Tag.COMPRESSION, Tiff.Type.LONG, 1, Tiff.Compression.NONE));
ifds.add(new TiffIFDEntry(Tiff.Tag.PHOTO_INTERPRETATION, Tiff.Type.SHORT, 1, photometric));
ifds.add(new TiffIFDEntry(Tiff.Tag.SAMPLES_PER_PIXEL, Tiff.Type.SHORT, 1, samplesPerPixel));
ifds.add(new TiffIFDEntry(Tiff.Tag.ORIENTATION, Tiff.Type.SHORT, 1, Tiff.Orientation.DEFAULT));
ifds.add(new TiffIFDEntry(Tiff.Tag.PLANAR_CONFIGURATION, Tiff.Type.SHORT, 1, Tiff.PlanarConfiguration.CHUNKY));
ifds.add(new TiffIFDEntry(Tiff.Tag.SAMPLE_FORMAT, Tiff.Type.SHORT, 1, sampleFormat));
offset = this.theChannel.position();
dataBuff = ByteBuffer.allocateDirect(stripOffsets.length * INTEGER_SIZEOF);
for (int stripOffset : stripOffsets)
{
dataBuff.putInt(stripOffset);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
ifds.add(new TiffIFDEntry(Tiff.Tag.STRIP_OFFSETS, Tiff.Type.LONG, stripOffsets.length, offset));
ifds.add(new TiffIFDEntry(Tiff.Tag.ROWS_PER_STRIP, Tiff.Type.LONG, 1, 1));
offset = this.theChannel.position();
dataBuff.clear(); // stripOffsets and stripCounts are same length by design; can reuse the ByteBuffer...
for (int stripCount : stripCounts)
{
dataBuff.putInt(stripCount);
}
dataBuff.flip();
this.theChannel.write(dataBuff);
ifds.add(new TiffIFDEntry(Tiff.Tag.STRIP_BYTE_COUNTS, Tiff.Type.LONG, stripCounts.length, offset));
this.appendGeoTiff(ifds, raster);
this.writeIFDs(ifds);
}
}