/* * #%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.in; import java.io.File; import java.io.IOException; import java.util.Vector; import loci.common.Location; import loci.common.RandomAccessInputStream; import loci.formats.CoreMetadata; import loci.formats.FormatException; import loci.formats.FormatReader; import loci.formats.FormatTools; import loci.formats.MetadataTools; import loci.formats.UnsupportedCompressionException; import loci.formats.codec.CodecOptions; import loci.formats.codec.JPEGCodec; import loci.formats.codec.MJPBCodec; import loci.formats.codec.MJPBCodecOptions; import loci.formats.codec.QTRLECodec; import loci.formats.codec.RPZACodec; import loci.formats.codec.ZlibCodec; import loci.formats.meta.MetadataStore; /** * NativeQTReader is the file format reader for QuickTime movie files. * It does not require any external libraries to be installed. * * Video codecs currently supported: raw, rle, jpeg, mjpb, rpza. * Additional video codecs will be added as time permits. * * @author Melissa Linkert melissa at glencoesoftware.com */ public class NativeQTReader extends FormatReader { // -- Constants -- /** List of identifiers for each container atom. */ private static final String[] CONTAINER_TYPES = { "moov", "trak", "udta", "tref", "imap", "mdia", "minf", "stbl", "edts", "mdra", "rmra", "imag", "vnrp", "dinf" }; // -- Fields -- /** Offset to start of pixel data. */ private long pixelOffset; /** Total number of bytes of pixel data. */ private long pixelBytes; /** Pixel depth. */ private int bitsPerPixel; /** Raw plane size, in bytes. */ private int rawSize; /** Offsets to each plane's pixel data. */ private Vector<Integer> offsets; /** Pixel data for the previous image plane. */ private byte[] prevPixels; /** Previous plane number. */ private int prevPlane; /** Flag indicating whether we can safely use prevPixels. */ private boolean canUsePrevious; /** Video codec used by this movie. */ private String codec; /** Some movies use two video codecs -- this is the second codec. */ private String altCodec; /** Number of frames that use the alternate codec. */ private int altPlanes; /** Amount to subtract from each offset. */ private int scale; /** Number of bytes in each plane. */ private Vector<Integer> chunkSizes; /** Set to true if the scanlines in a plane are interlaced (mjpb only). */ private boolean interlaced; /** Flag indicating whether the resource and data fork are separated. */ private boolean separatedFork; private String forkFile; private boolean flip; // -- Constructor -- /** Constructs a new QuickTime reader. */ public NativeQTReader() { super("QuickTime", "mov"); suffixNecessary = false; domains = new String[] {FormatTools.GRAPHICS_DOMAIN}; } // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(RandomAccessInputStream) */ @Override public boolean isThisType(RandomAccessInputStream stream) throws IOException { final int blockLen = 64; if (!FormatTools.validStream(stream, blockLen, false)) return false; // use a crappy hack for now String s = stream.readString(blockLen); for (int i=0; i<CONTAINER_TYPES.length; i++) { if (s.indexOf(CONTAINER_TYPES[i]) >= 0 && !CONTAINER_TYPES[i].equals("imag")) { return true; } } return s.indexOf("wide") >= 0 || s.indexOf("mdat") >= 0 || s.indexOf("ftypqt") >= 0; } /* @see loci.formats.IFormatReader#getSeriesUsedFiles(boolean) */ @Override public String[] getSeriesUsedFiles(boolean noPixels) { FormatTools.assertId(currentId, true, 1); if (noPixels) { return forkFile == null ? null : new String[] {forkFile}; } return forkFile == null ? new String[] {currentId} : new String[] {currentId, forkFile}; } /** * @see loci.formats.IFormatReader#openBytes(int, byte[], int, int, int, int) */ @Override public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) throws FormatException, IOException { FormatTools.checkPlaneParameters(this, no, buf.length, x, y, w, h); String code = codec; if (no >= getImageCount() - altPlanes) code = altCodec; int offset = offsets.get(no).intValue(); int nextOffset = (int) pixelBytes; scale = offsets.get(0).intValue(); offset -= scale; if (no < offsets.size() - 1) { nextOffset = offsets.get(no + 1).intValue() - scale; } if ((nextOffset - offset) < 0) { int temp = offset; offset = nextOffset; nextOffset = temp; } byte[] pixs = new byte[nextOffset - offset]; in.seek(pixelOffset + offset); in.read(pixs); canUsePrevious = (prevPixels != null) && (prevPlane == no - 1) && !code.equals(altCodec); byte[] t = prevPlane == no && prevPixels != null && !code.equals(altCodec) ? prevPixels : uncompress(pixs, code); if (code.equals("rpza")) { for (int i=0; i<t.length; i++) { t[i] = (byte) (255 - t[i]); } prevPlane = no; return buf; } // on rare occassions, we need to trim the data if (canUsePrevious && (prevPixels.length < t.length)) { byte[] temp = t; t = new byte[prevPixels.length]; System.arraycopy(temp, 0, t, 0, t.length); } if (t.length > 0) { prevPixels = t; } prevPlane = no; // determine whether we need to strip out any padding bytes int bytes = bitsPerPixel < 40 ? bitsPerPixel / 8 : (bitsPerPixel - 32) / 8; int pad = (4 - (getSizeX() % 4)) % 4; if (codec.equals("mjpb")) pad = 0; int expectedSize = FormatTools.getPlaneSize(this); if (prevPixels.length == expectedSize || (bitsPerPixel == 32 && (3 * (prevPixels.length / 4)) == expectedSize)) { pad = 0; } if (pad > 0) { t = new byte[prevPixels.length - getSizeY()*pad]; for (int row=0; row<getSizeY(); row++) { System.arraycopy(prevPixels, row * (bytes * getSizeX() + pad), t, row * getSizeX() * bytes, getSizeX() * bytes); } } if (t.length == 0) { t = prevPixels; } int bpp = FormatTools.getBytesPerPixel(getPixelType()); int srcRowLen = getSizeX() * bpp * getSizeC(); int destRowLen = w * bpp * getSizeC(); for (int row=0; row<h; row++) { if (bitsPerPixel == 32) { for (int col=0; col<w; col++) { int src = (row + y) * getSizeX() * bpp * 4 + (x + col) * bpp * 4 + 1; int dst = row * destRowLen + col * bpp * 3; if (src + 3 <= t.length && dst + 3 <= buf.length) { System.arraycopy(t, src, buf, dst, 3); } } } else { System.arraycopy(t, row*srcRowLen + x*bpp*getSizeC(), buf, row*destRowLen, destRowLen); } } if ((bitsPerPixel == 40 || bitsPerPixel == 8) && !code.equals("mjpb")) { // invert the pixels for (int i=0; i<buf.length; i++) { buf[i] = (byte) (255 - buf[i]); } } return buf; } /* @see loci.formats.IFormatReader#close(boolean) */ @Override public void close(boolean fileOnly) throws IOException { super.close(fileOnly); if (!fileOnly) { offsets = null; prevPixels = null; codec = altCodec = null; pixelOffset = pixelBytes = bitsPerPixel = rawSize = 0; prevPlane = altPlanes = 0; canUsePrevious = false; scale = 0; chunkSizes = null; interlaced = separatedFork = flip = false; forkFile = null; } } // -- Internal FormatReader API methods -- /* @see loci.formats.FormatReader#initFile(String) */ @Override protected void initFile(String id) throws FormatException, IOException { super.initFile(id); in = new RandomAccessInputStream(id); separatedFork = true; offsets = new Vector<Integer>(); chunkSizes = new Vector<Integer>(); LOGGER.info("Parsing tags"); parse(0, 0, in.length()); CoreMetadata m = core.get(0); m.imageCount = offsets.size(); if (chunkSizes.size() < getImageCount() && chunkSizes.size() > 0) { m.imageCount = chunkSizes.size(); } LOGGER.info("Populating metadata"); int bytes = (bitsPerPixel / 8) % 4; m.pixelType = bytes == 2 ? FormatTools.UINT16 : FormatTools.UINT8; m.sizeZ = 1; m.dimensionOrder = "XYCZT"; m.littleEndian = false; m.metadataComplete = true; m.indexed = false; m.falseColor = false; // this handles the case where the data and resource forks have been // separated if (separatedFork) { // first we want to check if there is a resource fork present // the resource fork will generally have the same name as the data fork, // but will have either the prefix "._" or the suffix ".qtr" // (or <filename>/rsrc on a Mac) String base = null; // it's not enough to just check the first index of "." // on Windows in particular, the directory name could contain "." while // the file name has no extension if (id.indexOf(".", id.lastIndexOf(File.separator) + 1) != -1) { base = id.substring(0, id.lastIndexOf(".")); } else base = id; Location f = new Location(base + ".qtr"); LOGGER.debug("Searching for resource fork:"); if (f.exists()) { LOGGER.debug("\t Found: {}", f); if (in != null) in.close(); in = new RandomAccessInputStream(f.getAbsolutePath()); forkFile = f.getAbsolutePath(); stripHeader(); parse(0, 0, in.length()); m.imageCount = offsets.size(); } else { LOGGER.debug("\tAbsent: {}", f); f = new Location(id.substring(0, id.lastIndexOf(File.separator) + 1) + "._" + id.substring(base.lastIndexOf(File.separator) + 1)); if (f.exists()) { LOGGER.debug("\t Found: {}", f); if (in != null) in.close(); in = new RandomAccessInputStream(f.getAbsolutePath()); forkFile = f.getAbsolutePath(); stripHeader(); parse(0, in.getFilePointer(), in.length()); m.imageCount = offsets.size(); } else { LOGGER.debug("\tAbsent: {}", f); f = new Location(id + "/..namedfork/rsrc"); if (f.exists()) { LOGGER.debug("\t Found: {}", f); if (in != null) in.close(); in = new RandomAccessInputStream(f.getAbsolutePath()); forkFile = f.getAbsolutePath(); stripHeader(); parse(0, in.getFilePointer(), in.length()); m.imageCount = offsets.size(); } else { LOGGER.debug("\tAbsent: {}", f); throw new FormatException("QuickTime resource fork not found. " + " To avoid this issue, please flatten your QuickTime movies " + "before importing with Bio-Formats."); } } } // reset the stream, otherwise openBytes will try to read pixels // from the resource fork in = new RandomAccessInputStream(currentId); } m.rgb = bitsPerPixel < 40; m.sizeC = isRGB() ? 3 : 1; m.interleaved = isRGB(); m.sizeT = getImageCount(); // The metadata store we're working with. MetadataStore store = makeFilterMetadata(); MetadataTools.populatePixels(store, this); } // -- Helper methods -- /** Parse all of the atoms in the file. */ private void parse(int depth, long offset, long length) throws FormatException, IOException { while (offset < length) { in.seek(offset); // first 4 bytes are the atom size long atomSize = in.readInt() & 0xffffffffL; // read the atom type String atomType = in.readString(4); // if atomSize is 1, then there is an 8 byte extended size if (atomSize == 1) { atomSize = in.readLong(); } if (atomSize < 0) { LOGGER.warn("QTReader: invalid atom size: {}", atomSize); } else if (atomSize > in.length()) { offset += 4; continue; } LOGGER.debug("Seeking to {}; atomType={}; atomSize={}", new Object[] {offset, atomType, atomSize}); // if this is a container atom, parse the children if (isContainer(atomType)) { parse(depth++, in.getFilePointer(), offset + atomSize); } else { if (atomSize == 0) atomSize = in.length(); long oldpos = in.getFilePointer(); if (atomType.equals("mdat")) { // we've found the pixel data pixelOffset = in.getFilePointer(); pixelBytes = atomSize; if (pixelBytes > (in.length() - pixelOffset)) { pixelBytes = in.length() - pixelOffset; } } else if (atomType.equals("tkhd")) { // we've found the dimensions in.skipBytes(38); int[][] matrix = new int[3][3]; for (int i=0; i<matrix.length; i++) { for (int j=0; j<matrix[0].length; j++) { matrix[i][j] = in.readInt(); } } // The contents of the matrix we just read determine whether or not // we should flip the width and height. We can check the first two // rows of the matrix - they should correspond to the first two rows // of an identity matrix. // TODO : adapt to use the value of flip flip = matrix[0][0] == 0 && matrix[1][0] != 0; if (getSizeX() == 0) core.get(0).sizeX = in.readInt(); if (getSizeY() == 0) core.get(0).sizeY = in.readInt(); } else if (atomType.equals("cmov")) { in.skipBytes(8); if ("zlib".equals(in.readString(4))) { atomSize = in.readInt(); in.skipBytes(4); int uncompressedSize = in.readInt(); byte[] b = new byte[(int) (atomSize - 12)]; in.read(b); byte[] output = new ZlibCodec().decompress(b, null); RandomAccessInputStream oldIn = in; in = new RandomAccessInputStream(output); parse(0, 0, output.length); in.close(); in = oldIn; } else { throw new UnsupportedCompressionException( "Compressed header not supported."); } } else if (atomType.equals("stco")) { // we've found the plane offsets if (offsets.size() > 0) break; separatedFork = false; in.skipBytes(4); int numPlanes = in.readInt(); if (numPlanes != getImageCount()) { in.seek(in.getFilePointer() - 4); int off = in.readInt(); offsets.add(new Integer(off)); for (int i=1; i<getImageCount(); i++) { if ((chunkSizes.size() > 0) && (i < chunkSizes.size())) { rawSize = chunkSizes.get(i).intValue(); } else i = getImageCount(); off += rawSize; offsets.add(new Integer(off)); } } else { for (int i=0; i<numPlanes; i++) { offsets.add(new Integer(in.readInt())); } } } else if (atomType.equals("stsd")) { // found video codec and pixel depth information in.skipBytes(4); int numEntries = in.readInt(); in.skipBytes(4); for (int i=0; i<numEntries; i++) { if (i == 0) { codec = in.readString(4); if (!codec.equals("raw ") && !codec.equals("rle ") && !codec.equals("rpza") && !codec.equals("mjpb") && !codec.equals("jpeg")) { throw new UnsupportedCompressionException( "Unsupported codec: " + codec); } in.skipBytes(16); if (in.readShort() == 0) { in.skipBytes(56); bitsPerPixel = in.readShort(); if (codec.equals("rpza")) bitsPerPixel = 8; in.skipBytes(10); interlaced = in.read() == 2; addGlobalMeta("Codec", codec); addGlobalMeta("Bits per pixel", bitsPerPixel); in.skipBytes(9); } } else { altCodec = in.readString(4); addGlobalMeta("Second codec", altCodec); } } } else if (atomType.equals("stsz")) { // found the number of planes in.skipBytes(4); rawSize = in.readInt(); core.get(0).imageCount = in.readInt(); if (rawSize == 0) { in.seek(in.getFilePointer() - 4); for (int b=0; b<getImageCount(); b++) { chunkSizes.add(new Integer(in.readInt())); } } } else if (atomType.equals("stsc")) { in.skipBytes(4); int numChunks = in.readInt(); if (altCodec != null) { int prevChunk = 0; for (int i=0; i<numChunks; i++) { int chunk = in.readInt(); int planesPerChunk = in.readInt(); int id = in.readInt(); if (id == 2) altPlanes += planesPerChunk * (chunk - prevChunk); prevChunk = chunk; } } } else if (atomType.equals("stts")) { in.skipBytes(12); int fps = in.readInt(); addGlobalMeta("Frames per second", fps); } if (oldpos + atomSize < in.length()) { in.seek(oldpos + atomSize); } else break; } if (atomSize == 0) offset = in.length(); else offset += atomSize; // if a 'udta' atom, skip ahead 4 bytes if (atomType.equals("udta")) offset += 4; print(depth, atomSize, atomType); } } /** Checks if the given String is a container atom type. */ private boolean isContainer(String type) { for (int i=0; i<CONTAINER_TYPES.length; i++) { if (type.equals(CONTAINER_TYPES[i])) return true; } return false; } /** Debugging method; prints information on an atom. */ private void print(int depth, long size, String type) { StringBuffer sb = new StringBuffer(); for (int i=0; i<depth; i++) sb.append(" "); sb.append(type + " : [" + size + "]"); LOGGER.debug(sb.toString()); } /** Uncompresses an image plane according to the the codec identifier. */ private byte[] uncompress(byte[] pixs, String code) throws FormatException, IOException { CodecOptions options = new MJPBCodecOptions(); options.width = getSizeX(); options.height = getSizeY(); options.bitsPerSample = bitsPerPixel; options.channels = bitsPerPixel < 40 ? bitsPerPixel / 8 : (bitsPerPixel - 32) / 8; options.previousImage = canUsePrevious ? prevPixels : null; options.littleEndian = isLittleEndian(); options.interleaved = isRGB(); if (code.equals("raw ")) return pixs; else if (code.equals("rle ")) { return new QTRLECodec().decompress(pixs, options); } else if (code.equals("rpza")) { return new RPZACodec().decompress(pixs, options); } else if (code.equals("mjpb")) { ((MJPBCodecOptions) options).interlaced = interlaced; return new MJPBCodec().decompress(pixs, options); } else if (code.equals("jpeg")) { return new JPEGCodec().decompress(pixs, options); } else { throw new UnsupportedCompressionException("Unsupported codec : " + code); } } /** Cut off header bytes from a resource fork file. */ private void stripHeader() throws IOException { in.seek(0); while (!in.readString(4).equals("moov")) { in.seek(in.getFilePointer() - 2); } in.seek(in.getFilePointer() - 8); } }