// // QTWriter.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.out; import java.awt.Image; import java.awt.image.*; import java.io.*; import java.util.Vector; import loci.formats.*; /** * QTWriter is the file format writer for uncompressed QuickTime movie files. * * <dl><dt><b>Source code:</b></dt> * <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/out/QTWriter.java">Trac</a>, * <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/out/QTWriter.java">SVN</a></dd></dl> * * @author Melissa Linkert linkert at wisc.edu */ public class QTWriter extends FormatWriter { // -- Constants -- // NB: Writing to Motion JPEG-B with QTJava seems to be broken. /** Value indicating Motion JPEG-B codec. */ public static final int CODEC_MOTION_JPEG_B = 1835692130; /** Value indicating Cinepack codec. */ public static final int CODEC_CINEPAK = 1668704612; /** Value indicating Animation codec. */ public static final int CODEC_ANIMATION = 1919706400; /** Value indicating H.263 codec. */ public static final int CODEC_H_263 = 1748121139; /** Value indicating Sorenson codec. */ public static final int CODEC_SORENSON = 1398165809; /** Value indicating Sorenson 3 codec. */ public static final int CODEC_SORENSON_3 = 0x53565133; /** Value indicating MPEG-4 codec. */ public static final int CODEC_MPEG_4 = 0x6d703476; /** Value indicating Raw codec. */ public static final int CODEC_RAW = 0; /** Value indicating Low quality. */ public static final int QUALITY_LOW = 256; /** Value indicating Normal quality. */ public static final int QUALITY_NORMAL = 512; /** Value indicating High quality. */ public static final int QUALITY_HIGH = 768; /** Value indicating Maximum quality. */ public static final int QUALITY_MAXIMUM = 1023; // -- Fields -- /** Current file. */ protected RandomAccessFile out; /** The codec to use. */ protected int codec = CODEC_RAW; /** The quality to use. */ protected int quality = QUALITY_NORMAL; /** Number of planes written. */ protected int numWritten; /** Seek to this offset to update the total number of pixel bytes. */ protected long byteCountOffset; /** Total number of pixel bytes. */ protected int numBytes; /** Vector of plane offsets. */ protected Vector offsets; /** Time the file was created. */ protected int created; /** Whether we need the legacy writer. */ protected boolean needLegacy = false; /** Legacy QuickTime writer. */ protected LegacyQTWriter legacy; // -- Constructor -- public QTWriter() { super("QuickTime", "mov"); compressionTypes = new String[] { "Uncompressed", // NB: Writing to Motion JPEG-B with QTJava seems to be broken. "Motion JPEG-B", "Cinepak", "Animation", "H.263", "Sorenson", "Sorenson 3", "MPEG 4" }; } // -- QTWriter API methods -- /** * Sets the encoded movie's codec. * @param codec Codec value:<ul> * <li>QTWriter.CODEC_CINEPAK</li> * <li>QTWriter.CODEC_ANIMATION</li> * <li>QTWriter.CODEC_H_263</li> * <li>QTWriter.CODEC_SORENSON</li> * <li>QTWriter.CODEC_SORENSON_3</li> * <li>QTWriter.CODEC_MPEG_4</li> * <li>QTWriter.CODEC_RAW</li> * </ul> */ public void setCodec(int codec) { this.codec = codec; } /** * Sets the quality of the encoded movie. * @param quality Quality value:<ul> * <li>QTWriter.QUALITY_LOW</li> * <li>QTWriter.QUALITY_MEDIUM</li> * <li>QTWriter.QUALITY_HIGH</li> * <li>QTWriter.QUALITY_MAXIMUM</li> * </ul> */ public void setQuality(int quality) { this.quality = quality; } // -- IFormatWriter API methods -- /* @see loci.formats.IFormatWriter#saveImage(Image, boolean) */ public void saveImage(Image image, boolean last) throws FormatException, IOException { if (image == null) throw new FormatException("Image is null"); if (legacy == null) legacy = new LegacyQTWriter(); if (needLegacy) { legacy.setId(currentId); legacy.saveImage(image, last); return; } BufferedImage img = (cm == null) ? ImageTools.makeBuffered(image) : ImageTools.makeBuffered(image, cm); // get the width and height of the image int width = img.getWidth(); int height = img.getHeight(); // retrieve pixel data for this plane byte[][] byteData = ImageTools.getPixelBytes(img, false); // need to check if the width is a multiple of 8 // if it is, great; if not, we need to pad each scanline with enough // bytes to make the width a multiple of 8 int pad = width % 4; pad = (4 - pad) % 4; int bytesPerPixel = byteData[0].length / (width * height); if (bytesPerPixel > 1) { throw new FormatException("Unsupported bits per pixel : " + (8 * bytesPerPixel) + "."); } pad *= bytesPerPixel; byte[][] temp = byteData; byteData = new byte[temp.length][temp[0].length + height*pad]; int rowLength = width * bytesPerPixel; for (int oldScanline=0; oldScanline<height; oldScanline++) { for (int k=0; k<temp.length; k++) { System.arraycopy(temp[k], oldScanline*rowLength, byteData[k], oldScanline*(rowLength + pad), rowLength); } } // invert each pixel // this will makes the colors look right in other readers (e.g. xine), // but needs to be reversed in QTReader if (byteData.length == 1 && bytesPerPixel == 1) { for (int i=0; i<byteData.length; i++) { for (int k=0; k<byteData[0].length; k++) { byteData[i][k] = (byte) (255 - byteData[i][k]); } } } if (!initialized) { initialized = true; setCodec(); if (codec != 0) { needLegacy = true; legacy.setCodec(codec); legacy.setId(currentId); legacy.saveImage(image, last); return; } // -- write the header -- offsets = new Vector(); out = new RandomAccessFile(currentId, "rw"); created = (int) System.currentTimeMillis(); numWritten = 0; numBytes = byteData.length * byteData[0].length; byteCountOffset = 8; if (out.length() == 0) { // -- write the first header -- DataTools.writeInt(out, 8, false); DataTools.writeString(out, "wide"); DataTools.writeInt(out, numBytes + 8, false); DataTools.writeString(out, "mdat"); } else { out.seek(byteCountOffset); numBytes = (int) DataTools.read4UnsignedBytes(out, false) - 8; numWritten = numBytes / (byteData[0].length * byteData.length); numBytes += byteData.length * byteData[0].length; out.seek(byteCountOffset); DataTools.writeInt(out, numBytes + 8, false); for (int i=0; i<numWritten; i++) { offsets.add( new Integer(16 + i * byteData.length * byteData[0].length)); } out.seek(out.length()); } // -- write the first plane of pixel data (mdat) -- offsets.add(new Integer((int) out.length())); numWritten++; for (int i=0; i<byteData.length; i++) { out.write(byteData[i]); } } else { // update the number of pixel bytes written int planeOffset = numBytes; numBytes += (byteData.length * byteData[0].length); out.seek(byteCountOffset); DataTools.writeInt(out, numBytes + 8, false); // write this plane's pixel data out.seek(out.length()); for (int i=0; i<byteData.length; i++) { out.write(byteData[i]); } offsets.add(new Integer(planeOffset + 16)); numWritten++; } if (last) { int timeScale = 100; int duration = numWritten * (timeScale / fps); int bitsPerPixel = (byteData.length > 1) ? bytesPerPixel * 24 : bytesPerPixel * 8 + 32; int channels = (bitsPerPixel >= 40) ? 1 : 3; // -- write moov atom -- int atomLength = 685 + 8*numWritten; DataTools.writeInt(out, atomLength, false); DataTools.writeString(out, "moov"); // -- write mvhd atom -- DataTools.writeInt(out, 108, false); DataTools.writeString(out, "mvhd"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, created, false); // creation time DataTools.writeInt(out, (int) System.currentTimeMillis(), false); DataTools.writeInt(out, timeScale, false); // time scale DataTools.writeInt(out, duration, false); // duration out.write(new byte[] {0, 1, 0, 0}); // preferred rate & volume out.write(new byte[] {0, -1, 0, 0, 0, 0, 0, 0, 0, 0}); // reserved // 3x3 matrix - tells reader how to rotate image DataTools.writeInt(out, 1, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 1, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 16384, false); DataTools.writeShort(out, 0, false); // not sure what this is DataTools.writeInt(out, 0, false); // preview duration DataTools.writeInt(out, 0, false); // preview time DataTools.writeInt(out, 0, false); // poster time DataTools.writeInt(out, 0, false); // selection time DataTools.writeInt(out, 0, false); // selection duration DataTools.writeInt(out, 0, false); // current time DataTools.writeInt(out, 2, false); // next track's id // -- write trak atom -- atomLength -= 116; DataTools.writeInt(out, atomLength, false); DataTools.writeString(out, "trak"); // -- write tkhd atom -- DataTools.writeInt(out, 92, false); DataTools.writeString(out, "tkhd"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 15, false); // flags DataTools.writeInt(out, created, false); // creation time DataTools.writeInt(out, (int) System.currentTimeMillis(), false); DataTools.writeInt(out, 1, false); // track id DataTools.writeInt(out, 0, false); // reserved DataTools.writeInt(out, duration, false); // duration DataTools.writeInt(out, 0, false); // reserved DataTools.writeInt(out, 0, false); // reserved DataTools.writeShort(out, 0, false); // reserved DataTools.writeInt(out, 0, false); // unknown // 3x3 matrix - tells reader how to rotate the image DataTools.writeInt(out, 1, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 1, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 0, false); DataTools.writeInt(out, 16384, false); DataTools.writeInt(out, width, false); // image width DataTools.writeInt(out, height, false); // image height DataTools.writeShort(out, 0, false); // reserved // -- write edts atom -- DataTools.writeInt(out, 36, false); DataTools.writeString(out, "edts"); // -- write elst atom -- DataTools.writeInt(out, 28, false); DataTools.writeString(out, "elst"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, 1, false); // number of entries in the table DataTools.writeInt(out, duration, false); // duration DataTools.writeShort(out, 0, false); // time DataTools.writeInt(out, 1, false); // rate DataTools.writeShort(out, 0, false); // unknown // -- write mdia atom -- atomLength -= 136; DataTools.writeInt(out, atomLength, false); DataTools.writeString(out, "mdia"); // -- write mdhd atom -- DataTools.writeInt(out, 32, false); DataTools.writeString(out, "mdhd"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, created, false); // creation time DataTools.writeInt(out, (int) System.currentTimeMillis(), false); DataTools.writeInt(out, timeScale, false); // time scale DataTools.writeInt(out, duration, false); // duration DataTools.writeShort(out, 0, false); // language DataTools.writeShort(out, 0, false); // quality // -- write hdlr atom -- DataTools.writeInt(out, 58, false); DataTools.writeString(out, "hdlr"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeString(out, "mhlr"); DataTools.writeString(out, "vide"); DataTools.writeString(out, "appl"); out.write(new byte[] {16, 0, 0, 0, 0, 1, 1, 11, 25}); DataTools.writeString(out, "Apple Video Media Handler"); // -- write minf atom -- atomLength -= 98; DataTools.writeInt(out, atomLength, false); DataTools.writeString(out, "minf"); // -- write vmhd atom -- DataTools.writeInt(out, 20, false); DataTools.writeString(out, "vmhd"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 1, false); // flags DataTools.writeShort(out, 64, false); // graphics mode DataTools.writeShort(out, 32768, false); // opcolor 1 DataTools.writeShort(out, 32768, false); // opcolor 2 DataTools.writeShort(out, 32768, false); // opcolor 3 // -- write hdlr atom -- DataTools.writeInt(out, 57, false); DataTools.writeString(out, "hdlr"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeString(out, "dhlr"); DataTools.writeString(out, "alis"); DataTools.writeString(out, "appl"); out.write(new byte[] {16, 0, 0, 1, 0, 1, 1, 31, 24}); DataTools.writeString(out, "Apple Alias Data Handler"); // -- write dinf atom -- DataTools.writeInt(out, 36, false); DataTools.writeString(out, "dinf"); // -- write dref atom -- DataTools.writeInt(out, 28, false); DataTools.writeString(out, "dref"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeShort(out, 0, false); // version 2 DataTools.writeShort(out, 1, false); // flags 2 out.write(new byte[] {0, 0, 0, 12}); DataTools.writeString(out, "alis"); DataTools.writeShort(out, 0, false); // version 3 DataTools.writeShort(out, 1, false); // flags 3 // -- write stbl atom -- atomLength -= 121; DataTools.writeInt(out, atomLength, false); DataTools.writeString(out, "stbl"); // -- write stsd atom -- DataTools.writeInt(out, 118, false); DataTools.writeString(out, "stsd"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, 1, false); // number of entries in the table out.write(new byte[] {0, 0, 0, 102}); DataTools.writeString(out, "raw "); // codec out.write(new byte[] {0, 0, 0, 0, 0, 0}); // reserved DataTools.writeShort(out, 1, false); // data reference DataTools.writeShort(out, 1, false); // version DataTools.writeShort(out, 1, false); // revision DataTools.writeString(out, "appl"); DataTools.writeInt(out, 0, false); // temporal quality DataTools.writeInt(out, 768, false); // spatial quality DataTools.writeShort(out, width, false); // image width DataTools.writeShort(out, height, false); // image height out.write(new byte[] {0, 72, 0, 0}); // horizontal dpi out.write(new byte[] {0, 72, 0, 0}); // vertical dpi DataTools.writeInt(out, 0, false); // data size DataTools.writeShort(out, 1, false); // frames per sample DataTools.writeShort(out, 12, false); // length of compressor name DataTools.writeString(out, "Uncompressed"); // compressor name DataTools.writeInt(out, bitsPerPixel, false); // unknown DataTools.writeInt(out, bitsPerPixel, false); // unknown DataTools.writeInt(out, bitsPerPixel, false); // unknown DataTools.writeInt(out, bitsPerPixel, false); // unknown DataTools.writeInt(out, bitsPerPixel, false); // unknown DataTools.writeShort(out, bitsPerPixel, false); // bits per pixel DataTools.writeInt(out, 65535, false); // ctab ID out.write(new byte[] {12, 103, 97, 108}); // gamma out.write(new byte[] {97, 1, -52, -52, 0, 0, 0, 0}); // unknown // -- write stts atom -- DataTools.writeInt(out, 24, false); DataTools.writeString(out, "stts"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, 1, false); // number of entries in the table DataTools.writeInt(out, numWritten, false); // number of planes DataTools.writeInt(out, (timeScale / fps), false); // frames per second // -- write stsc atom -- DataTools.writeInt(out, 28, false); DataTools.writeString(out, "stsc"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, 1, false); // number of entries in the table DataTools.writeInt(out, 1, false); // chunk DataTools.writeInt(out, 1, false); // samples DataTools.writeInt(out, 1, false); // id // -- write stsz atom -- DataTools.writeInt(out, 20 + 4*numWritten, false); DataTools.writeString(out, "stsz"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, 0, false); // sample size DataTools.writeInt(out, numWritten, false); // number of planes for (int i=0; i<numWritten; i++) { // sample size DataTools.writeInt(out, channels*height*(width+pad)*bytesPerPixel, false); } // -- write stco atom -- DataTools.writeInt(out, 16 + 4*numWritten, false); DataTools.writeString(out, "stco"); DataTools.writeShort(out, 0, false); // version DataTools.writeShort(out, 0, false); // flags DataTools.writeInt(out, numWritten, false); // number of planes for (int i=0; i<numWritten; i++) { // write the plane offset DataTools.writeInt(out, ((Integer) offsets.get(i)).intValue(), false); } out.close(); } } /* @see loci.formats.IFormatWriter#canDoStacks() */ public boolean canDoStacks() { return true; } /* @see loci.formats.IFormatWriter#getPixelTypes(String) */ public int[] getPixelTypes() { return new int[] {FormatTools.UINT8, FormatTools.UINT16}; } // -- IFormatHandler API methods -- /* @see loci.formats.IFormatHandler#close() */ public void close() throws IOException { if (out != null) out.close(); out = null; numWritten = 0; byteCountOffset = 0; numBytes = 0; created = 0; offsets = null; currentId = null; initialized = false; } // -- Helper methods -- private void setCodec() { if (compression == null) return; if (compression.equals("Uncompressed")) codec = CODEC_RAW; // NB: Writing to Motion JPEG-B with QTJava seems to be broken. else if (compression.equals("Motion JPEG-B")) codec = CODEC_MOTION_JPEG_B; else if (compression.equals("Cinepak")) codec = CODEC_CINEPAK; else if (compression.equals("Animation")) codec = CODEC_ANIMATION; else if (compression.equals("H.263")) codec = CODEC_H_263; else if (compression.equals("Sorenson")) codec = CODEC_SORENSON; else if (compression.equals("Sorenson 3")) codec = CODEC_SORENSON_3; else if (compression.equals("MPEG 4")) codec = CODEC_MPEG_4; } }