package ika.utils; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; /** * Writes TIFF rgb images. Can handle large images that do not fit into the * availble memory. * @author Bernhard Jenny, Institute of Cartography, ETH Zurich. */ public class TIFFImageWriter extends ImageWriter { private static final short kTiffTypeUShort = 3; private static final short kTiffTypeULong = 4; // tag IDs private static final short tagImageWidth = 256; private static final short tagImageLength = 257; private static final short tagBitsPerSample = 258; private static final short tagCompression = 259; private static final short tagPhotometricInterpretation = 262; private static final short tagStripOffsets = 273; private static final short tagSamplesPerPixel = 277; private static final short tagRowsPerStrip = 278; private static final short tagStripByteCounts = 279; private static final short tagXResolution = 282; private static final short tagYResolution = 283; private static final short tagResolutionUnit = 296; private static final short tagExtraSamples = 338; /** Creates a new instance of TIFFWriter and writes the header of the file. */ public TIFFImageWriter(OutputStream out, int cols, int rows) throws java.io.IOException { super(out, cols, rows); } @Override public void writeRGB(int r, int g, int b) throws IOException { this.out.write(r); this.out.write(g); this.out.write(b); this.out.write(255); } /** * Write an rgba value to the file. The r, g, and b values must be * premultiplied by the a value. * @param r Red in the range [0..255] * @param g Green in the range [0..255] * @param b Blue in the range [0..255] * @param a Alpha in the range [0..255] * @throws java.io.IOException */ @Override protected void writeRGB(int r, int g, int b, int a) throws java.io.IOException { // premultiplied rgb values must be smaller than a assert r <= a && g <= a && b <= a; this.out.write(r); this.out.write(g); this.out.write(b); this.out.write(a); } /** * Write an argb value to the file. The r, g, and b values must be * premultiplied by the a value. * @param color An rgba value packed in an integer * @throws java.io.IOException */ @Override public void write(int argb) throws java.io.IOException { // write single bytes, which is not slower than writing ints to a DataOutputStream. out.write((argb >> 16) & 0xff); out.write((argb >> 8) & 0xff); out.write(argb & 0xff); out.write((argb >> 24) & 0xff); } @Override protected void writeHeader() throws IOException { DataOutputStream dout = new DataOutputStream(out); // the number of directory entries final short TAG_COUNT = 13; // write 4 bytes per pixel: rgba final int CHANNEL_COUNT = 4; final int kFileHeaderLength = 8; final int kDirectoryHeaderLength = 2; final int kDirEntryLength = 12; final int kDirectoryFooterLength = 4; int dataSectionPos = kFileHeaderLength + kDirectoryHeaderLength + kDirEntryLength * TAG_COUNT + kDirectoryFooterLength; // write file header dout.write('M'); dout.write('M'); dout.write((byte)0); dout.write('*'); dout.writeInt(8); // start of first (and only) IFD (image file directory) // write directory header dout.writeShort(TAG_COUNT); // 1. image width: cols this.write4ByteTag(this.cols, tagImageWidth, dout); // 2. image length: rows write4ByteTag(this.rows, tagImageLength, dout); // 3. bits per sample writeOffsetTag(dataSectionPos, CHANNEL_COUNT, tagBitsPerSample, false, dout); dataSectionPos += CHANNEL_COUNT * 2; // 4. compression write2ByteTag((short)1, tagCompression, dout); // no compression // 5. photometric interpretation: the color space of the image data write2ByteTag((short)2, tagPhotometricInterpretation, dout); // 6. samples per pixel write2ByteTag((short)CHANNEL_COUNT, tagSamplesPerPixel, dout); // 7. rows per strip write4ByteTag(this.rows, tagRowsPerStrip, dout); // 8. strip byte counts write4ByteTag(this.cols * this.rows * CHANNEL_COUNT, tagStripByteCounts, dout); // 9. resolution in x direction writeOffsetTag(dataSectionPos, 2, tagXResolution, true, dout); dataSectionPos += 2 * 4; // 10 resolution in y direction writeOffsetTag (dataSectionPos, 2, tagYResolution, true, dout); dataSectionPos += 2 * 4; // 11 resolution unit write2ByteTag ((short)2, tagResolutionUnit, dout); // inch // 12 extra samples are transparency write2ByteTag ((short)1, tagExtraSamples, dout); // 13 strip offsets: write the offset to the pixels after dataSectionPos // is updated above write4ByteTag(dataSectionPos, tagStripOffsets, dout); // Directory footer: end of last (and only) IDF dout.writeInt(0); // write data for tagBitsPerSample for (int i = 0; i < CHANNEL_COUNT; i++) dout.writeShort((short)8); // resolution in x and y direction dout.writeInt(144); dout.writeInt(1); dout.writeInt(144); dout.writeInt(1); // here follow the pixel values } /** * writes a tiff tag consisting of four bytes */ void write4ByteTag(int i, short tagID, DataOutputStream dout) throws java.io.IOException { dout.writeShort(tagID); dout.writeShort(kTiffTypeULong); dout.writeInt(1); dout.writeInt(i); } /** * writes a tiff tag consisting of two bytes */ void write2ByteTag(short s, short tagID, DataOutputStream dout) throws java.io.IOException { dout.writeShort(tagID); dout.writeShort(kTiffTypeUShort); dout.writeInt(1); dout.writeShort(s); dout.writeShort(0); // filler } /** * writes a tiff tag consisting of an offset to its data */ void writeOffsetTag(int dataOffset, int nbrValues, short tagID, boolean writeLong, DataOutputStream dout) throws java.io.IOException { dout.writeShort(tagID); if (writeLong) { dout.writeShort(kTiffTypeULong); } else { dout.writeShort(kTiffTypeUShort); } dout.writeInt(nbrValues); // data offset must be even number assert dataOffset % 2 == 0; dout.writeInt(dataOffset); } }