All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.monte.media.anim.ANIMOutputStream Maven / Gradle / Ivy

There is a newer version: 1.1
Show newest version
/*
 * @(#)ANIMOutputStream.java  1.0  2010-12-26
 * 
 * Copyright © 2010 Werner Randelshofer, Goldau, Switzerland.
 * All rights reserved.
 * 
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.anim;

import java.util.Map;
import java.util.HashMap;
import org.monte.media.image.BitmapImage;
import org.monte.media.io.SeekableByteArrayOutputStream;
import org.monte.media.iff.IFFOutputStream;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import javax.imageio.stream.FileImageOutputStream;
import static java.lang.Math.*;
/**
 * {@code ANIMOutputStream}.
 * 

* Reference:
* Commodore-Amiga, Inc. (1991) Amiga ROM Kernel Reference Manual. Devices. * Third Edition. Reading: Addison-Wesley. * * @author Werner Randelshofer * @version 1.0 2010-12-26 Created. */ public class ANIMOutputStream { /** CAMG monitor ID mask. */ public final static int MONITOR_ID_MASK = 0xffff1000; /** Default ID chooses a system dependent screen mode. We always fall back * to NTSC OCS with 60fps. * * The default monitor ID triggers OCS mode! * OCS stands for "Original Chip Set". The OCS chip set only had 4 bits per color register. * All later chip sets hat 8 bits per color register. */ public final static int DEFAULT_MONITOR_ID = 0x00000000; /** NTSC, 60fps, 44:52. */ public final static int NTSC_MONITOR_ID = 0x00011000; /** PAL, 50fps, 44:44. */ public final static int PAL_MONITOR_ID = 0x00021000; /** MULTISCAN (VGA), 58fps, 44:44. */ public final static int MULTISCAN_MONITOR_ID = 0x00031000; /** A2024, 60fps (I don't know the real value). */ public final static int A2024_MONITOR_ID = 0x00041000; /** PROTO, 60fps (I don't know the real value). */ public final static int PROTO_MONITOR_ID = 0x00051000; /** EURO72, 69fps, 44:44. */ public final static int EURO72_MONITOR_ID = 0x00061000; /** EURO36, 73fps, 44:44. */ public final static int EURO36_MONITOR_ID = 0x00071000; /** SUPER72, 71fps, 34:40. */ public final static int SUPER72_MONITOR_ID = 0x00081000; /** DBLNTSC, 58fps, 44:52. */ public final static int DBLNTSC_MONITOR_ID = 0x00091000; /** DBLPAL, 48fps, 44:44. */ public final static int DBLPAL_MONITOR_ID = 0x00001000; /** CAMG Mode mask. */ public final static int MODE_MASK = 0x00000880; /** CAMG HAM mode. */ public final static int HAM_MODE = 0x00000800; /** CAMG EHB mode. */ public final static int EHB_MODE = 0x00000080; /** "jiffies" defines the time base of the movie. */ private int jiffies = 60; /** Commodore Amiga graphics mode. */ private int camg; private boolean debug = false; private IFFOutputStream out = null; /** Frame count. */ protected int frameCount = 0; /** Current absolute frame time. */ protected int absTime = 0; /** double buffering previous frame for odd frames. */ private BitmapImage oddPrev; /** double buffering previous frame for even frames. */ private BitmapImage evenPrev; /** store first frame so that we can write wrap up frame. */ private BitmapImage firstFrame; /** Duration of the first wrapup frame. */ private int firstWrapupDuration = 1; /** Duration of the second wrapup frame. */ private int secondWrapupDuration = 1; /** Offset of the DPAN chunk. */ private long numberOfFramesOffset = -1; /** * The states of the movie output stream. */ private static enum States { REALIZED, STARTED, FINISHED, CLOSED; } /** * The current state of the movie output stream. */ private States state = States.REALIZED; public ANIMOutputStream(File file) throws IOException { out = new IFFOutputStream(new FileImageOutputStream(file)); } /** Sets the time base of the movie. The default value is 60. */ public void setJiffies(int newValue) { this.jiffies = newValue; } /** Gets the time base of the movie. The default value is 60. */ public int getJiffies() { return this.jiffies; } /** Sets the Commodore Amiga Graphics Mode. The default value is 0. *

* The graphics mode is an or-combination of the monitor ID and the mode ID. *

* Example: *

     * setCAMG(PAL_MONITOR_ID|HAM_MODE);
     * 
* * Also sets the Jiffies for the Graphics Mode. */ public void setCAMG(int newValue) { this.camg = newValue; AmigaDisplayInfo info=AmigaDisplayInfo.getInfo(newValue); if (info!=null)this.jiffies=info.fps; } /** Gets the Commodore Amiga Graphics Mode. The default value is 0. */ public int getCAMG() { return this.camg; } /** * Check to make sure that this stream has not been closed */ private void ensureOpen() throws IOException { if (state == States.CLOSED) { throw new IOException("Stream closed"); } } /** * Sets the state of the QuickTimeWriter to started. *

* If the state is changed by this method, the prolog is * written. */ private void ensureStarted() throws IOException { ensureOpen(); if (state == States.FINISHED) { throw new IOException("Can not write into finished movie."); } if (state != States.STARTED) { writeProlog(); state = States.STARTED; } } /** * Finishes writing the contents of the QuickTime output stream without closing * the underlying stream. Use this method when applying multiple filters * in succession to the same output stream. * * @exception IllegalStateException if the dimension of the video track * has not been specified or determined yet. * @exception java.io.IOException if an I/O exception has occurred */ public void finish() throws IOException { ensureOpen(); if (state != States.FINISHED) { writeEpilog(); out.finish(); state = States.FINISHED; } } /** * Closes the movie file as well as the stream being filtered. * * @exception java.io.IOException if an I/O error has occurred */ public void close() throws IOException { try { if (state == States.STARTED) { finish(); } } finally { if (state != States.CLOSED) { out.close(); state = States.CLOSED; } } } private void writeProlog() throws IOException { out.pushCompositeChunk("FORM", "ANIM"); } private void writeEpilog() throws IOException { if (frameCount > 0) { // Write wrapup frames writeDeltaFrame(firstFrame, firstWrapupDuration); writeDeltaFrame(firstFrame, secondWrapupDuration); } out.popChunk(); if (numberOfFramesOffset!=-1) { long pos=out.getStreamPosition(); out.seek(numberOfFramesOffset); out.writeUWORD(max(0,frameCount-2)); out.seek(pos); } } public void writeFrame(BitmapImage image, int duration) throws IOException { ensureStarted(); if (frameCount == 0) { writeFirstFrame(image, duration); } else { writeDeltaFrame(image, duration); } } private void writeFirstFrame(BitmapImage img, int duration) throws IOException { out.pushCompositeChunk("FORM", "ILBM"); writeBMHD(out, img); writeCMAP(out, img); // writeDPAN(out); writeANHD(out, img.getWidth(), img.getHeight(), 0, absTime, duration); // 0=opDirect writeCAMG(out, camg); writeBODY(out, img); out.popChunk(); firstFrame = new BitmapImage(img.getWidth(), img.getHeight(), img.getDepth(), img.getPlanarColorModel()); oddPrev = new BitmapImage(img.getWidth(), img.getHeight(), img.getDepth(), img.getPlanarColorModel()); evenPrev = new BitmapImage(img.getWidth(), img.getHeight(), img.getDepth(), img.getPlanarColorModel()); System.arraycopy(img.getBitmap(), 0, firstFrame.getBitmap(), 0, img.getBitmap().length); System.arraycopy(img.getBitmap(), 0, oddPrev.getBitmap(), 0, img.getBitmap().length); System.arraycopy(img.getBitmap(), 0, evenPrev.getBitmap(), 0, img.getBitmap().length); absTime += duration; firstWrapupDuration = secondWrapupDuration = duration; frameCount++; } private void writeDeltaFrame(BitmapImage img, int duration) throws IOException { BitmapImage prev = (frameCount & 1) == 0 ? evenPrev : oddPrev; // double buffered previous BitmapImage immPrev = (frameCount & 1) == 0 ? oddPrev : evenPrev; // immediate previous out.pushCompositeChunk("FORM", "ILBM"); writeANHD(out, img.getWidth(), img.getHeight(), 0x5, absTime, duration); // 0x5=byteVerticalDeltaMode writeCMAP(out, img, immPrev); writeDLTA(out, img, prev); out.popChunk(); System.arraycopy(img.getBitmap(), 0, prev.getBitmap(), 0, prev.getBitmap().length); prev.setPlanarColorModel(img.getPlanarColorModel()); absTime += duration; firstWrapupDuration = secondWrapupDuration = duration; frameCount++; } public long getMovieTime() {return absTime;} /** * Writes the bitmap header (ILBM BMHD). * *

     * typedef UBYTE Masking; // Choice of masking technique
     *
     * #define mskNone                 0
     * #define mskHasMask              1
     * #define mskHasTransparentColor  2
     * #define mskLasso                3
     *
     * typedef UBYTE Compression; // Choice of compression algorithm
     *     // applied to the rows of all source and mask planes.
     *     // "cmpByteRun1" is the byte run encoding. Do not compress
     *     // accross rows!
     * #define cmpNone      0
     * #define cmpByteRun1  1
     *
     * typedef struct {
     *   UWORD       w, h; // raster width & height in pixels
     *   WORD        x, y; // pixel position for this image
     *   UBYTE       nbPlanes; // # source bitplanes
     *   Masking     masking;
     *   Compression compression;
     *   UBYTE       pad1;     // unused; ignore on read, write as 0
     *   UWORD       transparentColor; // transparent "color number" (sort of)
     *   UBYTE       xAspect, yAspect; // pixel aspect, a ratio width : height
     *   UWORD       pageWidth, pageHeight; // source "page" size in pixels
     *   } BitmapHeader;
     * 
*/ private void writeBMHD(IFFOutputStream out, BitmapImage img) throws IOException { AmigaDisplayInfo info=AmigaDisplayInfo.getInfo(camg); if (info==null)info=AmigaDisplayInfo.getInfo(AmigaDisplayInfo.DEFAULT_MONITOR_ID); out.pushDataChunk("BMHD"); out.writeUWORD(img.getWidth()); out.writeUWORD(img.getHeight()); out.writeWORD(0); out.writeWORD(0); out.writeUBYTE(img.getDepth()); out.writeUBYTE(0); // mskNone out.writeUBYTE(1); // cmpByteRun1 out.writeUBYTE(0); out.writeUWORD(0); out.writeUBYTE(info.resolutionX); out.writeUBYTE(info.resolutionY); out.writeUWORD(img.getWidth()); out.writeUWORD(img.getHeight()); out.popChunk(); } /** * Writes the color map (ILBM CMAP). */ private void writeCMAP(IFFOutputStream out, BitmapImage img) throws IOException { out.pushDataChunk("CMAP"); IndexColorModel cm = (IndexColorModel) img.getPlanarColorModel(); for (int i = 0, n = cm.getMapSize(); i < n; ++i) { out.writeUBYTE(cm.getRed(i)); out.writeUBYTE(cm.getGreen(i)); out.writeUBYTE(cm.getBlue(i)); } out.popChunk(); } /** * Writes the color map (ILBM CMAP) if it is different from the previous image. */ private void writeCMAP(IFFOutputStream out, BitmapImage img, BitmapImage prev) throws IOException { IndexColorModel cm = (IndexColorModel) img.getPlanarColorModel(); IndexColorModel prevCm = (IndexColorModel) prev.getPlanarColorModel(); boolean equals = true; for (int i = 0, n = cm.getMapSize(); i < n; ++i) { if (cm.getRGB(i) != prevCm.getRGB(i)) { equals = false; break; } } if (!equals) { writeCMAP(out, img); } } /** * Writes the color Amiga viewport mode display id (ILBM CAMG). */ private void writeCAMG(IFFOutputStream out, int camg) throws IOException { out.pushDataChunk("CAMG"); out.writeLONG(camg); out.popChunk(); } /** * Writes the DPAN (Deluxe Paint Animation) chunk. * We can fill in the value for numberOfFrames only after we have written * all frames. * * typedef struct { * UWORD version; // current version=4 * UWORD numberOfFrames; // number of frames in the animation. * ULONG flags; // Not used * } * animDPAnimChunk; * */ private void writeDPAN(IFFOutputStream out) throws IOException { out.pushDataChunk("DPAN"); out.writeUWORD(4); numberOfFramesOffset = out.getStreamPosition(); out.writeUWORD(-1); // we don't know the number of frames yet out.writeULONG(0); out.popChunk(); } /** * Writes the body (ILBM BODY). */ private void writeBODY(IFFOutputStream out, BitmapImage img) throws IOException { out.pushDataChunk("BODY"); int widthInBytes = (img.getWidth() + 7) / 8; int ss = img.getScanlineStride(); int bs = img.getBitplaneStride(); int offset = 0; byte[] data = img.getBitmap(); for (int y = 0, h = img.getHeight(); y < h; y++) { for (int p = 0, d = img.getDepth(); p < d; p++) { out.writeByteRun1(data, offset + bs * p, widthInBytes); } offset += ss; } out.popChunk(); } /** * Writes a delta frame (ILBM DLTA) with "byte vertical" (method 5). * *

* The DLTA chunk for method 5 has 16 long pointers at the start. * The first 8 are pointers to the start of the data for each of the * bitplanes (up to a max of 8 planes). The second set of 8 are not used. *

* Compression/decompression is performed on a plane-by-plane basis. * Each column is compressed separately. A 320x200 bitplane would have 40 * columns of 200 bytes each. Each column starts with an op-count followed * by a number of ops. If the op-count is zero, that's ok, it just means * there's no change in this column from the last frame. If there is only * one list of pointers ops for all planes, then the pointer to that list * is repeated in all positions so the playback code need not even be * aware of it.In fact, one could get fancy and have some bitplanes share * lists while others have different lists, or no lists (the problem in * these schemes lie in the generation, not in the playback). *

* The ops are of three classes, and followed by a varying amount of data * depending on which class: *

    *
  1. Skip ops - this is a byte whith the hi bit clear that says how * many rows to move the "dest" pointer forward, ie to skip. It is non-zero.
  2. *
  3. Uniq ops - this is a byte with the hi bit set. The hi bit is masked * down and the remainder is a count of the number of bytes of data to copy * literally. It's followed by the data to copy.
  4. *
  5. Same ops - this is a 0 byte followed by a count byte, followed by a * byte value to repeat count times.
  6. *
*

* Do bear in mind that the data is compressed vertically rather than * horizontally, so to get to the next byte in the destination we add the * number of bytes per row instead of one. *

* Reference:
* Commodore-Amiga, Inc. (1991) Amiga ROM Kernel Reference Manual. Devices. * Third Edition. Reading: Addison-Wesley. * Pages 445 - 449. */ private void writeDLTA(IFFOutputStream out, BitmapImage img, BitmapImage prev) throws IOException { out.pushDataChunk("DLTA"); int height = img.getHeight(); int widthInBytes = (img.getWidth() + 7) / 8; int ss = img.getScanlineStride(); int bs = img.getBitplaneStride(); int offset = 0; byte[] data = img.getBitmap(); byte[] prevData = prev.getBitmap(); SeekableByteArrayOutputStream buf = new SeekableByteArrayOutputStream(); // Buffers for a theoretical maximum of 16 planes. byte[][] planes = new byte[16][0]; // Repeat for each plane. int depth = img.getDepth(); for (int p = 0; p < depth; ++p) { buf.reset(); // Each column of the plane is compressed separately. for (int column = 0; column < widthInBytes; ++column) { writeByteVertical(buf, data, prevData, bs * p + column, height, ss); } planes[p] = buf.toByteArray(); if (planes[p].length == widthInBytes) { // => all columns have an op-count of 0. We can drop them entirely. planes[p] = new byte[0]; } } // pPointers are the pointers (index) to the op-codes of each plane. int[] pPointers = new int[16]; // Compute pointers. If two planes have the same delta, we only store // the delta once. for (int p = 0; p < depth; ++p) { if (planes[p].length == 0) { pPointers[p] = 0; } else { pPointers[p] = 16 * 4; for (int q = 0; q < p; ++q) { if (Arrays.equals(planes[q], planes[p])) { pPointers[p] = pPointers[q]; planes[p] = new byte[0]; break; } pPointers[p] += planes[q].length; } } } // write pointers for each bitmap plane for (int p = 0; p < pPointers.length; ++p) { out.writeULONG(pPointers[p]); } // write deltas for (int p = 0; p < planes.length; ++p) { out.write(planes[p]); } out.popChunk(); } /** * Encodes a column of an image with the "byte vertical" method (method 5). * * @param out * @param data * @param prev * @param offset * @param length * @param step * @throws java.io.IOException */ private void writeByteVertical(SeekableByteArrayOutputStream out, byte[] data, byte[] prev, int offset, int length, int step) throws IOException { int opCount = 0; // Reserve space for opCount in the stream long opCountPos = out.getStreamPosition(); out.write(0); // Start offset of the literal run int literalOffset = 0; int i; for (i = 0; i < length; i++) { // Count skips int skipCount = i; for (; skipCount < length; skipCount++) { if (data[offset + skipCount * step] != prev[offset + skipCount * step]) { break; } } skipCount = skipCount - i; // Can we skip until the end? if (skipCount + i == length) { break; } if (skipCount > 0 && literalOffset == i || skipCount > 1) { // Flush the literal run, if we have one if (literalOffset < i) { opCount++; out.write(0x80 | (i - literalOffset)); // Write Uniq Op for (int j = literalOffset; j < i; j++) { out.write(data[offset + j * step]); } } // Write the skip count i += skipCount - 1; literalOffset = i + 1; for (; skipCount > 127; skipCount -= 127) { opCount++; out.write(127); // Write Skip Op } opCount++; out.write(skipCount); // Write Skip Op } else { // Read a byte byte b = data[offset + i * step]; // Count repeats of that byte int repeatCount = i + 1; for (; repeatCount < length; repeatCount++) { if (data[offset + repeatCount * step] != b) { break; } } repeatCount = repeatCount - i; if (repeatCount == 1) { // Flush the literal run, if it gets too large if (i - literalOffset > 126) { opCount++; out.write(0x80 | (i - literalOffset)); // Write Uniq Op for (int j = literalOffset; j < i; j++) { out.write(data[offset + j * step]); } literalOffset = i; } // If the byte repeats less than 4 times, and we have a literal // run with enough space, add it to the literal run } else if (repeatCount < 4 && literalOffset < i && i - literalOffset < 126) { i++; } else { // Flush the literal run, if we have one if (literalOffset < i) { opCount++; out.write(0x80 | (i - literalOffset)); // Write Uniq Op for (int j = literalOffset; j < i; j++) { out.write(data[offset + j * step]); } } // Write the repeat run i += repeatCount - 1; literalOffset = i + 1; // We have to write multiple runs, if the byte repeats more // than 256 times. for (; repeatCount > 255; repeatCount -= 255) { opCount++; out.write(0); out.write(255); // Write Same Op out.write(b); } opCount++; out.write(0); out.write(repeatCount); // Write Same Op out.write(b); } } } // Flush the literal run, if we have one if (literalOffset < i) { opCount++; out.write(0x80 | (i - literalOffset)); // Write Uniq Op for (int j = literalOffset; j < i; j++) { out.write(data[offset + j * step]); } } // Write the opCount long pos = out.getStreamPosition(); out.seek(opCountPos); out.write(opCount); out.seek(pos); } /** * Writes the anim header (ILBM ANHD). * *

     * typedef UBYTE Operation; // Choice of compression algorithm.
     *
     * #define opDirect        0  // set directly (normal ILBM BODY)
     * #define opXOR           1  // XOR ILBM mode
     * #define opLongDelta     2  // Long Delta mode
     * #define opShortDelta    3  // Short Delta Mode
     * #define opGeneralDelta  4  // Generalized short/long Delta mode
     * #define opByteVertical  5  // Byte Vertical Delta mode
     * #define opStereoDelta   6  // Stereo op 5 (third party)
     * #define opVertical7     7  // Short/Long Vertical Delta mode (opcodes and data stored separately)
     * #define opVertical8     8  // Short/Long Vertical Delta mode (opcodes and data combined)
     * #define opJ            74  // (ascii 'J') reserved for Eric Graham's compression technique
     *
     * typedef struct {
     * Operation   operation; // The compression method.
     * UBYTE       mask;      // XOR mode only - plane mask where each
     *                        // bit is set =1 if there is data and =0
     *                        // if not.
     * UWORD       w,h;       // XOR mode only - width and height of the
     *                        // area represented by the BODY to eliminate
     *                        // unnecessary un-changed data.
     * UWORD        x,y;      // XOR mode only - position of rectangular
     *                        // area represented by the BODY.
     * ULONG       abstime;   // currently unused - timing for a frame
     *                        // relative to the time the first frame
     *                        // was displayed - in jiffies (1/60 sec).
     * ULONG       reltime;   // timing for frame relative to time
     *                        // previous frame was displayed - in
     *                        // jiffies (1/60 sec).
     * UBYTE       interleave;// unused so far - indicates how many frames
     *                        // back this data is to modify. =0 defaults
     *                        // to indicate two frames back (for double
     *                        // buffering). =n indicates n frames back.
     *                        // The main intent here is to allow values
     *                        // of =1 for special applications where
     *                        // frame data would modify the immediately
     *                        // previous frame.
     * UBYTE        pad0;     // Pad byte, not used at present.
     * ULONG        bits;     // 32 option bits used by opGeneralDelta,
     *                        // opByteVertical, opVertical7 and opVertical8.
     *                        // At present only 6 are identified, but the
     *                        // rest are set =0 so they can be used to
     *                        // implement future ideas. These are defined
     *                        // for opGeneralData only at this point. It is
     *                        // recommended that all bits be set =0 for
     *                        // opByteVertical and that any bit settings used in
     *                        // the future (such as for XOR mode) be compatible
     *                        // with the opGeneralData settings. Player code
     *                        // should check undefined bits in opGeneralData and
     *                        // opByteVertical to assure they are zero.
     *                        //
     *                        // The six bits for current use are:
     *                        //
     *                        // bit #    set =0          set =1
     *                        // =======================================
     *                        // 0        short data          long data
     *                        // 1        set                 XOR
     *                        // 2        separate info       one info list
     *                        //          for each plane      for all planes
     *                        // 3        not RLC             RLC (run length coded)
     *                        // 4        horizontal          vertical
     *                        // 5        short info offsets  long info offsets
     *
     * UBYTE        pad[16];  // This is a pad for future use for future
     *                        // compression modes.
     * } AnimHeader;
     * 
*/ private void writeANHD(IFFOutputStream out, int width, int height, int compressionMode, int absTime, int relTime) throws IOException { out.pushDataChunk("ANHD"); out.writeUBYTE(compressionMode); out.writeUBYTE(0); out.writeUWORD(width); out.writeUWORD(height); out.writeUWORD(0); out.writeUWORD(0); out.writeULONG(absTime); out.writeULONG(relTime); out.writeUBYTE(0); out.writeUBYTE(0); out.writeULONG(0); // bits out.writeULONG(0); // pad out.writeULONG(0); out.writeULONG(0); out.writeULONG(0); out.popChunk(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy