| TiffEncoder |

    package ij.io;
    import java.io.*;

    /**Saves an image described by a FileInfo object as an uncompressed, big-endian TIFF file.*/
    public class TiffEncoder {
        static final int HDR_SIZE = 8;
        static final int MAP_SIZE = 768; // in 16-bit words
        static final int BPS_DATA_SIZE = 6;
        static final int SCALE_DATA_SIZE = 16;

        private FileInfo fi;
        private int bitsPerSample;
        private int photoInterp;
        private int samplesPerPixel;
        private int nEntries;
        private int ifdSize;
        private long imageOffset;
        private int imageSize;
        private long stackSize;
        private byte[] description;
        private int metaDataSize;
        private int metaDataEntries;
        private int nSliceLabels;
        private int extraMetaDataEntries;
        private int scaleSize;

        public TiffEncoder (FileInfo fi) {
            this.fi = fi;
            fi.intelByteOrder = false;
            bitsPerSample = 8;
            samplesPerPixel = 1;
            nEntries = 9;
            int bytesPerPixel = 1;
            int bpsSize = 0;
            int colorMapSize = 0;

            switch (fi.fileType) {
                case FileInfo.GRAY8:
                    photoInterp = fi.whiteIsZero?0:1;
                    break;
                case FileInfo.GRAY16_UNSIGNED:
                case FileInfo.GRAY16_SIGNED:
                    bitsPerSample = 16;
                    photoInterp = fi.whiteIsZero?0:1;
                    bytesPerPixel = 2;
                    break;
                case FileInfo.GRAY32_FLOAT:
                    bitsPerSample = 32;
                    photoInterp = fi.whiteIsZero?0:1;
                    bytesPerPixel = 4;
                    break;
                case FileInfo.RGB:
                    photoInterp = 2;
                    samplesPerPixel = 3;
                    bytesPerPixel = 3;
                    bpsSize = BPS_DATA_SIZE;
                    break;
                case FileInfo.RGB48:
                    bitsPerSample = 16;
                    photoInterp = fi.whiteIsZero?0:1;
                    samplesPerPixel = 3;
                    bytesPerPixel = 6;
                    fi.nImages /= 3;
                    bpsSize = BPS_DATA_SIZE;
                    break;
                case FileInfo.COLOR8:
                    photoInterp = 3;
                    nEntries = 10;
                    colorMapSize = MAP_SIZE*2;
                    break;
                default:
                    photoInterp = 0;
            }
            if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0)
                nEntries += 3; // XResolution, YResolution and ResolutionUnit
            if (fi.fileType==fi.GRAY32_FLOAT)
                nEntries++; // SampleFormat tag
            makeDescriptionString();
            if (description!=null)
                nEntries++;  // ImageDescription tag
            imageSize = fi.width*fi.height*bytesPerPixel;
            stackSize = (long)imageSize*fi.nImages;
            metaDataSize = getMetaDataSize();
            if (metaDataSize>0)
                nEntries += 2; // MetaData & MetaDataCounts
            ifdSize = 2 + nEntries*12 + 4;
            int descriptionSize = description!=null?description.length:0;
            scaleSize = fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0?SCALE_DATA_SIZE:0;
            imageOffset = HDR_SIZE+ifdSize+bpsSize+descriptionSize+scaleSize+colorMapSize + metaDataEntries*4 + metaDataSize;
            fi.offset = (int)imageOffset;
            //ij.IJ.log(imageOffset+", "+ifdSize+", "+bpsSize+", "+descriptionSize+", "+scaleSize+", "+colorMapSize+", "+metaDataEntries*4+", "+metaDataSize);
        }

        /** Saves the image as a TIFF file. The DataOutputStream is not closed.
            The fi.pixels field must contain the image data. If fi.nImages>1
            then fi.pixels must be a 2D array. The fi.offset field is ignored. */
        public void write(DataOutputStream out) throws IOException {
            writeHeader(out);
            long nextIFD = 0L;
            if (fi.nImages>1)
                nextIFD = imageOffset+stackSize;
            if (nextIFD+fi.nImages*ifdSize>=0xffffffffL)
                nextIFD = 0L;
            writeIFD(out, (int)imageOffset, (int)nextIFD);
            if (fi.fileType==FileInfo.RGB||fi.fileType==FileInfo.RGB48)
                writeBitsPerPixel(out);
            if (description!=null)
                writeDescription(out);
            if (scaleSize>0)
                writeScale(out);
            if (fi.fileType==FileInfo.COLOR8)
                writeColorMap(out);
            if (metaDataSize>0)
                writeMetaData(out);
            new ImageWriter(fi).write(out);
            if (nextIFD>0L) {
                for (int i=2; i<=fi.nImages; i++) {
                    if (i==fi.nImages)
                        nextIFD = 0;
                    else
                        nextIFD += ifdSize;
                    imageOffset += imageSize;
                    writeIFD(out, (int)imageOffset, (int)nextIFD);
                }
            }
        }

        int getMetaDataSize() {
            //if (stackSize+IMAGE_START>0xffffffffL) return 0;
            nSliceLabels = 0;
            metaDataEntries = 0;
            int size = 0;
            int nTypes = 0;
            if (fi.info!=null) {
                metaDataEntries = 1;
                size = fi.info.length()*2;
                nTypes++;
            }
            if (fi.nImages>1 && fi.sliceLabels!=null) {
                int max = fi.sliceLabels.length;
                for (int i=0; i<fi.nImages&&i<max; i++) {
                    if (fi.sliceLabels[i]!=null&&fi.sliceLabels[i].length()>0) {
                        nSliceLabels++;
                        size += fi.sliceLabels[i].length()*2;
                    } else
                        break;
                }
                if (nSliceLabels>0) nTypes++;
                metaDataEntries += nSliceLabels;
            }
            if (fi.metaDataTypes!=null && fi.metaData!=null && fi.metaData[0]!=null
            && fi.metaDataTypes.length==fi.metaData.length) {
                extraMetaDataEntries = fi.metaData.length;
                nTypes += extraMetaDataEntries;
                metaDataEntries += extraMetaDataEntries;
                for (int i=0; i<extraMetaDataEntries; i++)
                    size += fi.metaData.length;
            }
            if (metaDataEntries>0) metaDataEntries++; // add entry for header
            int hdrSize = 4 + nTypes*8;
            if (size>0) size += hdrSize;
            return size;
        }

        /** Writes the 8-byte image file header. */
        void writeHeader(DataOutputStream out) throws IOException {
            byte[] hdr = new byte[8];
            hdr[0] = 77; // "MM" (Motorola byte order)
            hdr[1] = 77;
            hdr[2] = 0;  // 42 (magic number)
            hdr[3] = 42;
            hdr[4] = 0;  // 8 (offset to first IFD)
            hdr[5] = 0;
            hdr[6] = 0;
            hdr[7] = 8;
            out.write(hdr);
        }

        /** Writes one 12-byte IFD entry. */
        void writeEntry(DataOutputStream out, int tag, int fieldType, int count, int value) throws IOException {
            out.writeShort(tag);
            out.writeShort(fieldType);
            out.writeInt(count);
            if (count==1 && fieldType==TiffDecoder.SHORT)
                value <<= 16; //left justify 16-bit values
            out.writeInt(value); // may be an offset
        }

        /** Writes one IFD (Image File Directory). */
        void writeIFD(DataOutputStream out, int imageOffset, int nextIFD) throws IOException {
            int tagDataOffset = HDR_SIZE + ifdSize;
            out.writeShort(nEntries);
            writeEntry(out, TiffDecoder.NEW_SUBFILE_TYPE, 4, 1, 0);
            writeEntry(out, TiffDecoder.IMAGE_WIDTH,      3, 1, fi.width);
            writeEntry(out, TiffDecoder.IMAGE_LENGTH,     3, 1, fi.height);
            if (fi.fileType==FileInfo.RGB||fi.fileType==FileInfo.RGB48) {
                writeEntry(out, TiffDecoder.BITS_PER_SAMPLE,  3, 3, tagDataOffset);
                tagDataOffset += BPS_DATA_SIZE;
            } else
                writeEntry(out, TiffDecoder.BITS_PER_SAMPLE,  3, 1, bitsPerSample);
            writeEntry(out, TiffDecoder.PHOTO_INTERP,     3, 1, photoInterp);
            if (description!=null) {
                writeEntry(out, TiffDecoder.IMAGE_DESCRIPTION, 2, description.length, tagDataOffset);
                tagDataOffset += description.length;
            }
            writeEntry(out, TiffDecoder.STRIP_OFFSETS,    4, 1, imageOffset);
            writeEntry(out, TiffDecoder.SAMPLES_PER_PIXEL,3, 1, samplesPerPixel);
            writeEntry(out, TiffDecoder.ROWS_PER_STRIP,   3, 1, fi.height);
            writeEntry(out, TiffDecoder.STRIP_BYTE_COUNT, 4, 1, imageSize);
            if (fi.unit!=null && fi.pixelWidth!=0 && fi.pixelHeight!=0) {
                writeEntry(out, TiffDecoder.X_RESOLUTION, 5, 1, tagDataOffset);
                writeEntry(out, TiffDecoder.Y_RESOLUTION, 5, 1, tagDataOffset+8);
                tagDataOffset += SCALE_DATA_SIZE;
                int unit = 1;
                if (fi.unit.equals("inch"))
                    unit = 2;
                else if (fi.unit.equals("cm"))
                    unit = 3;
                writeEntry(out, TiffDecoder.RESOLUTION_UNIT, 3, 1, unit);
            }
            if (fi.fileType==fi.GRAY32_FLOAT) {
                int format = TiffDecoder.FLOATING_POINT;
                writeEntry(out, TiffDecoder.SAMPLE_FORMAT, 3, 1, format);
            }
            if (fi.fileType==FileInfo.COLOR8) {
                writeEntry(out, TiffDecoder.COLOR_MAP, 3, MAP_SIZE, tagDataOffset);
                tagDataOffset += MAP_SIZE*2;
            }
            if (metaDataSize>0) {
                writeEntry(out, TiffDecoder.META_DATA_BYTE_COUNTS, 4, metaDataEntries, tagDataOffset);
                writeEntry(out, TiffDecoder.META_DATA, 1, metaDataSize, tagDataOffset+4*metaDataEntries);
                tagDataOffset += metaDataEntries*4 + metaDataSize;
            }
            out.writeInt(nextIFD);
        }

        /** Writes the 6 bytes of data required by RGB BitsPerSample tag. */
        void writeBitsPerPixel(DataOutputStream out) throws IOException {
            int bitsPerPixel = fi.fileType==FileInfo.RGB48?16:8;
            out.writeShort(bitsPerPixel);
            out.writeShort(bitsPerPixel);
            out.writeShort(bitsPerPixel);
        }

        /** Writes the 16 bytes of data required by the XResolution and YResolution tags. */
        void writeScale(DataOutputStream out) throws IOException {
            double xscale = 1.0/fi.pixelWidth;
            double yscale = 1.0/fi.pixelHeight;
            double scale = 1000000.0;
            if (xscale>1000.0) scale = 1000.0;
            out.writeInt((int)(xscale*scale));
            out.writeInt((int)scale);
            out.writeInt((int)(yscale*scale));
            out.writeInt((int)scale);
        }

        /** Writes the variable length ImageDescription string. */
        void writeDescription(DataOutputStream out) throws IOException {
            out.write(description,0,description.length);
        }

        /** Writes color palette following the image. */
        void writeColorMap(DataOutputStream out) throws IOException {
            byte[] colorTable16 = new byte[MAP_SIZE*2];
            int j=0;
            for (int i=0; i<fi.lutSize; i++) {
                colorTable16[j] = fi.reds[i];
                colorTable16[512+j] = fi.greens[i];
                colorTable16[1024+j] = fi.blues[i];
                j += 2;
            }
            out.write(colorTable16);
        }

        /** Writes image metadata ("info" image propery, stack slice labels
            and extra metadata) following the image and any color palette. */
        void writeMetaData(DataOutputStream out) throws IOException {

            // write byte counts
            int nTypes = 0;
            if (fi.info!=null) nTypes++;
            if (nSliceLabels>0) nTypes++;
            nTypes += extraMetaDataEntries;
            out.writeInt(4+nTypes*8); // header size
            if (fi.info!=null)
                out.writeInt(fi.info.length()*2);
            for (int i=0; i<nSliceLabels; i++)
                out.writeInt(fi.sliceLabels[i].length()*2);
            for (int i=0; i<extraMetaDataEntries; i++)
                out.writeInt(fi.metaData[i].length);

            // write header
            out.writeInt(0x494a494a); // magic number ("IJIJ")
            if (fi.info!=null) {
                out.writeInt(0x696e666f); // type="info"
                out.writeInt(1); // count
            }
            if (nSliceLabels>0) {
                out.writeInt(0x6c61626c); // type="labl"
                out.writeInt(nSliceLabels); // count
            }
            for (int i=0; i<extraMetaDataEntries; i++) {
                out.writeInt(fi.metaDataTypes[i]);
                out.writeInt(1); // count
            }

            // write data
            if (fi.info!=null)
                out.writeChars(fi.info);
            for (int i=0; i<nSliceLabels; i++)
                out.writeChars(fi.sliceLabels[i]);
            for (int i=0; i<extraMetaDataEntries; i++)
                out.write(fi.metaData[i]);

        }

        /** Creates an optional image description string for saving calibration data.
            For stacks, also saves the stack size so ImageJ can open the stack without
            decoding an IFD for each slice.*/
        void makeDescriptionString() {
            if (fi.description!=null) {
                if (fi.description.charAt(fi.description.length()-1)!=(char)0)
                    fi.description += " ";
                description = fi.description.getBytes();
                description[description.length-1] = (byte)0;
            } else
                description = null;
        }

    }

| TiffEncoder |