Mercurial > jmeCapture
view src/ca/randelshofer/AVIOutputStream.java @ 39:784a3f4e6202
updating capture-video
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Thu, 03 Nov 2011 16:00:46 -0700 |
parents | 4c5fc53778c1 |
children | f2c5f1fca9a7 |
line wrap: on
line source
1 /**2 * @(#)AVIOutputStream.java 1.5.1 2011-01-173 *4 * Copyright (c) 2008-2011 Werner Randelshofer, Immensee, Switzerland.5 * All rights reserved.6 *7 * You may not use, copy or modify this file, except in compliance with the8 * license agreement you entered into with Werner Randelshofer.9 * For details see accompanying license terms.10 */11 package ca.randelshofer;13 import java.awt.Dimension;14 import java.awt.image.BufferedImage;15 import java.awt.image.DataBufferByte;16 import java.awt.image.IndexColorModel;17 import java.awt.image.WritableRaster;18 import java.io.File;19 import java.io.FileInputStream;20 import java.io.IOException;21 import java.io.InputStream;22 import java.io.OutputStream;23 import java.util.Arrays;24 import java.util.Date;25 import java.util.LinkedList;27 import javax.imageio.IIOImage;28 import javax.imageio.ImageIO;29 import javax.imageio.ImageWriteParam;30 import javax.imageio.ImageWriter;31 import javax.imageio.stream.FileImageOutputStream;32 import javax.imageio.stream.ImageOutputStream;33 import javax.imageio.stream.MemoryCacheImageOutputStream;35 /**36 * This class supports writing of images into an AVI 1.0 video file.37 * <p>38 * The images are written as video frames.39 * <p>40 * Video frames can be encoded with one of the following formats:41 * <ul>42 * <li>JPEG</li>43 * <li>PNG</li>44 * <li>RAW</li>45 * <li>RLE</li>46 * </ul>47 * All frames must have the same format.48 * When JPG is used each frame can have an individual encoding quality.49 * <p>50 * All frames in an AVI file must have the same duration. The duration can51 * be set by setting an appropriate pair of values using methods52 * {@link #setFrameRate} and {@link #setTimeScale}.53 * <p>54 * The length of an AVI 1.0 file is limited to 1 GB.55 * This class supports lengths of up to 4 GB, but such files may not work on56 * all players.57 * <p>58 * For detailed information about the AVI RIFF file format see:<br>59 * <a href="http://msdn.microsoft.com/en-us/library/ms779636.aspx">msdn.microsoft.com AVI RIFF</a><br>60 * <a href="http://www.microsoft.com/whdc/archive/fourcc.mspx">www.microsoft.com FOURCC for Video Compression</a><br>61 * <a href="http://www.saettler.com/RIFFMCI/riffmci.html">www.saettler.com RIFF</a><br>62 *63 * @author Werner Randelshofer64 * @version 1.5.1 2011-01-17 Fixes unintended closing of output stream..65 * <br>1.5 2011-01-06 Adds support for RLE 8-bit video format.66 * <br>1.4 2011-01-04 Adds support for RAW 4-bit and 8-bit video format. Fixes offsets67 * in "idx1" chunk.68 * <br>1.3.2 2010-12-27 File size limit is 1 GB.69 * <br>1.3.1 2010-07-19 Fixes seeking and calculation of offsets.70 * <br>1.3 2010-07-08 Adds constructor with ImageOutputStream.71 * Added method getVideoDimension().72 * <br>1.2 2009-08-29 Adds support for RAW video format.73 * <br>1.1 2008-08-27 Fixes computation of dwMicroSecPerFrame in avih74 * chunk. Changed the API to reflect that AVI works with frame rates instead of75 * with frame durations.76 * <br>1.0.1 2008-08-13 Uses FourCC "MJPG" instead of "jpg " for JPG77 * encoded video.78 * <br>1.0 2008-08-11 Created.79 */80 public class AVIOutputStream {82 /**83 * Underlying output stream.84 */85 private ImageOutputStream out;86 /** The offset of the QuickTime stream in the underlying ImageOutputStream.87 * Normally this is 0 unless the underlying stream already contained data88 * when it was passed to the constructor.89 */90 private long streamOffset;91 /** Previous frame for delta compression. */93 /**94 * Supported video encodings.95 */96 public static enum VideoFormat {98 RAW, RLE, JPG, PNG;99 }100 /**101 * Current video formats.102 */103 private VideoFormat videoFormat;104 /**105 * Quality of JPEG encoded video frames.106 */107 private float quality = 0.9f;108 /**109 * Width of the video frames. All frames must have the same width.110 * The value -1 is used to mark unspecified width.111 */112 private int imgWidth = -1;113 /**114 * Height of the video frames. All frames must have the same height.115 * The value -1 is used to mark unspecified height.116 */117 private int imgHeight = -1;118 /** Number of bits per pixel. */119 private int imgDepth = 24;120 /** Index color model for RAW_RGB4 and RAW_RGB8 formats. */121 private IndexColorModel palette;122 private IndexColorModel previousPalette;123 /** Video encoder. */125 /**126 * The timeScale of the movie.127 * <p>128 * Used with frameRate to specify the time scale that this stream will use.129 * Dividing frameRate by timeScale gives the number of samples per second.130 * For video streams, this is the frame rate. For audio streams, this rate131 * corresponds to the time needed to play nBlockAlign bytes of audio, which132 * for PCM audio is the just the sample rate.133 */134 private int timeScale = 1;135 /**136 * The frameRate of the movie in timeScale units.137 * <p>138 * @see timeScale139 */140 private int frameRate = 30;141 /**142 * The states of the movie output stream.143 */144 private static enum States {146 STARTED, FINISHED, CLOSED;147 }148 /**149 * The current state of the movie output stream.150 */151 private States state = States.FINISHED;153 /**154 * AVI stores media data in samples.155 * A sample is a single element in a sequence of time-ordered data.156 */157 private static class Sample {159 String chunkType;160 /** Offset of the sample relative to the start of the AVI file.161 */162 long offset;163 /** Data length of the sample. */164 long length;165 /** Whether the sample is a sync-sample. */166 boolean isSync;168 /**169 * Creates a new sample.170 * @param duration171 * @param offset172 * @param length173 */174 public Sample(String chunkId, int duration, long offset, long length, boolean isSync) {175 this.chunkType = chunkId;176 this.offset = offset;177 this.length = length;178 this.isSync = isSync;179 }180 }181 /**182 * List of video frames.183 */184 private LinkedList<Sample> videoFrames;185 /**186 * This chunk holds the whole AVI content.187 */188 private CompositeChunk aviChunk;189 /**190 * This chunk holds the movie frames.191 */192 private CompositeChunk moviChunk;193 /**194 * This chunk holds the AVI Main Header.195 */196 FixedSizeDataChunk avihChunk;197 /**198 * This chunk holds the AVI Stream Header.199 */200 FixedSizeDataChunk strhChunk;201 /**202 * This chunk holds the AVI Stream Format Header.203 */204 FixedSizeDataChunk strfChunk;206 /**207 * Chunk base class.208 */209 private abstract class Chunk {211 /**212 * The chunkType of the chunk. A String with the length of 4 characters.213 */214 protected String chunkType;215 /**216 * The offset of the chunk relative to the start of the217 * ImageOutputStream.218 */219 protected long offset;221 /**222 * Creates a new Chunk at the current position of the ImageOutputStream.223 * @param chunkType The chunkType of the chunk. A string with a length of 4 characters.224 */225 public Chunk(String chunkType) throws IOException {226 this.chunkType = chunkType;227 offset = getRelativeStreamPosition();228 }230 /**231 * Writes the chunk to the ImageOutputStream and disposes it.232 */233 public abstract void finish() throws IOException;235 /**236 * Returns the size of the chunk including the size of the chunk header.237 * @return The size of the chunk.238 */239 public abstract long size();240 }242 /**243 * A CompositeChunk contains an ordered list of Chunks.244 */245 private class CompositeChunk extends Chunk {247 /**248 * The type of the composite. A String with the length of 4 characters.249 */250 protected String compositeType;251 private LinkedList<Chunk> children;252 private boolean finished;254 /**255 * Creates a new CompositeChunk at the current position of the256 * ImageOutputStream.257 * @param compositeType The type of the composite.258 * @param chunkType The type of the chunk.259 */260 public CompositeChunk(String compositeType, String chunkType) throws IOException {261 super(chunkType);262 this.compositeType = compositeType;263 //out.write264 out.writeLong(0); // make room for the chunk header265 out.writeInt(0); // make room for the chunk header266 children = new LinkedList<Chunk>();267 }269 public void add(Chunk child) throws IOException {270 if (children.size() > 0) {271 children.getLast().finish();272 }273 children.add(child);274 }276 /**277 * Writes the chunk and all its children to the ImageOutputStream278 * and disposes of all resources held by the chunk.279 * @throws java.io.IOException280 */281 @Override282 public void finish() throws IOException {283 if (!finished) {284 if (size() > 0xffffffffL) {285 throw new IOException("CompositeChunk \"" + chunkType + "\" is too large: " + size());286 }288 long pointer = getRelativeStreamPosition();289 seekRelative(offset);291 DataChunkOutputStream headerData = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false);292 headerData.writeType(compositeType);293 headerData.writeUInt(size() - 8);294 headerData.writeType(chunkType);295 for (Chunk child : children) {296 child.finish();297 }298 seekRelative(pointer);299 if (size() % 2 == 1) {300 out.writeByte(0); // write pad byte301 }302 finished = true;303 }304 }306 @Override307 public long size() {308 long length = 12;309 for (Chunk child : children) {310 length += child.size() + child.size() % 2;311 }312 return length;313 }314 }316 /**317 * Data Chunk.318 */319 private class DataChunk extends Chunk {321 private DataChunkOutputStream data;322 private boolean finished;324 /**325 * Creates a new DataChunk at the current position of the326 * ImageOutputStream.327 * @param chunkType The chunkType of the chunk.328 */329 public DataChunk(String name) throws IOException {330 super(name);331 out.writeLong(0); // make room for the chunk header332 data = new DataChunkOutputStream(new ImageOutputStreamAdapter(out), false);333 }335 public DataChunkOutputStream getOutputStream() {336 if (finished) {337 throw new IllegalStateException("DataChunk is finished");338 }339 return data;340 }342 @Override343 public void finish() throws IOException {344 if (!finished) {345 long sizeBefore = size();347 if (size() > 0xffffffffL) {348 throw new IOException("DataChunk \"" + chunkType + "\" is too large: " + size());349 }351 long pointer = getRelativeStreamPosition();352 seekRelative(offset);354 DataChunkOutputStream headerData = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false);355 headerData.writeType(chunkType);356 headerData.writeUInt(size() - 8);357 seekRelative(pointer);358 if (size() % 2 == 1) {359 out.writeByte(0); // write pad byte360 }361 finished = true;362 long sizeAfter = size();363 if (sizeBefore != sizeAfter) {364 System.err.println("size mismatch " + sizeBefore + ".." + sizeAfter);365 }366 }367 }369 @Override370 public long size() {371 return 8 + data.size();372 }373 }375 /**376 * A DataChunk with a fixed size.377 */378 private class FixedSizeDataChunk extends Chunk {380 private DataChunkOutputStream data;381 private boolean finished;382 private long fixedSize;384 /**385 * Creates a new DataChunk at the current position of the386 * ImageOutputStream.387 * @param chunkType The chunkType of the chunk.388 */389 public FixedSizeDataChunk(String chunkType, long fixedSize) throws IOException {390 super(chunkType);391 this.fixedSize = fixedSize;392 data = new DataChunkOutputStream(new ImageOutputStreamAdapter(out),false);393 data.writeType(chunkType);394 data.writeUInt(fixedSize);395 data.clearCount();397 // Fill fixed size with nulls398 byte[] buf = new byte[(int) Math.min(512, fixedSize)];399 long written = 0;400 while (written < fixedSize) {401 data.write(buf, 0, (int) Math.min(buf.length, fixedSize - written));402 written += Math.min(buf.length, fixedSize - written);403 }404 if (fixedSize % 2 == 1) {405 out.writeByte(0); // write pad byte406 }407 seekToStartOfData();408 }410 public DataChunkOutputStream getOutputStream() {411 /*if (finished) {412 throw new IllegalStateException("DataChunk is finished");413 }*/414 return data;415 }417 public void seekToStartOfData() throws IOException {418 seekRelative(offset + 8);419 data.clearCount();420 }422 public void seekToEndOfChunk() throws IOException {423 seekRelative(offset + 8 + fixedSize + fixedSize % 2);424 }426 @Override427 public void finish() throws IOException {428 if (!finished) {429 finished = true;430 }431 }433 @Override434 public long size() {435 return 8 + fixedSize;436 }437 }439 /**440 * Creates a new AVI file with the specified video format and441 * frame rate. The video has 24 bits per pixel.442 *443 * @param file the output file444 * @param format Selects an encoder for the video format.445 * @param bitsPerPixel the number of bits per pixel.446 * @exception IllegalArgumentException if videoFormat is null or if447 * frame rate is <= 0448 */449 public AVIOutputStream(File file, VideoFormat format) throws IOException {450 this(file,format,24);451 }452 /**453 * Creates a new AVI file with the specified video format and454 * frame rate.455 *456 * @param file the output file457 * @param format Selects an encoder for the video format.458 * @param bitsPerPixel the number of bits per pixel.459 * @exception IllegalArgumentException if videoFormat is null or if460 * frame rate is <= 0461 */462 public AVIOutputStream(File file, VideoFormat format, int bitsPerPixel) throws IOException {463 if (format == null) {464 throw new IllegalArgumentException("format must not be null");465 }467 if (file.exists()) {468 file.delete();469 }470 this.out = new FileImageOutputStream(file);471 this.streamOffset = 0;472 this.videoFormat = format;473 this.videoFrames = new LinkedList<Sample>();474 this.imgDepth = bitsPerPixel;475 if (imgDepth == 4) {476 byte[] gray = new byte[16];477 for (int i = 0; i < gray.length; i++) {478 gray[i] = (byte) ((i << 4) | i);479 }480 palette = new IndexColorModel(4, 16, gray, gray, gray);481 } else if (imgDepth == 8) {482 byte[] gray = new byte[256];483 for (int i = 0; i < gray.length; i++) {484 gray[i] = (byte) i;485 }486 palette = new IndexColorModel(8, 256, gray, gray, gray);487 }489 }491 /**492 * Creates a new AVI output stream with the specified video format and493 * framerate.494 *495 * @param out the underlying output stream496 * @param format Selects an encoder for the video format.497 * @exception IllegalArgumentException if videoFormat is null or if498 * framerate is <= 0499 */500 public AVIOutputStream(ImageOutputStream out, VideoFormat format) throws IOException {501 if (format == null) {502 throw new IllegalArgumentException("format must not be null");503 }504 this.out = out;505 this.streamOffset = out.getStreamPosition();506 this.videoFormat = format;507 this.videoFrames = new LinkedList<Sample>();508 }510 /**511 * Used with frameRate to specify the time scale that this stream will use.512 * Dividing frameRate by timeScale gives the number of samples per second.513 * For video streams, this is the frame rate. For audio streams, this rate514 * corresponds to the time needed to play nBlockAlign bytes of audio, which515 * for PCM audio is the just the sample rate.516 * <p>517 * The default value is 1.518 *519 * @param newValue520 */521 public void setTimeScale(int newValue) {522 if (newValue <= 0) {523 throw new IllegalArgumentException("timeScale must be greater 0");524 }525 this.timeScale = newValue;526 }528 /**529 * Returns the time scale of this media.530 *531 * @return time scale532 */533 public int getTimeScale() {534 return timeScale;535 }537 /**538 * Sets the rate of video frames in time scale units.539 * <p>540 * The default value is 30. Together with the default value 1 of timeScale541 * this results in 30 frames pers second.542 *543 * @param newValue544 */545 public void setFrameRate(int newValue) {546 if (newValue <= 0) {547 throw new IllegalArgumentException("frameDuration must be greater 0");548 }549 if (state == States.STARTED) {550 throw new IllegalStateException("frameDuration must be set before the first frame is written");551 }552 this.frameRate = newValue;553 }555 /**556 * Returns the frame rate of this media.557 *558 * @return frame rate559 */560 public int getFrameRate() {561 return frameRate;562 }564 /** Sets the global color palette. */565 public void setPalette(IndexColorModel palette) {566 this.palette = palette;567 }569 /**570 * Sets the compression quality of the video track.571 * A value of 0 stands for "high compression is important" a value of572 * 1 for "high image quality is important".573 * <p>574 * Changing this value affects frames which are subsequently written575 * to the AVIOutputStream. Frames which have already been written576 * are not changed.577 * <p>578 * This value has only effect on videos encoded with JPG format.579 * <p>580 * The default value is 0.9.581 *582 * @param newValue583 */584 public void setVideoCompressionQuality(float newValue) {585 this.quality = newValue;586 }588 /**589 * Returns the video compression quality.590 *591 * @return video compression quality592 */593 public float getVideoCompressionQuality() {594 return quality;595 }597 /**598 * Sets the dimension of the video track.599 * <p>600 * You need to explicitly set the dimension, if you add all frames from601 * files or input streams.602 * <p>603 * If you add frames from buffered images, then AVIOutputStream604 * can determine the video dimension from the image width and height.605 *606 * @param width Must be greater than 0.607 * @param height Must be greater than 0.608 */609 public void setVideoDimension(int width, int height) {610 if (width < 1 || height < 1) {611 throw new IllegalArgumentException("width and height must be greater zero.");612 }613 this.imgWidth = width;614 this.imgHeight = height;615 }617 /**618 * Gets the dimension of the video track.619 * <p>620 * Returns null if the dimension is not known.621 */622 public Dimension getVideoDimension() {623 if (imgWidth < 1 || imgHeight < 1) {624 return null;625 }626 return new Dimension(imgWidth, imgHeight);627 }629 /**630 * Sets the state of the AVIOutputStream to 'started'.631 * <p>632 * If the state is changed by this method, the prolog is633 * written.634 */635 private void ensureStarted() throws IOException {636 if (state != States.STARTED) {637 new Date();638 writeProlog();639 state = States.STARTED;640 }641 }643 /**644 * Writes a frame to the video track.645 * <p>646 * If the dimension of the video track has not been specified yet, it647 * is derived from the first buffered image added to the AVIOutputStream.648 *649 * @param image The frame image.650 *651 * @throws IllegalArgumentException if the duration is less than 1, or652 * if the dimension of the frame does not match the dimension of the video653 * track.654 * @throws IOException if writing the image failed.655 */656 public void writeFrame(BufferedImage image) throws IOException {657 ensureOpen();658 ensureStarted();660 // Get the dimensions of the first image661 if (imgWidth == -1) {662 imgWidth = image.getWidth();663 imgHeight = image.getHeight();664 } else {665 // The dimension of the image must match the dimension of the video track666 if (imgWidth != image.getWidth() || imgHeight != image.getHeight()) {667 throw new IllegalArgumentException("Dimensions of image[" + videoFrames.size()668 + "] (width=" + image.getWidth() + ", height=" + image.getHeight()669 + ") differs from image[0] (width="670 + imgWidth + ", height=" + imgHeight);671 }672 }674 DataChunk videoFrameChunk;675 long offset = getRelativeStreamPosition();676 boolean isSync = true;677 switch (videoFormat) {678 case RAW: {679 switch (imgDepth) {680 case 4: {681 IndexColorModel imgPalette = (IndexColorModel) image.getColorModel();682 int[] imgRGBs = new int[16];683 imgPalette.getRGBs(imgRGBs);684 int[] previousRGBs = new int[16];685 if (previousPalette == null) {686 previousPalette = palette;687 }688 previousPalette.getRGBs(previousRGBs);689 if (!Arrays.equals(imgRGBs, previousRGBs)) {690 previousPalette = imgPalette;691 DataChunk paletteChangeChunk = new DataChunk("00pc");692 /*693 int first = imgPalette.getMapSize();694 int last = -1;695 for (int i = 0; i < 16; i++) {696 if (previousRGBs[i] != imgRGBs[i] && i < first) {697 first = i;698 }699 if (previousRGBs[i] != imgRGBs[i] && i > last) {700 last = i;701 }702 }*/703 int first = 0;704 int last = imgPalette.getMapSize() - 1;705 /*706 * typedef struct {707 BYTE bFirstEntry;708 BYTE bNumEntries;709 WORD wFlags;710 PALETTEENTRY peNew[];711 } AVIPALCHANGE;712 *713 * typedef struct tagPALETTEENTRY {714 BYTE peRed;715 BYTE peGreen;716 BYTE peBlue;717 BYTE peFlags;718 } PALETTEENTRY;719 */720 DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream();721 pOut.writeByte(first);//bFirstEntry722 pOut.writeByte(last - first + 1);//bNumEntries723 pOut.writeShort(0);//wFlags725 for (int i = first; i <= last; i++) {726 pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red727 pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green728 pOut.writeByte(imgRGBs[i] & 0xff); // blue729 pOut.writeByte(0); // reserved*/730 }732 moviChunk.add(paletteChangeChunk);733 paletteChangeChunk.finish();734 long length = getRelativeStreamPosition() - offset;735 videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false));736 offset = getRelativeStreamPosition();737 }739 videoFrameChunk = new DataChunk("00db");740 byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();741 byte[] rgb4 = new byte[imgWidth / 2];742 for (int y = (imgHeight - 1) * imgWidth; y >= 0; y -= imgWidth) { // Upside down743 for (int x = 0, xx = 0, n = imgWidth; x < n; x += 2, ++xx) {744 rgb4[xx] = (byte) (((rgb8[y + x] & 0xf) << 4) | (rgb8[y + x + 1] & 0xf));745 }746 videoFrameChunk.getOutputStream().write(rgb4);747 }748 break;749 }750 case 8: {751 IndexColorModel imgPalette = (IndexColorModel) image.getColorModel();752 int[] imgRGBs = new int[256];753 imgPalette.getRGBs(imgRGBs);754 int[] previousRGBs = new int[256];755 if (previousPalette == null) {756 previousPalette = palette;757 }758 previousPalette.getRGBs(previousRGBs);759 if (!Arrays.equals(imgRGBs, previousRGBs)) {760 previousPalette = imgPalette;761 DataChunk paletteChangeChunk = new DataChunk("00pc");762 /*763 int first = imgPalette.getMapSize();764 int last = -1;765 for (int i = 0; i < 16; i++) {766 if (previousRGBs[i] != imgRGBs[i] && i < first) {767 first = i;768 }769 if (previousRGBs[i] != imgRGBs[i] && i > last) {770 last = i;771 }772 }*/773 int first = 0;774 int last = imgPalette.getMapSize() - 1;775 /*776 * typedef struct {777 BYTE bFirstEntry;778 BYTE bNumEntries;779 WORD wFlags;780 PALETTEENTRY peNew[];781 } AVIPALCHANGE;782 *783 * typedef struct tagPALETTEENTRY {784 BYTE peRed;785 BYTE peGreen;786 BYTE peBlue;787 BYTE peFlags;788 } PALETTEENTRY;789 */790 DataChunkOutputStream pOut = paletteChangeChunk.getOutputStream();791 pOut.writeByte(first);//bFirstEntry792 pOut.writeByte(last - first + 1);//bNumEntries793 pOut.writeShort(0);//wFlags795 for (int i = first; i <= last; i++) {796 pOut.writeByte((imgRGBs[i] >>> 16) & 0xff); // red797 pOut.writeByte((imgRGBs[i] >>> 8) & 0xff); // green798 pOut.writeByte(imgRGBs[i] & 0xff); // blue799 pOut.writeByte(0); // reserved*/800 }802 moviChunk.add(paletteChangeChunk);803 paletteChangeChunk.finish();804 long length = getRelativeStreamPosition() - offset;805 videoFrames.add(new Sample(paletteChangeChunk.chunkType, 0, offset, length - 8, false));806 offset = getRelativeStreamPosition();807 }809 videoFrameChunk = new DataChunk("00db");810 byte[] rgb8 = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();811 for (int y = (imgHeight - 1) * imgWidth; y >= 0; y -= imgWidth) { // Upside down812 videoFrameChunk.getOutputStream().write(rgb8, y, imgWidth);813 }814 break;815 }816 default: {817 videoFrameChunk = new DataChunk("00db");818 WritableRaster raster = image.getRaster();819 int[] raw = new int[imgWidth * 3]; // holds a scanline of raw image data with 3 channels of 32 bit data820 byte[] bytes = new byte[imgWidth * 3]; // holds a scanline of raw image data with 3 channels of 8 bit data821 for (int y = imgHeight - 1; y >= 0; --y) { // Upside down822 raster.getPixels(0, y, imgWidth, 1, raw);823 for (int x = 0, n = imgWidth * 3; x < n; x += 3) {824 bytes[x + 2] = (byte) raw[x]; // Blue825 bytes[x + 1] = (byte) raw[x + 1]; // Green826 bytes[x] = (byte) raw[x + 2]; // Red827 }828 videoFrameChunk.getOutputStream().write(bytes);829 }830 break;831 }832 }833 break;834 }836 case JPG: {837 videoFrameChunk = new DataChunk("00dc");838 ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/jpeg").next();839 ImageWriteParam iwParam = iw.getDefaultWriteParam();840 iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);841 iwParam.setCompressionQuality(quality);842 MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(videoFrameChunk.getOutputStream());843 iw.setOutput(imgOut);844 IIOImage img = new IIOImage(image, null, null);845 iw.write(null, img, iwParam);846 iw.dispose();847 break;848 }849 case PNG:850 default: {851 videoFrameChunk = new DataChunk("00dc");852 ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/png").next();853 ImageWriteParam iwParam = iw.getDefaultWriteParam();854 MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(videoFrameChunk.getOutputStream());855 iw.setOutput(imgOut);856 IIOImage img = new IIOImage(image, null, null);857 iw.write(null, img, iwParam);858 iw.dispose();859 break;860 }861 }862 long length = getRelativeStreamPosition() - offset;863 moviChunk.add(videoFrameChunk);864 videoFrameChunk.finish();866 videoFrames.add(new Sample(videoFrameChunk.chunkType, frameRate, offset, length - 8, isSync));867 if (getRelativeStreamPosition() > 1L << 32) {868 throw new IOException("AVI file is larger than 4 GB");869 }870 }872 /**873 * Writes a frame from a file to the video track.874 * <p>875 * This method does not inspect the contents of the file.876 * For example, Its your responsibility to only add JPG files if you have877 * chosen the JPEG video format.878 * <p>879 * If you add all frames from files or from input streams, then you880 * have to explicitly set the dimension of the video track before you881 * call finish() or close().882 *883 * @param file The file which holds the image data.884 *885 * @throws IllegalStateException if the duration is less than 1.886 * @throws IOException if writing the image failed.887 */888 public void writeFrame(File file) throws IOException {889 FileInputStream in = null;890 try {891 in = new FileInputStream(file);892 writeFrame(in);893 } finally {894 if (in != null) {895 in.close();896 }897 }898 }900 /**901 * Writes a frame to the video track.902 * <p>903 * This method does not inspect the contents of the file.904 * For example, its your responsibility to only add JPG files if you have905 * chosen the JPEG video format.906 * <p>907 * If you add all frames from files or from input streams, then you908 * have to explicitly set the dimension of the video track before you909 * call finish() or close().910 *911 * @param in The input stream which holds the image data.912 *913 * @throws IllegalArgumentException if the duration is less than 1.914 * @throws IOException if writing the image failed.915 */916 public void writeFrame(InputStream in) throws IOException {917 ensureOpen();918 ensureStarted();920 DataChunk videoFrameChunk = new DataChunk(921 videoFormat == VideoFormat.RAW ? "00db" : "00dc");922 moviChunk.add(videoFrameChunk);923 OutputStream mdatOut = videoFrameChunk.getOutputStream();924 long offset = getRelativeStreamPosition();925 byte[] buf = new byte[512];926 int len;927 while ((len = in.read(buf)) != -1) {928 mdatOut.write(buf, 0, len);929 }930 long length = getRelativeStreamPosition() - offset;931 videoFrameChunk.finish();932 videoFrames.add(new Sample(videoFrameChunk.chunkType, frameRate, offset, length - 8, true));933 if (getRelativeStreamPosition() > 1L << 32) {934 throw new IOException("AVI file is larger than 4 GB");935 }936 }938 /**939 * Closes the movie file as well as the stream being filtered.940 *941 * @exception IOException if an I/O error has occurred942 */943 public void close() throws IOException {944 if (state == States.STARTED) {945 finish();946 }947 if (state != States.CLOSED) {948 out.close();949 state = States.CLOSED;950 }951 }953 /**954 * Finishes writing the contents of the AVI output stream without closing955 * the underlying stream. Use this method when applying multiple filters956 * in succession to the same output stream.957 *958 * @exception IllegalStateException if the dimension of the video track959 * has not been specified or determined yet.960 * @exception IOException if an I/O exception has occurred961 */962 public void finish() throws IOException {963 ensureOpen();964 if (state != States.FINISHED) {965 if (imgWidth == -1 || imgHeight == -1) {966 throw new IllegalStateException("image width and height must be specified");967 }969 moviChunk.finish();970 writeEpilog();971 state = States.FINISHED;972 imgWidth = imgHeight = -1;973 }974 }976 /**977 * Check to make sure that this stream has not been closed978 */979 private void ensureOpen() throws IOException {980 if (state == States.CLOSED) {981 throw new IOException("Stream closed");982 }983 }985 /** Gets the position relative to the beginning of the QuickTime stream.986 * <p>987 * Usually this value is equal to the stream position of the underlying988 * ImageOutputStream, but can be larger if the underlying stream already989 * contained data.990 *991 * @return The relative stream position.992 * @throws IOException993 */994 private long getRelativeStreamPosition() throws IOException {995 return out.getStreamPosition() - streamOffset;996 }998 /** Seeks relative to the beginning of the QuickTime stream.999 * <p>1000 * Usually this equal to seeking in the underlying ImageOutputStream, but1001 * can be different if the underlying stream already contained data.1002 *1003 */1004 private void seekRelative(long newPosition) throws IOException {1005 out.seek(newPosition + streamOffset);1006 }1008 private void writeProlog() throws IOException {1009 // The file has the following structure:1010 //1011 // .RIFF AVI1012 // ..avih (AVI Header Chunk)1013 // ..LIST strl1014 // ...strh (Stream Header Chunk)1015 // ...strf (Stream Format Chunk)1016 // ..LIST movi1017 // ...00dc (Compressed video data chunk in Track 00, repeated for each frame)1018 // ..idx1 (List of video data chunks and their location in the file)1020 // The RIFF AVI Chunk holds the complete movie1021 aviChunk = new CompositeChunk("RIFF", "AVI ");1022 CompositeChunk hdrlChunk = new CompositeChunk("LIST", "hdrl");1024 // Write empty AVI Main Header Chunk - we fill the data in later1025 aviChunk.add(hdrlChunk);1026 avihChunk = new FixedSizeDataChunk("avih", 56);1027 avihChunk.seekToEndOfChunk();1028 hdrlChunk.add(avihChunk);1030 CompositeChunk strlChunk = new CompositeChunk("LIST", "strl");1031 hdrlChunk.add(strlChunk);1033 // Write empty AVI Stream Header Chunk - we fill the data in later1034 strhChunk = new FixedSizeDataChunk("strh", 56);1035 strhChunk.seekToEndOfChunk();1036 strlChunk.add(strhChunk);1037 strfChunk = new FixedSizeDataChunk("strf", palette == null ? 40 : 40 + palette.getMapSize() * 4);1038 strfChunk.seekToEndOfChunk();1039 strlChunk.add(strfChunk);1041 moviChunk = new CompositeChunk("LIST", "movi");1042 aviChunk.add(moviChunk);1045 }1047 private void writeEpilog() throws IOException {1049 long bufferSize = 0;1050 for (Sample s : videoFrames) {1051 if (s.length > bufferSize) {1052 bufferSize = s.length;1053 }1054 }1057 DataChunkOutputStream d;1059 /* Create Idx1 Chunk and write data1060 * -------------1061 typedef struct _avioldindex {1062 FOURCC fcc;1063 DWORD cb;1064 struct _avioldindex_entry {1065 DWORD dwChunkId;1066 DWORD dwFlags;1067 DWORD dwOffset;1068 DWORD dwSize;1069 } aIndex[];1070 } AVIOLDINDEX;1071 */1072 DataChunk idx1Chunk = new DataChunk("idx1");1073 aviChunk.add(idx1Chunk);1074 d = idx1Chunk.getOutputStream();1075 long moviListOffset = moviChunk.offset + 8;1076 //moviListOffset = 0;1077 for (Sample f : videoFrames) {1079 d.writeType(f.chunkType); // dwChunkId1080 // Specifies a FOURCC that identifies a stream in the AVI file. The1081 // FOURCC must have the form 'xxyy' where xx is the stream number and yy1082 // is a two-character code that identifies the contents of the stream:1083 //1084 // Two-character code Description1085 // db Uncompressed video frame1086 // dc Compressed video frame1087 // pc Palette change1088 // wb Audio data1090 d.writeUInt((f.chunkType.endsWith("pc") ? 0x100 : 0x0)//1091 | (f.isSync ? 0x10 : 0x0)); // dwFlags1092 // Specifies a bitwise combination of zero or more of the following1093 // flags:1094 //1095 // Value Name Description1096 // 0x10 AVIIF_KEYFRAME The data chunk is a key frame.1097 // 0x1 AVIIF_LIST The data chunk is a 'rec ' list.1098 // 0x100 AVIIF_NO_TIME The data chunk does not affect the timing of the1099 // stream. For example, this flag should be set for1100 // palette changes.1102 d.writeUInt(f.offset - moviListOffset); // dwOffset1103 // Specifies the location of the data chunk in the file. The value1104 // should be specified as an offset, in bytes, from the start of the1105 // 'movi' list; however, in some AVI files it is given as an offset from1106 // the start of the file.1108 d.writeUInt(f.length); // dwSize1109 // Specifies the size of the data chunk, in bytes.1110 }1111 idx1Chunk.finish();1113 /* Write Data into AVI Main Header Chunk1114 * -------------1115 * The AVIMAINHEADER structure defines global information in an AVI file.1116 * see http://msdn.microsoft.com/en-us/library/ms779632(VS.85).aspx1117 typedef struct _avimainheader {1118 FOURCC fcc;1119 DWORD cb;1120 DWORD dwMicroSecPerFrame;1121 DWORD dwMaxBytesPerSec;1122 DWORD dwPaddingGranularity;1123 DWORD dwFlags;1124 DWORD dwTotalFrames;1125 DWORD dwInitialFrames;1126 DWORD dwStreams;1127 DWORD dwSuggestedBufferSize;1128 DWORD dwWidth;1129 DWORD dwHeight;1130 DWORD dwReserved[4];1131 } AVIMAINHEADER; */1132 avihChunk.seekToStartOfData();1133 d = avihChunk.getOutputStream();1135 d.writeUInt((1000000L * (long) timeScale) / (long) frameRate); // dwMicroSecPerFrame1136 // Specifies the number of microseconds between frames.1137 // This value indicates the overall timing for the file.1139 d.writeUInt(0); // dwMaxBytesPerSec1140 // Specifies the approximate maximum data rate of the file.1141 // This value indicates the number of bytes per second the system1142 // must handle to present an AVI sequence as specified by the other1143 // parameters contained in the main header and stream header chunks.1145 d.writeUInt(0); // dwPaddingGranularity1146 // Specifies the alignment for data, in bytes. Pad the data to multiples1147 // of this value.1149 d.writeUInt(0x10); // dwFlags (0x10 == hasIndex)1150 // Contains a bitwise combination of zero or more of the following1151 // flags:1152 //1153 // Value Name Description1154 // 0x10 AVIF_HASINDEX Indicates the AVI file has an index.1155 // 0x20 AVIF_MUSTUSEINDEX Indicates that application should use the1156 // index, rather than the physical ordering of the1157 // chunks in the file, to determine the order of1158 // presentation of the data. For example, this flag1159 // could be used to create a list of frames for1160 // editing.1161 // 0x100 AVIF_ISINTERLEAVED Indicates the AVI file is interleaved.1162 // 0x1000 AVIF_WASCAPTUREFILE Indicates the AVI file is a specially1163 // allocated file used for capturing real-time1164 // video. Applications should warn the user before1165 // writing over a file with this flag set because1166 // the user probably defragmented this file.1167 // 0x20000 AVIF_COPYRIGHTED Indicates the AVI file contains copyrighted1168 // data and software. When this flag is used,1169 // software should not permit the data to be1170 // duplicated.1172 d.writeUInt(videoFrames.size()); // dwTotalFrames1173 // Specifies the total number of frames of data in the file.1175 d.writeUInt(0); // dwInitialFrames1176 // Specifies the initial frame for interleaved files. Noninterleaved1177 // files should specify zero. If you are creating interleaved files,1178 // specify the number of frames in the file prior to the initial frame1179 // of the AVI sequence in this member.1180 // To give the audio driver enough audio to work with, the audio data in1181 // an interleaved file must be skewed from the video data. Typically,1182 // the audio data should be moved forward enough frames to allow1183 // approximately 0.75 seconds of audio data to be preloaded. The1184 // dwInitialRecords member should be set to the number of frames the1185 // audio is skewed. Also set the same value for the dwInitialFrames1186 // member of the AVISTREAMHEADER structure in the audio stream header1188 d.writeUInt(1); // dwStreams1189 // Specifies the number of streams in the file. For example, a file with1190 // audio and video has two streams.1192 d.writeUInt(bufferSize); // dwSuggestedBufferSize1193 // Specifies the suggested buffer size for reading the file. Generally,1194 // this size should be large enough to contain the largest chunk in the1195 // file. If set to zero, or if it is too small, the playback software1196 // will have to reallocate memory during playback, which will reduce1197 // performance. For an interleaved file, the buffer size should be large1198 // enough to read an entire record, and not just a chunk.1201 d.writeUInt(imgWidth); // dwWidth1202 // Specifies the width of the AVI file in pixels.1204 d.writeUInt(imgHeight); // dwHeight1205 // Specifies the height of the AVI file in pixels.1207 d.writeUInt(0); // dwReserved[0]1208 d.writeUInt(0); // dwReserved[1]1209 d.writeUInt(0); // dwReserved[2]1210 d.writeUInt(0); // dwReserved[3]1211 // Reserved. Set this array to zero.1213 /* Write Data into AVI Stream Header Chunk1214 * -------------1215 * The AVISTREAMHEADER structure contains information about one stream1216 * in an AVI file.1217 * see http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx1218 typedef struct _avistreamheader {1219 FOURCC fcc;1220 DWORD cb;1221 FOURCC fccType;1222 FOURCC fccHandler;1223 DWORD dwFlags;1224 WORD wPriority;1225 WORD wLanguage;1226 DWORD dwInitialFrames;1227 DWORD dwScale;1228 DWORD dwRate;1229 DWORD dwStart;1230 DWORD dwLength;1231 DWORD dwSuggestedBufferSize;1232 DWORD dwQuality;1233 DWORD dwSampleSize;1234 struct {1235 short int left;1236 short int top;1237 short int right;1238 short int bottom;1239 } rcFrame;1240 } AVISTREAMHEADER;1241 */1242 strhChunk.seekToStartOfData();1243 d = strhChunk.getOutputStream();1244 d.writeType("vids"); // fccType - vids for video stream1245 // Contains a FOURCC that specifies the type of the data contained in1246 // the stream. The following standard AVI values for video and audio are1247 // defined:1248 //1249 // FOURCC Description1250 // 'auds' Audio stream1251 // 'mids' MIDI stream1252 // 'txts' Text stream1253 // 'vids' Video stream1255 switch (videoFormat) {1256 case RAW:1257 d.writeType("DIB "); // fccHandler - DIB for Raw RGB1258 break;1259 case RLE:1260 d.writeType("RLE "); // fccHandler - Microsoft RLE1261 break;1262 case JPG:1263 d.writeType("MJPG"); // fccHandler - MJPG for Motion JPEG1264 break;1265 case PNG:1266 default:1267 d.writeType("png "); // fccHandler - png for PNG1268 break;1269 }1270 // Optionally, contains a FOURCC that identifies a specific data1271 // handler. The data handler is the preferred handler for the stream.1272 // For audio and video streams, this specifies the codec for decoding1273 // the stream.1275 if (imgDepth <= 8) {1276 d.writeUInt(0x00010000); // dwFlags - AVISF_VIDEO_PALCHANGES1277 } else {1278 d.writeUInt(0); // dwFlags1279 }1281 // Contains any flags for the data stream. The bits in the high-order1282 // word of these flags are specific to the type of data contained in the1283 // stream. The following standard flags are defined:1284 //1285 // Value Name Description1286 // AVISF_DISABLED 0x00000001 Indicates this stream should not1287 // be enabled by default.1288 // AVISF_VIDEO_PALCHANGES 0x000100001289 // Indicates this video stream contains1290 // palette changes. This flag warns the playback1291 // software that it will need to animate the1292 // palette.1294 d.writeUShort(0); // wPriority1295 // Specifies priority of a stream type. For example, in a file with1296 // multiple audio streams, the one with the highest priority might be1297 // the default stream.1299 d.writeUShort(0); // wLanguage1300 // Language tag.1302 d.writeUInt(0); // dwInitialFrames1303 // Specifies how far audio data is skewed ahead of the video frames in1304 // interleaved files. Typically, this is about 0.75 seconds. If you are1305 // creating interleaved files, specify the number of frames in the file1306 // prior to the initial frame of the AVI sequence in this member. For1307 // more information, see the remarks for the dwInitialFrames member of1308 // the AVIMAINHEADER structure.1310 d.writeUInt(timeScale); // dwScale1311 // Used with dwRate to specify the time scale that this stream will use.1312 // Dividing dwRate by dwScale gives the number of samples per second.1313 // For video streams, this is the frame rate. For audio streams, this1314 // rate corresponds to the time needed to play nBlockAlign bytes of1315 // audio, which for PCM audio is the just the sample rate.1317 d.writeUInt(frameRate); // dwRate1318 // See dwScale.1320 d.writeUInt(0); // dwStart1321 // Specifies the starting time for this stream. The units are defined by1322 // the dwRate and dwScale members in the main file header. Usually, this1323 // is zero, but it can specify a delay time for a stream that does not1324 // start concurrently with the file.1326 d.writeUInt(videoFrames.size()); // dwLength1327 // Specifies the length of this stream. The units are defined by the1328 // dwRate and dwScale members of the stream's header.1330 d.writeUInt(bufferSize); // dwSuggestedBufferSize1331 // Specifies how large a buffer should be used to read this stream.1332 // Typically, this contains a value corresponding to the largest chunk1333 // present in the stream. Using the correct buffer size makes playback1334 // more efficient. Use zero if you do not know the correct buffer size.1336 d.writeInt(-1); // dwQuality1337 // Specifies an indicator of the quality of the data in the stream.1338 // Quality is represented as a number between 0 and 10,000.1339 // For compressed data, this typically represents the value of the1340 // quality parameter passed to the compression software. If set to –1,1341 // drivers use the default quality value.1343 d.writeUInt(0); // dwSampleSize1344 // Specifies the size of a single sample of data. This is set to zero1345 // if the samples can vary in size. If this number is nonzero, then1346 // multiple samples of data can be grouped into a single chunk within1347 // the file. If it is zero, each sample of data (such as a video frame)1348 // must be in a separate chunk. For video streams, this number is1349 // typically zero, although it can be nonzero if all video frames are1350 // the same size. For audio streams, this number should be the same as1351 // the nBlockAlign member of the WAVEFORMATEX structure describing the1352 // audio.1354 d.writeUShort(0); // rcFrame.left1355 d.writeUShort(0); // rcFrame.top1356 d.writeUShort(imgWidth); // rcFrame.right1357 d.writeUShort(imgHeight); // rcFrame.bottom1358 // Specifies the destination rectangle for a text or video stream within1359 // the movie rectangle specified by the dwWidth and dwHeight members of1360 // the AVI main header structure. The rcFrame member is typically used1361 // in support of multiple video streams. Set this rectangle to the1362 // coordinates corresponding to the movie rectangle to update the whole1363 // movie rectangle. Units for this member are pixels. The upper-left1364 // corner of the destination rectangle is relative to the upper-left1365 // corner of the movie rectangle.1367 /* Write BITMAPINFOHEADR Data into AVI Stream Format Chunk1368 /* -------------1369 * see http://msdn.microsoft.com/en-us/library/ms779712(VS.85).aspx1370 typedef struct tagBITMAPINFOHEADER {1371 DWORD biSize;1372 LONG biWidth;1373 LONG biHeight;1374 WORD biPlanes;1375 WORD biBitCount;1376 DWORD biCompression;1377 DWORD biSizeImage;1378 LONG biXPelsPerMeter;1379 LONG biYPelsPerMeter;1380 DWORD biClrUsed;1381 DWORD biClrImportant;1382 } BITMAPINFOHEADER;1383 */1384 strfChunk.seekToStartOfData();1385 d = strfChunk.getOutputStream();1386 d.writeUInt(40); // biSize1387 // Specifies the number of bytes required by the structure. This value1388 // does not include the size of the color table or the size of the color1389 // masks, if they are appended to the end of structure.1391 d.writeInt(imgWidth); // biWidth1392 // Specifies the width of the bitmap, in pixels.1394 d.writeInt(imgHeight); // biHeight1395 // Specifies the height of the bitmap, in pixels.1396 //1397 // For uncompressed RGB bitmaps, if biHeight is positive, the bitmap is1398 // a bottom-up DIB with the origin at the lower left corner. If biHeight1399 // is negative, the bitmap is a top-down DIB with the origin at the1400 // upper left corner.1401 // For YUV bitmaps, the bitmap is always top-down, regardless of the1402 // sign of biHeight. Decoders should offer YUV formats with postive1403 // biHeight, but for backward compatibility they should accept YUV1404 // formats with either positive or negative biHeight.1405 // For compressed formats, biHeight must be positive, regardless of1406 // image orientation.1408 d.writeShort(1); // biPlanes1409 // Specifies the number of planes for the target device. This value must1410 // be set to 1.1412 d.writeShort(imgDepth); // biBitCount1413 // Specifies the number of bits per pixel (bpp). For uncompressed1414 // formats, this value is the average number of bits per pixel. For1415 // compressed formats, this value is the implied bit depth of the1416 // uncompressed image, after the image has been decoded.1418 switch (videoFormat) {1419 case RAW:1420 default:1421 d.writeInt(0); // biCompression - BI_RGB for uncompressed RGB1422 break;1423 case RLE:1424 if (imgDepth == 8) {1425 d.writeInt(1); // biCompression - BI_RLE81426 } else if (imgDepth == 4) {1427 d.writeInt(2); // biCompression - BI_RLE41428 } else {1429 throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images");1430 }1431 break;1432 case JPG:1433 d.writeType("MJPG"); // biCompression - MJPG for Motion JPEG1434 break;1435 case PNG:1436 d.writeType("png "); // biCompression - png for PNG1437 break;1438 }1439 // For compressed video and YUV formats, this member is a FOURCC code,1440 // specified as a DWORD in little-endian order. For example, YUYV video1441 // has the FOURCC 'VYUY' or 0x56595559. For more information, see FOURCC1442 // Codes.1443 //1444 // For uncompressed RGB formats, the following values are possible:1445 //1446 // Value Description1447 // BI_RGB 0x00000000 Uncompressed RGB.1448 // BI_BITFIELDS 0x00000003 Uncompressed RGB with color masks.1449 // Valid for 16-bpp and 32-bpp bitmaps.1450 //1451 // Note that BI_JPG and BI_PNG are not valid video formats.1452 //1453 // For 16-bpp bitmaps, if biCompression equals BI_RGB, the format is1454 // always RGB 555. If biCompression equals BI_BITFIELDS, the format is1455 // either RGB 555 or RGB 565. Use the subtype GUID in the AM_MEDIA_TYPE1456 // structure to determine the specific RGB type.1458 switch (videoFormat) {1459 case RAW:1460 d.writeInt(0); // biSizeImage1461 break;1462 case RLE:1463 case JPG:1464 case PNG:1465 default:1466 if (imgDepth == 4) {1467 d.writeInt(imgWidth * imgHeight / 2); // biSizeImage1468 } else {1469 int bytesPerPixel = Math.max(1, imgDepth / 8);1470 d.writeInt(imgWidth * imgHeight * bytesPerPixel); // biSizeImage1471 }1472 break;1473 }1474 // Specifies the size, in bytes, of the image. This can be set to 0 for1475 // uncompressed RGB bitmaps.1477 d.writeInt(0); // biXPelsPerMeter1478 // Specifies the horizontal resolution, in pixels per meter, of the1479 // target device for the bitmap.1481 d.writeInt(0); // biYPelsPerMeter1482 // Specifies the vertical resolution, in pixels per meter, of the target1483 // device for the bitmap.1485 d.writeInt(palette == null ? 0 : palette.getMapSize()); // biClrUsed1486 // Specifies the number of color indices in the color table that are1487 // actually used by the bitmap.1489 d.writeInt(0); // biClrImportant1490 // Specifies the number of color indices that are considered important1491 // for displaying the bitmap. If this value is zero, all colors are1492 // important.1494 if (palette != null) {1495 for (int i = 0, n = palette.getMapSize(); i < n; ++i) {1496 /*1497 * typedef struct tagRGBQUAD {1498 BYTE rgbBlue;1499 BYTE rgbGreen;1500 BYTE rgbRed;1501 BYTE rgbReserved; // This member is reserved and must be zero.1502 } RGBQUAD;1503 */1504 d.write(palette.getBlue(i));1505 d.write(palette.getGreen(i));1506 d.write(palette.getRed(i));1507 d.write(0);1508 }1509 }1512 // -----------------1513 aviChunk.finish();1514 }1515 }