//
// TiffSaver.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats.tiff;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
import loci.common.ByteArrayHandle;
import loci.common.RandomAccessInputStream;
import loci.common.RandomAccessOutputStream;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.codec.CodecOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parses TIFF data from an input source.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/tiff/TiffSaver.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/tiff/TiffSaver.java;hb=HEAD">Gitweb</a></dd></dl>
*
* @author Curtis Rueden ctrueden at wisc.edu
* @author Eric Kjellman egkjellman at wisc.edu
* @author Melissa Linkert melissa at glencoesoftware.com
* @author Chris Allan callan at blackcat.ca
*/
public class TiffSaver {
// -- Constructor --
private static final Logger LOGGER = LoggerFactory.getLogger(TiffSaver.class);
// -- Fields --
/** Output stream to use when saving TIFF data. */
protected RandomAccessOutputStream out;
/** Output filename. */
protected String filename;
/** Output bytes. */
protected ByteArrayHandle bytes;
/** Whether or not to write BigTIFF data. */
private boolean bigTiff = false;
private boolean sequentialWrite = false;
/** The codec options if set. */
private CodecOptions options;
// -- Constructors --
/**
* Constructs a new TIFF saver from the given filename.
* @param filename Filename of the output stream that we may use to create
* extra input or output streams as required.
*/
public TiffSaver(String filename) throws IOException {
this(new RandomAccessOutputStream(filename), filename);
}
/**
* Constructs a new TIFF saver from the given output source.
* @param out Output stream to save TIFF data to.
* @param filename Filename of the output stream that we may use to create
* extra input or output streams as required.
*/
public TiffSaver(RandomAccessOutputStream out, String filename) {
if (out == null) {
throw new IllegalArgumentException(
"Output stream expected to be not-null");
}
if (filename == null) {
throw new IllegalArgumentException(
"Filename expected to be not null");
}
this.out = out;
this.filename = filename;
}
/**
* Constructs a new TIFF saver from the given output source.
* @param out Output stream to save TIFF data to.
* @param bytes In memory byte array handle that we may use to create
* extra input or output streams as required.
*/
public TiffSaver(RandomAccessOutputStream out, ByteArrayHandle bytes) {
if (out == null) {
throw new IllegalArgumentException(
"Output stream expected to be not-null");
}
if (bytes == null) {
throw new IllegalArgumentException(
"Bytes expected to be not null");
}
this.out = out;
this.bytes = bytes;
}
// -- TiffSaver methods --
/**
* Sets whether or not we know that the planes will be written sequentially.
* If we are writing planes sequentially and set this flag, then performance
* is slightly improved.
*/
public void setWritingSequentially(boolean sequential) {
sequentialWrite = sequential;
}
/** Gets the stream from which TIFF data is being saved. */
public RandomAccessOutputStream getStream() {
return out;
}
/** Sets whether or not little-endian data should be written. */
public void setLittleEndian(boolean littleEndian) {
out.order(littleEndian);
}
/** Sets whether or not BigTIFF data should be written. */
public void setBigTiff(boolean bigTiff) {
this.bigTiff = bigTiff;
}
/** Returns whether or not we are writing little-endian data. */
public boolean isLittleEndian() {
return out.isLittleEndian();
}
/** Returns whether or not we are writing BigTIFF data. */
public boolean isBigTiff() { return bigTiff; }
/**
* Sets the codec options.
* @param options The value to set.
*/
public void setCodecOptions(CodecOptions options) {
this.options = options;
}
/** Writes the TIFF file header. */
public void writeHeader() throws IOException {
// write endianness indicator
out.seek(0);
if (isLittleEndian()) {
out.writeByte(TiffConstants.LITTLE);
out.writeByte(TiffConstants.LITTLE);
}
else {
out.writeByte(TiffConstants.BIG);
out.writeByte(TiffConstants.BIG);
}
// write magic number
if (bigTiff) {
out.writeShort(TiffConstants.BIG_TIFF_MAGIC_NUMBER);
}
else out.writeShort(TiffConstants.MAGIC_NUMBER);
// write the offset to the first IFD
// for vanilla TIFFs, 8 is the offset to the first IFD
// for BigTIFFs, 8 is the number of bytes in an offset
out.writeInt(8);
if (bigTiff) {
// write the offset to the first IFD for BigTIFF files
out.writeLong(16);
}
}
/**
*/
public void writeImage(byte[][] buf, IFDList ifds, int pixelType)
throws FormatException, IOException
{
if (ifds == null) {
throw new FormatException("IFD cannot be null");
}
if (buf == null) {
throw new FormatException("Image data cannot be null");
}
for (int i=0; i<ifds.size(); i++) {
if (i < buf.length) {
writeImage(buf[i], ifds.get(i), i, pixelType, i == ifds.size() - 1);
}
}
}
/**
*/
public void writeImage(byte[] buf, IFD ifd, int no, int pixelType,
boolean last)
throws FormatException, IOException
{
if (ifd == null) {
throw new FormatException("IFD cannot be null");
}
int w = (int) ifd.getImageWidth();
int h = (int) ifd.getImageLength();
writeImage(buf, ifd, no, pixelType, 0, 0, w, h, last);
}
/**
* Writes to any rectangle from the passed block.
*
* @param buf The block that is to be written.
* @param ifd The Image File Directories. Mustn't be <code>null</code>.
* @param no The image index within the current file, starting from 0.
* @param pixelType The type of pixels.
* @param x The X-coordinate of the top-left corner.
* @param y The Y-coordinate of the top-left corner.
* @param w The width of the rectangle.
* @param h The height of the rectangle.
* @param last Pass <code>true</code> if it is the last image,
* <code>false</code> otherwise.
* @throws FormatException
* @throws IOException
*/
public void writeImage(byte[] buf, IFD ifd, int no, int pixelType, int x,
int y, int w, int h, boolean last)
throws FormatException, IOException
{
writeImage(buf, ifd, no, pixelType, x, y, w, h, last, null, false);
}
public void writeImage(byte[] buf, IFD ifd, int no, int pixelType, int x,
int y, int w, int h, boolean last, Integer nChannels,
boolean copyDirectly)
throws FormatException, IOException
{
LOGGER.debug("Attempting to write image.");
//b/c method is public should check parameters again
if (buf == null) {
throw new FormatException("Image data cannot be null");
}
if (ifd == null) {
throw new FormatException("IFD cannot be null");
}
// These operations are synchronized
TiffCompression compression;
int tileWidth, tileHeight, nStrips;
ByteArrayOutputStream[] stripBuf;
synchronized (this) {
int bytesPerPixel = FormatTools.getBytesPerPixel(pixelType);
int blockSize = w * h * bytesPerPixel;
if (nChannels == null) {
nChannels = buf.length / (w * h * bytesPerPixel);
}
boolean interleaved = ifd.getPlanarConfiguration() == 1;
makeValidIFD(ifd, pixelType, nChannels);
// create pixel output buffers
compression = ifd.getCompression();
tileWidth = (int) ifd.getTileWidth();
tileHeight = (int) ifd.getTileLength();
int tilesPerRow = (int) ifd.getTilesPerRow();
int rowsPerStrip = (int) ifd.getRowsPerStrip()[0];
int stripSize = rowsPerStrip * tileWidth * bytesPerPixel;
nStrips =
((w + tileWidth - 1) / tileWidth) * ((h + tileHeight - 1) / tileHeight);
if (interleaved) stripSize *= nChannels;
else nStrips *= nChannels;
stripBuf = new ByteArrayOutputStream[nStrips];
DataOutputStream[] stripOut = new DataOutputStream[nStrips];
for (int strip=0; strip<nStrips; strip++) {
stripBuf[strip] = new ByteArrayOutputStream(stripSize);
stripOut[strip] = new DataOutputStream(stripBuf[strip]);
}
int[] bps = ifd.getBitsPerSample();
int off;
// write pixel strips to output buffers
int effectiveStrips = !interleaved ? nStrips / nChannels : nStrips;
if (effectiveStrips == 1 && copyDirectly) {
stripOut[0].write(buf);
}
else {
for (int strip = 0; strip < effectiveStrips; strip++) {
int xOffset = (strip % tilesPerRow) * tileWidth;
int yOffset = (strip / tilesPerRow) * tileHeight;
for (int row=0; row<tileHeight; row++) {
for (int col=0; col<tileWidth; col++) {
int ndx = ((row+yOffset) * w + col + xOffset) * bytesPerPixel;
for (int c=0; c<nChannels; c++) {
for (int n=0; n<bps[c]/8; n++) {
if (interleaved) {
off = ndx * nChannels + c * bytesPerPixel + n;
if (row >= h || col >= w) {
stripOut[strip].writeByte(0);
} else {
stripOut[strip].writeByte(buf[off]);
}
}
else {
off = c * blockSize + ndx + n;
if (row >= h || col >= w) {
stripOut[strip].writeByte(0);
} else {
stripOut[c * (nStrips / nChannels) + strip].writeByte(
buf[off]);
}
}
}
}
}
}
}
}
}
// Compress strips according to given differencing and compression schemes,
// this operation is NOT synchronized and is the ONLY portion of the
// TiffWriter.saveBytes() --> TiffSaver.writeImage() stack that is NOT
// synchronized.
byte[][] strips = new byte[nStrips][];
for (int strip=0; strip<nStrips; strip++) {
strips[strip] = stripBuf[strip].toByteArray();
TiffCompression.difference(strips[strip], ifd);
CodecOptions codecOptions = compression.getCompressionCodecOptions(
ifd, options);
codecOptions.height = tileHeight;
codecOptions.width = tileWidth;
strips[strip] = compression.compress(strips[strip], codecOptions);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("Compressed strip %d/%d length %d",
strip + 1, nStrips, strips[strip].length));
}
}
// This operation is synchronized
synchronized (this) {
writeImageIFD(ifd, no, strips, nChannels, last, x ,y);
}
}
/**
* Performs the actual work of dealing with IFD data and writing it to the
* TIFF for a given image or sub-image.
* @param ifd The Image File Directories. Mustn't be <code>null</code>.
* @param no The image index within the current file, starting from 0.
* @param strips The strips to write to the file.
* @param last Pass <code>true</code> if it is the last image,
* <code>false</code> otherwise.
* @param x The initial X offset of the strips/tiles to write.
* @param y The initial Y offset of the strips/tiles to write.
* @throws FormatException
* @throws IOException
*/
private void writeImageIFD(IFD ifd, int no, byte[][] strips,
int nChannels, boolean last, int x, int y)
throws FormatException, IOException {
LOGGER.debug("Attempting to write image IFD.");
int tilesPerRow = (int) ifd.getTilesPerRow();
int tilesPerColumn = (int) ifd.getTilesPerColumn();
boolean interleaved = ifd.getPlanarConfiguration() == 1;
boolean isTiled = ifd.isTiled();
if (!sequentialWrite) {
RandomAccessInputStream in = null;
if (filename != null) {
in = new RandomAccessInputStream(filename);
}
else if (bytes != null) {
in = new RandomAccessInputStream(bytes);
}
else {
throw new IllegalArgumentException(
"Filename and bytes are null, cannot create new input stream!");
}
try {
TiffParser parser = new TiffParser(in);
long[] ifdOffsets = parser.getIFDOffsets();
LOGGER.debug("IFD offsets: {}", Arrays.toString(ifdOffsets));
if (no < ifdOffsets.length) {
out.seek(ifdOffsets[no]);
LOGGER.debug("Reading IFD from {} in non-sequential write.",
ifdOffsets[no]);
ifd = parser.getIFD(ifdOffsets[no]);
}
}
finally {
in.close();
}
}
// record strip byte counts and offsets
List<Long> byteCounts = new ArrayList<Long>();
List<Long> offsets = new ArrayList<Long>();
long totalTiles = tilesPerRow * tilesPerColumn;
if (!interleaved) {
totalTiles *= nChannels;
}
if (ifd.containsKey(IFD.STRIP_BYTE_COUNTS) ||
ifd.containsKey(IFD.TILE_BYTE_COUNTS))
{
long[] ifdByteCounts = isTiled ?
ifd.getIFDLongArray(IFD.TILE_BYTE_COUNTS) : ifd.getStripByteCounts();
for (long stripByteCount : ifdByteCounts) {
byteCounts.add(stripByteCount);
}
}
else {
while (byteCounts.size() < totalTiles) {
byteCounts.add(0L);
}
}
int tileOrStripOffsetX = x / (int) ifd.getTileWidth();
int tileOrStripOffsetY = y / (int) ifd.getTileLength();
int firstOffset = (tileOrStripOffsetY * tilesPerRow) + tileOrStripOffsetX;
if (ifd.containsKey(IFD.STRIP_OFFSETS)
|| ifd.containsKey(IFD.TILE_OFFSETS)) {
long[] ifdOffsets = isTiled ?
ifd.getIFDLongArray(IFD.TILE_OFFSETS) : ifd.getStripOffsets();
for (int i = 0; i < ifdOffsets.length; i++) {
offsets.add(ifdOffsets[i]);
}
}
else {
while (offsets.size() < totalTiles) {
offsets.add(0L);
}
}
if (isTiled) {
ifd.putIFDValue(IFD.TILE_BYTE_COUNTS, toPrimitiveArray(byteCounts));
ifd.putIFDValue(IFD.TILE_OFFSETS, toPrimitiveArray(offsets));
}
else {
ifd.putIFDValue(IFD.STRIP_BYTE_COUNTS, toPrimitiveArray(byteCounts));
ifd.putIFDValue(IFD.STRIP_OFFSETS, toPrimitiveArray(offsets));
}
long fp = out.getFilePointer();
writeIFD(ifd, 0);
for (int i=0; i<strips.length; i++) {
out.seek(out.length());
int thisOffset = firstOffset + i;
offsets.set(thisOffset, out.getFilePointer());
byteCounts.set(thisOffset, new Long(strips[i].length));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format(
"Writing tile/strip %d/%d size: %d offset: %d",
thisOffset + 1, totalTiles, byteCounts.get(thisOffset),
offsets.get(thisOffset)));
}
out.write(strips[i]);
}
if (isTiled) {
ifd.putIFDValue(IFD.TILE_BYTE_COUNTS, toPrimitiveArray(byteCounts));
ifd.putIFDValue(IFD.TILE_OFFSETS, toPrimitiveArray(offsets));
}
else {
ifd.putIFDValue(IFD.STRIP_BYTE_COUNTS, toPrimitiveArray(byteCounts));
ifd.putIFDValue(IFD.STRIP_OFFSETS, toPrimitiveArray(offsets));
}
long endFP = out.getFilePointer();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Offset before IFD write: {} Seeking to: {}",
out.getFilePointer(), fp);
}
out.seek(fp);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Writing tile/strip offsets: {}",
Arrays.toString(toPrimitiveArray(offsets)));
LOGGER.debug("Writing tile/strip byte counts: {}",
Arrays.toString(toPrimitiveArray(byteCounts)));
}
writeIFD(ifd, last ? 0 : endFP);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Offset after IFD write: {}", out.getFilePointer());
}
}
public void writeIFD(IFD ifd, long nextOffset)
throws FormatException, IOException
{
TreeSet<Integer> keys = new TreeSet<Integer>(ifd.keySet());
int keyCount = keys.size();
if (ifd.containsKey(new Integer(IFD.LITTLE_ENDIAN))) keyCount--;
if (ifd.containsKey(new Integer(IFD.BIG_TIFF))) keyCount--;
if (ifd.containsKey(new Integer(IFD.REUSE))) keyCount--;
long fp = out.getFilePointer();
int bytesPerEntry = bigTiff ? TiffConstants.BIG_TIFF_BYTES_PER_ENTRY :
TiffConstants.BYTES_PER_ENTRY;
int ifdBytes = (bigTiff ? 16 : 6) + bytesPerEntry * keyCount;
if (bigTiff) out.writeLong(keyCount);
else out.writeShort(keyCount);
ByteArrayHandle extra = new ByteArrayHandle();
RandomAccessOutputStream extraStream = new RandomAccessOutputStream(extra);
for (Integer key : keys) {
if (key.equals(IFD.LITTLE_ENDIAN) || key.equals(IFD.BIG_TIFF) ||
key.equals(IFD.REUSE)) continue;
Object value = ifd.get(key);
writeIFDValue(extraStream, ifdBytes + fp, key.intValue(), value);
}
if (bigTiff) out.seek(out.getFilePointer());
writeIntValue(out, nextOffset);
out.write(extra.getBytes(), 0, (int) extra.length());
}
/**
* Writes the given IFD value to the given output object.
* @param extraOut buffer to which "extra" IFD information should be written
* @param offset global offset to use for IFD offset values
* @param tag IFD tag to write
* @param value IFD value to write
*/
public void writeIFDValue(RandomAccessOutputStream extraOut, long offset,
int tag, Object value)
throws FormatException, IOException
{
extraOut.order(isLittleEndian());
// convert singleton objects into arrays, for simplicity
if (value instanceof Short) {
value = new short[] {((Short) value).shortValue()};
}
else if (value instanceof Integer) {
value = new int[] {((Integer) value).intValue()};
}
else if (value instanceof Long) {
value = new long[] {((Long) value).longValue()};
}
else if (value instanceof TiffRational) {
value = new TiffRational[] {(TiffRational) value};
}
else if (value instanceof Float) {
value = new float[] {((Float) value).floatValue()};
}
else if (value instanceof Double) {
value = new double[] {((Double) value).doubleValue()};
}
int dataLength = bigTiff ? 8 : 4;
// write directory entry to output buffers
out.writeShort(tag); // tag
if (value instanceof short[]) {
short[] q = (short[]) value;
out.writeShort(IFDType.BYTE.getCode());
writeIntValue(out, q.length);
if (q.length <= dataLength) {
for (int i=0; i<q.length; i++) out.writeByte(q[i]);
for (int i=q.length; i<dataLength; i++) out.writeByte(0);
}
else {
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) extraOut.writeByte(q[i]);
}
}
else if (value instanceof String) { // ASCII
char[] q = ((String) value).toCharArray();
out.writeShort(IFDType.ASCII.getCode()); // type
writeIntValue(out, q.length + 1);
if (q.length < dataLength) {
for (int i=0; i<q.length; i++) out.writeByte(q[i]); // value(s)
for (int i=q.length; i<dataLength; i++) out.writeByte(0); // padding
}
else {
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) extraOut.writeByte(q[i]); // values
extraOut.writeByte(0); // concluding NULL byte
}
}
else if (value instanceof int[]) { // SHORT
int[] q = (int[]) value;
out.writeShort(IFDType.SHORT.getCode()); // type
writeIntValue(out, q.length);
if (q.length <= dataLength / 2) {
for (int i=0; i<q.length; i++) {
out.writeShort(q[i]); // value(s)
}
for (int i=q.length; i<dataLength / 2; i++) {
out.writeShort(0); // padding
}
}
else {
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) {
extraOut.writeShort(q[i]); // values
}
}
}
else if (value instanceof long[]) { // LONG
long[] q = (long[]) value;
int type = bigTiff ? IFDType.LONG8.getCode() : IFDType.LONG.getCode();
out.writeShort(type);
writeIntValue(out, q.length);
int div = bigTiff ? 8 : 4;
if (q.length <= dataLength / div) {
for (int i=0; i<q.length; i++) {
writeIntValue(out, q[0]);
}
for (int i=q.length; i<dataLength / div; i++) {
writeIntValue(out, 0);
}
}
else {
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) {
writeIntValue(extraOut, q[i]);
}
}
}
else if (value instanceof TiffRational[]) { // RATIONAL
TiffRational[] q = (TiffRational[]) value;
out.writeShort(IFDType.RATIONAL.getCode()); // type
writeIntValue(out, q.length);
if (bigTiff && q.length == 1) {
out.writeInt((int) q[0].getNumerator());
out.writeInt((int) q[0].getDenominator());
}
else {
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) {
extraOut.writeInt((int) q[i].getNumerator());
extraOut.writeInt((int) q[i].getDenominator());
}
}
}
else if (value instanceof float[]) { // FLOAT
float[] q = (float[]) value;
out.writeShort(IFDType.FLOAT.getCode()); // type
writeIntValue(out, q.length);
if (q.length <= dataLength / 4) {
for (int i=0; i<q.length; i++) {
out.writeFloat(q[0]); // value
}
for (int i=q.length; i<dataLength / 4; i++) {
out.writeInt(0); // padding
}
}
else {
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) {
extraOut.writeFloat(q[i]); // values
}
}
}
else if (value instanceof double[]) { // DOUBLE
double[] q = (double[]) value;
out.writeShort(IFDType.DOUBLE.getCode()); // type
writeIntValue(out, q.length);
writeIntValue(out, offset + extraOut.length());
for (int i=0; i<q.length; i++) {
extraOut.writeDouble(q[i]); // values
}
}
else {
throw new FormatException("Unknown IFD value type (" +
value.getClass().getName() + "): " + value);
}
}
public void overwriteLastIFDOffset(RandomAccessInputStream raf)
throws FormatException, IOException
{
if (raf == null)
throw new FormatException("Output cannot be null");
TiffParser parser = new TiffParser(raf);
long[] offsets = parser.getIFDOffsets();
out.seek(raf.getFilePointer() - (bigTiff ? 8 : 4));
writeIntValue(out, 0);
}
/**
* Surgically overwrites an existing IFD value with the given one. This
* method requires that the IFD directory entry already exist. It
* intelligently updates the count field of the entry to match the new
* length. If the new length is longer than the old length, it appends the
* new data to the end of the file and updates the offset field; if not, or
* if the old data is already at the end of the file, it overwrites the old
* data in place.
*/
public void overwriteIFDValue(RandomAccessInputStream raf,
int ifd, int tag, Object value) throws FormatException, IOException
{
if (raf == null)
throw new FormatException("Output cannot be null");
LOGGER.debug("overwriteIFDValue (ifd={}; tag={}; value={})",
new Object[] {ifd, tag, value});
raf.seek(0);
TiffParser parser = new TiffParser(raf);
Boolean valid = parser.checkHeader();
if (valid == null) {
throw new FormatException("Invalid TIFF header");
}
boolean little = valid.booleanValue();
boolean bigTiff = parser.isBigTiff();
setLittleEndian(little);
setBigTiff(bigTiff);
long offset = bigTiff ? 8 : 4; // offset to the IFD
int bytesPerEntry = bigTiff ?
TiffConstants.BIG_TIFF_BYTES_PER_ENTRY : TiffConstants.BYTES_PER_ENTRY;
raf.seek(offset);
// skip to the correct IFD
long[] offsets = parser.getIFDOffsets();
if (ifd >= offsets.length) {
throw new FormatException(
"No such IFD (" + ifd + " of " + offsets.length + ")");
}
raf.seek(offsets[ifd]);
// get the number of directory entries
long num = bigTiff ? raf.readLong() : raf.readUnsignedShort();
// search directory entries for proper tag
for (int i=0; i<num; i++) {
raf.seek(offsets[ifd] + (bigTiff ? 8 : 2) + bytesPerEntry * i);
TiffIFDEntry entry = parser.readTiffIFDEntry();
if (entry.getTag() == tag) {
// write new value to buffers
ByteArrayHandle ifdBuf = new ByteArrayHandle(bytesPerEntry);
RandomAccessOutputStream ifdOut = new RandomAccessOutputStream(ifdBuf);
ByteArrayHandle extraBuf = new ByteArrayHandle();
RandomAccessOutputStream extraOut =
new RandomAccessOutputStream(extraBuf);
extraOut.order(little);
TiffSaver saver = new TiffSaver(ifdOut, ifdBuf);
saver.setLittleEndian(isLittleEndian());
saver.writeIFDValue(extraOut, entry.getValueOffset(), tag, value);
ifdBuf.seek(0);
extraBuf.seek(0);
// extract new directory entry parameters
int newTag = ifdBuf.readShort();
int newType = ifdBuf.readShort();
int newCount;
long newOffset;
if (bigTiff) {
newCount = ifdBuf.readInt();
newOffset = ifdBuf.readLong();
}
else {
newCount = ifdBuf.readInt();
newOffset = ifdBuf.readInt();
}
LOGGER.debug("overwriteIFDValue:");
LOGGER.debug("\told ({});", entry);
LOGGER.debug("\tnew: (tag={}; type={}; count={}; offset={})",
new Object[] {newTag, newType, newCount, newOffset});
// determine the best way to overwrite the old entry
if (extraBuf.length() == 0) {
// new entry is inline; if old entry wasn't, old data is orphaned
// do not override new offset value since data is inline
LOGGER.debug("overwriteIFDValue: new entry is inline");
}
else if (entry.getValueOffset() + entry.getValueCount() *
entry.getType().getBytesPerElement() == raf.length())
{
// old entry was already at EOF; overwrite it
newOffset = entry.getValueOffset();
LOGGER.debug("overwriteIFDValue: old entry is at EOF");
}
else if (newCount <= entry.getValueCount()) {
// new entry is as small or smaller than old entry; overwrite it
newOffset = entry.getValueOffset();
LOGGER.debug("overwriteIFDValue: new entry is <= old entry");
}
else {
// old entry was elsewhere; append to EOF, orphaning old entry
newOffset = raf.length();
LOGGER.debug("overwriteIFDValue: old entry will be orphaned");
}
// overwrite old entry
out.seek(offsets[ifd] + (bigTiff ? 8 : 2) + bytesPerEntry * i + 2);
out.writeShort(newType);
writeIntValue(out, newCount);
writeIntValue(out, newOffset);
if (extraBuf.length() > 0) {
out.seek(newOffset);
out.write(extraBuf.getByteBuffer(), 0, newCount);
}
return;
}
}
throw new FormatException("Tag not found (" + IFD.getIFDTagName(tag) + ")");
}
/** Convenience method for overwriting a file's first ImageDescription. */
public void overwriteComment(RandomAccessInputStream in, Object value)
throws FormatException, IOException
{
overwriteIFDValue(in, 0, IFD.IMAGE_DESCRIPTION, value);
}
// -- Helper methods --
/**
* Coverts a list to a primitive array.
* @param l The list of <code>Long</code> to convert.
* @return A primitive array of type <code>long[]</code> with the values from
* </code>l</code>.
*/
private long[] toPrimitiveArray(List<Long> l) {
long[] toReturn = new long[l.size()];
for (int i = 0; i < l.size(); i++) {
toReturn[i] = l.get(i);
}
return toReturn;
}
/**
* Write the given value to the given RandomAccessOutputStream.
* If the 'bigTiff' flag is set, then the value will be written as an 8 byte
* long; otherwise, it will be written as a 4 byte integer.
*/
private void writeIntValue(RandomAccessOutputStream out, long offset)
throws IOException
{
if (bigTiff) {
out.writeLong(offset);
}
else {
out.writeInt((int) offset);
}
}
/**
* Makes a valid IFD.
*
* @param ifd The IFD to handle.
* @param pixelType The pixel type.
* @param nChannels The number of channels.
*/
private void makeValidIFD(IFD ifd, int pixelType, int nChannels) {
int bytesPerPixel = FormatTools.getBytesPerPixel(pixelType);
int bps = 8 * bytesPerPixel;
int[] bpsArray = new int[nChannels];
Arrays.fill(bpsArray, bps);
ifd.putIFDValue(IFD.BITS_PER_SAMPLE, bpsArray);
if (FormatTools.isFloatingPoint(pixelType)) {
ifd.putIFDValue(IFD.SAMPLE_FORMAT, 3);
}
if (ifd.getIFDValue(IFD.COMPRESSION) == null) {
ifd.putIFDValue(IFD.COMPRESSION, TiffCompression.UNCOMPRESSED.getCode());
}
boolean indexed = nChannels == 1 && ifd.getIFDValue(IFD.COLOR_MAP) != null;
PhotoInterp pi = indexed ? PhotoInterp.RGB_PALETTE :
nChannels == 1 ? PhotoInterp.BLACK_IS_ZERO : PhotoInterp.RGB;
ifd.putIFDValue(IFD.PHOTOMETRIC_INTERPRETATION, pi.getCode());
ifd.putIFDValue(IFD.SAMPLES_PER_PIXEL, nChannels);
if (ifd.get(IFD.X_RESOLUTION) == null) {
ifd.putIFDValue(IFD.X_RESOLUTION, new TiffRational(1, 1));
}
if (ifd.get(IFD.Y_RESOLUTION) == null) {
ifd.putIFDValue(IFD.Y_RESOLUTION, new TiffRational(1, 1));
}
if (ifd.get(IFD.SOFTWARE) == null) {
ifd.putIFDValue(IFD.SOFTWARE, "LOCI Bio-Formats");
}
if (ifd.get(IFD.ROWS_PER_STRIP) == null) {
ifd.putIFDValue(IFD.ROWS_PER_STRIP, new long[] {1});
}
if (ifd.get(IFD.IMAGE_DESCRIPTION) == null) {
ifd.putIFDValue(IFD.IMAGE_DESCRIPTION, "");
}
}
}