/* * #%L * BSD implementations of Bio-Formats readers and writers * %% * Copyright (C) 2005 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * #L% */ package loci.formats.out; import java.io.IOException; import loci.common.RandomAccessInputStream; import loci.common.RandomAccessOutputStream; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.FormatWriter; import loci.formats.ImageTools; import loci.formats.codec.CompressionType; import loci.formats.gui.AWTImageTools; import loci.formats.meta.MetadataRetrieve; import loci.formats.tiff.IFD; import loci.formats.tiff.TiffCompression; import loci.formats.tiff.TiffParser; import loci.formats.tiff.TiffRational; import loci.formats.tiff.TiffSaver; import ome.xml.model.primitives.PositiveFloat; import ome.units.quantity.Time; import ome.units.quantity.Length; import ome.units.UNITS; /** * TiffWriter is the file format writer for TIFF files. */ public class TiffWriter extends FormatWriter { // -- Constants -- public static final String COMPRESSION_UNCOMPRESSED = CompressionType.UNCOMPRESSED.getCompression(); public static final String COMPRESSION_LZW = CompressionType.LZW.getCompression(); public static final String COMPRESSION_J2K = CompressionType.J2K.getCompression(); public static final String COMPRESSION_J2K_LOSSY = CompressionType.J2K_LOSSY.getCompression(); public static final String COMPRESSION_JPEG = CompressionType.JPEG.getCompression(); private static final String[] BIG_TIFF_SUFFIXES = {"tf2", "tf8", "btf"}; /** * Number of bytes at which to automatically switch to BigTIFF * This is approximately 3.9 GB instead of 4 GB, * to allow space for the IFDs. */ private static final long BIG_TIFF_CUTOFF = (long) 1024 * 1024 * 3990; // -- Fields -- /** Whether or not the output file is a BigTIFF file. */ protected boolean isBigTiff; /** The TiffSaver that will do most of the writing. */ protected TiffSaver tiffSaver; /** Input stream to use when overwriting data. */ protected RandomAccessInputStream in; /** Whether or not to check the parameters passed to saveBytes. */ protected boolean checkParams = true; /** * Sets the compression code for the specified IFD. * * @param ifd The IFD table to handle. */ private void formatCompression(IFD ifd) throws FormatException { if (compression == null) compression = ""; TiffCompression compressType = TiffCompression.UNCOMPRESSED; if (compression.equals(COMPRESSION_LZW)) { compressType = TiffCompression.LZW; } else if (compression.equals(COMPRESSION_J2K)) { compressType = TiffCompression.JPEG_2000; } else if (compression.equals(COMPRESSION_J2K_LOSSY)) { compressType = TiffCompression.JPEG_2000_LOSSY; } else if (compression.equals(COMPRESSION_JPEG)) { compressType = TiffCompression.JPEG; } Object v = ifd.get(new Integer(IFD.COMPRESSION)); if (v == null) ifd.put(new Integer(IFD.COMPRESSION), compressType.getCode()); } // -- Constructors -- public TiffWriter() { this("Tagged Image File Format", new String[] {"tif", "tiff", "tf2", "tf8", "btf"}); } public TiffWriter(String format, String[] exts) { super(format, exts); compressionTypes = new String[] { COMPRESSION_UNCOMPRESSED, COMPRESSION_LZW, COMPRESSION_J2K, COMPRESSION_J2K_LOSSY, COMPRESSION_JPEG }; isBigTiff = false; } // -- FormatWriter API methods -- /* @see loci.formats.FormatWriter#setId(String) */ @Override public void setId(String id) throws FormatException, IOException { super.setId(id); // if a BigTIFF extension is used, or we know that // more than 4GB of data will be written, then automatically // switch to BigTIFF if (!isBigTiff) { if (checkSuffix(id, BIG_TIFF_SUFFIXES)) { LOGGER.info("Switching to BigTIFF (by file extension)"); isBigTiff = true; } else if (compression == null || compression.equals(COMPRESSION_UNCOMPRESSED)) { MetadataRetrieve retrieve = getMetadataRetrieve(); long totalBytes = 0; for (int i=0; i<retrieve.getImageCount(); i++) { int sizeX = retrieve.getPixelsSizeX(i).getValue(); int sizeY = retrieve.getPixelsSizeY(i).getValue(); int sizeZ = retrieve.getPixelsSizeZ(i).getValue(); int sizeC = retrieve.getPixelsSizeC(i).getValue(); int sizeT = retrieve.getPixelsSizeT(i).getValue(); int type = FormatTools.pixelTypeFromString( retrieve.getPixelsType(i).toString()); long bpp = FormatTools.getBytesPerPixel(type); totalBytes += sizeX * sizeY * sizeZ * sizeC * sizeT * bpp; } if (totalBytes >= BIG_TIFF_CUTOFF) { LOGGER.info("Switching to BigTIFF (by file size)"); isBigTiff = true; } } } synchronized (this) { setupTiffSaver(); } } // -- TiffWriter API methods -- /** * Saves the given image to the specified (possibly already open) file. * The IFD hashtable allows specification of TIFF parameters such as bit * depth, compression and units. */ public void saveBytes(int no, byte[] buf, IFD ifd) throws IOException, FormatException { MetadataRetrieve r = getMetadataRetrieve(); int w = r.getPixelsSizeX(series).getValue().intValue(); int h = r.getPixelsSizeY(series).getValue().intValue(); saveBytes(no, buf, ifd, 0, 0, w, h); } /** * Saves the given image to the specified series in the current file. * The IFD hashtable allows specification of TIFF parameters such as bit * depth, compression and units. */ public void saveBytes(int no, byte[] buf, IFD ifd, int x, int y, int w, int h) throws IOException, FormatException { if (checkParams) checkParams(no, buf, x, y, w, h); if (ifd == null) ifd = new IFD(); MetadataRetrieve retrieve = getMetadataRetrieve(); int type = FormatTools.pixelTypeFromString( retrieve.getPixelsType(series).toString()); int index = no; // This operation is synchronized synchronized (this) { // This operation is synchronized against the TIFF saver. synchronized (tiffSaver) { index = prepareToWriteImage(no, buf, ifd, x, y, w, h); if (index == -1) { return; } } } tiffSaver.writeImage(buf, ifd, index, type, x, y, w, h, no == getPlaneCount() - 1 && getSeries() == retrieve.getImageCount() - 1); } /** * Performs the preparation for work prior to the usage of the TIFF saver. * This method is factored out from <code>saveBytes()</code> in an attempt to * ensure thread safety. */ protected int prepareToWriteImage( int no, byte[] buf, IFD ifd, int x, int y, int w, int h) throws IOException, FormatException { MetadataRetrieve retrieve = getMetadataRetrieve(); Boolean bigEndian = retrieve.getPixelsBinDataBigEndian(series, 0); boolean littleEndian = bigEndian == null ? false : !bigEndian.booleanValue(); // Ensure that no more than one thread manipulated the initialized array // at one time. synchronized (this) { if (no < initialized[series].length && !initialized[series][no]) { initialized[series][no] = true; RandomAccessInputStream tmp = new RandomAccessInputStream(currentId); if (tmp.length() == 0) { synchronized (this) { // write TIFF header tiffSaver.writeHeader(); } } tmp.close(); } } int c = getSamplesPerPixel(); int type = FormatTools.pixelTypeFromString( retrieve.getPixelsType(series).toString()); int bytesPerPixel = FormatTools.getBytesPerPixel(type); int blockSize = w * h * c * bytesPerPixel; if (blockSize > buf.length) { c = buf.length / (w * h * bytesPerPixel); } if (bytesPerPixel > 1 && c != 1 && c != 3) { // split channels checkParams = false; if (no == 0) { initialized[series] = new boolean[initialized[series].length * c]; } for (int i=0; i<c; i++) { byte[] b = ImageTools.splitChannels(buf, i, c, bytesPerPixel, false, interleaved); saveBytes(no * c + i, b, (IFD) ifd.clone(), x, y, w, h); } checkParams = true; return -1; } formatCompression(ifd); byte[][] lut = AWTImageTools.get8BitLookupTable(cm); if (lut != null) { int[] colorMap = new int[lut.length * lut[0].length]; for (int i=0; i<lut.length; i++) { for (int j=0; j<lut[0].length; j++) { colorMap[i * lut[0].length + j] = (int) ((lut[i][j] & 0xff) << 8); } } ifd.putIFDValue(IFD.COLOR_MAP, colorMap); } else { short[][] lut16 = AWTImageTools.getLookupTable(cm); if (lut16 != null) { int[] colorMap = new int[lut16.length * lut16[0].length]; for (int i=0; i<lut16.length; i++) { for (int j=0; j<lut16[0].length; j++) { colorMap[i * lut16[0].length + j] = (int) (lut16[i][j] & 0xffff); } } ifd.putIFDValue(IFD.COLOR_MAP, colorMap); } } int width = retrieve.getPixelsSizeX(series).getValue().intValue(); int height = retrieve.getPixelsSizeY(series).getValue().intValue(); ifd.put(new Integer(IFD.IMAGE_WIDTH), new Long(width)); ifd.put(new Integer(IFD.IMAGE_LENGTH), new Long(height)); Length px = retrieve.getPixelsPhysicalSizeX(series); Double physicalSizeX = px == null || px.value(UNITS.MICROM) == null ? null : px.value(UNITS.MICROM).doubleValue(); if (physicalSizeX == null || physicalSizeX.doubleValue() == 0) { physicalSizeX = 0d; } else physicalSizeX = 1d / physicalSizeX; Length py = retrieve.getPixelsPhysicalSizeY(series); Double physicalSizeY = py == null || py.value(UNITS.MICROM) == null ? null : py.value(UNITS.MICROM).doubleValue(); if (physicalSizeY == null || physicalSizeY.doubleValue() == 0) { physicalSizeY = 0d; } else physicalSizeY = 1d / physicalSizeY; ifd.put(IFD.RESOLUTION_UNIT, 3); ifd.put(IFD.X_RESOLUTION, new TiffRational((long) (physicalSizeX * 1000 * 10000), 1000)); ifd.put(IFD.Y_RESOLUTION, new TiffRational((long) (physicalSizeY * 1000 * 10000), 1000)); if (!isBigTiff) { isBigTiff = (out.length() + 2 * (width * height * c * bytesPerPixel)) >= 4294967296L; if (isBigTiff) { throw new FormatException("File is too large; call setBigTiff(true)"); } } // write the image ifd.put(new Integer(IFD.LITTLE_ENDIAN), new Boolean(littleEndian)); if (!ifd.containsKey(IFD.REUSE)) { ifd.put(IFD.REUSE, out.length()); out.seek(out.length()); } else { out.seek((Long) ifd.get(IFD.REUSE)); } ifd.putIFDValue(IFD.PLANAR_CONFIGURATION, interleaved || getSamplesPerPixel() == 1 ? 1 : 2); int sampleFormat = 1; if (FormatTools.isSigned(type)) sampleFormat = 2; if (FormatTools.isFloatingPoint(type)) sampleFormat = 3; ifd.putIFDValue(IFD.SAMPLE_FORMAT, sampleFormat); int channels = retrieve.getPixelsSizeC(series).getValue().intValue(); int z = retrieve.getPixelsSizeZ(series).getValue().intValue(); int t = retrieve.getPixelsSizeT(series).getValue().intValue(); ifd.putIFDValue(IFD.IMAGE_DESCRIPTION, "ImageJ=\nhyperstack=true\nimages=" + (channels * z * t) + "\nchannels=" + channels + "\nslices=" + z + "\nframes=" + t); int index = no; for (int i=0; i<getSeries(); i++) { index += getPlaneCount(i); } return index; } // -- FormatWriter API methods -- /* (non-Javadoc) * @see loci.formats.FormatWriter#close() */ @Override public void close() throws IOException { super.close(); if (in != null) { in.close(); } if (tiffSaver != null) { tiffSaver.close(); } } /* @see loci.formats.FormatWriter#getPlaneCount() */ @Override public int getPlaneCount() { return getPlaneCount(series); } @Override protected int getPlaneCount(int series) { MetadataRetrieve retrieve = getMetadataRetrieve(); int c = getSamplesPerPixel(series); int type = FormatTools.pixelTypeFromString( retrieve.getPixelsType(series).toString()); int bytesPerPixel = FormatTools.getBytesPerPixel(type); if (bytesPerPixel > 1 && c != 1 && c != 3) { return super.getPlaneCount(series) * c; } return super.getPlaneCount(series); } // -- IFormatWriter API methods -- /** * @see loci.formats.IFormatWriter#saveBytes(int, byte[], int, int, int, int) */ @Override public void saveBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { IFD ifd = new IFD(); if (!sequential) { TiffParser parser = new TiffParser(currentId); try { long[] ifdOffsets = parser.getIFDOffsets(); if (no < ifdOffsets.length) { ifd = parser.getIFD(ifdOffsets[no]); } } finally { RandomAccessInputStream tiffParserStream = parser.getStream(); if (tiffParserStream != null) { tiffParserStream.close(); } } } saveBytes(no, buf, ifd, x, y, w, h); } /* @see loci.formats.IFormatWriter#canDoStacks(String) */ @Override public boolean canDoStacks() { return true; } /* @see loci.formats.IFormatWriter#getPixelTypes(String) */ @Override public int[] getPixelTypes(String codec) { if (codec != null && codec.equals(COMPRESSION_JPEG)) { return new int[] {FormatTools.INT8, FormatTools.UINT8, FormatTools.INT16, FormatTools.UINT16}; } else if (codec != null && codec.equals(COMPRESSION_J2K)) { return new int[] {FormatTools.INT8, FormatTools.UINT8, FormatTools.INT16, FormatTools.UINT16, FormatTools.INT32, FormatTools.UINT32, FormatTools.FLOAT}; } return new int[] {FormatTools.INT8, FormatTools.UINT8, FormatTools.INT16, FormatTools.UINT16, FormatTools.INT32, FormatTools.UINT32, FormatTools.FLOAT, FormatTools.DOUBLE}; } // -- TiffWriter API methods -- /** * Sets whether or not BigTIFF files should be written. * This flag is not reset when close() is called. */ public void setBigTiff(boolean bigTiff) { FormatTools.assertId(currentId, false, 1); isBigTiff = bigTiff; } // -- Helper methods -- protected void setupTiffSaver() throws IOException { out.close(); out = new RandomAccessOutputStream(currentId); tiffSaver = new TiffSaver(out, currentId); MetadataRetrieve retrieve = getMetadataRetrieve(); Boolean bigEndian = retrieve.getPixelsBinDataBigEndian(series, 0); boolean littleEndian = bigEndian == null ? false : !bigEndian.booleanValue(); tiffSaver.setWritingSequentially(sequential); tiffSaver.setLittleEndian(littleEndian); tiffSaver.setBigTiff(isBigTiff); tiffSaver.setCodecOptions(options); } }