//
// QTReader.java
//
/*
LOCI Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan,
Eric Kjellman and Brian Loranger.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Library 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 Library General Public License for more details.
You should have received a copy of the GNU Library 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.in;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Vector;
import java.util.zip.*;
import loci.formats.*;
import loci.formats.codec.*;
/**
* QTReader 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.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/in/QTReader.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/in/QTReader.java">SVN</a></dd></dl>
*
* @author Melissa Linkert linkert at wisc.edu
*/
public class QTReader 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 int pixelOffset;
/** Total number of bytes of pixel data. */
private int pixelBytes;
/** Pixel depth. */
private int bitsPerPixel;
/** Raw plane size, in bytes. */
private int rawSize;
/** Offsets to each plane's pixel data. */
private Vector 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;
/** An instance of the old QuickTime reader, in case this one fails. */
private LegacyQTReader legacy;
/** Flag indicating whether to use legacy reader by default. */
private boolean useLegacy;
/** Amount to subtract from each offset. */
private int scale;
/** Number of bytes in each plane. */
private Vector 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 spork;
private boolean flip;
// -- Constructor --
/** Constructs a new QuickTime reader. */
public QTReader() { super("QuickTime", "mov"); }
// -- QTReader API methods --
/** Sets whether to use the legacy reader (QTJava) by default. */
public void setLegacy(boolean legacy) { useLegacy = legacy; }
// -- IFormatReader API methods --
/* @see loci.formats.IFormatReader#isThisType(byte[]) */
public boolean isThisType(byte[] block) {
return false;
}
/* @see loci.formats.IFormatReader#setMetadataStore(MetadataStore) */
public void setMetadataStore(MetadataStore store) {
FormatTools.assertId(currentId, false, 1);
super.setMetadataStore(store);
if (useLegacy) legacy.setMetadataStore(store);
}
/* @see loci.formats.IFormatReader#openBytes(int, byte[]) */
public byte[] openBytes(int no, byte[] buf)
throws FormatException, IOException
{
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
FormatTools.checkBufferSize(this, buf.length);
String code = codec;
if (no >= getImageCount() - altPlanes) code = altCodec;
boolean doLegacy = useLegacy;
if (!doLegacy && !code.equals("raw ") && !code.equals("rle ") &&
!code.equals("jpeg") && !code.equals("mjpb") && !code.equals("rpza"))
{
if (debug) {
debug("Unsupported codec (" + code + "); using QTJava reader");
}
doLegacy = true;
}
if (doLegacy) {
if (legacy == null) legacy = createLegacyReader();
legacy.setId(currentId);
return legacy.openBytes(no);
}
int offset = ((Integer) offsets.get(no)).intValue();
int nextOffset = pixelBytes;
scale = ((Integer) offsets.get(0)).intValue();
offset -= scale;
if (no < offsets.size() - 1) {
nextOffset = ((Integer) offsets.get(no + 1)).intValue();
nextOffset -= 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);
buf = uncompress(pixs, code);
if (code.equals("rpza")) {
for (int i=0; i<buf.length; i++) {
buf[i] = (byte) (255 - buf[i]);
}
prevPlane = no;
return buf;
}
// on rare occassions, we need to trim the data
if (canUsePrevious && (prevPixels.length < buf.length)) {
byte[] temp = buf;
buf = new byte[prevPixels.length];
System.arraycopy(temp, 0, buf, 0, buf.length);
}
prevPixels = buf;
prevPlane = no;
// determine whether we need to strip out any padding bytes
int pad = core.sizeX[0] % 4;
pad = (4 - pad) % 4;
if (codec.equals("mjpb")) pad = 0;
int size = core.sizeX[0] * core.sizeY[0];
if (size * (bitsPerPixel / 8) == prevPixels.length) pad = 0;
if (pad > 0) {
buf = new byte[prevPixels.length - core.sizeY[0]*pad];
for (int row=0; row<core.sizeY[0]; row++) {
System.arraycopy(prevPixels, row*(core.sizeX[0]+pad), buf,
row*core.sizeX[0], core.sizeX[0]);
}
}
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;
}
else if (bitsPerPixel == 32) {
// strip out alpha channel
byte[][] data = new byte[3][buf.length / 4];
for (int i=0; i<data[0].length; i++) {
data[0][i] = buf[4*i + 1];
data[1][i] = buf[4*i + 2];
data[2][i] = buf[4*i + 3];
}
byte[] rtn = new byte[data.length * data[0].length];
for (int i=0; i<data.length; i++) {
System.arraycopy(data[i], 0, rtn, i * data[0].length, data[i].length);
}
return rtn;
}
return buf;
}
/* @see loci.formats.IFormatReader#openImage(int) */
public BufferedImage openImage(int no) throws FormatException, IOException {
FormatTools.assertId(currentId, true, 1);
FormatTools.checkPlaneNumber(this, no);
String code = codec;
if (no >= getImageCount() - altPlanes) code = altCodec;
boolean doLegacy = useLegacy;
if (!doLegacy && !code.equals("raw ") && !code.equals("rle ") &&
!code.equals("jpeg") && !code.equals("mjpb") && !code.equals("rpza"))
{
if (debug) {
debug("Unsupported codec (" + code + "); using QTJava reader");
}
doLegacy = true;
}
if (doLegacy) {
if (legacy == null) legacy = createLegacyReader();
legacy.setId(currentId);
return legacy.openImage(no);
}
int bpp = bitsPerPixel / 8;
if (bpp == 3 || bpp == 4 || bpp == 5) bpp = 1;
return ImageTools.makeImage(openBytes(no), core.sizeX[0],
core.sizeY[0], core.sizeC[0], false, bpp, core.littleEndian[0]);
}
// -- IFormatHandler API methods --
/* @see loci.formats.IFormatHandler#close() */
public void close() throws IOException {
super.close();
prevPixels = null;
}
// -- Internal FormatReader API methods --
/* @see loci.formats.FormatReader#initFile(String) */
protected void initFile(String id) throws FormatException, IOException {
if (debug) debug("QTReader.initFile(" + id + ")");
super.initFile(id);
in = new RandomAccessStream(id);
spork = true;
offsets = new Vector();
chunkSizes = new Vector();
status("Parsing tags");
Exception exc = null;
try { parse(0, 0, in.length()); }
catch (FormatException e) { exc = e; }
catch (IOException e) { exc = e; }
if (exc != null) {
if (debug) trace(exc);
useLegacy = true;
legacy = createLegacyReader();
legacy.setId(id, true);
core = legacy.getCoreMetadata();
return;
}
core.imageCount[0] = offsets.size();
if (chunkSizes.size() < core.imageCount[0] && chunkSizes.size() > 0) {
core.imageCount[0] = chunkSizes.size();
}
status("Populating metadata");
int bytesPerPixel = bitsPerPixel / 8;
bytesPerPixel %= 4;
switch (bytesPerPixel) {
case 0:
case 1:
case 3:
core.pixelType[0] = FormatTools.UINT8;
break;
case 2:
core.pixelType[0] = FormatTools.UINT16;
break;
}
core.rgb[0] = bitsPerPixel < 40;
core.sizeZ[0] = 1;
core.sizeC[0] = core.rgb[0] ? 3 : 1;
core.sizeT[0] = core.imageCount[0];
core.currentOrder[0] = "XYCZT";
core.littleEndian[0] = false;
core.interleaved[0] = false;
core.metadataComplete[0] = true;
core.indexed[0] = false;
core.falseColor[0] = false;
// The metadata store we're working with.
MetadataStore store = getMetadataStore();
store.setImage(currentId, null, null, null);
FormatTools.populatePixels(store, this);
for (int i=0; i<core.sizeC[0]; i++) {
store.setLogicalChannel(i, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null);
}
// this handles the case where the data and resource forks have been
// separated
if (spork) {
// 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;
if (id.indexOf(".") != -1) {
base = id.substring(0, id.lastIndexOf("."));
}
else base = id;
Location f = new Location(base + ".qtr");
if (f.exists()) {
in = new RandomAccessStream(f.getAbsolutePath());
stripHeader();
parse(0, 0, in.length());
core.imageCount[0] = offsets.size();
return;
}
else {
f = new Location(base.substring(0,
base.lastIndexOf(File.separator) + 1) + "._" +
base.substring(base.lastIndexOf(File.separator) + 1));
if (f.exists()) {
in = new RandomAccessStream(f.getAbsolutePath());
stripHeader();
parse(0, 0, in.length());
core.imageCount[0] = offsets.size();
return;
}
else {
f = new Location(base + "/rsrc");
if (f.exists()) {
in = new RandomAccessStream(f.getAbsolutePath());
stripHeader();
parse(0, 0, in.length());
core.imageCount[0] = offsets.size();
return;
}
}
}
throw new FormatException("QuickTime resource fork not found. " +
" To avoid this issue, please flatten your QuickTime movies " +
"before importing with Bio-Formats.");
/* TODO
// If we didn't find the resource fork, we can check to see if the file
// uses a JPEG-compatible codec. In this case, we can do some guesswork
// to read the file; otherwise we will fail gracefully.
if (debug) {
debug("Failed to find the QuickTime resource fork. " +
"Attempting to proceed using only the data fork.");
}
// read through the file looking for occurences of the codec string
core.imageCount[0] = 0;
String codecString = new String(pixels, 4, 4);
if (codecString.equals("mjpg")) codec = "mjpb";
else codec = codecString;
if (codec.equals("mjpb") || codec.equals("jpeg")) {
// grab the width, height, and bits per pixel from the first plane
}
else {
throw new FormatException("Sorry, this QuickTime movie does not " +
"contain a Resource Fork. Support for this case will be improved " +
"as time permits. To avoid this issue, please flatten your " +
"QuickTime movies before importing with Bio-Formats.");
}
boolean canAdd = true;
for (int i=0; i<pixels.length-5; i++) {
if (codecString.equals(new String(pixels, i, 4))) {
if (canAdd) {
offsets.add(new Integer(i - 4));
core.imageCount[0]++;
canAdd = false;
}
else {
canAdd = true;
}
i += 1000;
}
}
*/
}
}
// -- 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();
if (atomSize < 0) atomSize += 4294967296L;
// 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) {
LogTools.println("QTReader: invalid atom size: " + atomSize);
}
if (debug) {
debug("Seeking to " + offset +
"; atomType=" + atomType + "; atomSize=" + atomSize);
}
byte[] data = new byte[0];
// 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();
int oldpos = (int) in.getFilePointer();
if (atomType.equals("mdat")) {
// we've found the pixel data
pixelOffset = (int) in.getFilePointer();
pixelBytes = (int) atomSize;
if (pixelBytes > (in.length() - pixelOffset)) {
pixelBytes = (int) (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 (core.sizeX[0] == 0) core.sizeX[0] = in.readInt();
if (core.sizeY[0] == 0) core.sizeY[0] = in.readInt();
}
else if (atomType.equals("cmov")) {
in.skipBytes(8);
byte[] b = new byte[4];
in.read(b);
if ("zlib".equals(new String(b))) {
atomSize = in.readInt();
in.skipBytes(4);
int uncompressedSize = in.readInt();
b = new byte[(int) (atomSize - 12)];
in.read(b);
Inflater inf = new Inflater();
inf.setInput(b, 0, b.length);
byte[] output = new byte[uncompressedSize];
try {
inf.inflate(output);
}
catch (DataFormatException exc) {
if (debug) trace(exc);
throw new FormatException("Compressed header not supported.");
}
inf.end();
RandomAccessStream oldIn = in;
in = new RandomAccessStream(output);
parse(0, 0, output.length);
in.close();
in = oldIn;
}
else throw new FormatException("Compressed header not supported.");
}
else if (atomType.equals("stco")) {
// we've found the plane offsets
if (offsets.size() > 0) break;
spork = false;
in.readInt();
int numPlanes = in.readInt();
if (numPlanes != core.imageCount[0]) {
in.seek(in.getFilePointer() - 4);
int off = in.readInt();
offsets.add(new Integer(off));
for (int i=1; i<core.imageCount[0]; i++) {
if ((chunkSizes.size() > 0) && (i < chunkSizes.size())) {
rawSize = ((Integer) chunkSizes.get(i)).intValue();
}
else i = core.imageCount[0];
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.readInt();
int numEntries = in.readInt();
in.readInt();
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 FormatException("Unsupported codec: " + codec);
}
in.skipBytes(16);
if (in.readShort() == 0) {
in.skipBytes(56);
bitsPerPixel = in.readShort();
if (codec.equals("rpza")) bitsPerPixel = 8;
in.readShort();
in.readDouble();
int fieldsPerPlane = in.read();
interlaced = fieldsPerPlane == 2;
addMeta("Codec", codec);
addMeta("Bits per pixel", new Integer(bitsPerPixel));
in.readDouble();
in.read();
}
}
else {
altCodec = in.readString(4);
addMeta("Second codec", altCodec);
}
}
}
else if (atomType.equals("stsz")) {
// found the number of planes
in.readInt();
rawSize = in.readInt();
core.imageCount[0] = in.readInt();
if (rawSize == 0) {
in.seek(in.getFilePointer() - 4);
for (int b=0; b<core.imageCount[0]; b++) {
chunkSizes.add(new Integer(in.readInt()));
}
}
}
else if (atomType.equals("stsc")) {
in.readInt();
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.readDouble();
in.readInt();
int fps = in.readInt();
addMeta("Frames per second", new Integer(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;
if (debug) print(depth, atomSize, atomType, data);
}
}
/** 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, byte[] data) {
StringBuffer sb = new StringBuffer();
for (int i=0; i<depth; i++) sb.append(" ");
sb.append(type + " : [" + size + "]");
debug(sb.toString());
}
/** Uncompresses an image plane according to the the codec identifier. */
private byte[] uncompress(byte[] pixs, String code)
throws FormatException, IOException
{
if (code.equals("raw ")) return pixs;
else if (code.equals("rle ")) {
Object[] options = new Object[2];
options[0] = new int[] {core.sizeX[0], core.sizeY[0],
bitsPerPixel < 40 ? bitsPerPixel / 8 : (bitsPerPixel - 32) / 8};
options[1] = canUsePrevious ? prevPixels : null;
return new QTRLECodec().decompress(pixs, options);
}
else if (code.equals("rpza")) {
int[] options = new int[2];
options[0] = core.sizeX[0];
options[1] = core.sizeY[0];
return new RPZACodec().decompress(pixs, options);
}
else if (code.equals("mjpb")) {
int[] options = new int[4];
options[0] = core.sizeX[0];
options[1] = core.sizeY[0];
options[2] = bitsPerPixel;
options[3] = interlaced ? 1 : 0;
return new MJPBCodec().decompress(pixs, options);
}
else if (code.equals("jpeg")) {
return new JPEGCodec().decompress(pixs);
}
else throw new FormatException("Unsupported codec : " + code);
}
/** Cut off header bytes from a resource fork file. */
private void stripHeader() throws IOException {
// seek to 4 bytes before first occurence of 'moov'
String test = null;
boolean found = false;
while (!found && in.getFilePointer() < (in.length() - 4)) {
test = in.readString(4);
if (test.equals("moov")) {
found = true;
in.seek(in.getFilePointer() - 8);
}
else in.seek(in.getFilePointer() - 3);
}
}
/** Creates a legacy QT reader. */
private LegacyQTReader createLegacyReader() {
// use the same id mappings that this reader does
return new LegacyQTReader();
}
}