diff --git a/trunk/lib/avi.jar b/trunk/lib/avi.jar new file mode 100644 index 000000000..5409c7678 Binary files /dev/null and b/trunk/lib/avi.jar differ diff --git a/trunk/lib/gif.jar b/trunk/lib/gif.jar new file mode 100644 index 000000000..05ad57001 Binary files /dev/null and b/trunk/lib/gif.jar differ diff --git a/trunk/libsrc/avi/build.xml b/trunk/libsrc/avi/build.xml new file mode 100644 index 000000000..f35d65806 --- /dev/null +++ b/trunk/libsrc/avi/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + Builds, tests, and runs the project avi. + + + diff --git a/trunk/libsrc/avi/nbproject/build-impl.xml b/trunk/libsrc/avi/nbproject/build-impl.xml new file mode 100644 index 000000000..675a2b235 --- /dev/null +++ b/trunk/libsrc/avi/nbproject/build-impl.xml @@ -0,0 +1,1407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/trunk/libsrc/avi/nbproject/genfiles.properties b/trunk/libsrc/avi/nbproject/genfiles.properties new file mode 100644 index 000000000..4b63314ae --- /dev/null +++ b/trunk/libsrc/avi/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=505ab359 +build.xml.script.CRC32=fa68b8cb +build.xml.stylesheet.CRC32=8064a381@1.68.1.46 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=505ab359 +nbproject/build-impl.xml.script.CRC32=b5283dd3 +nbproject/build-impl.xml.stylesheet.CRC32=5a01deb7@1.68.1.46 diff --git a/trunk/libsrc/avi/nbproject/project.properties b/trunk/libsrc/avi/nbproject/project.properties new file mode 100644 index 000000000..b26bc8e4c --- /dev/null +++ b/trunk/libsrc/avi/nbproject/project.properties @@ -0,0 +1,72 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/avi.jar +dist.javadoc.dir=${dist.dir}/javadoc +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.7 +javac.target=1.7 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +main.class=org.monte.media.math.IntMath +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=true +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/trunk/libsrc/avi/nbproject/project.xml b/trunk/libsrc/avi/nbproject/project.xml new file mode 100644 index 000000000..53a131c45 --- /dev/null +++ b/trunk/libsrc/avi/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + avi + + + + + + + + + diff --git a/trunk/libsrc/avi/src/org/monte/media/AbortException.java b/trunk/libsrc/avi/src/org/monte/media/AbortException.java new file mode 100644 index 000000000..cd20a6ecf --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AbortException.java @@ -0,0 +1,39 @@ +/* + * @(#)AbortException.java + * + * Copyright (c) 1999-2012 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; + +/** + * This exception is thrown when the production of an image + * has been aborted. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * + * @version $Id: AbortException.java 299 2013-01-03 07:40:18Z werner $ + */ +public class AbortException extends Exception { + + public static final long serialVersionUID = 1L; + + /** + Creates a new exception. + */ + public AbortException() { + super(); + } + + /** + Creates a new exception. + + */ + public AbortException(String message) { + super(message); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java b/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java new file mode 100644 index 000000000..6a2a5e816 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AbstractCodec.java @@ -0,0 +1,99 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.monte.media; + +import java.util.ArrayList; +/** + * {@code AbstractCodec}. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-12 Created. + */ +public abstract class AbstractCodec implements Codec { + + protected Format[] inputFormats; + protected Format[] outputFormats; + protected Format inputFormat; + protected Format outputFormat; + protected String name="unnamed codec"; + + public AbstractCodec(Format[] supportedInputFormats, Format[] supportedOutputFormats) { + this.inputFormats = supportedInputFormats; + this.outputFormats = supportedOutputFormats; + } + public AbstractCodec(Format[] supportedInputOutputFormats) { + this.inputFormats = supportedInputOutputFormats; + this.outputFormats = supportedInputOutputFormats; + } + + @Override + public Format[] getInputFormats() { + return inputFormats.clone(); + } + + @Override + public Format[] getOutputFormats(Format input) { + ArrayListof=new ArrayList(outputFormats.length); + for (Format f:outputFormats) { + of.add(input==null?f:f.append(input)); + } + return of.toArray(new Format[of.size()]); + } + + @Override + public Format setInputFormat(Format f) { + if (f!=null) + for (Format sf : getInputFormats()) { + if (sf.matches(f)) { + this.inputFormat = sf.append(f); + return inputFormat; + } + } + this.inputFormat=null; + return null; + } + + @Override + public Format setOutputFormat(Format f) { + for (Format sf : getOutputFormats(f)) { + if (sf.matches(f)) { + this.outputFormat = f; + return sf; + } + } + this.outputFormat=null; + return null; + } + + @Override + public Format getInputFormat() { + return inputFormat; + } + + @Override + public Format getOutputFormat() { + return outputFormat; + } + + @Override + public String getName() { + return name; + } + + /** Empty implementation of the reset method. Don't call super. */ + @Override + public void reset() { + // empty + } + + @Override + public String toString() { + String className=getClass().getName(); + int p=className.lastIndexOf('.'); + return className.substring(p+1)+"{" + "inputFormat=" + inputFormat + ", outputFormat=" + outputFormat+'}'; + } + + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java b/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java new file mode 100644 index 000000000..f180e7233 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AbstractVideoCodec.java @@ -0,0 +1,226 @@ +/* + * @(#)AbstractVideoCodec.java + * + * Copyright (c) 2011 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; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.DirectColorModel; +import java.awt.image.WritableRaster; +import java.io.IOException; +import javax.imageio.stream.ImageOutputStream; +import static org.monte.media.VideoFormatKeys.*; + +/** + * {@code AbstractVideoCodec}. + * + * @author Werner Randelshofer + * @version $Id: AbstractVideoCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class AbstractVideoCodec extends AbstractCodec { + + private BufferedImage imgConverter; + + public AbstractVideoCodec(Format[] supportedInputFormats, Format[] supportedOutputFormats) { + super(supportedInputFormats, supportedOutputFormats); + } + + /** Gets 8-bit indexed pixels from a buffer. Returns null if conversion failed. */ + protected byte[] getIndexed8(Buffer buf) { + if (buf.data instanceof byte[]) { + return (byte[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getRaster().getDataBuffer() instanceof DataBufferByte) { + return ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + } + } + return null; + } + + /** Gets 15-bit RGB pixels from a buffer. Returns null if conversion failed. */ + protected short[] getRGB15(Buffer buf) { + if (buf.data instanceof int[]) { + return (short[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (image.getRaster().getDataBuffer() instanceof DataBufferShort) { + // FIXME - Implement additional checks + return ((DataBufferShort) image.getRaster().getDataBuffer()).getData(); + } else if (image.getRaster().getDataBuffer() instanceof DataBufferUShort) { + // FIXME - Implement additional checks + return ((DataBufferUShort) image.getRaster().getDataBuffer()).getData(); + } + } + if (imgConverter == null) { + int width = outputFormat.get(WidthKey); + int height = outputFormat.get(HeightKey); + imgConverter = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB); + } + Graphics2D g = imgConverter.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return ((DataBufferUShort) imgConverter.getRaster().getDataBuffer()).getData(); + } + return null; + } + /** Gets 16-bit RGB-5-6-5 pixels from a buffer. Returns null if conversion failed. */ + protected short[] getRGB16(Buffer buf) { + if (buf.data instanceof int[]) { + return (short[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (image.getRaster().getDataBuffer() instanceof DataBufferShort) { + // FIXME - Implement additional checks + return ((DataBufferShort) image.getRaster().getDataBuffer()).getData(); + } else if (image.getRaster().getDataBuffer() instanceof DataBufferUShort) { + // FIXME - Implement additional checks + return ((DataBufferUShort) image.getRaster().getDataBuffer()).getData(); + } + } + if (imgConverter == null) { + int width = outputFormat.get(WidthKey); + int height = outputFormat.get(HeightKey); + imgConverter = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_565_RGB); + } + Graphics2D g = imgConverter.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return ((DataBufferUShort) imgConverter.getRaster().getDataBuffer()).getData(); + } + return null; + } + + + /** Gets 24-bit RGB pixels from a buffer. Returns null if conversion failed. */ + protected int[] getRGB24(Buffer buf) { + if (buf.data instanceof int[]) { + return (int[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (dcm.getBlueMask() == 0xff && dcm.getGreenMask() == 0xff00 && dcm.getRedMask() == 0xff0000) { + if (image.getRaster().getDataBuffer() instanceof DataBufferInt) { + return ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + } + } + return image.getRGB(0, 0, // + outputFormat.get(WidthKey), outputFormat.get(HeightKey), // + null, 0, outputFormat.get(WidthKey)); + } + return null; + } + + /** Gets 32-bit ARGB pixels from a buffer. Returns null if conversion failed. */ + protected int[] getARGB32(Buffer buf) { + if (buf.data instanceof int[]) { + return (int[]) buf.data; + } + if (buf.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) buf.data; + if (image.getColorModel() instanceof DirectColorModel) { + DirectColorModel dcm = (DirectColorModel) image.getColorModel(); + if (dcm.getBlueMask() == 0xff && dcm.getGreenMask() == 0xff00 && dcm.getRedMask() == 0xff0000) { + if (image.getRaster().getDataBuffer() instanceof DataBufferInt) { + return ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + } + } + return image.getRGB(0, 0, // + outputFormat.get(WidthKey), outputFormat.get(HeightKey), // + null, 0, outputFormat.get(WidthKey)); + } + return null; + } + + /** Gets a buffered image from a buffer. Returns null if conversion failed. */ + protected BufferedImage getBufferedImage(Buffer buf) { + if (buf.data instanceof BufferedImage) { + return (BufferedImage) buf.data; + } + return null; + } + private byte[] byteBuf = new byte[4]; + + protected void writeInt24(ImageOutputStream out, int v) throws IOException { + byteBuf[0] = (byte) (v >>> 16); + byteBuf[1] = (byte) (v >>> 8); + byteBuf[2] = (byte) (v >>> 0); + out.write(byteBuf, 0, 3); + } + + protected void writeInt24LE(ImageOutputStream out, int v) throws IOException { + byteBuf[2] = (byte) (v >>> 16); + byteBuf[1] = (byte) (v >>> 8); + byteBuf[0] = (byte) (v >>> 0); + out.write(byteBuf, 0, 3); + } + + protected void writeInts24(ImageOutputStream out, int[] i, int off, int len) throws IOException { + // Fix 4430357 - if off + len < 0, overflow occurred + if (off < 0 || len < 0 || off + len > i.length || off + len < 0) { + throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > i.length!"); + } + + byte[] b = new byte[len * 3]; + int boff = 0; + for (int j = 0; j < len; j++) { + int v = i[off + j]; + //b[boff++] = (byte)(v >>> 24); + b[boff++] = (byte) (v >>> 16); + b[boff++] = (byte) (v >>> 8); + b[boff++] = (byte) (v >>> 0); + } + + out.write(b, 0, len * 3); + } + + protected void writeInts24LE(ImageOutputStream out, int[] i, int off, int len) throws IOException { + // Fix 4430357 - if off + len < 0, overflow occurred + if (off < 0 || len < 0 || off + len > i.length || off + len < 0) { + throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > i.length!"); + } + + byte[] b = new byte[len * 3]; + int boff = 0; + for (int j = 0; j < len; j++) { + int v = i[off + j]; + b[boff++] = (byte) (v >>> 0); + b[boff++] = (byte) (v >>> 8); + b[boff++] = (byte) (v >>> 16); + //b[boff++] = (byte)(v >>> 24); + } + + out.write(b, 0, len * 3); + } + + /** Copies a buffered image. */ + protected static BufferedImage copyImage(BufferedImage img) { + ColorModel cm = img.getColorModel(); + boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); + WritableRaster raster = img.copyData(null); + return new BufferedImage(cm, raster, isAlphaPremultiplied, null); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java new file mode 100644 index 000000000..4d5e2558c --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/AudioFormatKeys.java @@ -0,0 +1,126 @@ +/* + * @(#)AudioFormatKeys.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import org.monte.media.math.Rational; +import java.nio.ByteOrder; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioFormat.Encoding; + +/** + * Defines common format keys for audio media. + * + * @author Werner Randelshofer + * @version $Id: AudioFormatKeys.java 299 2013-01-03 07:40:18Z werner $ + */ +public class AudioFormatKeys extends FormatKeys { + // Standard video EncodingKey strings for use onlyWith FormatKey.Encoding. + + /** + * Specifies SignedKey, linear PCM data. + */ + public static final String ENCODING_PCM_SIGNED = javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED.toString(); + /** + * Specifies unsigned, linear PCM data. + */ + public static final String ENCODING_PCM_UNSIGNED = javax.sound.sampled.AudioFormat.Encoding.PCM_UNSIGNED.toString(); + /** + * Specifies u-law encoded data. + */ + public static final String ENCODING_ULAW = javax.sound.sampled.AudioFormat.Encoding.ULAW.toString(); + /** + * Specifies a-law encoded data. + */ + public static final String ENCODING_ALAW = javax.sound.sampled.AudioFormat.Encoding.ALAW.toString(); + /** + * AVI PCM encoding. + */ + public static final String ENCODING_AVI_PCM = "\u0000\u0000\u0000\u0001"; + /** + * QuickTime 16-bit big endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_TWOS_PCM = "twos"; + /** + * QuickTime 16-bit little endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_SOWT_PCM = "sowt"; + /** + * QuickTime 24-bit big endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_IN24_PCM = "in24"; + /** + * QuickTime 32-bit big endian signed PCM encoding. + */ + public static final String ENCODING_QUICKTIME_IN32_PCM = "in32"; + /** + * QuickTime 8-bit unsigned PCM encoding. + */ + public static final String ENCODING_QUICKTIME_RAW_PCM = "raw "; + /** + * Specifies MP3 encoded data. + */ + public static final String ENCODING_MP3 = "MP3"; + /** + * The sample size in bits. + */ + public final static FormatKey SampleSizeInBitsKey = new FormatKey("sampleSizeInBits", Integer.class); + /** + * The numer of ChannelsKey. + */ + public final static FormatKey ChannelsKey = new FormatKey("channels", Integer.class); + /** + * The size of a frame. + */ + public final static FormatKey FrameSizeKey = new FormatKey("frameSize", Integer.class); + /** + * The compressor name. + */ + public final static FormatKey ByteOrderKey = new FormatKey("byteOrder", ByteOrder.class); + /** + * The number of frames per second. + */ + public final static FormatKey SampleRateKey = new FormatKey("sampleRate", Rational.class); + /** + * Whether values are signed. + */ + public final static FormatKey SignedKey = new FormatKey("signed", Boolean.class); + /** + * Whether silence is encoded as -128 instead of 0. + */ + public final static FormatKey SilenceBugKey = new FormatKey("silenceBug", Boolean.class); + + public static Format fromAudioFormat(javax.sound.sampled.AudioFormat fmt) { + return new Format( + MediaTypeKey, MediaType.AUDIO, + EncodingKey, fmt.getEncoding().toString(), + SampleRateKey, Rational.valueOf(fmt.getSampleRate()), + SampleSizeInBitsKey, fmt.getSampleSizeInBits(), + ChannelsKey, fmt.getChannels(), + FrameSizeKey, fmt.getFrameSize(), + FrameRateKey, Rational.valueOf(fmt.getFrameRate()), + ByteOrderKey, fmt.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN, + SignedKey, AudioFormat.Encoding.PCM_SIGNED.equals(fmt.getEncoding())//, + // + ); + } + + public static javax.sound.sampled.AudioFormat toAudioFormat(Format fmt) { + // We always use PCM_SIGNED or PCM_UNSIGNED + return new javax.sound.sampled.AudioFormat( + !fmt.containsKey(SignedKey) || fmt.get(SignedKey) ? Encoding.PCM_SIGNED : Encoding.PCM_UNSIGNED, + fmt.get(SampleRateKey).floatValue(), + fmt.get(SampleSizeInBitsKey, 16), + fmt.get(ChannelsKey, 1), + fmt.containsKey(FrameSizeKey) ? fmt.get(FrameSizeKey) : (fmt.get(SampleSizeInBitsKey, 16) + 7) / 8 * fmt.get(ChannelsKey, 1), + fmt.containsKey(FrameRateKey) ? fmt.get(FrameRateKey).floatValue() : fmt.get(SampleRateKey).floatValue(), + fmt.containsKey(ByteOrderKey) ? fmt.get(ByteOrderKey) == ByteOrder.BIG_ENDIAN : true); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Buffer.java b/trunk/libsrc/avi/src/org/monte/media/Buffer.java new file mode 100644 index 000000000..94c6da20e --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Buffer.java @@ -0,0 +1,164 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.monte.media; + +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.WritableRaster; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.monte.media.math.Rational; +import org.monte.media.util.Methods; + +/** + * A {@code Buffer} carries media data from one media processing unit to another. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-12 Created. + */ +public class Buffer { + + /** A flag mask that describes the boolean attributes for this buffer. + */ + public EnumSet flags = EnumSet.noneOf(BufferFlag.class); + /** Values which are not specified must have this value. */ + public static final int NOT_SPECIFIED = -1; + /** The track number. + * This can be set to NOT_SPECIFIED or to a number >= 0. + */ + public int track; + /** Header information, such as RTP header for this chunk. */ + public Object header; + /** The media data. */ + public Object data; + /** The data offset. This field is only used if {@code data} is an array. */ + public int offset; + /** The data length. This field is only used if {@code data} is an array. */ + public int length; + /** Duration of a sample in seconds. + * Multiply this with {@code sampleCount} to get the buffer duration. + */ + public Rational sampleDuration; + /** The time stamp of this buffer in seconds. */ + public Rational timeStamp; + /** The format of the data in this buffer. */ + public Format format; + /** The number of samples in the data field. */ + public int sampleCount = 1; + + /** Sequence number of the buffer. This can be used for debugging. */ + public long sequenceNumber; + + /** Sets all variables of this buffer to that buffer except for {@code data}, + * {@code offset}, {@code length} and {@code header}. + */ + public void setMetaTo(Buffer that) { + this.flags = EnumSet.copyOf(that.flags); + //this.data=that.data; + //this.offset=that.offset; + //this.length=that.length; + //this.header=that.header; + this.track = that.track; + this.sampleDuration = that.sampleDuration; + this.timeStamp = that.timeStamp; + this.format = that.format; + this.sampleCount = that.sampleCount; + this.format = that.format; + this.sequenceNumber=that.sequenceNumber; + } + + /** Sets {@code data}, {@code offset}, {@code length} and {@code header} + * of this buffer to that buffer. + * Note that this method creates copies of the {@code data} and + * {@code header}, so that these fields in that buffer can be discarded + * without affecting the contents of this buffer. + *

+ * FIXME - This method does not always create a copy!! + */ + public void setDataTo(Buffer that) { + this.offset = that.offset; + this.length = that.length; + this.data = copy(that.data, this.data); + this.header = copy(that.header, this.header); + + } + + private Object copy(Object from, Object into) { + if (from instanceof byte[]) { + byte[] b=(byte[])from; + if (!(into instanceof byte[]) || ((byte[]) into).length < b.length) { + into = new byte[b.length]; + } + System.arraycopy(b, 0, (byte[])into, 0, b.length); + } else if (from instanceof BufferedImage) { + // FIXME - Try to reuse BufferedImage in output! + BufferedImage img = (BufferedImage) from; + ColorModel cm = img.getColorModel(); + boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); + WritableRaster raster = img.copyData(null); + into = new BufferedImage(cm, raster, isAlphaPremultiplied, null); + } else if (from instanceof Cloneable) { + try { + into=Methods.invoke(from, "clone"); + } catch (NoSuchMethodException ex) { + into=from; + } + } else { + // FIXME - This is very fragile, since we do not know, if the + // input data stays valid until the output data is processed! + into = from; + } + + return into; + } + + /** Returns true if the specified flag is set. */ + public boolean isFlag(BufferFlag flag) { + return flags.contains(flag); + } + + /** Convenience method for setting a flag. */ + public void setFlag(BufferFlag flag) { + setFlag(flag, true); + } + + /** Convenience method for clearing a flag. */ + public void clearFlag(BufferFlag flag) { + setFlag(flag, false); + } + + /** Sets or clears the specified flag. */ + public void setFlag(BufferFlag flag, boolean value) { + if (value) { + flags.add(flag); + } else { + flags.remove(flag); + } + } + + /** Clears all flags, and then sets the specified flag. */ + public void setFlagsTo(BufferFlag... flags) { + if (flags.length == 0) { + this.flags = EnumSet.noneOf(BufferFlag.class); + } else { + this.flags = EnumSet.copyOf(Arrays.asList(flags)); + } + } + + /** Clears all flags, and then sets the specified flag. */ + public void setFlagsTo(EnumSet flags) { + if (flags == null) { + this.flags = EnumSet.noneOf(BufferFlag.class); + } else { + this.flags = EnumSet.copyOf(flags); + } + } + + public void clearFlags() { + flags.clear(); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java b/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java new file mode 100644 index 000000000..92c1e730d --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/BufferFlag.java @@ -0,0 +1,42 @@ +/* + * @(#)BufferFlag.java + * + * Copyright (c) 2011 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; + +/** + * {@code BufferFlag}. + * + * @author Werner Randelshofer + * @version $Id: BufferFlag.java 299 2013-01-03 07:40:18Z werner $ + */ +public enum BufferFlag { + + /** Indicates that the data in this buffer should be ignored. */ + DISCARD, + /** Indicates that this Buffer holds an intra-coded picture, which can be + * decoded independently. */ + KEYFRAME, + /** Indicates that the data in this buffer is at the end of the media. */ + END_OF_MEDIA, + /** Indicates that the data in this buffer is used for initializing the + * decoding queue. + *

+ * This flag is used when the media time of a track is set to a non-keyframe + * sample. Thus decoding must start at a keyframe at an earlier time. + *

+ * Decoders should decode the buffer. + * Encoders and Multiplexers should discard the buffer. + */ + PREFETCH, + /** Indicates that this buffer is known to have the same data as the + * previous buffer. This may improve encoding performance. + */ + SAME_DATA; +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Codec.java b/trunk/libsrc/avi/src/org/monte/media/Codec.java new file mode 100644 index 000000000..908707a8e --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Codec.java @@ -0,0 +1,73 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.monte.media; + +/** + * A {@code Codec} processes a {@code Buffer} and stores the result in another + * {@code Buffer}. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-12 Created. + */ +public interface Codec { + + /** The codec successfully converted the input to output. */ + public final static int CODEC_OK = 0; + /** The codec could not handle the input. */ + public final static int CODEC_FAILED = 1; + /** The codec did not fully consume the input buffer. + * The codec has updated the input buffer to + * reflect the amount of data that it has processed. + * The codec must be called again with the same input buffer. + */ + public final static int CODEC_INPUT_NOT_CONSUMED = 2; + /** The codec did not fully fill the output buffer. + * The codec has updated the output buffer to + * reflect the amount of data that it has processed. + * The codec must be called again with the same output buffer. + */ + public final static int CODEC_OUTPUT_NOT_FILLED = 4; + + /** Lists all of the input formats that this codec accepts. */ + public Format[] getInputFormats(); + + /** Lists all of the output formats that this codec can generate + * with the provided input format. If the input format is null, returns + * all supported output formats. + */ + public Format[] getOutputFormats(Format input); + + /** Sets the input format. + * Returns the format that was actually set. This is the closest format + * that the Codec supports. Returns null if the specified format is not + * supported and no reasonable match could be found. + */ + public Format setInputFormat(Format input); + + public Format getInputFormat(); + + /** Sets the output format. + * Returns the format that was actually set. This is the closest format + * that the Codec supports. Returns null if the specified format is not + * supported and no reasonable match could be found. + */ + public Format setOutputFormat(Format output); + + public Format getOutputFormat(); + + /** Performs the media processing defined by this codec. + *

+ * Copies the data from the input buffer into the output buffer. + * + * @return A combination of processing flags. + */ + public int process(Buffer in, Buffer out); + + /** Returns a human readable name of the codec. */ + public String getName(); + + /** Resets the state of the codec. */ + public void reset(); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java b/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java new file mode 100644 index 000000000..16496597b --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/DefaultRegistry.java @@ -0,0 +1,395 @@ +/* + * @(#)DefaultRegistry.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.AudioFormatKeys.*; + +/** + * {@code DefaultRegistry}. + *

+ * FIXME - The registry should be read from a file. + * + * @author Werner Randelshofer + * @version $Id: DefaultRegistry.java 299 2013-01-03 07:40:18Z werner $ + */ +public class DefaultRegistry extends Registry { + + private HashMap> codecMap; + private HashMap> readerMap; + private HashMap> writerMap; + private HashMap fileFormatMap; + + @Override + public Format[] getReaderFormats() { + return getFileFormats(); + } + + @Override + public Format[] getWriterFormats() { + return getFileFormats(); + } + + @Override + public Format[] getFileFormats() { + return fileFormatMap.values().toArray(new Format[fileFormatMap.size()]); + } + + private static class RegistryEntry { + + Format inputFormat; + Format outputFormat; + String className; + + public RegistryEntry(Format inputFormat, Format outputFormat, String className) { + this.inputFormat = inputFormat; + this.outputFormat = outputFormat; + this.className = className; + } + } + + public DefaultRegistry() { + } + + @Override + protected void init() { + codecMap = new HashMap>(); + readerMap = new HashMap>(); + writerMap = new HashMap>(); + fileFormatMap = new HashMap(); + + // IFF ANIM + // -------- + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_ANIM, EncodingKey, ENCODING_BITMAP_IMAGE), + "org.monte.media.anim.BitmapCodec"); + + // AVI + // -------- + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_DIB), + "org.monte.media.avi.DIBCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_MJPG), + "org.monte.media.jpeg.JPEGCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PNG), + "org.monte.media.png.PNGCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_RLE), + "org.monte.media.avi.RunLengthCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + "org.monte.media.avi.TechSmithCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_DOSBOX_SCREEN_CAPTURE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + "org.monte.media.avi.ZMBVCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PCM), + "org.monte.media.avi.AVIPCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_PCM), + "org.monte.media.avi.AVIPCMAudioCodec"); + + // QuickTime + // -------- + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW), + "org.monte.media.quicktime.RawCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_ANIMATION), + "org.monte.media.quicktime.AnimationCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_JPEG), + "org.monte.media.jpeg.JPEGCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_MJPG), + "org.monte.media.jpeg.JPEGCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_PNG), + "org.monte.media.png.PNGCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE), + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE), + "org.monte.media.avi.TechSmithCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_SOWT_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_SOWT_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN24_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN24_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN32_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_IN32_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_SIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + putBidiCodec( + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_PCM_UNSIGNED), + new Format(MediaTypeKey, MediaType.AUDIO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_QUICKTIME_RAW_PCM), + "org.monte.media.quicktime.QuickTimePCMAudioCodec"); + + + putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI), "org.monte.media.avi.AVIReader"); + putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), "org.monte.media.quicktime.QuickTimeReader"); + putReader(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM), "org.monte.media.anim.ANIMReader"); + putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI), "org.monte.media.avi.AVIWriter"); + putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), "org.monte.media.quicktime.QuickTimeWriter"); + putWriter(new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM), "org.monte.media.anim.ANIMWriter"); + + putFileFormat("avi", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI)); + putFileFormat("mov", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME)); + putFileFormat("anim", new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_ANIM)); + } + + /** + * + * @param inputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param outputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param codecClass + */ + public void putBidiCodec(Format inputFormat, Format outputFormat, String codecClass) { + putCodec(inputFormat, outputFormat, codecClass); + putCodec(outputFormat, inputFormat, codecClass); + } + + /** + * + * @param inputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param outputFormat Must have {@code MediaTypeKey}, {@code EncodingKey}, {@code MimeTypeKey}. + * @param codecClass + */ + @Override + public void putCodec(Format inputFormat, Format outputFormat, String codecClass) { + RegistryEntry entry = new RegistryEntry(inputFormat, outputFormat, codecClass); + addCodecEntry(inputFormat.get(EncodingKey), entry); + addCodecEntry(outputFormat.get(EncodingKey), entry); + } + + private void addCodecEntry(String key, RegistryEntry entry) { + LinkedList list = codecMap.get(key); + if (list == null) { + list = new LinkedList(); + codecMap.put(key, list); + } + list.add(entry); + } + + /** + * + * @param fileFormat Must have {@code MediaTypeKey}, {@code MimeTypeKey}. + * @param readerClass + */ + @Override + public void putReader(Format fileFormat, String readerClass) { + RegistryEntry entry = new RegistryEntry(null, fileFormat, readerClass); + String key = fileFormat.get(MimeTypeKey); + LinkedList list = readerMap.get(key); + if (list == null) { + list = new LinkedList(); + readerMap.put(key, list); + } + list.add(entry); + } + + /** + * + * @param fileFormat Must have {@code MediaTypeKey}, {@code MimeTypeKey}. + * @param writerClass + */ + @Override + public void putWriter(Format fileFormat, String writerClass) { + RegistryEntry entry = new RegistryEntry(fileFormat, null, writerClass); + String key = fileFormat.get(MimeTypeKey); + LinkedList list = writerMap.get(key); + if (list == null) { + list = new LinkedList(); + writerMap.put(key, list); + } + list.add(entry); + } + + @Override + public String[] getCodecClasses(Format inputFormat, Format outputFormat) { + HashSet classNames = new HashSet(); + HashSet entries = new HashSet(); + if (inputFormat != null) { + LinkedList re; + if (inputFormat.get(EncodingKey) == null) { + re = new LinkedList(); + for (Map.Entry> i : codecMap.entrySet()) { + for (RegistryEntry j : i.getValue()) { + if (inputFormat.matches(j.inputFormat)) { + re.add(j); + } + } + } + } else { + re = codecMap.get(inputFormat.get(EncodingKey)); + } + if (re != null) { + entries.addAll(re); + } + } + if (outputFormat != null) { + LinkedList re; + if (outputFormat.get(EncodingKey) == null) { + re = new LinkedList(); + for (Map.Entry> i : codecMap.entrySet()) { + for (RegistryEntry j : i.getValue()) { + if (outputFormat.matches(j.outputFormat)) { + re.add(j); + } + } + } + } else { + re = codecMap.get(outputFormat.get(EncodingKey)); + } + if (re != null) { + entries.addAll(re); + } + } + for (RegistryEntry e : entries) { + if ((inputFormat == null || e.inputFormat == null || inputFormat.matches(e.inputFormat)) + && (outputFormat == null || e.outputFormat == null || outputFormat.matches(e.outputFormat))) { + classNames.add(e.className); + } + } + return classNames.toArray(new String[classNames.size()]); + } + + @Override + public String[] getReaderClasses(Format fileFormat) { + LinkedList rr = readerMap.get(fileFormat.get(MimeTypeKey)); + String[] names = new String[rr == null ? 0 : rr.size()]; + if (rr != null) { + int i = 0; + for (RegistryEntry e : rr) { + names[i++] = e.className; + } + } + return names; + } + + @Override + public Format getFileFormat(File file) { + String ext = file.getName(); + int p = ext.lastIndexOf('.'); + if (p != -1) { + ext = ext.substring(p + 1); + } + ext = ext.toLowerCase(); + return fileFormatMap.get(ext); + } + + @Override + public String[] getWriterClasses(Format fileFormat) { + LinkedList rr = writerMap.get(fileFormat.get(MimeTypeKey)); + String[] names = new String[rr == null ? 0 : rr.size()]; + if (rr != null) { + int i = 0; + for (RegistryEntry e : rr) { + names[i++] = e.className; + } + } + return names; + } + + @Override + public void putFileFormat(String extension, Format format) { + fileFormatMap.put(extension.toLowerCase(), format); + } + + @Override + public String getExtension(Format ff) { + for (Map.Entry e : fileFormatMap.entrySet()) { + if (e.getValue().get(MimeTypeKey).equals(ff.get(MimeTypeKey))) { + return e.getKey(); + } + } + return ""; + } + + @Override + public void unregisterCodec(String codecClass) { + for (Map.Entry> i:codecMap.entrySet()) { + LinkedList ll=i.getValue(); + for (Iterator j=ll.iterator();j.hasNext();) { + RegistryEntry e=j.next(); + if (e.className.equals(codecClass)) { + j.remove(); + } + } + } + } + + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Format.java b/trunk/libsrc/avi/src/org/monte/media/Format.java new file mode 100644 index 000000000..edabcfc04 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Format.java @@ -0,0 +1,325 @@ +/* + * @(#)Format.java + * + * Copyright (c) 2011-2012 Werner Randelshofer, Goldau, Switzerland. + * All rights reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. + * For details see accompanying license terms. + */ +package org.monte.media; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Specifies the format of a media, for example of audio and video. + * + * @author Werner Randelshofer + * @version $Id: Format.java 299 2013-01-03 07:40:18Z werner $ + */ +public class Format { + + /** + * Holds the properties of the format. + */ + private HashMap properties; + + /** + * Creates a new format onlyWith the specified properties. + */ + public Format(Map properties) { + this(properties, true); + } + + /** + * Creates a new format onlyWith the specified properties. + */ + private Format(Map properties, boolean copy) { + if (copy || ! (properties instanceof HashMap)) { + for (Map.Entry e : properties.entrySet()) { + if (!e.getKey().isAssignable(e.getValue())) { + throw new ClassCastException(e.getValue() + " must be of type " + e.getKey().getValueClass()); + } + } + this.properties = new HashMap< FormatKey, Object>(properties); + } else { + this.properties = (HashMap< FormatKey, Object>) properties; + } + } + + /** + * Creates a new format onlyWith the specified properties. The properties + * must be given as key value pairs. + */ + public Format(Object... p) { + this.properties = new HashMap< FormatKey, Object>(); + for (int i = 0; i < p.length; i += 2) { + FormatKey key = (FormatKey) p[i]; + if (!key.isAssignable(p[i + 1])) { + throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass()); + } + this.properties.put(key, p[i + 1]); + } + } + + @SuppressWarnings("unchecked") + public T get(FormatKey key) { + return (T) properties.get(key); + } + + @SuppressWarnings("unchecked") + public T get(FormatKey key, T defaultValue) { + return (properties.containsKey(key)) ? (T) properties.get(key) : defaultValue; + } + + public boolean containsKey(FormatKey key) { + return properties.containsKey(key); + } + + /** + * Gets the properties of the format as an unmodifiable map. + */ + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + /** + * Gets the keys of the format as an unmodifiable set. + */ + public Set getKeys() { + return Collections.unmodifiableSet(properties.keySet()); + } + + /** + * Returns true if that format matches this format. That is iff all + * properties defined in both format objects are identical. Properties which + * are only defined in one of the format objects are not considered. + * + * @param that Another format. + * @return True if the other format matches this format. + */ + public boolean matches(Format that) { + for (Map.Entry e : properties.entrySet()) { + if (!e.getKey().isComment()) { + if (that.properties.containsKey(e.getKey())) { + Object a = e.getValue(); + Object b = that.properties.get(e.getKey()); + if (a != b && a == null || !a.equals(b)) { + return false; + } + + } + } + } + return true; + } + + public boolean matchesWithout(Format that, FormatKey... without) { + OuterLoop: + for (Map.Entry e : properties.entrySet()) { + FormatKey k = e.getKey(); + if (!e.getKey().isComment()) { + if (that.properties.containsKey(k)) { + for (int i = 0; i < without.length; i++) { + if (without[i] == k) { + continue OuterLoop; + } + } + Object a = e.getValue(); + Object b = that.properties.get(k); + if (a != b && a == null || !a.equals(b)) { + return false; + } + + } + } + } + return true; + } + + /** + * Creates a new format which contains all properties from this format and + * additional properties from that format.

If a property is specified in + * both formats, then the property value from this format is used. It + * overwrites that format.

If one of the format has more properties than + * the other, then the new format is more specific than this format. + * + * @param that + * @return That format with properties overwritten by this format. + */ + public Format append(Format that) { + HashMap m = new HashMap(this.properties); + for (Map.Entry e : that.properties.entrySet()) { + if (!m.containsKey(e.getKey())) { + m.put(e.getKey(), e.getValue()); + } + } + return new Format(m,false); + } + + /** + * Creates a new format which contains all properties from this format and + * additional properties listed.

If a property is specified in both + * formats, then the property value from this format is used. It overwrites + * that format.

If one of the format has more properties than the other, + * then the new format is more specific than this format. + * + * @param p The properties must be given as key value pairs. + * @return That format with properties overwritten by this format. + */ + public Format append(Object... p) { + HashMap m = new HashMap(this.properties); + for (int i = 0; i < p.length; i += 2) { + FormatKey key = (FormatKey) p[i]; + if (!key.isAssignable(p[i + 1])) { + throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass()); + } + m.put(key, p[i + 1]); + } + return new Format(m,false); + } + + /** + * Creates a new format which contains all properties from the specified + * format and additional properties from this format. + *

If a property is specified in both formats, then the property value + * from this format is used. It overwrites that format. + *

If one of the format has more properties than the other, then the new + * format is more specific than this format. + * + * @param that + * @return That format with properties overwritten by this format. + */ + public Format prepend(Format that) { + HashMap m = new HashMap(that.properties); + for (Map.Entry e : this.properties.entrySet()) { + if (!m.containsKey(e.getKey())) { + m.put(e.getKey(), e.getValue()); + } + } + return new Format(m,false); + } + + /** + * Creates a new format which contains all specified properties and + * additional properties from this format. + *

If a property is specified in both formats, then the property value + * from this format is used. It overwrites that format. + *

If one of the format has more properties than the other, then the new + * format is more specific than this format. + * + * @param p The properties must be given as key value pairs. + * @return That format with properties overwritten by this format. + */ + public Format prepend(Object... p) { + HashMap m = new HashMap(); + for (int i = 0; i < p.length; i += 2) { + FormatKey key = (FormatKey) p[i]; + if (!key.isAssignable(p[i + 1])) { + throw new ClassCastException(key + ": " + p[i + 1] + " must be of type " + key.getValueClass()); + } + m.put(key, p[i + 1]); + } + for (Map.Entry e : this.properties.entrySet()) { + if (!m.containsKey(e.getKey())) { + m.put(e.getKey(), e.getValue()); + } + } + return new Format(m,false); + } + /** + * Creates a new format which only has the specified keys (or less).

If + * the keys are reduced, then the new format is less specific than this + * format. + */ + public Format intersectKeys(FormatKey... keys) { + HashMap m = new HashMap(); + for (FormatKey k : keys) { + if (properties.containsKey(k)) { + m.put(k, properties.get(k)); + } + } + return new Format(m,false); + } + + /** + * Creates a new format without the specified keys.

If the keys are + * reduced, then the new format is less specific than this format. + */ + public Format removeKeys(FormatKey... keys) { + boolean needsRemoval = false; + for (FormatKey k : keys) { + if (properties.containsKey(k)) { + needsRemoval = true; + break; + } + } + if (!needsRemoval) { + return this; + } + + HashMap m = new HashMap(properties); + for (FormatKey k : keys) { + m.remove(k); + } + return new Format(m,false); + } + + /** + * Returns true if the format has the specified keys. + */ + public Format containsKeys(FormatKey... keys) { + HashMap m = new HashMap(properties); + for (FormatKey k : keys) { + m.remove(k); + } + return new Format(m,false); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder("Format{"); + boolean isFirst = true; + for (Map.Entry e : properties.entrySet()) { + if (isFirst) { + isFirst = false; + } else { + buf.append(','); + } + buf.append(e.getKey().toString()); + buf.append(':'); + appendStuffedString(e.getValue(), buf); + } + buf.append('}'); + return buf.toString(); + } + + /** + * This method is used by #toString. + */ + private static void appendStuffedString(Object value, StringBuilder stuffed) { + if (value == null) { + stuffed.append("null"); + } + value = value.toString(); + if (value instanceof String) { + for (char ch : ((String) value).toCharArray()) { + if (ch >= ' ') { + stuffed.append(ch); + } else { + String hex = Integer.toHexString(ch); + stuffed.append("\\u"); + for (int i = hex.length(); i < 4; i++) { + stuffed.append('0'); + } + stuffed.append(hex); + } + } + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/FormatKey.java b/trunk/libsrc/avi/src/org/monte/media/FormatKey.java new file mode 100644 index 000000000..374d76ca0 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/FormatKey.java @@ -0,0 +1,112 @@ +/* + * @(#)FormatKey.java + * + * Copyright (c) 2011 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; + +import java.io.Serializable; + +/** + * A FormatKey provides type-safe access to an attribute of + * a {@link Format}. + *

+ * A format key has a name, a type and a value. + * + * @author Werner Randelshofer + * @version $Id: FormatKey.java 299 2013-01-03 07:40:18Z werner $ + */ +public class FormatKey implements Serializable, Comparable { + + public static final long serialVersionUID = 1L; + /** + * Holds a String representation of the attribute key. + */ + private String key; + /** + * Holds a pretty name. This can be null, if the value is self-explaining. + */ + private String name; + /** This variable is used as a "type token" so that we can check for + * assignability of attribute values at runtime. + */ + private Class clazz; + + /** Comment keys are ignored when matching two media formats with each other. */ + private boolean comment; + + /** Creates a new instance with the specified attribute key, type token class, + * default value null, and allowing null values. */ + public FormatKey(String key, Class clazz) { + this(key, key, clazz); + } + + /** Creates a new instance with the specified attribute key, type token class, + * default value null, and allowing null values. */ + public FormatKey(String key, String name, Class clazz) { + this(key,name,clazz,false); + } + /** Creates a new instance with the specified attribute key, type token class, + * default value null, and allowing null values. */ + public FormatKey(String key, String name, Class clazz, boolean comment) { + this.key = key; + this.name = name; + this.clazz = clazz; + this.comment=comment; + } + + /** + * Returns the key string. + * @return key string. + */ + public String getKey() { + return key; + } + + /** + * Returns the pretty name string. + * @return name string. + */ + public String getName() { + return name; + } + + /** Returns the key string. */ + @Override + public String toString() { + return key; + } + + /** + * Returns true if the specified value is assignable with this key. + * + * @param value + * @return True if assignable. + */ + public boolean isAssignable(Object value) { + return clazz.isInstance(value); + } + + public boolean isComment() { + return comment; + } + + + public Class getValueClass() { + return clazz; + } + + @Override + public int compareTo(Object o) { + return compareTo((FormatKey) o); + } + + public int compareTo(FormatKey that) { + return this.key.compareTo(that.key); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java new file mode 100644 index 000000000..71dff637f --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/FormatKeys.java @@ -0,0 +1,61 @@ +/* + * @(#)FormatKeys.java + * + * Copyright (c) 2011 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; + +import org.monte.media.math.Rational; + +/** + * Defines common {@code FormatKey}'s. + * + * @author Werner Randelshofer + * @version $Id: FormatKeys.java 299 2013-01-03 07:40:18Z werner $ + */ +public class FormatKeys { + public static enum MediaType { + AUDIO, + VIDEO, + MIDI, + TEXT, + META, + FILE + } + /** + * The media MediaTypeKey. + */ + public final static FormatKey MediaTypeKey = new FormatKey("mediaType", MediaType.class); + /** + * The EncodingKey. + */ + public final static FormatKey EncodingKey = new FormatKey("encoding", String.class); + + // + public final static String MIME_AVI = "video/avi"; + public final static String MIME_QUICKTIME = "video/quicktime"; + public final static String MIME_MP4 = "video/mp4"; + public final static String MIME_JAVA = "Java"; + public final static String MIME_ANIM = "x-iff/anim"; + public final static String MIME_IMAGE_SEQUENCE = "ImageSequence"; + /** + * The mime type. + */ + public final static FormatKey MimeTypeKey = new FormatKey("mimeType", String.class); + /** + * The number of frames per second. + */ + public final static FormatKey FrameRateKey = new FormatKey("frameRate", Rational.class); + + /** + * The interval between key frames. + * If this value is not specified, most codecs will use {@code FrameRateKey} + * as a hint and try to produce one key frame per second. + */ + public final static FormatKey KeyFrameIntervalKey = new FormatKey("keyFrameInterval", Integer.class); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/MovieReader.java b/trunk/libsrc/avi/src/org/monte/media/MovieReader.java new file mode 100644 index 000000000..0370eea0c --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/MovieReader.java @@ -0,0 +1,93 @@ +/* + * @(#)MovieReader.java + * + * Copyright (c) 2011 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; + +import org.monte.media.math.Rational; +import java.io.IOException; + +/** + * A simple API for reading movie data (audio and video) from a file. + * + *

+ * FIXME - MovieReader should extend Demultiplexer + * + * @author Werner Randelshofer + * @version $Id: MovieReader.java 299 2013-01-03 07:40:18Z werner $ + */ +public interface MovieReader { + /** Returns the number of tracks. */ + public int getTrackCount() throws IOException; + + /** Finds a track with the specified format. + * + * @param fromTrack the start track number. + * @param format A format specification. + * @return The track number >= fromTrack or -1 if no track has been found. + */ + public int findTrack(int fromTrack, Format format) throws IOException; + + /** Returns the total duration of the movie . */ + public Rational getDuration() throws IOException; + /** Returns the duration of the specified track. */ + public Rational getDuration(int track) throws IOException; + + /** Returns the sample number for the specified time. */ + public long timeToSample(int track, Rational seconds) throws IOException; + /** Returns the time for the specified sample number. */ + public Rational sampleToTime(int track, long sample) throws IOException; + + /** Returns the file format. */ + public Format getFileFormat() throws IOException; + + /** Returns the media format of the specified track. + * + * @param track Track number. + * @return The media format of the track. + */ + public Format getFormat(int track) throws IOException; + + /** Returns the number of media data chunks in the specified track. + * A chunk contains one or more samples. + */ + public long getChunkCount(int track) throws IOException; + + /** Reads the next sample chunk from the specified track. + * + * @param track Track number. + * @param buffer The buffer into which to store the sample data. + */ + public void read(int track, Buffer buffer) throws IOException; + /** Reads the next sample chunk from the next track in playback sequence. + * The variable buffer.track contains the track number. + * + * @param buf The buffer into which to store the sample data. + */ + //public void read(Buffer buffer) throws IOException; + + /** Returns the index of the next track in playback sequence. + * + * @return Index of next track or -1 if end of media reached. + */ + public int nextTrack() throws IOException; + + public void close() throws IOException; + + /** Sets the read time of all tracks to the closest sync sample before or + * at the specified time. + * + * @param newValue Time in seconds. + */ + public void setMovieReadTime(Rational newValue) throws IOException; + + /** Returns the current time of the track. */ + public Rational getReadTime(int track) throws IOException; + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java b/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java new file mode 100644 index 000000000..e4fd559cf --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/MovieWriter.java @@ -0,0 +1,82 @@ +/* + * @(#)MovieWriter.java + * + * Copyright (c) 2011 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; + +import org.monte.media.math.Rational; +import java.io.IOException; + +/** + * A simple API for writing movie data (audio and video) into a file. + * + * @author Werner Randelshofer + * @version $Id: MovieWriter.java 299 2013-01-03 07:40:18Z werner $ + */ +public interface MovieWriter extends Multiplexer { + /** Returns the file format. */ + public Format getFileFormat() throws IOException; + + /** Adds a track to the writer for a suggested input format. + *

+ * The format should at least specify the desired {@link FormatKeys.MediaType}. + * The actual input format is a refined version of the suggested format. For + * example, if a MovieWriter only supports fixed frame rate video, then the + * MovieWriter will extend the format with that information. + *

+ * If the suggested input format is not compatible, then an IOException is + * thrown. For example, if a MovieWriter only supports fixed frame rate video, + * but a format with variable frame rate was requested. + * + * @param format The desired input format of the track. The actual input + * format may be a refined version of the specified format. + * @return The track number. + */ + public int addTrack(Format format) throws IOException; + + /** Returns the media format of the specified track. + * This is a refined version of the format that was requested when the + * track was added. See {@link #addTrack}. + * + * @param track Track number. + * @return The media format of the track. + */ + public Format getFormat(int track); + + /** Returns the number of tracks. */ + public int getTrackCount(); + + /** Writes a sample into the specified track. + * Does nothing if the discard-flag in the buffer is set to true. + * + * @param track The track number. + * @param buf The buffer containing the sample data. + */ + @Override + public void write(int track, Buffer buf) throws IOException; + + /** Closes the writer. */ + @Override + public void close() throws IOException; + + /** Returns true if the limit for media data has been reached. + * If this limit is reached, no more samples should be added to the movie. + *

+ * This limit is imposed by data structures of the movie file + * which will overflow if more samples are added to the movie. + *

+ * FIXME - Maybe replace by getCapacity():long. + */ + public boolean isDataLimitReached(); + + /** Returns the duration of the track in seconds. */ + public Rational getDuration(int track); + /** Returns true if the specified track has no samples. */ + public boolean isEmpty(int track); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java b/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java new file mode 100644 index 000000000..04311c5d9 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Multiplexer.java @@ -0,0 +1,34 @@ +/* + * @(#)Multiplexer.java 1.0 2011-02-19 + * + * Copyright (c) 2011 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; + +import java.io.IOException; + +/** + * A {@code Multiplexer} can write multiple media tracks into a + * single output stream. + * + * @author Werner Randelshofer + * @version 1.0 2011-02-19 Created. + */ +public interface Multiplexer { + /** Writes a sample. + * Does nothing if the discard-flag in the buffer is set to true. + * + * @param track The track number. + * @param buf The buffer containing the sample data. + */ + public void write(int track, Buffer buf) throws IOException; + + /** Closes the Multiplexer. */ + public void close() throws IOException; +} diff --git a/trunk/libsrc/avi/src/org/monte/media/ParseException.java b/trunk/libsrc/avi/src/org/monte/media/ParseException.java new file mode 100644 index 000000000..7aee64949 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/ParseException.java @@ -0,0 +1,30 @@ +/* + * @(#)ParseException.java + * + * Copyright (c) 1999-2012 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; + +/** + * Exception thrown by IFFParse. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version $Id: ParseException.java 299 2013-01-03 07:40:18Z werner $ + */ +public class ParseException extends Exception { + + public static final long serialVersionUID = 1L; + + public ParseException(String message) { + super(message); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/Registry.java b/trunk/libsrc/avi/src/org/monte/media/Registry.java new file mode 100644 index 000000000..e0f660a93 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/Registry.java @@ -0,0 +1,310 @@ +/* + * @(#)Registry.java + * + * Copyright (c) 2011 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; + +import java.io.File; +import java.util.ArrayList; +import static org.monte.media.FormatKeys.*; + +/** + * The {@code Registry} for audio and video codecs. + * + * @author Werner Randelshofer + * @version $Id: Registry.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class Registry { + + private static Registry instance; + + public static Registry getInstance() { + if (instance == null) { + instance = new DefaultRegistry(); + instance.init(); + } + return instance; + } + + /** + * Initializes the registry. + */ + protected abstract void init(); + + /** + * Puts a codec into the registry. + * + * @param inputFormat The input format. Must not be null. + * @param outputFormat The output format. Must not be null. + * @param codecClass The codec class name. Must not be null. + */ + public abstract void putCodec(Format inputFormat, Format outputFormat, String codecClass); + + /** + * Gets all codecs which can decode the specified format. + * + * @param format The format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public final String[] getDecoderClasses(Format format) { + return getCodecClasses(format, null); + } + + /** + * Gets all codecs which can decode the specified format. + * + * @param format The format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public final String[] getEncoderClasses(Format format) { + return getCodecClasses(null, format); + } + + /** + * Gets all codecs which can transcode from the specified input format to + * the specified output format. + * + * @param inputFormat The input format. + * @param outputFormat The output format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public abstract String[] getCodecClasses(// + Format inputFormat, + Format outputFormat); + + /** + * Gets all codecs which can decode the specified format. + * + * @param inputFormat The input format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public final Codec[] getDecoders(Format inputFormat) { + return getCodecs(inputFormat, null); + } + + /** + * Gets the first codec which can decode the specified format. + * + * @param inputFormat The output format. + * @return A codec. Returns null if no codec was found. + */ + public Codec getDecoder(Format inputFormat) { + return getCodec(inputFormat, null); + } + + /** + * Gets all codecs which can encode the specified format. + * + * @param outputFormat The output format. + * @return An array of codecs. If no codec was found, an empty array is + * returned. + */ + public final Codec[] getEncoders(Format outputFormat) { + return getCodecs(null, outputFormat); + } + + /** + * Gets the first codec which can encode the specified foramt. + * + * @param outputFormat The output format. + * @return A codec. Returns null if no codec was found. + */ + public Codec getEncoder(Format outputFormat) { + return getCodec(null, outputFormat); + } + + /** + * Gets all codecs which can transcode from the specified input format to + * the specified output format. + * + * @param inputFormat The input format. + * @param outputFormat The output format. + * @return An array of codec class names. If no codec was found, an empty + * array is returned. + */ + public Codec[] getCodecs(Format inputFormat, Format outputFormat) { + String[] clazz = getCodecClasses(inputFormat, outputFormat); + ArrayList codecs = new ArrayList(clazz.length); + for (int i = 0; i < clazz.length; i++) { + try { + codecs.add((Codec) Class.forName(clazz[i]).newInstance()); + } catch (Exception ex) { + //ex.printStackTrace(); + System.err.println("Monte Registry. Codec class not found: " + clazz[i]); + unregisterCodec(clazz[i]); + } + } + return codecs.toArray(new Codec[codecs.size()]); + } + + /** + * Gets a codec which can transcode from the specified input format to the + * specified output format. + * + * @param inputFormat The input format. + * @param outputFormat The output format. + * @return A codec or null. + */ + public Codec getCodec(Format inputFormat, Format outputFormat) { + String[] clazz = getCodecClasses(inputFormat, outputFormat); + for (int i = 0; i < clazz.length; i++) { + try { + Codec codec = ((Codec) Class.forName(clazz[i]).newInstance()); + codec.setInputFormat(inputFormat); + if (outputFormat != null) { + codec.setOutputFormat(outputFormat); + } + return codec; + } catch (Exception ex) { + //ex.printStackTrace(); + System.err.println("Monte Registry. Codec class not found: " + clazz[i]); + unregisterCodec(clazz[i]); + } + } + return null; + } + + /** + * Puts a reader into the registry. + * + * @param fileFormat The file format, e.g."video/avi", "video/quicktime". + * Use "Java" for formats which are not tied to a file format. Must not be + * null. + * @param readerClass The reader class name. Must not be null. + */ + public abstract void putReader(Format fileFormat, String readerClass); + + /** + * Puts a writer into the registry. + * + * @param fileFormat The file format, e.g."video/avi", "video/quicktime". + * Use "Java" for formats which are not tied to a file format. Must not be + * null. + * @param writerClass The writer class name. Must not be null. + */ + public abstract void putWriter(Format fileFormat, String writerClass); + + /** + * Gets all reader class names from the registry for the specified file + * format. + * + * @param fileFormat The file format, e.g."AVI", "QuickTime". + * @return The reader class names. + */ + public abstract String[] getReaderClasses(Format fileFormat); + + /** + * Gets all writer class names from the registry for the specified file + * format. + * + * @param fileFormat The file format, e.g."AVI", "QuickTime". + * @return The writer class names. + */ + public abstract String[] getWriterClasses(Format fileFormat); + + public MovieReader getReader(Format fileFormat, File file) { + String[] clazz = getReaderClasses(fileFormat); + for (int i = 0; i < clazz.length; i++) { + try { + return ((MovieReader) Class.forName(clazz[i]).getConstructor(File.class).newInstance(file)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + return null; + } + + public MovieWriter getWriter(File file) { + Format format = getFileFormat(file); + return format == null ? null : getWriter(format, file); + } + + public MovieWriter getWriter(Format fileFormat, File file) { + String[] clazz = getWriterClasses(fileFormat); + for (int i = 0; i < clazz.length; i++) { + try { + return ((MovieWriter) Class.forName(clazz[i]).getConstructor(File.class).newInstance(file)); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + return null; + } + + public MovieReader getReader(File file) { + Format format = getFileFormat(file); + return format == null ? null : getReader(format, file); + } + + public abstract void putFileFormat(String extension, Format format); + + public abstract Format getFileFormat(File file); + + public abstract Format[] getReaderFormats(); + + public abstract Format[] getWriterFormats(); + + public abstract Format[] getFileFormats(); + + public abstract String getExtension(Format ff); + + /** + * Suggests output formats for the given input media format and specified + * file format. + * + * @param inputMediaFormat + * @param outputFileFormat + * @return List of output media formats. + */ + public ArrayList suggestOutputFormats(Format inputMediaFormat, Format outputFileFormat) { + ArrayList formats = new ArrayList(); + Format matchFormat = new Format(// + MimeTypeKey, outputFileFormat.get(MimeTypeKey),// + MediaTypeKey, inputMediaFormat.get(MediaTypeKey)); + Codec[] codecs = getEncoders(matchFormat); + int matchingCount = 0; + for (Codec c : codecs) { + for (Format mf : c.getOutputFormats(null)) { + if (mf.matches(matchFormat)) { + if (inputMediaFormat.matchesWithout(mf, MimeTypeKey)) { + // add matching formats first + formats.add(0, mf.append(inputMediaFormat)); + matchingCount++; + } else if (inputMediaFormat.matchesWithout(mf, MimeTypeKey, EncodingKey)) { + // add formats which match everything but the encoding second + formats.add(matchingCount, mf.append(inputMediaFormat)); + } else { + // add remaining formats last + formats.add(mf.append(inputMediaFormat)); + } + } + } + } + + // remove duplicates + for (int i = formats.size() - 1; i >= 0; i--) { + Format fi = formats.get(i); + for (int j = i - 1; j >= 0; j--) { + Format fj = formats.get(j); + if (fi.matches(fj)) { + formats.remove(i); + break; + } + } + } + + return formats; + } + + public abstract void unregisterCodec(String codecClass); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java b/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java new file mode 100644 index 000000000..2bb943c3b --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/VideoFormatKeys.java @@ -0,0 +1,77 @@ +/* + * @(#)VideoFormatKeys.java + * + * Copyright (c) 2011 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; + +import org.monte.media.math.Rational; + +/** + * Defines common format keys for video media. + * + * @author Werner Randelshofer + * @version $Id: VideoFormatKeys.java 299 2013-01-03 07:40:18Z werner $ + */ +public class VideoFormatKeys extends FormatKeys { + // Standard video ENCODING strings for use with FormatKey.Encoding. + public static final String ENCODING_BUFFERED_IMAGE = "image"; + /** Cinepak format. */ + public static final String ENCODING_QUICKTIME_CINEPAK = "cvid"; + public static final String COMPRESSOR_NAME_QUICKTIME_CINEPAK = "Cinepak"; + /** JPEG format. */ + public static final String ENCODING_QUICKTIME_JPEG = "jpeg"; + public static final String COMPRESSOR_NAME_QUICKTIME_JPEG = "Photo - JPEG"; + /** PNG format. */ + public static final String ENCODING_QUICKTIME_PNG = "png "; + public static final String COMPRESSOR_NAME_QUICKTIME_PNG = "PNG"; + /** Animation format. */ + public static final String ENCODING_QUICKTIME_ANIMATION = "rle "; + public static final String COMPRESSOR_NAME_QUICKTIME_ANIMATION = "Animation"; + /** Raw format. */ + public static final String ENCODING_QUICKTIME_RAW = "raw "; + public static final String COMPRESSOR_NAME_QUICKTIME_RAW = "NONE"; + // AVI Formats + /** Microsoft Device Independent Bitmap (DIB) format. */ + public static final String ENCODING_AVI_DIB = "DIB "; + /** Microsoft Run Length format. */ + public static final String ENCODING_AVI_RLE = "RLE "; + /** Techsmith Screen Capture format. */ + public static final String ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE = "tscc"; + public static final String COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE = "Techsmith Screen Capture"; + /** DosBox Screen Capture format. */ + public static final String ENCODING_AVI_DOSBOX_SCREEN_CAPTURE = "ZMBV"; + /** JPEG format. */ + public static final String ENCODING_AVI_MJPG = "MJPG"; + /** PNG format. */ + public static final String ENCODING_AVI_PNG = "png "; + /** Interleaved planar bitmap format. */ + public static final String ENCODING_BITMAP_IMAGE = "ILBM"; + + // + + /** The WidthKey of a video frame. */ + public final static FormatKey WidthKey = new FormatKey("dimX","width", Integer.class); + /** The HeightKey of a video frame. */ + public final static FormatKey HeightKey = new FormatKey("dimY","height", Integer.class); + /** The number of bits per pixel. */ + public final static FormatKey DepthKey = new FormatKey("dimZ","depth", Integer.class); + /** The data class. */ + public final static FormatKey DataClassKey = new FormatKey("dataClass", Class.class); + /** The compressor name. */ + public final static FormatKey CompressorNameKey = new FormatKey("compressorName", "compressorName",String.class, true); + /** The pixel aspect ratio WidthKey : HeightKey; + */ + public final static FormatKey PixelAspectRatioKey = new FormatKey("pixelAspectRatio", Rational.class); + /** Whether the frame rate must be fixed. False means variable frame rate. */ + public final static FormatKey FixedFrameRateKey = new FormatKey("fixedFrameRate", Boolean.class); + /** Whether the video is interlaced. */ + public final static FormatKey InterlaceKey = new FormatKey("interlace", Boolean.class); + /** Encoding quality. Value between 0 and 1. */ + public final static FormatKey QualityKey = new FormatKey("quality", Float.class); +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java new file mode 100644 index 000000000..4f9a68bee --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIBMPDIB.java @@ -0,0 +1,106 @@ +/* + * @(#)AVIBMPDIB.java 1.0 2009-12-30 + * + * Copyright (c) 2009 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.avi; + +import org.monte.media.io.ImageInputStreamAdapter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.*; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; + +/** + * This class defines the JPEG Huffman table, which is omitted in AVI MJPEG + * files. + *

+ * Source: + * Microsoft Windows Bitmap Format. + * Multimedia Technical Note: JPEG DIB Format. + * (c) 1993 Microsoft Corporation. All rights reserved. + * BMPDIB.txt + * + * @author Werner Randelshofer + * @version 1.0 2009-12-30 Created. + */ + public class AVIBMPDIB { + + /** MJPG DHT Segment */ + private static byte[] MJPGDHTSeg = { + /* JPEG DHT Segment for YCrCb omitted from MJPG data */ + (byte) 0xFF, (byte) 0xC4, (byte) 0x01, (byte) 0xA2, + (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B, (byte) 0x01, + (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B, (byte) 0x10, (byte) 0x00, + (byte) 0x02, (byte) 0x01, (byte) 0x03, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x03, (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x7D, + (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x11, (byte) 0x05, (byte) 0x12, (byte) 0x21, (byte) 0x31, (byte) 0x41, (byte) 0x06, (byte) 0x13, (byte) 0x51, (byte) 0x61, + (byte) 0x07, (byte) 0x22, (byte) 0x71, (byte) 0x14, (byte) 0x32, (byte) 0x81, (byte) 0x91, (byte) 0xA1, (byte) 0x08, (byte) 0x23, (byte) 0x42, (byte) 0xB1, (byte) 0xC1, (byte) 0x15, (byte) 0x52, + (byte) 0xD1, (byte) 0xF0, (byte) 0x24, (byte) 0x33, (byte) 0x62, (byte) 0x72, (byte) 0x82, (byte) 0x09, (byte) 0x0A, (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x25, + (byte) 0x26, (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, + (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, + (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x83, + (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, + (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, + (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, (byte) 0xD3, + (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, + (byte) 0xE9, (byte) 0xEA, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x11, (byte) 0x00, (byte) 0x02, + (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x03, (byte) 0x04, (byte) 0x07, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x77, (byte) 0x00, + (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x11, (byte) 0x04, (byte) 0x05, (byte) 0x21, (byte) 0x31, (byte) 0x06, (byte) 0x12, (byte) 0x41, (byte) 0x51, (byte) 0x07, (byte) 0x61, (byte) 0x71, + (byte) 0x13, (byte) 0x22, (byte) 0x32, (byte) 0x81, (byte) 0x08, (byte) 0x14, (byte) 0x42, (byte) 0x91, (byte) 0xA1, (byte) 0xB1, (byte) 0xC1, (byte) 0x09, (byte) 0x23, (byte) 0x33, (byte) 0x52, + (byte) 0xF0, (byte) 0x15, (byte) 0x62, (byte) 0x72, (byte) 0xD1, (byte) 0x0A, (byte) 0x16, (byte) 0x24, (byte) 0x34, (byte) 0xE1, (byte) 0x25, (byte) 0xF1, (byte) 0x17, (byte) 0x18, (byte) 0x19, + (byte) 0x1A, (byte) 0x26, (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, + (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, + (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x82, + (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, + (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, + (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, + (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, + (byte) 0xE9, (byte) 0xEA, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA + }; + /** JFIF Start of Image (SOI) segment. */ + private static byte[] JFIFSOISeg = { + (byte) 0xff, + (byte) 0xd8 + }; + + public static InputStream prependDHTSeg(byte[] jpgWithoutDHT) { + return prependDHTSeg(jpgWithoutDHT, 0, jpgWithoutDHT.length); + } + public static InputStream prependDHTSeg(byte[] jpgWithoutDHT, int offset, int length) { + + // FIXME - Only add DHT Segment if none is present + + Vector v = new Vector(); + v.add(new ByteArrayInputStream(JFIFSOISeg)); + v.add(new ByteArrayInputStream(MJPGDHTSeg)); + v.add(new ByteArrayInputStream(jpgWithoutDHT,offset+JFIFSOISeg.length,length-JFIFSOISeg.length)); + return new SequenceInputStream(v.elements()); + } + + public static ImageInputStream prependDHTSeg(ImageInputStream iisWithoutDHT) throws IOException { + Vector v = new Vector(); + v.add(new ByteArrayInputStream(JFIFSOISeg)); + v.add(new ByteArrayInputStream(MJPGDHTSeg)); + iisWithoutDHT.seek(2);// skip JFIF SOI + v.add(new ImageInputStreamAdapter(iisWithoutDHT)); + return new MemoryCacheImageInputStream(new SequenceInputStream(v.elements())); + } + public static ImageInputStream prependDHTSeg(InputStream inWithoutDHT) throws IOException { + Vector v = new Vector(); + v.add(new ByteArrayInputStream(JFIFSOISeg)); + v.add(new ByteArrayInputStream(MJPGDHTSeg)); + inWithoutDHT.skip(2);// skip JFIF SOI + v.add(inWithoutDHT); + return new MemoryCacheImageInputStream(new SequenceInputStream(v.elements())); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java new file mode 100644 index 000000000..6207d2013 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIOutputStream.java @@ -0,0 +1,1117 @@ +/** + * @(#)AVIOutputStream.java + * + * Copyright (c) 2011-2012 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.avi; + +import java.awt.image.ColorModel; +import org.monte.media.riff.RIFFChunk; +import org.monte.media.math.Rational; +import java.util.ArrayList; +import org.monte.media.Format; +import org.monte.media.riff.RIFFParser; +import java.awt.Dimension; +import java.awt.image.IndexColorModel; +import java.io.*; +import java.nio.ByteOrder; +import javax.imageio.stream.*; +import static java.lang.Math.*; +import static org.monte.media.FormatKeys.*; +import static org.monte.media.AudioFormatKeys.*; +import static org.monte.media.VideoFormatKeys.*; + +/** + * Provides low-level support for writing already encoded audio and video + * samples into an AVI 1.0 file.

The length of an AVI 1.0 file is limited to + * 1 GB. This class supports lengths of up to 4 GB, but such files may not work + * on all players.

For detailed information about the AVI 1.0 file format + * see:
msdn.microsoft.com + * AVI RIFF
www.microsoft.com + * FOURCC for Video Compression
www.saettler.com + * RIFF
+ * + * @author Werner Randelshofer + * @version $Id: AVIOutputStream.java 306 2013-01-04 16:19:29Z werner $ + */ +public class AVIOutputStream extends AbstractAVIStream { + + /** + * The states of the movie output stream. + */ + protected static enum States { + + STARTED, FINISHED, CLOSED; + } + /** + * The current state of the movie output stream. + */ + protected States state = States.FINISHED; + /** + * This chunk holds the whole AVI content. + */ + protected CompositeChunk aviChunk; + /** + * This chunk holds the movie frames. + */ + protected CompositeChunk moviChunk; + /** + * This chunk holds the AVI Main Header. + */ + protected FixedSizeDataChunk avihChunk; + ArrayList idx1 = new ArrayList(); + + /** + * Creates a new instance. + * + * @param file the output file + */ + public AVIOutputStream(File file) throws IOException { + if (file.exists()) { + file.delete(); + } + this.out = new FileImageOutputStream(file); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + this.streamOffset = 0; + } + + /** + * Creates a new instance. + * + * @param out the output stream. + */ + public AVIOutputStream(ImageOutputStream out) throws IOException { + this.out = out; + this.streamOffset = out.getStreamPosition(); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + /** + * Adds a video track. + * + * @param fccHandler The 4-character code of the format. + * @param scale The numerator of the sample rate. + * @param rate The denominator of the sample rate. + * @param width The width of a video image. Must be greater than 0. + * @param height The height of a video image. Must be greater than 0. + * @param depth The number of bits per pixel. Must be greater than 0. + * @param syncInterval Interval for sync-samples. 0=automatic. 1=all frames + * are keyframes. Values larger than 1 specify that for every n-th frame is + * a keyframe. + * + * @return Returns the track index. + * + * @throws IllegalArgumentException if the width or the height is smaller + * than 1. + */ + public int addVideoTrack(String fccHandler, long scale, long rate, int width, int height, int depth, int syncInterval) throws IOException { + ensureFinished(); + if (fccHandler == null || fccHandler.length() != 4) { + throw new IllegalArgumentException("fccHandler must be 4 characters long:" + fccHandler); + } + VideoTrack vt = new VideoTrack(tracks.size(), typeToInt(fccHandler),// + new Format(MediaTypeKey, MediaType.VIDEO, + MimeTypeKey, MIME_AVI, + EncodingKey, fccHandler, + DataClassKey, byte[].class, + WidthKey, width, HeightKey, height, DepthKey, depth, + FixedFrameRateKey, true, + FrameRateKey, new Rational(rate, scale))); + vt.scale = scale; + vt.rate = rate; + vt.syncInterval = syncInterval; + vt.frameLeft = 0; + vt.frameTop = 0; + vt.frameRight = width; + vt.frameBottom = height; + vt.bitCount = depth; + vt.planes = 1; // must be 1 + + if (depth == 4) { + byte[] gray = new byte[16]; + for (int i = 0; i < gray.length; i++) { + gray[i] = (byte) ((i << 4) | i); + } + vt.palette = new IndexColorModel(4, 16, gray, gray, gray); + } else if (depth == 8) { + byte[] gray = new byte[256]; + for (int i = 0; i < gray.length; i++) { + gray[i] = (byte) i; + } + vt.palette = new IndexColorModel(8, 256, gray, gray, gray); + } + + tracks.add(vt); + return tracks.size() - 1; + } + + /** + * Adds an audio track. + * + * @param waveFormatTag The format of the audio stream given in MMREG.H, for + * example 0x0001 for WAVE_FORMAT_PCM. + * @param scale The numerator of the sample rate. + * @param rate The denominator of the sample rate. + * @param numberOfChannels The number of channels: 1 for mono, 2 for stereo. + * @param sampleSizeInBits The number of bits in a sample: 8 or 16. + * @param isCompressed Whether the sound is compressed. + * @param frameDuration The frame duration, expressed in the media’s + * timescale, where the timescale is equal to the sample rate. For + * uncompressed formats, this field is always 1. + * @param frameSize For uncompressed audio, the number of bytes in a sample + * for a single channel (sampleSize divided by 8). For compressed audio, the + * number of bytes in a frame. + * + * @throws IllegalArgumentException if the format is not 4 characters long, + * if the time scale is not between 1 and 2^32, if the integer portion of + * the sampleRate is not equal to the scale, if numberOfChannels is not 1 or + * 2. + * @return Returns the track index. + */ + public int addAudioTrack(int waveFormatTag, // + long scale, long rate, // + int numberOfChannels, int sampleSizeInBits, // + boolean isCompressed, // + int frameDuration, int frameSize) throws IOException { + ensureFinished(); + + if (scale < 1 || scale > (2L << 32)) { + throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + scale); + } + if (numberOfChannels != 1 && numberOfChannels != 2) { + throw new IllegalArgumentException("numberOfChannels must be 1 or 2: " + numberOfChannels); + } + if (sampleSizeInBits != 8 && sampleSizeInBits != 16) { + throw new IllegalArgumentException("sampleSize must be 8 or 16: " + numberOfChannels); + } + + AudioTrack t = new AudioTrack(tracks.size(), typeToInt("\u0000\u0000\u0000\u0000")); + t.wFormatTag = waveFormatTag; + + float afSampleRate = (float) rate / (float) scale; + + t.format = new Format(MediaTypeKey, MediaType.AUDIO, + MimeTypeKey, MIME_AVI, + EncodingKey, RIFFParser.idToString(waveFormatTag), + SampleRateKey, Rational.valueOf(afSampleRate), + SampleSizeInBitsKey, sampleSizeInBits, + ChannelsKey, numberOfChannels, + FrameSizeKey, frameSize, + FrameRateKey, Rational.valueOf(afSampleRate), + SignedKey, sampleSizeInBits != 8, + ByteOrderKey, ByteOrder.LITTLE_ENDIAN); + + t.scale = scale; + t.rate = rate; + t.samplesPerSec = rate / scale; + t.channels = numberOfChannels; + t.avgBytesPerSec = t.samplesPerSec * frameSize; + t.blockAlign = t.channels * sampleSizeInBits / 8; + t.bitsPerSample = sampleSizeInBits; + tracks.add(t); + return tracks.size() - 1; + } + + /** + * Sets the global color palette. + */ + public void setPalette(int track, ColorModel palette) { + if (palette instanceof IndexColorModel) { + ((VideoTrack) tracks.get(track)).palette = (IndexColorModel) palette; + } + } + + /** + * Gets the dimension of a track. + */ + public Dimension getVideoDimension(int track) { + Track tr = tracks.get(track); + if (tr instanceof VideoTrack) { + VideoTrack vt = (VideoTrack) tr; + Format fmt = vt.format; + return new Dimension(fmt.get(WidthKey), fmt.get(HeightKey)); + } else { + return new Dimension(0, 0); + } + } + + /** + * Returns the contents of the extra track header. Returns null if the + * header is not present.

Note: this method can only be performed before + * media data has been written into the tracks. + * + * @param track + * @param fourcc + * @param data the extra header as a byte array + * @throws IOException + */ + public void putExtraHeader(int track, String fourcc, byte[] data) throws IOException { + if (state == States.STARTED) { + throw new IllegalStateException("Stream headers have already been written!"); + } + Track tr = tracks.get(track); + int id = RIFFParser.stringToID(fourcc); + // Remove duplicate entries + for (int i = tr.extraHeaders.size() - 1; i >= 0; i--) { + if (tr.extraHeaders.get(i).getID() == id) { + tr.extraHeaders.remove(i); + } + } + + // Add new entry + RIFFChunk chunk = new RIFFChunk(STRH_ID, id, data.length, -1); + chunk.setData(data); + tr.extraHeaders.add(chunk); + } + + /** + * Returns the fourcc's of all extra stream headers. + * + * @param track + * @return An array of fourcc's of all extra stream headers. + * @throws IOException + */ + public String[] getExtraHeaderFourCCs(int track) throws IOException { + Track tr = tracks.get(track); + String[] fourccs = new String[tr.extraHeaders.size()]; + for (int i = 0; i < fourccs.length; i++) { + fourccs[i] = RIFFParser.idToString(tr.extraHeaders.get(i).getID()); + } + return fourccs; + } + + public void setName(int track, String name) { + tracks.get(track).name = name; + } + + /** + * Sets the compression quality of a track.

A value of 0 stands for + * "high compression is important" a value of 1 for "high image quality is + * important".

Changing this value affects the encoding of video frames + * which are subsequently written into the track. Frames which have already + * been written are not changed.

This value has no effect on videos + * encoded with lossless encoders such as the PNG format.

The default + * value is 0.97. + * + * @param newValue + */ + public void setCompressionQuality(int track, float newValue) { + VideoTrack vt = (VideoTrack) tracks.get(track); + vt.videoQuality = newValue; + } + + /** + * Returns the compression quality of a track. + * + * @return compression quality + */ + public float getCompressionQuality(int track) { + return ((VideoTrack) tracks.get(track)).videoQuality; + } /** + * Sets the state of the QuickTimeOutpuStream to started.

If the state + * is changed by this method, the prolog is written. + */ + protected void ensureStarted() throws IOException { + if (state != States.STARTED) { + writeProlog(); + state = States.STARTED; + } + } + + /** + * Sets the state of the QuickTimeOutpuStream to finished.

If the state + * is changed by this method, the prolog is written. + */ + protected void ensureFinished() throws IOException { + if (state != States.FINISHED) { + throw new IllegalStateException("Writer is in illegal state for this operation."); + } + } + + /** + * Writes an already encoded palette change into the specified track.

If + * a track contains palette changes, then all key frames must be immediately + * preceeded by a palette change chunk which also is a key frame. If a key + * frame is not preceeded by a key frame palette change chunk, it will be + * downgraded to a delta frame. + * + * @throws IllegalArgumentException if the track is not a video track. + */ + public void writePalette(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException { + Track tr = tracks.get(track); + if (!(tr instanceof VideoTrack)) { + throw new IllegalArgumentException("Error: track " + track + " is not a video track."); + } + if (!isKeyframe && tr.samples.isEmpty()) { + throw new IllegalStateException("The first sample in a track must be a keyframe."); + } + + VideoTrack vt = (VideoTrack) tr; + tr.flags |= STRH_FLAG_VIDEO_PALETTE_CHANGES; + + DataChunk paletteChangeChunk = new DataChunk(vt.twoCC | PC_ID); + long offset = getRelativeStreamPosition(); + ImageOutputStream pOut = paletteChangeChunk.getOutputStream(); + pOut.write(data, off, len); + moviChunk.add(paletteChangeChunk); + paletteChangeChunk.finish(); + long length = getRelativeStreamPosition() - offset; + Sample s = new Sample(paletteChangeChunk.chunkType, 0, offset, length, isKeyframe); + tr.addSample(s); + idx1.add(s); + //tr.length+=0; Length is not affected by this chunk! + offset = getRelativeStreamPosition(); + } + + /** + * Writes an already encoded sample from a file to the specified track.

+ * This method does not inspect the contents of the file. For example, Its + * your responsibility to only append JPG files if you have chosen the JPEG + * video format.

If you append all frames from files or from input + * streams, then you have to explicitly set the dimension of the video track + * before you call finish() or close(). + * + * @param file The file which holds the sample data. + * + * @throws IllegalStateException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSample(int track, File file, boolean isKeyframe) throws IOException { + FileInputStream in = null; + try { + in = new FileInputStream(file); + writeSample(track, in, isKeyframe); + } finally { + if (in != null) { + in.close(); + } + } + } + + /** + * Writes an already encoded sample from an input stream to the specified + * track.

This method does not inspect the contents of the file. For + * example, its your responsibility to only append JPG files if you have + * chosen the JPEG video format.

If you append all frames from files or + * from input streams, then you have to explicitly set the dimension of the + * video track before you call finish() or close(). + * + * @param track The track number. + * @param in The input stream which holds the sample data. + * @param isKeyframe True if the sample is a key frame. + * + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSample(int track, InputStream in, boolean isKeyframe) throws IOException { + ensureStarted(); + + Track tr = tracks.get(track); + + if (!isKeyframe && tr.samples.isEmpty()) { + throw new IllegalStateException("The first sample in a track must be a keyframe."); + } + + // If a stream has palette changes, then only palette change samples can + // be marked as keyframe. + if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) { + // If a keyframe sample is immediately preceeded by a palette change + // we can raise the palette change to a keyframe. + if (tr.samples.size() > 0) { + Sample s = tr.samples.get(tr.samples.size() - 1); + if ((s.chunkType & 0xffff) == PC_ID) { + s.isKeyframe = true; + } + } + isKeyframe = false; + } + + + DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe)); + moviChunk.add(dc); + ImageOutputStream mdatOut = dc.getOutputStream(); + long offset = getRelativeStreamPosition(); + byte[] buf = new byte[512]; + int len; + while ((len = in.read(buf)) != -1) { + mdatOut.write(buf, 0, len); + } + long length = getRelativeStreamPosition() - offset; + dc.finish(); + Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe); + tr.addSample(s); + idx1.add(s); + tr.length++; + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + } + } + + /** + * Writes an already encoded sample from a byte array into a track.

This + * method does not inspect the contents of the samples. The contents has to + * match the format and dimensions of the media in this track.

If a + * track contains palette changes, then all key frames must be immediately + * preceeded by a palette change chunk. If a key frame is not preceeded by a + * palette change chunk, it will be downgraded to a delta frame. + * + * @param track The track index. + * @param data The encoded sample data. + * @param off The startTime offset in the data. + * @param len The number of bytes to write. + * @param isKeyframe Whether the sample is a sync sample (keyframe). + * + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSample(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException { + ensureStarted(); + Track tr = tracks.get(track); + + // The first sample in a track is always a key frame + if (!isKeyframe && tr.samples.isEmpty()) { + throw new IllegalStateException("The first sample in a track must be a keyframe.\nTrack="+track+", "+tr.format); + } + + // If a stream has palette changes, then only palette change samples can + // be marked as keyframe. + if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) { + throw new IllegalStateException("Only palette changes can be marked as keyframe.\nTrack="+track+", "+tr.format); + } + + DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len); + moviChunk.add(dc); + ImageOutputStream mdatOut = dc.getOutputStream(); + long offset = getRelativeStreamPosition(); + mdatOut.write(data, off, len); + long length = getRelativeStreamPosition() - offset; + dc.finish(); + Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe); + tr.addSample(s); + idx1.add(s); + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + } + } + + /** + * Writes multiple already encoded samples from a byte array into a track. + *

This method does not inspect the contents of the data. The contents + * has to match the format and dimensions of the media in this track. + * + * @param track The track index. + * @param sampleCount The number of samples. + * @param data The encoded sample data. + * @param off The startTime offset in the data. + * @param len The number of bytes to write. Must be dividable by + * sampleCount. + * @param isKeyframe Whether the samples are sync samples. All samples must + * either be sync samples or non-sync samples. + * + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the sample data failed. + */ + public void writeSamples(int track, int sampleCount, byte[] data, int off, int len, boolean isKeyframe) throws IOException { + ensureStarted(); + Track tr = tracks.get(track); + if (tr.mediaType == AVIMediaType.AUDIO) { + DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len); + moviChunk.add(dc); + ImageOutputStream mdatOut = dc.getOutputStream(); + long offset = getRelativeStreamPosition(); + mdatOut.write(data, off, len); + long length = getRelativeStreamPosition() - offset; + dc.finish(); + Sample s = new Sample(dc.chunkType, sampleCount, offset, length, isKeyframe | tr.samples.isEmpty()); + tr.addSample(s); + idx1.add(s); + tr.length += sampleCount; + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + } + } else { + for (int i = 0; i < sampleCount; i++) { + writeSample(track, data, off, len / sampleCount, isKeyframe); + off += len / sampleCount; + } + } + } + + /** + * Returns the duration of the track in media time scale. + */ + public long getMediaDuration(int track) { + Track tr = tracks.get(track); + long duration = tr.startTime; + if (!tr.samples.isEmpty()) { + Sample s = tr.samples.get(tr.samples.size() - 1); + duration += s.timeStamp + s.duration; + } + return duration; + } + + /** + * Closes the stream. + * + * @exception IOException if an I/O error has occurred + */ + public void close() throws IOException { + if (state == States.STARTED) { + finish(); + } + if (state != States.CLOSED) { + out.close(); + state = States.CLOSED; + } + } + + /** + * Finishes writing the contents of the AVI 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 IOException if an I/O exception has occurred + */ + public void finish() throws IOException { + ensureOpen(); + if (state != States.FINISHED) { + moviChunk.finish(); + writeEpilog(); + state = States.FINISHED; + } + } + + /** + * 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"); + } + } + + /** + * Returns true if the limit for media samples has been reached. If this + * limit is reached, no more samples should be added to the movie.

AVI + * 1.0 files have a file size limit of 2 GB. This method returns true if a + * file size of 1.8 GB has been reached. + */ + public boolean isDataLimitReached() { + try { + return getRelativeStreamPosition() > (long) (1.8 * 1024 * 1024 * 1024); + } catch (IOException ex) { + return true; + } + } + + private void writeProlog() throws IOException { + // The file has the following structure: + // + // .RIFF AVI + // ..avih (AVI Header Chunk) + // ..LIST strl (for each track) + // ...strh (Stream Header Chunk) + // ...strf (Stream Format Chunk) + // ...**** (Extra Stream Header Chunks) + // ...strn (Stream Name Chunk) + // ..LIST movi + // ...00dc (Compressed video data chunk in Track 00, repeated for each frame) + // ..idx1 (List of video data chunks and their location in the file) + + // The RIFF AVI Chunk holds the complete movie + aviChunk = new CompositeChunk(RIFF_ID, AVI_ID); + CompositeChunk hdrlChunk = new CompositeChunk(LIST_ID, HDRL_ID); + + // Write empty AVI Main Header Chunk - we fill the data in later + aviChunk.add(hdrlChunk); + avihChunk = new FixedSizeDataChunk(AVIH_ID, 56); + avihChunk.seekToEndOfChunk(); + hdrlChunk.add(avihChunk); + + // Write empty AVI Stream Header Chunk - we fill the data in later + for (Track tr : tracks) { + + CompositeChunk strlChunk = new CompositeChunk(LIST_ID, STRL_ID); + hdrlChunk.add(strlChunk); + + tr.strhChunk = new FixedSizeDataChunk(STRH_ID, 56); + tr.strhChunk.seekToEndOfChunk(); + strlChunk.add(tr.strhChunk); + + tr.strfChunk = new FixedSizeDataChunk(STRF_ID, tr.getSTRFChunkSize()); + tr.strfChunk.seekToEndOfChunk(); + strlChunk.add(tr.strfChunk); + + for (RIFFChunk c : tr.extraHeaders) { + DataChunk d = new DataChunk(c.getID(), + c.getSize()); + ImageOutputStream dout = d.getOutputStream(); + dout.write(c.getData()); + d.finish(); + strlChunk.add(d); + } + + if (tr.name != null) { + byte[] data = (tr.name + "\u0000").getBytes("ASCII"); + DataChunk d = new DataChunk(STRN_ID, + data.length); + ImageOutputStream dout = d.getOutputStream(); + dout.write(data); + d.finish(); + strlChunk.add(d); + } + } + + moviChunk = new CompositeChunk(LIST_ID, MOVI_ID); + aviChunk.add(moviChunk); + + + } + + private void writeEpilog() throws IOException { + + ImageOutputStream d; + + /* Create Idx1 Chunk and write data + * ------------- + typedef struct _avioldindex { + FOURCC fcc; + DWORD cb; + struct _avioldindex_entry { + DWORD dwChunkId; + DWORD flags; + DWORD dwOffset; + DWORD dwSize; + } aIndex[]; + } AVIOLDINDEX; + */ + { + DataChunk idx1Chunk = new DataChunk(IDX1_ID); + aviChunk.add(idx1Chunk); + d = idx1Chunk.getOutputStream(); + long moviListOffset = moviChunk.offset + 8 + 8; + + { + double movieTime = 0; + int nTracks = tracks.size(); + int[] trackSampleIndex = new int[nTracks]; + long[] trackSampleCount = new long[nTracks]; + for (Sample s : idx1) { + d.setByteOrder(ByteOrder.BIG_ENDIAN); + d.writeInt(s.chunkType); // dwChunkId + d.setByteOrder(ByteOrder.LITTLE_ENDIAN); + // Specifies a FOURCC that identifies a stream in the AVI file. The + // FOURCC must have the form 'xxyy' where xx is the stream number and yy + // is a two-character code that identifies the contents of the stream: + // + // Two-character code Description + // db Uncompressed video frame + // dc Compressed video frame + // header Palette change + // wb Audio data + + d.writeInt(((s.chunkType & 0xffff) == PC_ID ? 0x100 : 0x0)// + | (s.isKeyframe ? 0x10 : 0x0)); // flags + // Specifies a bitwise combination of zero or more of the following + // flags: + // + // Value Name Description + // 0x10 AVIIF_KEYFRAME The data chunk is a key frame. + // 0x1 AVIIF_LIST The data chunk is a 'rec ' list. + // 0x100 AVIIF_NO_TIME The data chunk does not affect the timing of the + // stream. For example, this flag should be set for + // palette changes. + + d.writeInt((int) (s.offset - moviListOffset)); // dwOffset + // Specifies the location of the data chunk in the file. The value + // should be specified as an offset, in bytes, from the startTime of the + // 'movi' list; however, in some AVI files it is given as an offset from + // the startTime of the file. + + d.writeInt((int) (s.length)); // dwSize + // Specifies the size of the data chunk, in bytes. + } + + } + + idx1Chunk.finish(); + } + + /* Write Data into AVI Main Header Chunk + * ------------- + * The AVIMAINHEADER structure defines global information in an AVI file. + * see http://msdn.microsoft.com/en-us/library/ms779632(VS.85).aspx + typedef struct _avimainheader { + FOURCC fcc; + DWORD cb; + DWORD dwMicroSecPerFrame; + DWORD dwMaxBytesPerSec; + DWORD dwPaddingGranularity; + DWORD flags; + DWORD dwTotalFrames; + DWORD initialFrames; + DWORD dwStreams; + DWORD dwSuggestedBufferSize; + DWORD dwWidth; + DWORD dwHeight; + DWORD dwReserved[4]; + } AVIMAINHEADER; */ + { + avihChunk.seekToStartOfData(); + d = avihChunk.getOutputStream(); + + // compute largest buffer size + long largestBufferSize = 0; + long duration = 0; + for (Track tr : tracks) { + long trackDuration = 0; + for (Sample s : tr.samples) { + trackDuration += s.duration; + } + duration = max(duration, trackDuration); + for (Sample s : tr.samples) { + if (s.length > largestBufferSize) { + largestBufferSize = s.length; + } + } + } + + + + // FIXME compute dwMicroSecPerFrame properly! + Track tt = tracks.get(0); + + d.writeInt((int) ((1000000L * tt.scale) / tt.rate)); // dwMicroSecPerFrame + // Specifies the number of microseconds between frames. + // This value indicates the overall timing for the file. + + d.writeInt((int)largestBufferSize); // dwMaxBytesPerSec + // Specifies the approximate maximum data rate of the file. + // This value indicates the number of bytes per second the system + // must handle to present an AVI sequence as specified by the other + // parameters contained in the main header and stream header chunks. + + d.writeInt(0); // dwPaddingGranularity + // Specifies the alignment for data, in bytes. Pad the data to multiples + // of this value. + + d.writeInt(0x10|0x100|0x800); // flags + // Contains a bitwise combination of zero or more of the following + // flags: + // + // Value Name Description + // 0x10 AVIF_HASINDEX Indicates the AVI file has an index. + // 0x20 AVIF_MUSTUSEINDEX Indicates that application should use the + // index, rather than the physical ordering of the + // chunks in the file, to determine the order of + // presentation of the data. For example, this flag + // could be used to create a list of frames for + // editing. + // 0x100 AVIF_ISINTERLEAVED Indicates the AVI file is interleaved. + // 0x800 AVIF_TRUST_CK_TYPE ??? + // 0x1000 AVIF_WASCAPTUREFILE Indicates the AVI file is a specially + // allocated file used for capturing real-time + // video. Applications should warn the user before + // writing over a file with this flag set because + // the user probably defragmented this file. + // 0x20000 AVIF_COPYRIGHTED Indicates the AVI file contains copyrighted + // data and software. When this flag is used, + // software should not permit the data to be + // duplicated. + + /*long dwTotalFrames = 0; + for (Track t : tracks) { + dwTotalFrames += t.samples.size(); + }*/ + d.writeInt(tt.samples.size()); // dwTotalFrames + // Specifies the total number of frames of data in the file. + + d.writeInt(0); // initialFrames + // Specifies the initial frame for interleaved files. Noninterleaved + // files should specify zero. If you are creating interleaved files, + // specify the number of frames in the file prior to the initial frame + // of the AVI sequence in this member. + // To give the audio driver enough audio to work with, the audio data in + // an interleaved file must be skewed from the video data. Typically, + // the audio data should be moved forward enough frames to allow + // approximately 0.75 seconds of audio data to be preloaded. The + // dwInitialRecords member should be set to the number of frames the + // audio is skewed. Also set the same value for the initialFrames + // member of the AVISTREAMHEADER structure in the audio stream header + + d.writeInt(tracks.size()); // dwStreams + // Specifies the number of streams in the file. For example, a file with + // audio and video has two streams. + + d.writeInt((int) largestBufferSize); // dwSuggestedBufferSize + // Specifies the suggested buffer size for reading the file. Generally, + // this size should be large enough to contain the largest chunk in the + // file. If set to zero, or if it is too small, the playback software + // will have to reallocate memory during playback, which will reduce + // performance. For an interleaved file, the buffer size should be large + // enough to read an entire record, and not just a chunk. + { + VideoTrack vt = null; + int width = 0, height = 0; + // FIXME - Maybe we should support a global video dimension property + for (Track tr : tracks) { + width = max(width, max(tr.frameLeft, tr.frameRight)); + height = max(height, max(tr.frameTop, tr.frameBottom)); + } + d.writeInt(width); // dwWidth + // Specifies the width of the AVI file in pixels. + + d.writeInt(height); // dwHeight + // Specifies the height of the AVI file in pixels. + } + d.writeInt(0); // dwReserved[0] + d.writeInt(0); // dwReserved[1] + d.writeInt(0); // dwReserved[2] + d.writeInt(0); // dwReserved[3] + // Reserved. Set this array to zero. + } + + for (Track tr : tracks) { + /* Write Data into AVI Stream Header Chunk + * ------------- + * The AVISTREAMHEADER structure contains information about one stream + * in an AVI file. + * see http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx + typedef struct _avistreamheader { + FOURCC fcc; + DWORD cb; + FOURCC fccType; + FOURCC fccHandler; + DWORD flags; + WORD priority; + WORD language; + DWORD initialFrames; + DWORD scale; + DWORD rate; + DWORD startTime; + DWORD dwLength; + DWORD dwSuggestedBufferSize; + DWORD quality; + DWORD dwSampleSize; + struct { + short int left; + short int top; + short int right; + short int bottom; + } rcFrame; + } AVISTREAMHEADER; + */ + tr.strhChunk.seekToStartOfData(); + d = tr.strhChunk.getOutputStream(); + d.setByteOrder(ByteOrder.BIG_ENDIAN); + d.writeInt(typeToInt(tr.mediaType.fccType)); // fccType: "vids" for video stream + d.writeInt(tr.fccHandler); // fccHandler: specifies the codec + d.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + d.writeInt(tr.flags); + // Contains any flags for the data stream. The bits in the high-order + // word of these flags are specific to the type of data contained in the + // stream. The following standard flags are defined: + // + // Value Name Description + // AVISF_DISABLED 0x00000001 Indicates this stream should not + // be enabled by default. + // AVISF_VIDEO_PALCHANGES 0x00010000 + // Indicates this video stream contains + // palette changes. This flag warns the playback + // software that it will need to animate the + // palette. + + d.writeShort(tr.priority); // priority: highest priority denotes default stream + d.writeShort(tr.language); // language: language code (?) + d.writeInt((int) tr.initialFrames); // initialFrames: how far audio data is ahead of the video frames + d.writeInt((int) tr.scale); // scale: time scale + d.writeInt((int) tr.rate); // rate: sample rate in scale units + d.writeInt((int) tr.startTime); // startTime: starting time of stream + d.writeInt((int) tr.length); // dwLength: length of stream ! WRONG + + long dwSuggestedBufferSize = 0; + long dwSampleSize = -1; // => -1 indicates unknown + for (Sample s : tr.samples) { + if (s.length > dwSuggestedBufferSize) { + dwSuggestedBufferSize = s.length; + } + if (dwSampleSize == -1) { + dwSampleSize = s.length; + } else if (dwSampleSize != s.length) { + dwSampleSize = 0; + } + } + if (dwSampleSize == -1) { + dwSampleSize = 0; + } + + d.writeInt((int) dwSuggestedBufferSize); // dwSuggestedBufferSize + // Specifies how large a buffer should be used to read this stream. + // Typically, this contains a value corresponding to the largest chunk + // present in the stream. Using the correct buffer size makes playback + // more efficient. Use zero if you do not know the correct buffer size. + + d.writeInt(tr.quality); // quality + // Specifies an indicator of the quality of the data in the stream. + // Quality is represented as a number between 0 and 10,000. + // For compressed data, this typically represents the value of the + // quality parameter passed to the compression software. If set to –1, + // drivers use the default quality value. + + d.writeInt(tr instanceof AudioTrack ? ((AudioTrack) tr).blockAlign : (int) dwSampleSize); // dwSampleSize + // Specifies the size of a single sample of data. This is set to zero + // if the samples can vary in size. If this number is nonzero, then + // multiple samples of data can be grouped into a single chunk within + // the file. If it is zero, each sample of data (such as a video frame) + // must be in a separate chunk. For video streams, this number is + // typically zero, although it can be nonzero if all video frames are + // the same size. For audio streams, this number should be the same as + // the blockAlign member of the WAVEFORMATEX structure describing the + // audio. + + d.writeShort(tr.frameLeft); // rcFrame.left + d.writeShort(tr.frameTop); // rcFrame.top + d.writeShort(tr.frameRight); // rcFrame.right + d.writeShort(tr.frameBottom); // rcFrame.bottom + // Specifies the destination rectangle for a text or video stream within + // the movie rectangle specified by the dwWidth and dwHeight members of + // the AVI main header structure. The rcFrame member is typically used + // in support of multiple video streams. Set this rectangle to the + // coordinates corresponding to the movie rectangle to update the whole + // movie rectangle. Units for this member are pixels. The upper-left + // corner of the destination rectangle is relative to the upper-left + // corner of the movie rectangle. + + if (tr instanceof VideoTrack) { + VideoTrack vt = (VideoTrack) tr; + Format vf = tr.format; + + /* Write BITMAPINFOHEADR Data into AVI Stream Format Chunk + /* ------------- + * see http://msdn.microsoft.com/en-us/library/ms779712(VS.85).aspx + typedef struct tagBITMAPINFOHEADER { + DWORD biSize; + LONG width; + LONG height; + WORD planes; + WORD bitCount; + DWORD compression; + DWORD sizeImage; + LONG xPelsPerMeter; + LONG yPelsPerMeter; + DWORD clrUsed; + DWORD clrImportant; + } BITMAPINFOHEADER; + */ + tr.strfChunk.seekToStartOfData(); + d = tr.strfChunk.getOutputStream(); + d.writeInt(40); // biSize: number of bytes required by the structure. + d.writeInt(vf.get(WidthKey)); // width + d.writeInt(vf.get(HeightKey)); // height + d.writeShort(1); // planes + d.writeShort(vf.get(DepthKey)); // bitCount + + String enc = vf.get(EncodingKey); + if (enc.equals(ENCODING_AVI_DIB)) { + d.writeInt(0); // compression - BI_RGB for uncompressed RGB + } else if (enc.equals(ENCODING_AVI_RLE)) { + if (vf.get(DepthKey) == 8) { + d.writeInt(1); // compression - BI_RLE8 + } else if (vf.get(DepthKey) == 4) { + d.writeInt(2); // compression - BI_RLE4 + } else { + throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images"); + } + } else { + d.setByteOrder(ByteOrder.BIG_ENDIAN); + d.writeInt(typeToInt(vt.format.get(EncodingKey))); // compression + d.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } + + if (enc.equals(ENCODING_AVI_DIB)) { + d.writeInt(0); // sizeImage + } else { + if (vf.get(DepthKey) == 4) { + d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) / 2); // sizeImage + } else { + int bytesPerPixel = Math.max(1, vf.get(DepthKey) / 8); + d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) * bytesPerPixel); // sizeImage + } + } + + d.writeInt(0); // xPelsPerMeter + d.writeInt(0); // yPelsPerMeter + + d.writeInt(vt.palette == null ? 0 : vt.palette.getMapSize()); // clrUsed + + d.writeInt(0); // clrImportant + + if (vt.palette != null) { + for (int i = 0, n = vt.palette.getMapSize(); i < n; ++i) { + /* + * typedef struct tagRGBQUAD { + BYTE rgbBlue; + BYTE rgbGreen; + BYTE rgbRed; + BYTE rgbReserved; // This member is reserved and must be zero. + } RGBQUAD; + */ + d.write(vt.palette.getBlue(i)); + d.write(vt.palette.getGreen(i)); + d.write(vt.palette.getRed(i)); + d.write(0); + } + } + } else if (tr instanceof AudioTrack) { + AudioTrack at = (AudioTrack) tr; + + /* Write WAVEFORMATEX Data into AVI Stream Format Chunk + /* ------------- + * see http://msdn.microsoft.com/en-us/library/dd757720(v=vs.85).aspx + typedef struct { + WORD wFormatTag; + WORD channels; + DWORD samplesPerSec; + DWORD avgBytesPerSec; + WORD blockAlign; + WORD bitsPerSample; + WORD cbSize; + } WAVEFORMATEX; + */ + tr.strfChunk.seekToStartOfData(); + d = tr.strfChunk.getOutputStream(); + + d.writeShort(at.wFormatTag); // wFormatTag: WAVE_FORMAT_PCM=0x0001 + d.writeShort(at.channels); // channels + d.writeInt((int) at.samplesPerSec);// samplesPerSec + d.writeInt((int) at.avgBytesPerSec); // avgBytesPerSec + d.writeShort(at.blockAlign); // blockAlign + d.writeShort(at.bitsPerSample); // bitsPerSample + + d.writeShort(0); //cbSize + // cbSize: Size, in bytes, of extra format information appended + // to the end of the WAVEFORMATEX structure. This information + // can be used by non-PCM formats to store extra attributes for + // the wFormatTag. If no extra information is required by the + // wFormatTag, this member must be set to zero. If this value is + // 22, the format is most likely described using the + // WAVEFORMATEXTENSIBLE structure, of which WAVEFORMATEX is the + // first member. + } + } + + // ----------------- + aviChunk.finish(); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java b/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java new file mode 100644 index 000000000..6a41f0c7d --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AVIWriter.java @@ -0,0 +1,517 @@ +/** + * @(#)AVIWriter.java + * + * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland. All rights + * reserved. + * + * You may not use, copy or modify this file, except in compliance onlyWith the + * license agreement you entered into onlyWith Werner Randelshofer. For details + * see accompanying license terms. + */ +package org.monte.media.avi; + +import java.util.EnumSet; +import org.monte.media.math.Rational; +import org.monte.media.Format; +import org.monte.media.Codec; +import org.monte.media.Buffer; +import org.monte.media.MovieWriter; +import org.monte.media.Registry; +import org.monte.media.io.ByteArrayImageOutputStream; +import org.monte.media.riff.RIFFParser; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.*; +import java.nio.ByteOrder; +import java.util.Arrays; +import javax.imageio.stream.*; +import static org.monte.media.AudioFormatKeys.*; +import static org.monte.media.VideoFormatKeys.*; +import org.monte.media.BufferFlag; +import static org.monte.media.BufferFlag.*; + +/** + * Provides high-level support for encoding and writing audio and video samples + * into an AVI 1.0 file. + * + * @author Werner Randelshofer + * @version $Id: AVIWriter.java 306 2013-01-04 16:19:29Z werner $ + */ +public class AVIWriter extends AVIOutputStream implements MovieWriter { + + public final static Format AVI = new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_AVI); + public final static Format VIDEO_RAW = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_JPEG = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_MJPG, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_PNG = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_PNG, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_RLE = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_RLE, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + public final static Format VIDEO_SCREEN_CAPTURE = new Format( + MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_RAW); + + /** + * Creates a new AVI writer. + * + * @param file the output file + */ + public AVIWriter(File file) throws IOException { + super(file); + } + + /** + * Creates a new AVI writer. + * + * @param out the output stream. + */ + public AVIWriter(ImageOutputStream out) throws IOException { + super(out); + } + + @Override + public Format getFileFormat() throws IOException { + return AVI; + } + + @Override + public Format getFormat(int track) { + return tracks.get(track).format; + } + + /** + * Returns the media duration of the track in seconds. + */ + @Override + public Rational getDuration(int track) { + Track tr = tracks.get(track); + long duration = getMediaDuration(track); + return new Rational(duration * tr.scale, tr.rate); + } + + /** + * Adds a track. + * + * @param format The format of the track. + * @return The track number. + */ + @Override + public int addTrack(Format format) throws IOException { + if (format.get(MediaTypeKey) == MediaType.VIDEO) { + return addVideoTrack(format); + } else { + return addAudioTrack(format); + } + } + + /** + * Adds a video track. + * + * @param format The format of the track. + * @return The track number. + */ + private int addVideoTrack(Format vf) throws IOException { + if (!vf.containsKey(EncodingKey)) { + throw new IllegalArgumentException("EncodingKey missing in " + vf); + } + if (!vf.containsKey(FrameRateKey)) { + throw new IllegalArgumentException("FrameRateKey missing in " + vf); + } + if (!vf.containsKey(WidthKey)) { + throw new IllegalArgumentException("WidthKey missing in " + vf); + } + if (!vf.containsKey(HeightKey)) { + throw new IllegalArgumentException("HeightKey missing in " + vf); + } + if (!vf.containsKey(DepthKey)) { + throw new IllegalArgumentException("DepthKey missing in " + vf); + } + int tr = addVideoTrack(vf.get(EncodingKey), + vf.get(FrameRateKey).getDenominator(), vf.get(FrameRateKey).getNumerator(), + vf.get(WidthKey), vf.get(HeightKey), vf.get(DepthKey), + vf.get(FrameRateKey).floor(1).intValue()); + setCompressionQuality(tr, vf.get(QualityKey, 1.0f)); + return tr; + } + + /** + * Adds an audio track. + * + * @param format The format of the track. + * @return The track number. + */ + private int addAudioTrack(Format format) throws IOException { + int waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + + + long timeScale = 1; + long sampleRate = format.get(SampleRateKey, new Rational(41000, 0)).longValue(); + int numberOfChannels = format.get(ChannelsKey, 1); + int sampleSizeInBits = format.get(SampleSizeInBitsKey, 16); // + boolean isCompressed = false; // FIXME + int frameDuration = 1; + int frameSize = format.get(FrameSizeKey, (sampleSizeInBits + 7) / 8 * numberOfChannels); + + + String enc = format.get(EncodingKey); + if (enc == null) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_ALAW)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_PCM_SIGNED)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_PCM_UNSIGNED)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_ULAW)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM + } else if (enc.equals(ENCODING_MP3)) { + waveFormatTag = 0x0001; // WAVE_FORMAT_PCM - FIXME + } else { + waveFormatTag = RIFFParser.stringToID(format.get(EncodingKey)) & 0xffff; + } + + return addAudioTrack(waveFormatTag, // + timeScale, sampleRate, // + numberOfChannels, sampleSizeInBits, // + isCompressed, // + frameDuration, frameSize); + } + + /** + * Returns the codec of the specified track. + */ + public Codec getCodec(int track) { + return tracks.get(track).codec; + } + + /** + * Sets the codec for the specified track. + */ + public void setCodec(int track, Codec codec) { + tracks.get(track).codec = codec; + } + + @Override + public int getTrackCount() { + return tracks.size(); + } + + /** + * Encodes the provided image and writes its sample data into the specified + * track. + * + * @param track The track index. + * @param image The image of the video frame. + * @param duration Duration given in media time units. + * + * @throws IndexOutofBoundsException if the track index is out of bounds. + * @throws if the duration is less than 1, or if the dimension of the frame + * does not match the dimension of the video. + * @throws UnsupportedOperationException if the {@code MovieWriter} does not + * have a built-in encoder for this video format. + * @throws IOException if writing the sample data failed. + */ + public void write(int track, BufferedImage image, long duration) throws IOException { + ensureStarted(); + + VideoTrack vt = (VideoTrack) tracks.get(track); + if (vt.codec == null) { + createCodec(track); + } + if (vt.codec == null) { + throw new UnsupportedOperationException("No codec for this format: " + vt.format); + } + + // The dimension of the image must match the dimension of the video track + Format fmt = vt.format; + if (fmt.get(WidthKey) != image.getWidth() || fmt.get(HeightKey) != image.getHeight()) { + throw new IllegalArgumentException("Dimensions of image[" + vt.samples.size() + + "] (width=" + image.getWidth() + ", height=" + image.getHeight() + + ") differs from video format of track: " + fmt); + } + + // Encode pixel data + { + if (vt.outputBuffer == null) { + vt.outputBuffer = new Buffer(); + } + + boolean isKeyframe = vt.syncInterval == 0 ? false : vt.samples.size() % vt.syncInterval == 0; + + Buffer inputBuffer = new Buffer(); + inputBuffer.flags = (isKeyframe) ? EnumSet.of(KEYFRAME) : EnumSet.noneOf(BufferFlag.class); + inputBuffer.data = image; + vt.codec.process(inputBuffer, vt.outputBuffer); + if (vt.outputBuffer.flags.contains(DISCARD)) { + return; + } + + // Encode palette data + isKeyframe = vt.outputBuffer.flags.contains(KEYFRAME); + boolean paletteChange = writePalette(track, image, isKeyframe); + writeSample(track, (byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length, isKeyframe && !paletteChange); + /* + long offset = getRelativeStreamPosition(); + + DataChunk videoFrameChunk = new DataChunk(vt.getSampleChunkFourCC(isKeyframe)); + moviChunk.add(videoFrameChunk); + videoFrameChunk.getOutputStream().write((byte[]) vt.outputBuffer.data, vt.outputBuffer.offset, vt.outputBuffer.length); + videoFrameChunk.finish(); + long length = getRelativeStreamPosition() - offset; + + Sample s=new Sample(videoFrameChunk.chunkType, 1, offset, length, isKeyframe&&!paletteChange); + vt.addSample(s); + idx1.add(s); + + if (getRelativeStreamPosition() > 1L << 32) { + throw new IOException("AVI file is larger than 4 GB"); + }*/ + } + } + + /** + * Encodes the data provided in the buffer and then writes it into the + * specified track.

Does nothing if the discard-flag in the buffer is + * set to true. + * + * @param track The track number. + * @param buf The buffer containing a data sample. + */ + @Override + public void write(int track, Buffer buf) throws IOException { + ensureStarted(); + if (buf.flags.contains(DISCARD)) { + return; + } + + Track tr = tracks.get(track); + + boolean isKeyframe = buf.flags.contains(KEYFRAME); + if (buf.data instanceof BufferedImage) { + if (tr.syncInterval != 0) { + isKeyframe = buf.flags.contains(KEYFRAME) | (tr.samples.size() % tr.syncInterval == 0); + } + } + // Encode palette data + boolean paletteChange = false; + if (buf.data instanceof BufferedImage && tr instanceof VideoTrack) { + paletteChange = writePalette(track, (BufferedImage) buf.data, isKeyframe); + } else if (buf.header instanceof IndexColorModel) { + paletteChange = writePalette(track, (IndexColorModel) buf.header, isKeyframe); + } + // Encode sample data + { + if (buf.format == null) { + throw new IllegalArgumentException("Buffer.format must not be null"); + } + if (buf.format.matchesWithout(tr.format, FrameRateKey) && buf.data instanceof byte[]) { + writeSamples(track, buf.sampleCount, (byte[]) buf.data, buf.offset, buf.length, + buf.isFlag(KEYFRAME) && !paletteChange); + return; + } + + // We got here, because the buffer format does not match the track + // format. Lets see if we can create a codec which can perform the + // encoding for us. + + if (tr.codec == null) { + createCodec(track); + if (tr.codec == null) { + throw new UnsupportedOperationException("No codec for this format " + tr.format); + } + } + + if (tr.outputBuffer == null) { + tr.outputBuffer = new Buffer(); + } + Buffer outBuf = tr.outputBuffer; + if (tr.codec.process(buf, outBuf) != Codec.CODEC_OK) { + throw new IOException("Codec failed or could not encode the sample in a single step."); + } + if (outBuf.isFlag(DISCARD)) { + return; + } + writeSamples(track, outBuf.sampleCount, (byte[]) outBuf.data, outBuf.offset, outBuf.length, + isKeyframe && !paletteChange); + } + } + + private boolean writePalette(int track, BufferedImage image, boolean isKeyframe) throws IOException { + if ((image.getColorModel() instanceof IndexColorModel)) { + return writePalette(track, (IndexColorModel) image.getColorModel(), isKeyframe); + } + return false; + } + + private boolean writePalette(int track, IndexColorModel imgPalette, boolean isKeyframe) throws IOException { + ensureStarted(); + + VideoTrack vt = (VideoTrack) tracks.get(track); + int imgDepth = vt.bitCount; + ByteArrayImageOutputStream tmp = null; + boolean paletteChange = false; + switch (imgDepth) { + case 4: { + //IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); + int[] imgRGBs = new int[16]; + imgPalette.getRGBs(imgRGBs); + int[] previousRGBs = new int[16]; + if (vt.previousPalette == null) { + vt.previousPalette = vt.palette; + } + vt.previousPalette.getRGBs(previousRGBs); + if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) { + paletteChange = true; + vt.previousPalette = imgPalette; + /* + int first = imgPalette.getMapSize(); + int last = -1; + for (int i = 0; i < 16; i++) { + if (previousRGBs[i] != imgRGBs[i] && i < first) { + first = i; + } + if (previousRGBs[i] != imgRGBs[i] && i > last) { + last = i; + } + }*/ + int first = 0; + int last = imgPalette.getMapSize() - 1; + /* + * typedef struct { + BYTE bFirstEntry; + BYTE bNumEntries; + WORD wFlags; + PALETTEENTRY peNew[]; + } AVIPALCHANGE; + * + * typedef struct tagPALETTEENTRY { + BYTE peRed; + BYTE peGreen; + BYTE peBlue; + BYTE peFlags; + } PALETTEENTRY; + */ + tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN); + tmp.writeByte(first);//bFirstEntry + tmp.writeByte(last - first + 1);//bNumEntries + tmp.writeShort(0);//wFlags + + for (int i = first; i <= last; i++) { + tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red + tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green + tmp.writeByte(imgRGBs[i] & 0xff); // blue + tmp.writeByte(0); // reserved*/ + } + + } + break; + } + case 8: { + //IndexColorModel imgPalette = (IndexColorModel) image.getColorModel(); + int[] imgRGBs = new int[256]; + imgPalette.getRGBs(imgRGBs); + int[] previousRGBs = new int[256]; + if (vt.previousPalette != null) { + vt.previousPalette.getRGBs(previousRGBs); + } + if (isKeyframe || !Arrays.equals(imgRGBs, previousRGBs)) { + paletteChange = true; + vt.previousPalette = imgPalette; + /* + int first = imgPalette.getMapSize(); + int last = -1; + for (int i = 0; i < 16; i++) { + if (previousRGBs[i] != imgRGBs[i] && i < first) { + first = i; + } + if (previousRGBs[i] != imgRGBs[i] && i > last) { + last = i; + } + }*/ + int first = 0; + int last = imgPalette.getMapSize() - 1; + /* + * typedef struct { + BYTE bFirstEntry; + BYTE bNumEntries; + WORD wFlags; + PALETTEENTRY peNew[]; + } AVIPALCHANGE; + * + * typedef struct tagPALETTEENTRY { + BYTE peRed; + BYTE peGreen; + BYTE peBlue; + BYTE peFlags; + } PALETTEENTRY; + */ + tmp = new ByteArrayImageOutputStream(ByteOrder.LITTLE_ENDIAN); + tmp.writeByte(first);//bFirstEntry + tmp.writeByte(last - first + 1);//bNumEntries + tmp.writeShort(0);//wFlags + for (int i = first; i <= last; i++) { + tmp.writeByte((imgRGBs[i] >>> 16) & 0xff); // red + tmp.writeByte((imgRGBs[i] >>> 8) & 0xff); // green + tmp.writeByte(imgRGBs[i] & 0xff); // blue + tmp.writeByte(0); // reserved*/ + } + } + + break; + } + } + if (tmp != null) { + tmp.close(); + writePalette(track, tmp.toByteArray(), 0, (int) tmp.length(), isKeyframe); + } + return paletteChange; + } + + private Codec createCodec(Format fmt) { + return Registry.getInstance().getEncoder(fmt.prepend(MimeTypeKey, MIME_AVI)); + } + + private void createCodec(int track) { + Track tr = tracks.get(track); + Format fmt = tr.format; + tr.codec = createCodec(fmt); + String enc = fmt.get(EncodingKey); + if (tr.codec != null) { + if (fmt.get(MediaTypeKey) == MediaType.VIDEO) { + tr.codec.setInputFormat(fmt.prepend( + EncodingKey, ENCODING_BUFFERED_IMAGE, + DataClassKey, BufferedImage.class)); + if (null == tr.codec.setOutputFormat( + fmt.prepend(FixedFrameRateKey, true, + QualityKey, getCompressionQuality(track), + MimeTypeKey, MIME_AVI, + DataClassKey, byte[].class))) { + throw new UnsupportedOperationException("Track " + tr + " codec does not support format " + fmt + ". codec=" + tr.codec); + } + } else { + tr.codec.setInputFormat(null); + if (null == tr.codec.setOutputFormat( + fmt.prepend(FixedFrameRateKey, true, + QualityKey, getCompressionQuality(track), + MimeTypeKey, MIME_AVI, + DataClassKey, byte[].class))) { + throw new UnsupportedOperationException("Track " + tr + " codec " + tr.codec + " does not support format. " + fmt); + } + } + } + } + + public boolean isVFRSupported() { + return false; + } + + @Override + public boolean isEmpty(int track) { + return tracks.get(track).samples.isEmpty(); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java b/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java new file mode 100644 index 000000000..8e2a66df6 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/AbstractAVIStream.java @@ -0,0 +1,1734 @@ +/* + * @(#)AbstractAVIStream.java + * + * Copyright (c) 2011 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.avi; + +import org.monte.media.riff.RIFFChunk; +import java.util.Map; +import org.monte.media.Buffer; +import org.monte.media.Codec; +import org.monte.media.Format; +import org.monte.media.io.SubImageOutputStream; +import java.awt.Dimension; +import java.awt.image.IndexColorModel; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import javax.imageio.stream.ImageOutputStream; +import static org.monte.media.VideoFormatKeys.*; + +/** + * This is the base class for low-level AVI stream IO. + * + * @author Werner Randelshofer + * @version $Id: AbstractAVIStream.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class AbstractAVIStream { + + /** + * Chunk IDs. + */ + /* + protected final static int RIFF_ID = 0x46464952;//0x52494646;// "RIFF" + protected final static int AVI_ID = 0x20495641; //0x41564920;// "AVI " + protected final static int LIST_ID = 0x5453494c;//0x4c495354;// "LIST" + protected final static int MOVI_ID = 0x69766f6d;//0x6d6f7669;// "movi" + protected final static int HDRL_ID = 0x6c726468;//0x6864726c;// "hdrl" + protected final static int AVIH_ID = 0x68697661;//0x61766968;// "avih" + protected final static int STRL_ID = 0x6c727473;//0x7374726c;// "strl" + protected final static int STRH_ID = 0x68727473;//0x73747268;// "strh" + protected final static int STRN_ID = 0x6e727473;//0x7374726e;// "strn" + protected final static int STRF_ID = 0x66727473;//0x73747266;// "strf" + protected final static int STRD_ID = 0x64727473;//0x73747264;// "strd" + protected final static int IDX1_ID = 0x31786469;//0x69647831;// "idx1" + protected final static int REC_ID = 0x20636572;//0x72656320;// "rec " + protected final static int PC_ID = 0x63700000;//0x00007063;// "??pc" + protected final static int DB_ID = 0x62640000;//0x00006462;// "??db" + protected final static int DC_ID = 0x63640000;//0x00006463;// "??dc" + protected final static int WB_ID = 0x62770000;//0x00007762;// "??wb" + */ + protected final static int RIFF_ID =0x52494646;// "RIFF" + protected final static int AVI_ID = 0x41564920;// "AVI " + protected final static int AVIX_ID = 0x41564958;// "AVIX" + protected final static int LIST_ID = 0x4c495354;// "LIST" + protected final static int MOVI_ID = 0x6d6f7669;// "movi" + protected final static int HDRL_ID = 0x6864726c;// "hdrl" + protected final static int AVIH_ID = 0x61766968;// "avih" + protected final static int STRL_ID = 0x7374726c;// "strl" + protected final static int STRH_ID = 0x73747268;// "strh" + protected final static int STRN_ID = 0x7374726e;// "strn" + protected final static int STRF_ID = 0x73747266;// "strf" + protected final static int STRD_ID = 0x73747264;// "strd" + protected final static int IDX1_ID = 0x69647831;// "idx1" + protected final static int REC_ID = 0x72656320;// "rec " + protected final static int CHUNK_SUBTYPE_MASK = 0xffff;// "??xx" + protected final static int PC_ID = 0x00007063;// "??pc" + protected final static int DB_ID = 0x00006462;// "??db" + protected final static int DC_ID = 0x00006463;// "??dc" + protected final static int WB_ID = 0x00007762;// "??wb" + + /** + * Indicates the AVI file has an index. + */ + public final static int AVIH_FLAG_HAS_INDEX = 0x00000010; + /** + * Indicates that application should use the index, rather than the physical + * ordering of the chunks in the file, to determine the order of + * presentation of the data. For example, this flag could be used to create + * a list of frames for editing. + */ + public final static int AVIH_FLAG_MUST_USE_INDEX = 0x00000020; + /** + * Indicates the AVI file is interleaved. + */ + public final static int AVIH_FLAG_IS_INTERLEAVED = 0x00000100; + /** + * ?? + */ + public final static int AVIH_FLAG_TRUST_CK_TYPE = 0x00000800; + /** + * // Indicates the AVI file is a specially allocated file used for + * capturing real-time video. Applications should warn the user before + * writing over a file with this flag set because the user probably + * defragmented this file. + */ + public final static int AVIH_FLAG_WAS_CAPTURE_FILE = 0x00010000; + /* Indicates the AVI file contains copyrighted data and + * software. When this flag is used, software should not + * permit the data to be duplicated. */ + public final static int AVIH_FLAG_COPYRIGHTED = 0x00020000; + /** + * Indicates this stream should not be enabled by default. + */ + public final static int STRH_FLAG_DISABLED = 0x00000001; + /** + * Indicates this video stream contains palette changes. This flag warns the + * playback software that it will need to animate the palette. + */ + public final static int STRH_FLAG_VIDEO_PALETTE_CHANGES = 0x00010000; + /** + * Underlying output stream. + */ + protected ImageOutputStream out; + /** + * The offset in the underlying ImageOutputStream. Normally this is 0 unless + * the underlying stream already contained data when it was passed to the + * constructor. + */ + protected long streamOffset; + + /** + * Supported media types. + */ + public static enum AVIMediaType { + + AUDIO("auds"),// + MIDI("mids"),// + TEXT("txts"),// + VIDEO("vids")// + ; + protected final String fccType; + + @Override + public String toString() { + return fccType; + } + + AVIMediaType(String fourCC) { + this.fccType = fourCC; + } + } + /** + * The list of tracks in the file. + */ + protected ArrayList tracks = new ArrayList(); + + /** + * Gets the position relative to the beginning of the QuickTime stream.

+ * Usually this value is equal to the stream position of the underlying + * ImageOutputStream, but can be larger if the underlying stream already + * contained data. + * + * @return The relative stream position. + * @throws IOException + */ + protected long getRelativeStreamPosition() throws IOException { + return out.getStreamPosition() - streamOffset; + } + + /** + * Seeks relative to the beginning of the AVI stream.

Usually this equal + * to seeking in the underlying ImageOutputStream, but can be different if + * the underlying stream already contained data. + * + */ + protected void seekRelative(long newPosition) throws IOException { + out.seek(newPosition + streamOffset); + } + + /** + * AVI stores media data in sample chunks. A sample chunk may contain one or + * more media samples. A media sample is a single element in a sequence of + * time-ordered data. + */ + protected static class Sample { + + int chunkType; + /** + * Offset of the sample chunk relative to the startTime of the AVI file. + */ + long offset; + /** + * Data length of the sample chunk. + */ + long length; + /** + * The number of media samples in the sample chunk. + */ + int duration; + /** + * Whether the sample is a sync-sample. + */ + boolean isKeyframe; + long timeStamp; + /** + * Palette change sample. + */ + Sample header; + + /** + * Creates a new sample. + * + * @param duration The number of media samples contained in the sample + * chunk. + * @param offset The offset in the AVI stream. + * @param length The length in the AVI stream. + */ + public Sample(int chunkId, int duration, long offset, long length, boolean isSync) { + this.chunkType = chunkId; + this.duration = duration; + this.offset = offset; + this.length = length; + this.isKeyframe = isSync; + } + } + + /** + * Represents a track (or "stream") in an AVI file.

A track is defined + * by an "strh" chunk, which contains an {@code AVISTREAMHEADER} struct. + * Additional chunks can be provided depending on the media type of the + * track.

See + * http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx

+ *
+     * -----------------
+     * AVI Stream Header
+     * -----------------
+     *
+     * enum {
+     *    audioStream = "auds",
+     *   midiStream = "mids",
+     *   textStream = "txts",
+     *   videoStream = "vids"
+     * } aviStrhType;
+     *
+     * set {
+     *    disabled = 0x00000001, // Indicates this stream should not be enabled by default.
+     *    videoPaletteChanges = 0x00010000
+     *        // Indicates this video stream contains palette changes. This flag
+     *        // warns the playback software that it will need to animate the palette.
+     *} aviStrhFlags;
+     *
+     *typedef struct {
+     *    Int16 left;
+     *    Int16 top;
+     *    Int16 right;
+     *    Int16 bottom;
+     *} aviRectangle;
+     *
+     *typedef struct {
+     *     FOURCC enum aviStrhType type;
+     *        // Contains a FOURCC that specifies the type of the data contained in
+     *        // the stream. The following standard AVI values for video and audio are
+     *        // defined.
+     *     FOURCC handler;
+     *        // Optionally, contains a FOURCC that identifies a specific data
+     *        // handler.
+     *        // The data handler is the preferred handler for the stream. For audio
+     *        // and video streams, this specifies the codec for decoding the stream.
+     *     DWORD  set aviStrhFlags flags;
+     *        // Contains any flags for the data stream. The bits in the high-order
+     *        // word of these flags are specific to the type of data contained in the
+     *        // stream.
+     *     WORD   priority;
+     *        // Specifies priority of a stream type. For example, in a file with
+     *        // multiple audio streams, the one with the highest priority might be
+     *        // the default stream.
+     *     WORD   language;
+     *     DWORD  initialFrames;
+     *        // Specifies how far audio data is skewed ahead of the video frames in
+     *        // interleaved files. Typically, this is about 0.75 seconds. If you are
+     *        // creating interleaved files, specify the number of frames in the file
+     *        // prior to the initial frame of the AVI sequence in this member. For
+     *        // more information about the contents of this member, see "Special
+     *        // Information for Interleaved Files" in the Video for Windows
+     *        // Programmer's Guide.
+     *     DWORD  scale;
+     *        // Used with "rate" to specify the time scale that this stream will use.
+     *        // Dividing "rate" by "scale" gives the number of samples per second.
+     *        // For video streams, this is the frame rate. For audio streams, this
+     *        // rate corresponds to the time needed to play blockAlign bytes of
+     *        // audio, which for PCM audio is the just the sample rate.
+     *     DWORD  rate;
+     *        // See "scale".
+     *     DWORD  startTime;
+     *        // Specifies the starting time for this stream. The units are defined by
+     *        // the "rate" and "scale" members in the main file header. Usually, this
+     *        // is zero, but it can specify a delay time for a stream that does not
+     *        // startTime concurrently with the file.
+     *     DWORD  length;
+     *        // Specifies the length of this stream. The units are defined by the
+     *        // "rate" and "scale" members of the stream's header.
+     *     DWORD  suggestedBufferSize;
+     *        // Specifies how large a buffer should be used to read this stream.
+     *        // Typically, this contains a value corresponding to the largest chunk
+     *        // present in the stream. Using the correct buffer size makes playback
+     *        // more efficient. Use zero if you do not know the correct buffer size.
+     *     DWORD  quality;
+     *        // Specifies an indicator of the quality of the data in the stream.
+     *        // Quality is represented as a number between 0 and 10,000. For
+     *        // compressed data, this typically represents the value of the quality
+     *        // parameter passed to the compression software. If set to �1, drivers
+     *        // use the default quality value.
+     *     DWORD  sampleSize;
+     *        // Specifies the size of a single sample of data. This is set to zero if
+     *        // the samples can vary in size. If this number is nonzero, then
+     *        // multiple samples of data can be grouped into a single chunk within
+     *        // the file. If it is zero, each sample of data (such as a video frame)
+     *        // must be in a separate chunk. For video streams, this number is
+     *        // typically zero, although it can be nonzero if all video frames are
+     *        // the same size. For audio streams, this number should be the same as
+     *        // the blockAlign member of the WAVEFORMATEX structure describing the audio.
+     *    aviRectangle frame;
+     *        // Specifies the destination rectangle for a text or video stream within
+     *        // the movie rectangle specified by the "frameWidth" and "frameHeight"
+     *        // members of the AVI main header structure. The "frame" member is
+     *        // typically used in support of multiple video streams. Set this
+     *        // rectangle to the coordinates corresponding to the movie rectangle to
+     *        // update the whole movie rectangle. Units for this member are pixels.
+     *        // The upper-left corner of the destination rectangle is relative to the
+     *        // upper-left corner of the movie rectangle.
+     * } AVISTREAMHEADER; * 
+ */ + protected abstract class Track { + + /** + * The media format. + * + * FIXME - AbstractAVIStream should have no dependencies to Format. + */ + protected Format format; + // Common metadata + /** + * The scale of the media in the track.

Used with rate to specify + * the time scale that this stream will use. Dividing rate by scale + * gives the number of samples per second. For video streams, this is + * the frame rate. For audio streams, this rate corresponds to the time + * needed to play blockAlign bytes of audio, which for PCM audio is just + * the sample rate. + */ + /** + * The rate of the media in scale units.

+ * + * @see scale + */ + /** + * List of samples. + */ + protected ArrayList samples; + /** + * Interval between sync samples (keyframes). 0 = automatic. 1 = write + * all samples as sync samples. n = sync every n-th sample. + */ + protected int syncInterval = 30; + /** + * The twoCC code is used for the ids of the chunks which hold the data + * samples. + */ + protected int twoCC; + // + // AVISTREAMHEADER structure + // ------------------------- + /** + * {@code mediaType.fccType} contains a FOURCC that specifies the type + * of the data contained in the stream. The following standard AVI + * values for video and audio are defined. + * + * FOURCC Description 'auds' Audio stream 'mids' MIDI stream 'txts' Text + * stream 'vids' Video stream + * + */ + protected final AVIMediaType mediaType; + //protected String fccType; + /** + * Optionally, contains a FOURCC that identifies a specific data + * handler. The data handler is the preferred handler for the stream. + * For audio and video streams, this specifies the codec for decoding + * the stream. + */ + protected int fccHandler; + /** + * Contains any flags for the data stream. The bits in the high-order + * word of these flags are specific to the type of data contained in the + * stream. The following standard flags are defined. + * + * Value Description + * + * AVISF_DISABLED 0x00000001 Indicates this stream should not be enabled + * by default. + * + * AVISF_VIDEO_PALCHANGES 0x00010000 Indicates this video stream + * contains palette changes. This flag warns the playback software that + * it will need to animate the palette. + */ + protected int flags; + /** + * Specifies priority of a stream type. For example, in a file with + * multiple audio streams, the one with the highest priority might be + * the default stream. + */ + protected int priority = 0; + /** + * Language tag. + */ + protected int language = 0; + /** + * Specifies how far audio data is skewed ahead of the video frames in + * interleaved files. Typically, this is about 0.75 seconds. If you are + * creating interleaved files, specify the number of frames in the file + * prior to the initial frame of the AVI sequence in this member. For + * more information, see the remarks for the initialFrames member of the + * AVIMAINHEADER structure. + */ + protected long initialFrames = 0; + /** + * Used with rate to specify the time scale that this stream will use. + * Dividing rate by scale gives the number of samples per second. For + * video streams, this is the frame rate. For audio streams, this rate + * corresponds to the time needed to play blockAlign bytes of audio, + * which for PCM audio is the just the sample rate. + */ + protected long scale = 1; + /** + * The rate of the media in scale units. + */ + protected long rate = 30; + /** + * Specifies the starting time for this stream. The units are defined by + * the rate and scale members in the main file header. Usually, this is + * zero, but it can specify a delay time for a stream that does not + * startTime concurrently with the file. + */ + protected long startTime = 0; + /** + * Specifies the length of this stream. The units are defined by the + * rate and scale members of the stream's header. + */ + protected long length; + /** + * Specifies how large a buffer should be used to read this stream. + * Typically, this contains a value corresponding to the largest chunk + * present in the stream. Using the correct buffer size makes playback + * more efficient. Use zero if you do not know the correct buffer size. + */ + //protected long dwSuggestedBufferSize; => this field is computed from tr.samples + /** + * Specifies an indicator of the quality of the data in the stream. + * Quality is represented as a number between 0 and 10,000. For + * compressed data, this typically represents the value of the quality + * parameter passed to the compression software. If set to –1, drivers + * use the default quality value. + */ + protected int quality = -1; + /** + * Specifies the size of a single sample of data. This is set to zero if + * the samples can vary in size. If this number is nonzero, then + * multiple samples of data can be grouped into a single chunk within + * the file. If it is zero, each sample of data (such as a video frame) + * must be in a separate chunk. For video streams, this number is + * typically zero, although it can be nonzero if all video frames are + * the same size. For audio streams, this number should be the same as + * the blockAlign member of the WAVEFORMATEX structure describing the + * audio. + */ + //protected long dwSampleSize; => computed from tr.samples + /** + * Specifies the destination rectangle for a text or video stream within + * the movie rectangle specified by the dwWidth and dwHeight members of + * the AVI main header structure. The rcFrame member is typically used + * in support of multiple video streams. Set this rectangle to the + * coordinates corresponding to the movie rectangle to update the whole + * movie rectangle. Units for this member are pixels. The upper-left + * corner of the destination rectangle is relative to the upper-left + * corner of the movie rectangle. + */ + int frameLeft; + int frameTop; + int frameRight; + int frameBottom; + // -------------------------------- + // End of AVISTREAMHEADER structure + /** + * This chunk holds the AVI Stream Header. + */ + protected FixedSizeDataChunk strhChunk; + /** + * This chunk holds the AVI Stream Format Header. + */ + protected FixedSizeDataChunk strfChunk; + /** + * The optional name of the track. + */ + protected String name; + /** + * The codec. + */ + protected Codec codec; + /** + * The output buffer is used to store the output of the codec. + */ + protected Buffer outputBuffer; + /** + * The input buffer is used when one of the convenience methods without + * a Buffer parameter is used. + */ + protected Buffer inputBuffer; + /** + * The current chunk index of the reader. + */ + protected long readIndex = 0; + /** + * List of additional header chunks. + */ + protected ArrayList extraHeaders; + + public Track(int trackIndex, AVIMediaType mediaType, int fourCC) { + this.mediaType = mediaType; + twoCC = (('0'+trackIndex/10)<<24) | (('0'+trackIndex%10)<<16); + + this.fccHandler = fourCC; + this.samples = new ArrayList(); + this.extraHeaders = new ArrayList(); + } + + public abstract long getSTRFChunkSize(); + + public abstract int getSampleChunkFourCC(boolean isSync); + + public void addSample(Sample s) { + if (!samples.isEmpty()) { + s.timeStamp = samples.get(samples.size() - 1).timeStamp + samples.get(samples.size() - 1).duration; + } + samples.add(s); + length++; + } + } + + /** + * Represents a video track in an AVI file.

The format of a video track + * is defined in a "strf" chunk, which contains a {@code BITMAPINFOHEADER} + * struct. + * + * //---------------------- // AVI Bitmap Info Header // + * ---------------------- typedef struct { BYTE blue; BYTE green; BYTE red; + * BYTE reserved; } RGBQUAD; + * + * // Values for this enum taken from: // + * http://www.fourcc.org/index.php?http%3A//www.fourcc.org/rgb.php enum { + * BI_RGB = 0x00000000, RGB = 0x32424752, // Alias for BI_RGB BI_RLE8 = + * 0x01000000, RLE8 = 0x38454C52, // Alias for BI_RLE8 BI_RLE4 = 0x00000002, + * RLE4 = 0x34454C52, // Alias for BI_RLE4 BI_BITFIELDS = 0x00000003, raw = + * 0x32776173, RGBA = 0x41424752, RGBT = 0x54424752, cvid = "cvid" } + * bitmapCompression; + * + * typedef struct { DWORD structSize; // Specifies the number of bytes + * required by the structure. LONG width; // Specifies the width of the + * bitmap. // - For RGB formats, the width is specified in pixels. // - The + * same is true for YUV formats if the bitdepth is an even power // of 2. // + * - For YUV formats where the bitdepth is not an even power of 2, // + * however, the width is specified in bytes. // Decoders and video sources + * should propose formats where "width" is // the width of the image. If the + * video renderer is using DirectDraw, it // modifies the format so that + * "width" equals the stride of the surface, // and the "target" member of + * the VIDEOINFOHEADER or VIDEOINFOHEADER2 // structure specifies the image + * width. Then it proposes the modified // format using IPin::QueryAccept. + * // For RGB and even-power-of-2 YUV formats, if the video renderer does // + * not specify the stride, then round the width up to the nearst DWORD // + * boundary to find the stride. LONG height; // Specifies the height of the + * bitmap, in pixels. // - For uncompressed RGB bitmaps, if "height" is + * positive, the bitmap // is a bottom-up DIB with the origin at the lower + * left corner. If // "height" is negative, the bitmap is a top-down DIB + * with the origin // at the upper left corner. // - For YUV bitmaps, the + * bitmap is always top-down, regardless of the // sign of "height". + * Decoders should offer YUV formats with postive // "height", but for + * backward compatibility they should accept YUV // formats with either + * positive or negative "height". // - For compressed formats, height must + * be positive, regardless of // image orientation. WORD planes; // + * Specifies the number of planes for the target device. This value must // + * be set to 1. WORD bitCount; // Specifies the number of bits per pixel. + * //DWORD enum bitmapCompression compression; FOURCC enum bitmapCompression + * compression; // If the bitmap is compressed, this member is a FOURCC the + * specifies // the compression. // Value Description // BI_RLE8 A + * run-length encoded (RLE) format for bitmaps with 8 // bpp. The + * compression format is a 2-byte format // consisting of a count byte + * followed by a byte containing a color index. For more information, see + * Bitmap Compression. // BI_RLE4 An RLE format for bitmaps with 4 bpp. The + * compression // format is a 2-byte format consisting of a count byte // + * followed by two word-length color indexes. For more // information, see + * Bitmap Compression. // BI_JPEG Windows 98/Me, Windows 2000/XP: Indicates + * that the // image is a JPEG image. // BI_PNG Windows 98/Me, Windows + * 2000/XP: Indicates that the // image is a PNG image. // For uncompressed + * formats, the following values are possible: // Value Description // + * BI_RGB Uncompressed RGB. // BI_BITFIELDS Specifies that the bitmap is not + * compressed and that // the color table consists of three DWORD color + * masks // that specify the red, green, and blue components, // + * respectively, of each pixel. This is valid when used // with 16- and + * 32-bpp bitmaps. DWORD imageSizeInBytes; // Specifies the size, in bytes, + * of the image. This can be set to 0 for // uncompressed RGB bitmaps. LONG + * xPelsPerMeter; // Specifies the horizontal resolution, in pixels per + * meter, of the // target device for the bitmap. LONG yPelsPerMeter; // + * Specifies the vertical resolution, in pixels per meter, of the target // + * device for the bitmap. DWORD numberOfColorsUsed; // Specifies the number + * of color indices in the color table that are // actually used by the + * bitmap DWORD numberOfColorsImportant; // Specifies the number of color + * indices that are considered important // for displaying the bitmap. If + * this value is zero, all colors are // important. RGBQUAD colors[]; // If + * the bitmap is 8-bpp or less, the bitmap uses a color table, which // + * immediately follows the BITMAPINFOHEADER. The color table consists of // + * an array of RGBQUAD values. The size of the array is given by the // + * "clrUsed" member. If "clrUsed" is zero, the array contains the // maximum + * number of colors for the given bitdepth; that is, // 2^"bitCount" colors. + * } BITMAPINFOHEADER; + * + */ + protected class VideoTrack extends Track { + // Video metadata + + /** + * The video compression quality. + */ + protected float videoQuality = 0.97f; + /** + * Index color model for RAW_RGB4 and RAW_RGB8 formats. + */ + protected IndexColorModel palette; + protected IndexColorModel previousPalette; + /** + * Previous frame for delta compression. + */ + protected Object previousData; + //protected Rectangle rcFrame; + // BITMAPINFOHEADER structure + /** + * Specifies the number of bytes required by the structure. This value + * does not include the size of the color table or the size of the color + * masks, if they are appended to the end of structure. + */ + //protected long biSize; => computed when writing chukn + /** + * Specifies the width of the bitmap, in pixels. For information about + * calculating the stride of the bitmap. + * + */ + int width; + /** + * Specifies the height of the bitmap, in pixels. + * + * For uncompressed RGB bitmaps, if height is positive, the bitmap is a + * bottom-up DIB with the origin at the lower left corner. If height is + * negative, the bitmap is a top-down DIB with the origin at the upper + * left corner. For YUV bitmaps, the bitmap is always top-down, + * regardless of the sign of height. Decoders should offer YUV formats + * with positive height, but for backward compatibility they should + * accept YUV formats with either positive or negative height. For + * compressed formats, height must be positive, regardless of image + * orientation. + */ + int height; + /** + * Specifies the number of planes for the target device. This value must + * be set to 1. + */ + int planes; + /** + * Specifies the number of bits per pixel (bpp). For uncompressed + * formats, this value is the average number of bits per pixel. For + * compressed formats, this value is the implied bit depth of the + * uncompressed image, after the image has been decoded. + */ + int bitCount; + /** + * For compressed video and YUV formats, this member is a FOURCC code, + * specified as a DWORD in little-endian order. For example, YUYV video + * has the FOURCC 'VYUY' or 0x56595559. + * + * For uncompressed RGB formats, the following values are possible: + * + * Value Description + * + * BI_RGB Uncompressed RGB. + * + * BI_BITFIELDS Uncompressed RGB with color masks. Valid for 16-bpp and + * 32-bpp bitmaps. + * + * Note that BI_JPG and BI_PNG are not valid video formats. + * + * For 16-bpp bitmaps, if compression equals BI_RGB, the format is + * always RGB 555. If compression equals BI_BITFIELDS, the format is + * either RGB 555 or RGB 565. Use the subtype GUID in the AM_MEDIA_TYPE + * structure to determine the specific RGB type. + */ + String compression; + /** + * Specifies the size, in bytes, of the image. This can be set to 0 for + * uncompressed RGB bitmaps. + */ + long sizeImage; + /** + * Specifies the horizontal resolution, in pixels per meter, of the + * target device for the bitmap. + */ + long xPelsPerMeter; + /** + * Specifies the vertical resolution, in pixels per meter, of the target + * device for the bitmap. + */ + long yPelsPerMeter; + /** + * Specifies the number of color indices in the color table that are + * actually used by the bitmap. See Remarks for more information. + */ + long clrUsed; + /** + * Specifies the number of color indices that are considered important + * for displaying the bitmap. If this value is zero, all colors are + * important. + */ + long clrImportant; + private int sampleChunkFourCC; + + public VideoTrack(int trackIndex, int fourCC, Format videoFormat) { + super(trackIndex, AVIMediaType.VIDEO, fourCC); + this.format = videoFormat; + sampleChunkFourCC = videoFormat != null && videoFormat.get(EncodingKey).equals(ENCODING_AVI_DIB) ? twoCC | DB_ID : twoCC | DC_ID; + } + + @Override + public long getSTRFChunkSize() { + return palette == null ? 40 : 40 + palette.getMapSize() * 4; + + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + return sampleChunkFourCC; + } + } + + /** + *

The format of a video track is defined in a "strf" chunk, which + * contains a {@code WAVEFORMATEX} struct. + *

+     * ----------------------
+     * AVI Wave Format Header
+     * ----------------------
+     * // values for this enum taken from mmreg.h
+     * enum {
+     *         WAVE_FORMAT_PCM = 0x0001,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_ADPCM = 0x0002,
+     *         //  Microsoft Corporation
+     *          *   IEEE754: range (+1, -1]
+     *          *  32-bit/64-bit format as defined by
+     *          *  MSVC++ float/double type
+     *
+     *         WAVE_FORMAT_IEEE_FLOAT = 0x0003,
+     *         //  IBM Corporation
+     *         WAVE_FORMAT_IBM_CVSD = 0x0005,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_ALAW = 0x0006,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_MULAW = 0x0007,
+     *         //  OKI
+     *         WAVE_FORMAT_OKI_ADPCM = 0x0010,
+     *         //  Intel Corporation
+     *         WAVE_FORMAT_DVI_ADPCM = 0x0011,
+     *         //  Intel Corporation
+     *         WAVE_FORMAT_IMA_ADPCM = 0x0011,
+     *         //  Videologic
+     *         WAVE_FORMAT_MEDIASPACE_ADPCM = 0x0012,
+     *         //  Sierra Semiconductor Corp
+     *         WAVE_FORMAT_SIERRA_ADPCM = 0x0013,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_G723_ADPCM = 0x0014,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGISTD = 0x0015,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGIFIX = 0x0016,
+     *         //  Dialogic Corporation
+     *         WAVE_FORMAT_DIALOGIC_OKI_ADPCM = 0x0017,
+     *         //  Media Vision, Inc.
+     *         WAVE_FORMAT_MEDIAVISION_ADPCM = 0x0018,
+     *         //  Yamaha Corporation of America
+     *         WAVE_FORMAT_YAMAHA_ADPCM = 0x0020,
+     *         //  Speech Compression
+     *         WAVE_FORMAT_SONARC = 0x0021,
+     *         //  DSP Group, Inc
+     *         WAVE_FORMAT_DSPGROUP_TRUESPEECH = 0x0022,
+     *         //  Echo Speech Corporation
+     *         WAVE_FORMAT_ECHOSC1 = 0x0023,
+     *         //
+     *         WAVE_FORMAT_AUDIOFILE_AF36 = 0x0024,
+     *         //  Audio Processing Technology
+     *         WAVE_FORMAT_APTX = 0x0025,
+     *         //
+     *         WAVE_FORMAT_AUDIOFILE_AF10 = 0x0026,
+     *         //  Dolby Laboratories
+     *         WAVE_FORMAT_DOLBY_AC2 = 0x0030,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_GSM610 = 0x0031,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_MSNAUDIO = 0x0032,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_ANTEX_ADPCME = 0x0033,
+     *         //  Control Resources Limited
+     *         WAVE_FORMAT_CONTROL_RES_VQLPC = 0x0034,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGIREAL = 0x0035,
+     *         //  DSP Solutions, Inc.
+     *         WAVE_FORMAT_DIGIADPCM = 0x0036,
+     *         //  Control Resources Limited
+     *         WAVE_FORMAT_CONTROL_RES_CR10 = 0x0037,
+     *         //  Natural MicroSystems
+     *         WAVE_FORMAT_NMS_VBXADPCM = 0x0038,
+     *         // Crystal Semiconductor IMA ADPCM
+     *         WAVE_FORMAT_CS_IMAADPCM = 0x0039,
+     *         // Echo Speech Corporation
+     *         WAVE_FORMAT_ECHOSC3 = 0x003A,
+     *         // Rockwell International
+     *         WAVE_FORMAT_ROCKWELL_ADPCM = 0x003B,
+     *         // Rockwell International
+     *         WAVE_FORMAT_ROCKWELL_DIGITALK = 0x003C,
+     *         // Xebec Multimedia Solutions Limited
+     *         WAVE_FORMAT_XEBEC = 0x003D,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_G721_ADPCM = 0x0040,
+     *         //  Antex Electronics Corporation
+     *         WAVE_FORMAT_G728_CELP = 0x0041,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_MPEG = 0x0050,
+     *         //  ISO/MPEG Layer3 Format Tag
+     *         WAVE_FORMAT_MPEGLAYER3 = 0x0055,
+     *         //  Cirrus Logic
+     *         WAVE_FORMAT_CIRRUS = 0x0060,
+     *         //  ESS Technology
+     *         WAVE_FORMAT_ESPCM = 0x0061,
+     *         //  Voxware Inc
+     *         WAVE_FORMAT_VOXWARE = 0x0062,
+     *         //  Canopus, co., Ltd.
+     *         WAVE_FORMAT_CANOPUS_ATRAC = 0x0063,
+     *         //  APICOM
+     *         WAVE_FORMAT_G726_ADPCM = 0x0064,
+     *         //  APICOM
+     *         WAVE_FORMAT_G722_ADPCM = 0x0065,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_DSAT = 0x0066,
+     *         //  Microsoft Corporation
+     *         WAVE_FORMAT_DSAT_DISPLAY = 0x0067,
+     *         //  Softsound, Ltd.
+     *         WAVE_FORMAT_SOFTSOUND = 0x0080,
+     *         //  Rhetorex Inc
+     *         WAVE_FORMAT_RHETOREX_ADPCM = 0x0100,
+     *         //  Creative Labs, Inc
+     *         WAVE_FORMAT_CREATIVE_ADPCM = 0x0200,
+     *         //  Creative Labs, Inc
+     *         WAVE_FORMAT_CREATIVE_FASTSPEECH8 = 0x0202,
+     *         //  Creative Labs, Inc
+     *         WAVE_FORMAT_CREATIVE_FASTSPEECH10 = 0x0203,
+     *         //  Quarterdeck Corporation
+     *         WAVE_FORMAT_QUARTERDECK = 0x0220,
+     *         //  Fujitsu Corp.
+     *         WAVE_FORMAT_FM_TOWNS_SND = 0x0300,
+     *         //  Brooktree Corporation
+     *         WAVE_FORMAT_BTV_DIGITAL = 0x0400,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLIGSM = 0x1000,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLIADPCM = 0x1001,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLICELP = 0x1002,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLISBC = 0x1003,
+     *         //  Ing C. Olivetti & C., S.p.A.
+     *         WAVE_FORMAT_OLIOPR = 0x1004,
+     *         //  Lernout & Hauspie
+     *         WAVE_FORMAT_LH_CODEC = 0x1100,
+     *         //  Norris Communications, Inc.
+     *         WAVE_FORMAT_NORRIS = 0x1400,
+     *         //
+     *          *  the WAVE_FORMAT_DEVELOPMENT format tag can be used during the
+     *          *  development phase of a new wave format.  Before shipping, you MUST
+     *          *  acquire an official format tag from Microsoft.
+     *
+     *         WAVE_FORMAT_DEVELOPMENT = 0xFFFF,
+     * } wFormatTagEnum;
+     *
+     * typedef struct {
+     *   WORD enum wFormatTagEnum formatTag;
+     *     // Waveform-audio format type. Format tags are registered with Microsoft
+     *     // Corporation for many compression algorithms. A complete list of format
+     *     // tags can be found in the Mmreg.h header file. For one- or two-channel
+     *     // Pulse Code Modulation (PCM) data, this value should be WAVE_FORMAT_PCM.
+     *   WORD  numberOfChannels;
+     *     // Number of channels in the waveform-audio data. Monaural data uses one
+     *     // channel and stereo data uses two channels.
+     *   DWORD samplesPerSec;
+     *     // Sample rate, in samples per second (hertz). If "formatTag" is
+     *     // "WAVE_FORMAT_PCM", then common values for "samplesPerSec" are 8.0 kHz,
+     *     // 11.025 kHz, 22.05 kHz, and 44.1 kHz. For non-PCM formats, this member
+     *     // must be computed according to the manufacturer's specification of the
+     *     // format tag.
+     *   DWORD avgBytesPerSec;
+     *     // Required average data-transfer rate, in bytes per second, for the format
+     *     // tag. If "formatTag" is "WAVE_FORMAT_PCM", "avgBytesPerSec" should be
+     *     // equal to the product of "samplesPerSec" and "blockAlignment". For non-PCM
+     *     // formats, this member must be computed according to the manufacturer's
+     *     // specification of the format tag.
+     *   WORD  blockAlignment;
+     *     // Block alignment, in bytes. The block alignment is the minimum atomic unit
+     *     // of data for the "formatTag" format type. If "formatTag" is
+     *     // "WAVE_FORMAT_PCM" or "WAVE_FORMAT_EXTENSIBLE, "blockAlignment" must be equal
+     *     // to the product of "numberOfChannels" and "bitsPerSample" divided by 8 (bits per
+     *     // byte). For non-PCM formats, this member must be computed according to the
+     *     // manufacturer's specification of the format tag.
+     *     // Software must process a multiple of "blockAlignment" bytes of data at a
+     *     // time. Data written to and read from a device must always start at the
+     *     // beginning of a block. For example, it is illegal to start playback of PCM
+     *     // data in the middle of a sample (that is, on a non-block-aligned boundary).
+     *   WORD  bitsPerSample;
+     *     // Bits per sample for the waveFormatTag format type. If "formatTag" is
+     *     // "WAVE_FORMAT_PCM", then "bitsPerSample" should be equal to 8 or 16. For
+     *     // non-PCM formats, this member must be set according to the manufacturer's
+     *     // specification of the format tag. If "formatTag" is
+     *     // "WAVE_FORMAT_EXTENSIBLE", this value can be any integer multiple of 8.
+     *     // Some compression schemes cannot define a value for "bitsPerSample", so
+     *     // this member can be zero.
+     *   WORD  cbSize;
+     *     // Size, in bytes, of extra format information appended to the end of the
+     *     // WAVEFORMATEX structure. This information can be used by non-PCM formats
+     *     // to store extra attributes for the "wFormatTag". If no extra information
+     *     // is required by the "wFormatTag", this member must be set to zero. For
+     *     // WAVE_FORMAT_PCM formats (and only WAVE_FORMAT_PCM formats), this member
+     *     // is ignored.
+     *   byte[cbSize] extra;
+     * } WAVEFORMATEX;
+     * 
+ */ + protected class AudioTrack extends Track { + + // WAVEFORMATEX Structure + /** + * Waveform-audio format type. Format tags are registered with Microsoft + * Corporation for many compression algorithms. A complete list of + * format tags can be found in the Mmreg.h header file. For one- or + * two-channel Pulse Code Modulation (PCM) data, this value should be + * WAVE_FORMAT_PCM=0x0001. + * + * If wFormatTag equals WAVE_FORMAT_EXTENSIBLE=0xFFFE, the structure is + * interpreted as a WAVEFORMATEXTENSIBLE structure. If wFormatTag equals + * WAVE_FORMAT_MPEG, the structure is interpreted as an MPEG1WAVEFORMAT + * structure. If wFormatTag equals MPEGLAYER3WAVEFORMAT, the structure + * is interpreted as an MPEGLAYER3WAVEFORMAT structure. Before + * reinterpreting a WAVEFORMATEX structure as one of these extended + * structures, verify that the actual structure size is sufficiently + * large and that the cbSize member indicates a valid size. + */ + protected int wFormatTag; + /** + * Number of channels in the waveform-audio data. Monaural data uses one + * channel and stereo data uses two channels. + */ + protected int channels; + /** + * Sample rate, in samples per second (hertz). If wFormatTag is + * WAVE_FORMAT_PCM, then common values for samplesPerSec are 8.0 kHz, + * 11.025 kHz, 22.05 kHz, and 44.1 kHz. For non-PCM formats, this member + * must be computed according to the manufacturer's specification of the + * format tag. + */ + protected long samplesPerSec; + /** + * Required average data-transfer rate, in bytes per second, for the + * format tag. If wFormatTag is WAVE_FORMAT_PCM, avgBytesPerSec should + * be equal to the product of samplesPerSec and blockAlign. For non-PCM + * formats, this member must be computed according to the manufacturer's + * specification of the format tag. + */ + protected long avgBytesPerSec; + /** + * Block alignment, in bytes. The block alignment is the minimum atomic + * unit of data for the wFormatTag format type. If wFormatTag is + * WAVE_FORMAT_PCM or WAVE_FORMAT_EXTENSIBLE, blockAlign must be equal + * to the product of channels and bitsPerSample divided by 8 (bits per + * byte). For non-PCM formats, this member must be computed according to + * the manufacturer's specification of the format tag. + * + * Software must process a multiple of blockAlign bytes of data at a + * time. Data written to and read from a device must always startTime at + * the beginning of a block. For example, it is illegal to startTime + * playback of PCM data in the middle of a sample (that is, on a + * non-block-aligned boundary). + */ + protected int blockAlign; + /** + * Bits per sample for the wFormatTag format type. If wFormatTag is + * WAVE_FORMAT_PCM, then bitsPerSample should be equal to 8 or 16. For + * non-PCM formats, this member must be set according to the + * manufacturer's specification of the format tag. If wFormatTag is + * WAVE_FORMAT_EXTENSIBLE, this value can be any integer multiple of 8. + * Some compression schemes cannot define a value for bitsPerSample, so + * this member can be zero. + */ + protected int bitsPerSample; + /** + * Size, in bytes, of extra format information appended to the end of + * the WAVEFORMATEX structure. This information can be used by non-PCM + * formats to store extra attributes for the wFormatTag. If no extra + * information is required by the wFormatTag, this member must be set to + * zero. For WAVE_FORMAT_PCM formats (and only WAVE_FORMAT_PCM formats), + * this member is ignored. + */ + //int cbSize; => this value is computed + // Well known wave format tags + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_PCM = 0x0001; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_ADPCM = 0x0002; + /** + * Microsoft Corporation IEEE754: range (+1, -1] 32-bit/64-bit format as + * defined by MSVC++ float/double type + */ + protected final static int WAVE_FORMAT_IEEE_FLOAT = 0x0003; + /** + * IBM Corporation + */ + protected final static int WAVE_FORMAT_IBM_CVSD = 0x0005; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_ALAW = 0x0006; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_MULAW = 0x0007; + /** + * OKI + */ + protected final static int WAVE_FORMAT_OKI_ADPCM = 0x0010; + /** + * Intel Corporation + */ + protected final static int WAVE_FORMAT_DVI_ADPCM = 0x0011; + /** + * Intel Corporation + */ + protected final static int WAVE_FORMAT_IMA_ADPCM = WAVE_FORMAT_DVI_ADPCM; + /** + * Videologic + */ + protected final static int WAVE_FORMAT_MEDIASPACE_ADPCM = 0x0012; + /** + * Sierra Semiconductor Corp + */ + protected final static int WAVE_FORMAT_SIERRA_ADPCM = 0x0013; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_G723_ADPCM = 0x0014; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGISTD = 0x0015; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGIFIX = 0x0016; + /** + * Dialogic Corporation + */ + protected final static int WAVE_FORMAT_DIALOGIC_OKI_ADPCM = 0x0017; + /** + * Media Vision, Inc. + */ + protected final static int WAVE_FORMAT_MEDIAVISION_ADPCM = 0x0018; + /** + * Yamaha Corporation of America + */ + protected final static int WAVE_FORMAT_YAMAHA_ADPCM = 0x0020; + /** + * Speech Compression + */ + protected final static int WAVE_FORMAT_SONARC = 0x0021; + /** + * DSP Group, Inc + */ + protected final static int WAVE_FORMAT_DSPGROUP_TRUESPEECH = 0x0022; + /** + * Echo Speech Corporation + */ + protected final static int WAVE_FORMAT_ECHOSC1 = 0x0023; + /** + * + */ + protected final static int WAVE_FORMAT_AUDIOFILE_AF36 = 0x0024; + /** + * Audio Processing Technology + */ + protected final static int WAVE_FORMAT_APTX = 0x0025; + /** + * + */ + protected final static int WAVE_FORMAT_AUDIOFILE_AF10 = 0x0026; + /** + * Dolby Laboratories + */ + protected final static int WAVE_FORMAT_DOLBY_AC2 = 0x0030; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_GSM610 = 0x0031; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_MSNAUDIO = 0x0032; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_ANTEX_ADPCME = 0x0033; + /** + * Control Resources Limited + */ + protected final static int WAVE_FORMAT_CONTROL_RES_VQLPC = 0x0034; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGIREAL = 0x0035; + /** + * DSP Solutions, Inc. + */ + protected final static int WAVE_FORMAT_DIGIADPCM = 0x0036; + /** + * Control Resources Limited + */ + protected final static int WAVE_FORMAT_CONTROL_RES_CR10 = 0x0037; + /** + * Natural MicroSystems + */ + protected final static int WAVE_FORMAT_NMS_VBXADPCM = 0x0038; + /** + * Crystal Semiconductor IMA ADPCM + */ + protected final static int WAVE_FORMAT_CS_IMAADPCM = 0x0039; + /** + * Echo Speech Corporation + */ + protected final static int WAVE_FORMAT_ECHOSC3 = 0x003A; + /** + * Rockwell International + */ + protected final static int WAVE_FORMAT_ROCKWELL_ADPCM = 0x003B; + /** + * Rockwell International + */ + protected final static int WAVE_FORMAT_ROCKWELL_DIGITALK = 0x003C; + /** + * Xebec Multimedia Solutions Limited + */ + protected final static int WAVE_FORMAT_XEBEC = 0x003D; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_G721_ADPCM = 0x0040; + /** + * Antex Electronics Corporation + */ + protected final static int WAVE_FORMAT_G728_CELP = 0x0041; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_MPEG = 0x0050; + /** + * ISO/MPEG Layer3 Format Tag + */ + protected final static int WAVE_FORMAT_MPEGLAYER3 = 0x0055; + /** + * Cirrus Logic + */ + protected final static int WAVE_FORMAT_CIRRUS = 0x0060; + /** + * ESS Technology + */ + protected final static int WAVE_FORMAT_ESPCM = 0x0061; + /** + * Voxware Inc + */ + protected final static int WAVE_FORMAT_VOXWARE = 0x0062; + /** + * Canopus, co., Ltd. + */ + protected final static int WAVE_FORMAT_CANOPUS_ATRAC = 0x0063; + /** + * APICOM + */ + protected final static int WAVE_FORMAT_G726_ADPCM = 0x0064; + /** + * APICOM + */ + protected final static int WAVE_FORMAT_G722_ADPCM = 0x0065; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_DSAT = 0x0066; + /** + * Microsoft Corporation + */ + protected final static int WAVE_FORMAT_DSAT_DISPLAY = 0x0067; + /** + * Softsound, Ltd. + */ + protected final static int WAVE_FORMAT_SOFTSOUND = 0x0080; + /** + * Rhetorex Inc + */ + protected final static int WAVE_FORMAT_RHETOREX_ADPCM = 0x0100; + /** + * Creative Labs, Inc + */ + protected final static int WAVE_FORMAT_CREATIVE_ADPCM = 0x0200; + /** + * Creative Labs, Inc + */ + protected final static int WAVE_FORMAT_CREATIVE_FASTSPEECH8 = 0x0202; + /** + * Creative Labs, Inc + */ + protected final static int WAVE_FORMAT_CREATIVE_FASTSPEECH10 = 0x0203; + /** + * Quarterdeck Corporation + */ + protected final static int WAVE_FORMAT_QUARTERDECK = 0x0220; + /** + * Fujitsu Corp. + */ + protected final static int WAVE_FORMAT_FM_TOWNS_SND = 0x0300; + /** + * Brooktree Corporation + */ + protected final static int WAVE_FORMAT_BTV_DIGITAL = 0x0400; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLIGSM = 0x1000; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLIADPCM = 0x1001; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLICELP = 0x1002; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLISBC = 0x1003; + /** + * Ing C. Olivetti & C., S.p.A. + */ + protected final static int WAVE_FORMAT_OLIOPR = 0x1004; + /** + * Lernout & Hauspie + */ + protected final static int WAVE_FORMAT_LH_CODEC = 0x1100; + /** + * Norris Communications, Inc. + */ + protected final static int WAVE_FORMAT_NORRIS = 0x1400; + /** + * the WAVE_FORMAT_DEVELOPMENT format tag can be used during the + * development phase of a new wave format. Before shipping, you MUST + * acquire an official format tag from Microsoft. + */ + protected final static int WAVE_FORMAT_DEVELOPMENT = 0xFFFF; + private int sampleChunkFourCC; + + public AudioTrack(int trackIndex, int fourCC) { + super(trackIndex, AVIMediaType.AUDIO, fourCC); + sampleChunkFourCC = twoCC | WB_ID; + + } + + @Override + public long getSTRFChunkSize() { + return 18; + + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + return sampleChunkFourCC; + } + } + + /** + * Chunk base class. + */ + protected abstract class Chunk { + + /** + * The chunkType of the chunk. A String with the length of 4 characters. + */ + protected int chunkType; + /** + * The offset of the chunk relative to the startTime of the + * ImageOutputStream. + */ + protected long offset; + + /** + * Creates a new Chunk at the current position of the ImageOutputStream. + * + * @param chunkType The chunkType of the chunk. A string with a length + * of 4 characters. + */ + public Chunk(int chunkType) throws IOException { + this.chunkType = chunkType; + offset = getRelativeStreamPosition(); + } + + /** + * Writes the chunk to the ImageOutputStream and disposes it. + */ + public abstract void finish() throws IOException; + + /** + * Returns the size of the chunk including the size of the chunk header. + * + * @return The size of the chunk. + */ + public abstract long size(); + } + + /** + * A CompositeChunk contains an ordered list of Chunks. + */ + protected class CompositeChunk extends Chunk { + + /** + * The type of the composite. A String with the length of 4 characters. + */ + protected int compositeType; + protected LinkedList children; + protected boolean finished; + + /** + * Creates a new CompositeChunk at the current position of the + * ImageOutputStream. + * + * @param compositeType The type of the composite. + * @param chunkType The type of the chunk. + */ + public CompositeChunk(int compositeType, int chunkType) throws IOException { + super(chunkType); + this.compositeType = compositeType; + //out.write + out.writeLong(0); // make room for the chunk header + out.writeInt(0); // make room for the chunk header + children = new LinkedList(); + } + + public void add(Chunk child) throws IOException { + if (children.size() > 0) { + children.getLast().finish(); + } + children.add(child); + } + + /** + * Writes the chunk and all its children to the ImageOutputStream and + * disposes of all resources held by the chunk. + * + * @throws java.io.IOException + */ + @Override + public void finish() throws IOException { + if (!finished) { + if (size() > 0xffffffffL) { + throw new IOException("CompositeChunk \"" + chunkType + "\" is too large: " + size()); + } + + long pointer = getRelativeStreamPosition(); + seekRelative(offset); + + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(compositeType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + out.writeInt((int) (size() - 8)); + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(chunkType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + for (Chunk child : children) { + child.finish(); + } + seekRelative(pointer); + if (size() % 2 == 1) { + out.writeByte(0); // write pad byte + } + finished = true; + } + } + + @Override + public long size() { + long length = 12; + for (Chunk child : children) { + length += child.size() + child.size() % 2; + } + return length; + } + } + + /** + * Data Chunk. + */ + protected class DataChunk extends Chunk { + + //protected SubImageOutputStream data; + protected boolean finished; + private long finishedSize; + + /** + * Creates a new DataChunk at the current position of the + * ImageOutputStream. + * + * @param name The name of the chunk. + */ + public DataChunk(int name) throws IOException { + this(name, -1); + } + + /** + * Creates a new DataChunk at the current position of the + * ImageOutputStream. + * + * @param name The name of the chunk. + * @param dataSize The size of the chunk data, or -1 if not known. + */ + public DataChunk(int name, long dataSize) throws IOException { + super(name); + /* + data = new SubImageOutputStream(out, ByteOrder.LITTLE_ENDIAN, false); + data.writeInt(typeToInt(chunkType)); + data.writeInt((int)Math.max(0, dataSize)); */ + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(chunkType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + out.writeInt((int) Math.max(0, dataSize)); + finishedSize = dataSize == -1 ? -1 : dataSize + 8; + } + + public ImageOutputStream getOutputStream() { + if (finished) { + throw new IllegalStateException("DataChunk is finished"); + } + //return data; + return out; + } + + /** + * Returns the offset of this chunk to the beginning of the random + * access file + */ + public long getOffset() { + return offset; + } + + @Override + public void finish() throws IOException { + if (!finished) { + if (finishedSize == -1) { + finishedSize = size(); + + if (finishedSize > 0xffffffffL) { + throw new IOException("DataChunk \"" + chunkType + "\" is too large: " + size()); + } + + seekRelative(offset + 4); + out.writeInt((int) (finishedSize - 8)); + seekRelative(offset + finishedSize); + } else { + if (size() != finishedSize) { + throw new IOException("DataChunk \"" + chunkType + "\" actual size differs from given size: actual size:" + size() + " given size:" + finishedSize); + } + } + if (size() % 2 == 1) { + out.writeByte(0); // write pad byte + } + + + //data.dispose(); + //data = null; + finished = true; + } + } + + @Override + public long size() { + if (finished) { + return finishedSize; + } + + try { + // return data.length(); + return out.getStreamPosition() - offset; + } catch (IOException ex) { + InternalError ie = new InternalError("IOException"); + ie.initCause(ex); + throw ie; + } + } + } + + /** + * A DataChunk with a fixed size. + */ + protected class FixedSizeDataChunk extends Chunk { + + protected boolean finished; + protected long fixedSize; + + /** + * Creates a new DataChunk at the current position of the + * ImageOutputStream. + * + * @param chunkType The chunkType of the chunk. + */ + public FixedSizeDataChunk(int chunkType, long fixedSize) throws IOException { + super(chunkType); + this.fixedSize = fixedSize; + out.setByteOrder(ByteOrder.BIG_ENDIAN); + out.writeInt(chunkType); + out.setByteOrder(ByteOrder.LITTLE_ENDIAN); + out.writeInt((int) fixedSize); + + // Fill fixed size with nulls + byte[] buf = new byte[(int) Math.min(512, fixedSize)]; + long written = 0; + while (written < fixedSize) { + out.write(buf, 0, (int) Math.min(buf.length, fixedSize - written)); + written += Math.min(buf.length, fixedSize - written); + } + if (fixedSize % 2 == 1) { + out.writeByte(0); // write pad byte + } + seekToStartOfData(); + } + + public ImageOutputStream getOutputStream() { + /*if (finished) { + throw new IllegalStateException("DataChunk is finished"); + }*/ + return out; + } + + /** + * Returns the offset of this chunk to the beginning of the random + * access file + */ + public long getOffset() { + return offset; + } + + public void seekToStartOfData() throws IOException { + seekRelative(offset + 8); + + } + + public void seekToEndOfChunk() throws IOException { + seekRelative(offset + 8 + fixedSize + fixedSize % 2); + } + + @Override + public void finish() throws IOException { + if (!finished) { + finished = true; + } + } + + @Override + public long size() { + return 8 + fixedSize; + } + } + + protected class MidiTrack extends Track { + + private final int sampleChunkFourCC; + + public MidiTrack(int trackIndex, int fourCC) { + super(trackIndex, AVIMediaType.MIDI, fourCC); + sampleChunkFourCC = twoCC | WB_ID; + + } + + @Override + public long getSTRFChunkSize() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + protected class TextTrack extends Track { + + private final int sampleChunkFourCC; + + public TextTrack(int trackIndex, int fourCC) { + super(trackIndex, AVIMediaType.TEXT, fourCC); + sampleChunkFourCC = twoCC | WB_ID; + + } + + @Override + public long getSTRFChunkSize() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getSampleChunkFourCC(boolean isSync) { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + /** + *

Holds information about the entire movie.

+ * + *
+     * ---------------
+     * AVI Main Header
+     * ---------------
+     *
+     * Set values taken from
+     * http://graphics.cs.uni-sb.de/NMM/dist-0.4.0/Docs/Doxygen/html/avifmt_8h.html
+     *
+     * typedef struct {
+     *     DWORD  microSecPerFrame;
+     *             // Specifies the number of microseconds between frames.
+     *             // This value indicates the overall timing for the file.
+     *     DWORD  maxBytesPerSec;
+     *             // Specifies the approximate maximum data rate of the file. This
+     *             // value indicates the number of bytes per second the system must
+     *             // handle to present an AVI sequence as specified by the other
+     *             // parameters contained in the main header and stream header chunks.
+     *     DWORD  paddingGranularity;
+     *             // Specifies the alignment for data, in bytes. Pad the data to
+     *             // multiples of this value.
+     *     DWORD set avihFlags  flags;
+     *             // Contains a bitwise combination of zero or more of the following flags:
+     *     DWORD  totalFrames;
+     *             // Specifies the total number of frames of data in the file.
+     *     DWORD  initialFrames;
+     *             // Specifies the initial frame for interleaved files. Noninterleaved
+     *             // files should specify zero. If you are creating interleaved files,
+     *             // specify the number of frames in the file prior to the initial
+     *             // frame of the AVI sequence in this member. For more information
+     *             // about the contents of this member, see "Special Information for
+     *             // Interleaved Files" in the Video for Windows Programmer's Guide.
+     *     DWORD  streams;
+     *             // Specifies the number of streams in the file. For example, a file
+     *             // with audio and video has two streams.
+     *     DWORD  suggestedBufferSize;
+     *             // Specifies the suggested buffer size for reading the file.
+     *             // Generally, this size should be large enough to contain the
+     *             // largest chunk in the file. If set to zero, or if it is too small,
+     *             // the playback software will have to reallocate memory during
+     *             // playback, which will reduce performance. For an interleaved file,
+     *             // the buffer size should be large enough to read an entire record,
+     *             // and not just a chunk.
+     *     DWORD  width;
+     *             // Specifies the width of the AVI file in pixels.
+     *     DWORD  height;
+     *             // Specifies the height of the AVI file in pixels.
+     *     DWORD[]  reserved;
+     *             // Reserved. Set this array to zero.
+     * } AVIMAINHEADER;
+     * 
+ */ + protected static class MainHeader { + + /** + * Specifies the number of microseconds (=10E-6 seconds) between frames. + * This value indicates the overall timing for the file. + */ + protected long microSecPerFrame; + protected long maxBytesPerSec; + protected long paddingGranularity; + protected int flags; + protected long totalFrames; + protected long initialFrames; + protected long streams; + protected long suggestedBufferSize; + /** + * Width and height of the movie. Null if not specified. + */ + protected Dimension size; + } + + protected static int typeToInt(String str) { + int value = ((str.charAt(0) & 0xff) << 24) | ((str.charAt(1) & 0xff) << 16) | ((str.charAt(2) & 0xff) << 8) | (str.charAt(3) & 0xff); + return value; + } + + protected static String intToType(int id) { + char[] b=new char[4]; + + b[0] = (char) ((id >>> 24) & 0xff); + b[1] = (char) ((id >>> 16) & 0xff); + b[2] = (char) ((id >>> 8) & 0xff); + b[3] = (char) (id & 0xff); + return String.valueOf(b); + } + + /** + * Returns true, if the specified mask is set on the flag. + */ + protected static boolean isFlagSet(int flag, int mask) { + return (flag & mask) == mask; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java b/trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java new file mode 100644 index 000000000..fee2740b6 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/avi/DIBCodec.java @@ -0,0 +1,343 @@ +/* + * @(#)DIBCodec.java + * + * Copyright © 2011-2012 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.avi; + +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferByte; +import org.monte.media.AbstractVideoCodec; +import org.monte.media.Buffer; +import org.monte.media.Format; +import org.monte.media.io.SeekableByteArrayOutputStream; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.io.OutputStream; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.BufferFlag.*; + +/** + * {@code DIBCodec} encodes a BufferedImage as a Microsoft Device Independent + * Bitmap (DIB) into a byte array. + *

+ * The DIB codec only works with the AVI file format. Other file formats, such + * as QuickTime, use a different encoding for uncompressed video. + *

+ * This codec currently only supports encoding from a {@code BufferedImage} into + * the file format. Decoding support may be added in the future. + *

+ * This codec does not encode the color palette of an image. This must be done + * separately. + *

+ * The pixels of a frame are written row by row from bottom to top and from + * the left to the right. 24-bit pixels are encoded as BGR. + *

+ * Supported input formats: + *

    + * {@code Format} with {@code BufferedImage.class}, any width, any height, + * depth=4. + *
+ * Supported output formats: + *
    + * {@code Format} with {@code byte[].class}, same width and height as input + * format, depth=4. + *
+ * + * @author Werner Randelshofer + * @version $Id: DIBCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public class DIBCodec extends AbstractVideoCodec { + + public DIBCodec() { + super(new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE, FixedFrameRateKey, true), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 4), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 8), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 24), // + }, + new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE, FixedFrameRateKey, true), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 4), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 8), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_DIB, DataClassKey, byte[].class, + FixedFrameRateKey, true, DepthKey, 24), // + }); + } + + @Override + public int process(Buffer in, Buffer out) { + if (outputFormat.get(EncodingKey) == ENCODING_BUFFERED_IMAGE) { + return decode(in, out); + } else { + return encode(in, out); + } + } + + public int decode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + + out.sampleCount = 1; + BufferedImage img = null; + + int imgType; + switch (outputFormat.get(DepthKey)) { + case 4: + imgType = BufferedImage.TYPE_BYTE_INDEXED; + break; + case 8: + imgType = BufferedImage.TYPE_BYTE_INDEXED; + break; + case 24: + imgType = BufferedImage.TYPE_INT_RGB; + break; + default: + imgType = BufferedImage.TYPE_INT_RGB; + break; + } + + if (out.data instanceof BufferedImage) { + img = (BufferedImage) out.data; + // Fixme: Handle sub-image + if (img.getWidth() != outputFormat.get(WidthKey) + || img.getHeight() != outputFormat.get(HeightKey) + || img.getType() != imgType) { + img = null; + } + } + if (img == null) { + img = new BufferedImage(outputFormat.get(WidthKey), outputFormat.get(HeightKey), imgType); + } + out.data = img; + + switch (outputFormat.get(DepthKey)) { + case 4: + readKey4((byte[]) in.data, in.offset, in.length, img); + break; + case 8: + readKey8((byte[]) in.data, in.offset, in.length, img); + break; + case 24: + default: + readKey24((int[]) in.data, in.offset, in.length, img); + break; + } + + + return CODEC_OK; + } + + public int encode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + + SeekableByteArrayOutputStream tmp; + if (out.data instanceof byte[]) { + tmp = new SeekableByteArrayOutputStream((byte[]) out.data); + } else { + tmp = new SeekableByteArrayOutputStream(); + } + + // Handle sub-image + // FIXME - Scanline stride must be a multiple of four. + Rectangle r; + int scanlineStride; + if (in.data instanceof BufferedImage) { + BufferedImage image = (BufferedImage) in.data; + WritableRaster raster = image.getRaster(); + scanlineStride = raster.getSampleModel().getWidth(); + r = raster.getBounds(); + r.x -= raster.getSampleModelTranslateX(); + r.y -= raster.getSampleModelTranslateY(); + out.header = image.getColorModel(); + } else { + r = new Rectangle(0, 0, outputFormat.get(WidthKey), outputFormat.get(HeightKey)); + scanlineStride = outputFormat.get(WidthKey); + out.header = null; + } + + try { + switch (outputFormat.get(DepthKey)) { + case 4: { + byte[] pixels = getIndexed8(in); + if (pixels == null) { + out.setFlag(DISCARD); + return CODEC_OK; + } + writeKey4(tmp, pixels, r.width, r.height, r.x + r.y * scanlineStride, scanlineStride); + break; + } + case 8: { + byte[] pixels = getIndexed8(in); + if (pixels == null) { + out.setFlag(DISCARD); + return CODEC_OK; + } + writeKey8(tmp, pixels, r.width, r.height, r.x + r.y * scanlineStride, scanlineStride); + break; + } + case 24: { + int[] pixels = getRGB24(in); + if (pixels == null) { + out.setFlag(DISCARD); + return CODEC_OK; + } + writeKey24(tmp, pixels, r.width, r.height, r.x + r.y * scanlineStride, scanlineStride); + break; + } + default: + out.setFlag(DISCARD); + return CODEC_OK; + } + + out.setFlag(KEYFRAME); + out.data = tmp.getBuffer(); + out.sampleCount = 1; + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } + + public void readKey4(byte[] in, int offset, int length, BufferedImage img) { + DataBufferByte buf = (DataBufferByte) img.getRaster().getDataBuffer(); + WritableRaster raster = img.getRaster(); + int scanlineStride = raster.getSampleModel().getWidth(); + Rectangle r = raster.getBounds(); + r.x -= raster.getSampleModelTranslateX(); + r.y -= raster.getSampleModelTranslateY(); + + throw new UnsupportedOperationException("readKey4 not yet implemented"); + } + + public void readKey8(byte[] in, int offset, int length, BufferedImage img) { + DataBufferByte buf = (DataBufferByte) img.getRaster().getDataBuffer(); + WritableRaster raster = img.getRaster(); + int scanlineStride = raster.getSampleModel().getWidth(); + Rectangle r = raster.getBounds(); + r.x -= raster.getSampleModelTranslateX(); + r.y -= raster.getSampleModelTranslateY(); + + int h=img.getHeight(); + int w=img.getWidth(); + int i=offset; + int j=r.x+r.y*scanlineStride+(h-1)*scanlineStride; + byte[] out=buf.getData(); + for (int y=0;y= 0; y -= scanlineStride) { // Upside down + for (int x = offset, xx = 0, n = offset + width; x < n; x += 2, ++xx) { + bytes[xx] = (byte) (((pixels[y + x] & 0xf) << 4) | (pixels[y + x + 1] & 0xf)); + } + out.write(bytes); + } + + } + + /** Encodes an 8-bit key frame. + * + * @param out The output stream. + * @param pixels The image data. + * @param offset The offset to the first pixel in the data array. + * @param width The width of the image in data elements. + * @param scanlineStride The number to append to offset to get to the next scanline. + */ + public void writeKey8(OutputStream out, byte[] pixels, int width, int height, int offset, int scanlineStride) + throws IOException { + + for (int y = (height - 1) * scanlineStride; y >= 0; y -= scanlineStride) { // Upside down + out.write(pixels, y + offset, width); + } + } + + /** Encodes a 24-bit key frame. + * + * @param out The output stream. + * @param pixels The image data. + * @param offset The offset to the first pixel in the data array. + * @param width The width of the image in data elements. + * @param scanlineStride The number to append to offset to get to the next scanline. + */ + public void writeKey24(OutputStream out, int[] pixels, int width, int height, int offset, int scanlineStride) + throws IOException { + int w3 = width * 3; + byte[] bytes = new byte[w3]; // holds a scanline of raw image data with 3 channels of 8 bit data + for (int xy = (height - 1) * scanlineStride + offset; xy >= offset; xy -= scanlineStride) { // Upside down + for (int x = 0, xp = 0; x < w3; x += 3, ++xp) { + int p = pixels[xy + xp]; + bytes[x] = (byte) (p); // Blue + bytes[x + 1] = (byte) (p >> 8); // Green + bytes[x + 2] = (byte) (p >> 16); // Red + } + out.write(bytes); + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/color/Colors.java b/trunk/libsrc/avi/src/org/monte/media/color/Colors.java new file mode 100644 index 000000000..79562e2f4 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/color/Colors.java @@ -0,0 +1,213 @@ +/* + * @(#)Colors.java 1.0 2011-03-13 + * + * Copyright (c) 2011 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.color; + +import java.awt.image.IndexColorModel; +import static java.lang.Math.*; + +/** + * {@code Colors}. + * + * @author Werner Randelshofer + * @version 1.0 2011-03-13 Created. + */ +public class Colors { + + /** Prevent instance creation. */ + private Colors() { + } + + /** + * The macintosh palette is arranged as follows: there are 256 colours to + * allocate, an even distribution of colors through the color cube might be + * desirable but 256 is not the cube of an integer. 6x6x6 is 216 and so the + * first 216 colors are an equal 6x6x6 sampling of the color cube. + * This leaves 40 colours to allocate, this has been done by choosing a ramp of + * 10 shades each for red, green, blue and grey. + * + *

+ * References:
+ * http://paulbourke.net/texture_colour/colourramp/ + * + * @return The Macintosh color palette. + */ + public static IndexColorModel createMacColors() { + byte[] r = new byte[256]; + byte[] g = new byte[256]; + byte[] b = new byte[256]; + + // Generate color cube with 216 colors + int index = 0; + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 6; j++) { + for (int k = 0; k < 6; k++) { + r[index] = (byte) (255 - 51 * i); + g[index] = (byte) (255 - 51 * j); + b[index] = (byte) (255 - 51 * k); + index++; + } + } + } + + index--; // overwrite last color (black) with color ramp + + // Generate red ramp + byte[] ramp = {(byte) 238, (byte) 221, (byte) 187, (byte) 170, (byte) 136, (byte) 119, 85, 68, 34, 17}; + for (int i = 0; i < 10; i++) { + r[index] = ramp[i]; + g[index] = (byte) (0); + b[index] = (byte) (0); + index++; + } + // Generate green ramp + for (int j = 0; j < 10; j++) { + r[index] = (byte) (0); + g[index] = ramp[j]; + b[index] = (byte) (0); + index++; + } + // Generate blue ramp + for (int k = 0; k < 10; k++) { + r[index] = (byte) (0); + g[index] = (byte) (0); + b[index] = ramp[k]; + index++; + } + // Generate gray ramp + for (int ijk = 0; ijk < 10; ijk++) { + r[index] = ramp[ijk]; + g[index] = ramp[ijk]; + b[index] = ramp[ijk]; + index++; + } + // last color is black (nothing to do) + + /* + for (int i=0;i<256;i++) { + if (i%6==0) System.out.println(); else System.out.print(" "); + System.out.print(Integer.toHexString(r[i]&0xff)+","+Integer.toHexString(g[i]&0xff)+","+Integer.toHexString(b[i]&0xff)); + }*/ + + IndexColorModel icm = new IndexColorModel(8, 256, r, g, b); + return icm; + } + + private static void RGBtoYCC(float[] rgb, float[] ycc) { + float R = rgb[0]; + float G = rgb[1]; + float B = rgb[2]; + float Y = 0.3f * R + 0.6f * G + 0.1f * B; + float V = R - Y; + float U = B - Y; + float Cb = (U / 2f) + 0.5f; + float Cr = (V / 1.6f) + 0.5f; + ycc[0] = Y; + ycc[1] = Cb; + ycc[2] = Cr; + } + + private static void YCCtoRGB(float[] ycc, float[] rgb) { + float Y = ycc[0]; + float Cb = ycc[1]; + float Cr = ycc[2]; + float U = (Cb - 0.5f) * 2f; + float V = (Cr - 0.5f) * 1.6f; + float R = V + Y; + float B = U + Y; + float G = (Y - 0.3f * R - 0.1f * B) / 0.6f; + rgb[0] = R; + rgb[1] = G; + rgb[2] = B; + } + + /** RGB 8-bit per channel to YCC 16-bit per channel. */ + private static void RGB8toYCC16(int[] rgb, int[] ycc) { + int R = rgb[0]; + int G = rgb[1]; + int B = rgb[2]; + int Y = 77 * R + 153 * G + 26 * B; + int V = R * 256 - Y; + int U = B * 256 - Y; + int Cb = (U / 2) + 128 * 256; + int Cr = (V * 5 / 8) + 128 * 256; + ycc[0] = Y; + ycc[1] = Cb; + ycc[2] = Cr; + } + + /** RGB 8-bit per channel to YCC 16-bit per channel. */ + private static void RGB8toYCC16(int rgb, int[] ycc) { + int R = (rgb & 0xff0000) >>> 16; + int G = (rgb & 0xff00) >>> 8; + int B = rgb & 0xff; + int Y = 77 * R + 153 * G + 26 * B; + int V = R * 256 - Y; + int U = B * 256 - Y; + int Cb = (U / 2) + 128 * 256; + int Cr = (V * 5 / 8) + 128 * 256; + ycc[0] = Y; + ycc[1] = Cb; + ycc[2] = Cr; + } + + /** YCC 16-bit per channel to RGB 8-bit per channel. */ + private static void YCC16toRGB8(int[] ycc, int[] rgb) { + int Y = ycc[0]; + int Cb = ycc[1]; + int Cr = ycc[2]; + int U = (Cb - 128 * 256) * 2; + int V = (Cr - 128 * 256) * 8 / 5; + int R = min(255, max(0, (V + Y) / 256)); + int B = min(255, max(0, (U + Y) / 256)); + int G = min(255, max(0, (Y - 77 * R - 26 * B) / 153)); + rgb[0] = R; + rgb[1] = G; + rgb[2] = B; + } + + /** YCC 8-bit per channel to RGB 8-bit per channel. + */ + private static void YCC8toRGB8(int[] ycc, int[] rgb) { + int Y = ycc[0]; + int Cb = ycc[1]; + int Cr = ycc[2]; + // Source: JPEG File Interchange Format Version 1.02, September 1, 1992 + //RGB can be computed directly from YCbCr (256 levels) as follows: + //R = Y + 1.402 (Cr-128) + //G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) + //B = Y + 1.772 (Cb-128) + int R = (1000 * Y + 1402 * (Cr - 128)) / 1000; + int G = (100000 * Y - 34414 * (Cb - 128) - 71414 * (Cr - 128)) / 100000; + int B = (1000 * Y + 1772 * (Cb - 128)) / 1000; + rgb[0] = min(255, max(0, R)); + rgb[1] = min(255, max(0, G)); + rgb[2] = min(255, max(0, B)); + } + + /** YCC 8-bit per channel to RGB 8-bit per channel. + */ + private static void RGB8toYCC8(int[] rgb, int[] ycc) { + int R = rgb[0]; + int G = rgb[1]; + int B = rgb[2]; + // Source: JPEG File Interchange Format Version 1.02, September 1, 1992 + //YCbCr (256 levels) can be computed directly from 8-bit RGB as follows: + //Y = 0.299R +0.587G +0.114B + //Cb = - 0.1687 R - 0.3313 G + 0.5 B + 128 + //Cr = 0.5 R - 0.4187 G - 0.0813 B + 128 + int Y = (299 * R + 587 * G + 114 * B) / 1000; + int Cb = (-1687 * R - 3313 * G + 5000 * B) / 10000 + 128; + int Cr = (5000 * R - 4187 * G - 813 * B) / 10000 + 128; + ycc[0] = min(255, max(0, Y)); + ycc[1] = min(255, max(0, Cb)); + ycc[2] = min(255, max(0, Cr)); + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java new file mode 100644 index 000000000..4baa50420 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageInputStream.java @@ -0,0 +1,216 @@ +/* + * @(#)ByteArrayImageInputStream.java + * + * Copyright (c) 2008-2011 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.io; + +import java.io.IOException; +import java.nio.ByteOrder; + +/** + * A {@code ByteArrayImageInputStream} contains + * an internal buffer that contains bytes that + * may be read from the stream. An internal + * counter keeps track of the next byte to + * be supplied by the {@code read} method. + *

+ * Closing a {@code ByteArrayImageInputStream} has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an {@code IOException}. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau + * @version $Id: ByteArrayImageInputStream.java 299 2013-01-03 07:40:18Z werner $ + */ +public class ByteArrayImageInputStream extends ImageInputStreamImpl2 { + /** + * An array of bytes that was provided + * by the creator of the stream. Elements buf[0] + * through buf[count-1] are the + * only bytes that can ever be read from the + * stream; element buf[streamPos] is + * the next byte to be read. + */ + protected byte buf[]; + + /** + * The index one greater than the last valid character in the input + * stream buffer. + * This value should always be nonnegative + * and not larger than the length of buf. + * It is one greater than the position of + * the last byte within buf that + * can ever be read from the input stream buffer. + */ + protected int count; + + /** The offset to the start of the array. */ + private final int arrayOffset; + + public ByteArrayImageInputStream(byte[] buf) { + this(buf, ByteOrder.BIG_ENDIAN); + } + + public ByteArrayImageInputStream(byte[] buf, ByteOrder byteOrder) { + this(buf, 0, buf.length, byteOrder); + } + + public ByteArrayImageInputStream(byte[] buf, int offset, int length, ByteOrder byteOrder) { + this.buf = buf; + this.streamPos = offset; + this.count = Math.min(offset + length, buf.length); + this.arrayOffset = offset; + this.byteOrder = byteOrder; + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. + *

+ * This read method + * cannot block. + * + * @return the next byte of data, or -1 if the end of the + * stream has been reached. + */ + @Override + public synchronized int read() { + flushBits(); + return (streamPos < count) ? (buf[(int)(streamPos++)] & 0xff) : -1; + } + + /** + * Reads up to len bytes of data into an array of bytes + * from this input stream. + * If streamPos equals count, + * then -1 is returned to indicate + * end of file. Otherwise, the number k + * of bytes read is equal to the smaller of + * len and count-streamPos. + * If k is positive, then bytes + * buf[streamPos] through buf[streamPos+k-1] + * are copied into b[off] through + * b[off+k-1] in the manner performed + * by System.arraycopy. The + * value k is added into streamPos + * and k is returned. + *

+ * This read method cannot block. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + */ + @Override + public synchronized int read(byte b[], int off, int len) { + flushBits(); + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + if (streamPos >= count) { + return -1; + } + if (streamPos + len > count) { + len = (int)(count - streamPos); + } + if (len <= 0) { + return 0; + } + System.arraycopy(buf, (int)streamPos, b, off, len); + streamPos += len; + return len; + } + + /** + * Skips n bytes of input from this input stream. Fewer + * bytes might be skipped if the end of the input stream is reached. + * The actual number k + * of bytes to be skipped is equal to the smaller + * of n and count-streamPos. + * The value k is added into streamPos + * and k is returned. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + */ + public synchronized long skip(long n) { + if (streamPos + n > count) { + n = count - streamPos; + } + if (n < 0) { + return 0; + } + streamPos += n; + return n; + } + + /** + * Returns the number of remaining bytes that can be read (or skipped over) + * from this input stream. + *

+ * The value returned is count - streamPos, + * which is the number of bytes remaining to be read from the input buffer. + * + * @return the number of remaining bytes that can be read (or skipped + * over) from this input stream without blocking. + */ + public synchronized int available() { + return (int)(count - streamPos); + } + + + + /** + * Closing a ByteArrayInputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ */ + @Override + public void close() { + // does nothing!! + } + + @Override + public long getStreamPosition() throws IOException { + checkClosed(); + return streamPos-arrayOffset; + } + @Override + public void seek(long pos) throws IOException { + checkClosed(); + flushBits(); + + // This test also covers pos < 0 + if (pos < flushedPos) { + throw new IndexOutOfBoundsException("pos < flushedPos!"); + } + + this.streamPos = pos+arrayOffset; + } + + private void flushBits() { + bitOffset=0; + } + @Override + public long length() { + return count-arrayOffset; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java new file mode 100644 index 000000000..beb456ad9 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ByteArrayImageOutputStream.java @@ -0,0 +1,336 @@ +/* + * @(#)ByteArrayImageOutputStream.java 1.0.1 2011-01-23 + * + * Copyright (c) 2011 Werner Randelshofer + * Staldenmattweg 2, Goldau, CH-6405, Switzerland. + * All rights reserved. + * + * The copyright of this software is owned by Werner Randelshofer. + * You may not use, copy or modify this software, except in + * accordance with the license agreement you entered into with + * Werner Randelshofer. For details see accompanying license terms. + */ +package org.monte.media.io; + +import java.io.OutputStream; +import javax.imageio.stream.ImageOutputStreamImpl; +import java.io.ByteArrayOutputStream; +import javax.imageio.stream.ImageOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.nio.ByteOrder; +import static java.lang.Math.*; + +/** + * This class implements an image output stream in which the data is + * written into a byte array. The buffer automatically grows as data + * is written to it. + * The data can be retrieved using {@code toByteArray()}, {@code toImageOutputStream()} + * and {@code toOutputStream()}. + *

+ * Closing a {@code ByteArrayImageOutputStream} has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an {@code IOException}. + * + * @author Werner Randelshofer + * @version 1.0.1 2011-01-23 Implements length method. + *
1.0 2011-01-18 Created. + */ +public class ByteArrayImageOutputStream extends ImageOutputStreamImpl { + + + /** + * An array of bytes that was provided + * by the creator of the stream. Elements buf[0] + * through buf[count-1] are the + * only bytes that can ever be read from the + * stream; element buf[streamPos] is + * the next byte to be read. + */ + protected byte buf[]; + /** + * The index one greater than the last valid character in the input + * stream buffer. + * This value should always be nonnegative + * and not larger than the length of buf. + * It is one greater than the position of + * the last byte within buf that + * can ever be read from the input stream buffer. + */ + protected int count; + /** The offset to the start of the array. */ + private final int arrayOffset; + + public ByteArrayImageOutputStream() { + this(16); + } + + public ByteArrayImageOutputStream(int initialCapacity) { + this(new byte[initialCapacity]); + } + + public ByteArrayImageOutputStream(byte[] buf) { + this(buf, ByteOrder.BIG_ENDIAN); + } + + public ByteArrayImageOutputStream(byte[] buf, ByteOrder byteOrder) { + this(buf, 0, buf.length, byteOrder); + } + + public ByteArrayImageOutputStream(byte[] buf, int offset, int length, ByteOrder byteOrder) { + this.buf = buf; + this.streamPos = offset; + this.count = Math.min(offset + length, buf.length); + this.arrayOffset = offset; + this.byteOrder = byteOrder; + } + + public ByteArrayImageOutputStream(ByteOrder byteOrder) { + this(new byte[16],byteOrder); + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. + *

+ * This read method + * cannot block. + * + * @return the next byte of data, or -1 if the end of the + * stream has been reached. + */ + @Override + public synchronized int read() throws IOException { + flushBits(); + return (streamPos < count) ? (buf[(int) (streamPos++)] & 0xff) : -1; + } + + /** + * Reads up to len bytes of data into an array of bytes + * from this input stream. + * If streamPos equals count, + * then -1 is returned to indicate + * end of file. Otherwise, the number k + * of bytes read is equal to the smaller of + * len and count-streamPos. + * If k is positive, then bytes + * buf[streamPos] through buf[streamPos+k-1] + * are copied into b[off] through + * b[off+k-1] in the manner performed + * by System.arraycopy. The + * value k is added into streamPos + * and k is returned. + *

+ * This read method cannot block. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + */ + @Override + public synchronized int read(byte b[], int off, int len) throws IOException { + flushBits(); + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + if (streamPos >= count) { + return -1; + } + if (streamPos + len > count) { + len = (int) (count - streamPos); + } + if (len <= 0) { + return 0; + } + System.arraycopy(buf, (int) streamPos, b, off, len); + streamPos += len; + return len; + } + + /** + * Skips n bytes of input from this input stream. Fewer + * bytes might be skipped if the end of the input stream is reached. + * The actual number k + * of bytes to be skipped is equal to the smaller + * of n and count-streamPos. + * The value k is added into streamPos + * and k is returned. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + */ + public synchronized long skip(long n) { + if (streamPos + n > count) { + n = count - streamPos; + } + if (n < 0) { + return 0; + } + streamPos += n; + return n; + } + + /** + * Returns the number of remaining bytes that can be read (or skipped over) + * from this input stream. + *

+ * The value returned is count - streamPos, + * which is the number of bytes remaining to be read from the input buffer. + * + * @return the number of remaining bytes that can be read (or skipped + * over) from this input stream without blocking. + */ + public synchronized int available() { + return (int) (count - streamPos); + } + + /** + * Closing a ByteArrayInputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ */ + @Override + public void close() { + // does nothing!! + } + + @Override + public long getStreamPosition() throws IOException { + checkClosed(); + return streamPos - arrayOffset; + } + + @Override + public void seek(long pos) throws IOException { + checkClosed(); + flushBits(); + + // This test also covers pos < 0 + if (pos < flushedPos) { + throw new IndexOutOfBoundsException("pos < flushedPos!"); + } + + this.streamPos = pos + arrayOffset; + } + + /** + * Writes the specified byte to this output stream. + * + * @param b the byte to be written. + */ + @Override + public synchronized void write(int b) throws IOException { + flushBits(); + long newcount = max(streamPos + 1, count); + if (newcount> Integer.MAX_VALUE) { + throw new IndexOutOfBoundsException(newcount+" > max array size"); + } + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, max(buf.length << 1, (int) newcount)); + } + buf[(int) streamPos++] = (byte) b; + count = (int)newcount; + } + + /** + * Writes the specified byte array to this output stream. + * + * @param b the data. + */ + @Override + public synchronized void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + /** + * Writes len bytes from the specified byte array + * starting at offset off to this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + */ + @Override + public synchronized void write(byte b[], int off, int len) throws IOException { + flushBits(); + if ((off < 0) || (off > b.length) || (len < 0) + || ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException("off="+off+", len="+len+", b.length="+b.length); + } else if (len == 0) { + return; + } + int newcount = max((int) streamPos + len, count); + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); + } + System.arraycopy(b, off, buf, (int) streamPos, len); + streamPos += len; + count = newcount; + } + + /** Writes the contents of the byte array into the specified output + * stream. + * @param out + */ + public void toOutputStream(OutputStream out) throws IOException { + out.write(buf, arrayOffset, count); + } + + /** Writes the contents of the byte array into the specified image output + * stream. + * @param out + */ + public void toImageOutputStream(ImageOutputStream out) throws IOException { + out.write(buf, arrayOffset, count); + } + + /** + * Creates a newly allocated byte array. Its size is the current + * size of this output stream and the valid contents of the buffer + * have been copied into it. + * + * @return the current contents of this output stream, as a byte array. + * @see java.io.ByteArrayOutputStream#size() + */ + public synchronized byte[] toByteArray() { + byte[] copy = new byte[count - arrayOffset]; + System.arraycopy(buf, arrayOffset, copy, 0, count); + return copy; + } + + /** Returns the internally used byte buffer. */ + public byte[] getBuffer() { + return buf; + } + + @Override + public long length() { + return count-arrayOffset; + } + + /** + * Resets the count field of this byte array output + * stream to zero, so that all currently accumulated output in the + * output stream is discarded. The output stream can be used again, + * reusing the already allocated buffer space. + * + * @see java.io.ByteArrayInputStream#count + */ + public synchronized void clear() { + count = arrayOffset; + streamPos=arrayOffset; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java new file mode 100644 index 000000000..30fd47813 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamAdapter.java @@ -0,0 +1,189 @@ +/* + * @(#)ImageInputStreamAdapter.java 1.0 2009-12-17 + * + * Copyright (c) 2009 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.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import javax.imageio.stream.ImageInputStream; + +/** + * ImageInputStreamAdapter. + * + * @author Werner Randelshofer + * @version 1.0 2009-12-17 Created. + */ +public class ImageInputStreamAdapter extends FilterInputStream { + private ImageInputStream iis; + public ImageInputStreamAdapter(ImageInputStream iis) { + super(null); + this.iis=iis; + } + + /** + * Reads the next byte of data from this input stream. The value + * byte is returned as an int in the range + * 0 to 255. If no byte is available + * because the end of the stream has been reached, the value + * -1 is returned. This method blocks until input data + * is available, the end of the stream is detected, or an exception + * is thrown. + *

+ * This method + * simply performs in.read() and returns the result. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + return iis.read(); + } + + /** + * Reads up to len bytes of data from this input stream + * into an array of bytes. If len is not zero, the method + * blocks until some input is available; otherwise, no + * bytes are read and 0 is returned. + *

+ * This method simply performs in.read(b, off, len) + * and returns the result. + * + * @param b the buffer into which the data is read. + * @param off the start offset in the destination array b + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception NullPointerException If b is null. + * @exception IndexOutOfBoundsException If off is negative, + * len is negative, or len is greater than + * b.length - off + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read(byte b[], int off, int len) throws IOException { + return iis.read(b, off, len); + } + + /** + * {@inheritDoc} + *

+ * This method simply performs in.skip(n). + */ + @Override + public long skip(long n) throws IOException { + return iis.skipBytes(n); + } + + /** + * Returns an estimate of the number of bytes that can be read (or + * skipped over) from this input stream without blocking by the next + * caller of a method for this input stream. The next caller might be + * the same thread or another thread. A single read or skip of this + * many bytes will not block, but may read or skip fewer bytes. + *

+ * This method returns the result of {@link #in in}.available(). + * + * @return an estimate of the number of bytes that can be read (or skipped + * over) from this input stream without blocking. + * @exception IOException if an I/O error occurs. + */ + @Override + public int available() throws IOException { + return (iis.isCached()) ? // + (int)Math.min(Integer.MAX_VALUE, iis.length() - iis.getStreamPosition()) : + 0; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * This + * method simply performs in.close(). + * + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public void close() throws IOException { + iis.close(); + } + + /** + * Marks the current position in this input stream. A subsequent + * call to the reset method repositions this stream at + * the last marked position so that subsequent reads re-read the same bytes. + *

+ * The readlimit argument tells this input stream to + * allow that many bytes to be read before the mark position gets + * invalidated. + *

+ * This method simply performs in.mark(readlimit). + * + * @param readlimit the maximum limit of bytes that can be read before + * the mark position becomes invalid. + * @see java.io.FilterInputStream#in + * @see java.io.FilterInputStream#reset() + */ + @Override + public synchronized void mark(int readlimit) { + iis.mark(); + } + + /** + * Repositions this stream to the position at the time the + * mark method was last called on this input stream. + *

+ * This method + * simply performs in.reset(). + *

+ * Stream marks are intended to be used in + * situations where you need to read ahead a little to see what's in + * the stream. Often this is most easily done by invoking some + * general parser. If the stream is of the type handled by the + * parse, it just chugs along happily. If the stream is not of + * that type, the parser should toss an exception when it fails. + * If this happens within readlimit bytes, it allows the outer + * code to reset the stream and try another parser. + * + * @exception IOException if the stream has not been marked or if the + * mark has been invalidated. + * @see java.io.FilterInputStream#in + * @see java.io.FilterInputStream#mark(int) + */ + @Override + public synchronized void reset() throws IOException { + iis.reset(); + } + + /** + * Tests if this input stream supports the mark + * and reset methods. + * This method + * simply performs in.markSupported(). + * + * @return true if this stream type supports the + * mark and reset method; + * false otherwise. + * @see java.io.FilterInputStream#in + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + @Override + public boolean markSupported() { + return true; + } + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java new file mode 100644 index 000000000..501224398 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/ImageInputStreamImpl2.java @@ -0,0 +1,65 @@ +/* + * @(#)ImageInputStreamImpl2.java + * + * Copyright (c) 2011 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.io; + +import java.io.IOException; +import java.nio.ByteOrder; +import javax.imageio.stream.ImageInputStreamImpl; + +/** + * {@code ImageInputStreamImpl2} fixes bugs in ImageInputStreamImpl. + *

+ * ImageInputStreamImpl uses read(byte[]) instead of readFully(byte[]) inside of + * readShort. This results in corrupt data input if the underlying stream can + * not fulfill the read operation in a single step. + * + * @author Werner Randelshofer + * @version $Id: ImageInputStreamImpl2.java 299 2013-01-03 07:40:18Z werner $ + */ +public abstract class ImageInputStreamImpl2 extends ImageInputStreamImpl { + // Length of the buffer used for readFully(type[], int, int) + private static final int BYTE_BUF_LENGTH = 8192; + /** + * Byte buffer used for readFully(type[], int, int). Note that this + * array is also used for bulk reads in readShort(), readInt(), etc, so + * it should be large enough to hold a primitive value (i.e. >= 8 bytes). + * Also note that this array is package protected, so that it can be + * used by ImageOutputStreamImpl in a similar manner. + */ + byte[] byteBuf = new byte[BYTE_BUF_LENGTH]; + + @Override + public short readShort() throws IOException { + readFully(byteBuf, 0, 2); + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + return (short) + (((byteBuf[0] & 0xff) << 8) | ((byteBuf[1] & 0xff) << 0)); + } else { + return (short) + (((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0)); + } + } + public int readInt() throws IOException { + readFully(byteBuf, 0, 4); + + if (byteOrder == ByteOrder.BIG_ENDIAN) { + return + (((byteBuf[0] & 0xff) << 24) | ((byteBuf[1] & 0xff) << 16) | + ((byteBuf[2] & 0xff) << 8) | ((byteBuf[3] & 0xff) << 0)); + } else { + return + (((byteBuf[3] & 0xff) << 24) | ((byteBuf[2] & 0xff) << 16) | + ((byteBuf[1] & 0xff) << 8) | ((byteBuf[0] & 0xff) << 0)); + } + } + +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java new file mode 100644 index 000000000..9f37a3522 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/SeekableByteArrayOutputStream.java @@ -0,0 +1,163 @@ +/* + * @(#)SeekableByteArrayOutputStream.java + * + * Copyright © 2010-2011 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.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import static java.lang.Math.*; +/** + * {@code SeekableByteArrayOutputStream}. + * + * @author Werner Randelshofer + * @version $Id: SeekableByteArrayOutputStream.java 299 2013-01-03 07:40:18Z werner $ + */ +public class SeekableByteArrayOutputStream extends ByteArrayOutputStream { + + /** + * The current stream position. + */ + private int pos; + + /** + * Creates a new byte array output stream. The buffer capacity is + * initially 32 bytes, though its size increases if necessary. + */ + public SeekableByteArrayOutputStream() { + this(32); + } + + /** + * Creates a new byte array output stream, with a buffer capacity of + * the specified size, in bytes. + * + * @param size the initial size. + * @exception IllegalArgumentException if size is negative. + */ + public SeekableByteArrayOutputStream(int size) { + if (size < 0) { + throw new IllegalArgumentException("Negative initial size: " + + size); + } + buf = new byte[size]; + } + /** + * Creates a new byte array output stream, which reuses the supplied buffer. + */ + public SeekableByteArrayOutputStream(byte[] buf) { + this.buf = buf; + } + + /** + * Writes the specified byte to this byte array output stream. + * + * @param b the byte to be written. + */ + @Override + public synchronized void write(int b) { + int newcount = max(pos + 1, count); + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); + } + buf[pos++] = (byte)b; + count = newcount; + } + + /** + * Writes len bytes from the specified byte array + * starting at offset off to this byte array output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + */ + @Override + public synchronized void write(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newcount = max(pos+len,count); + if (newcount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount)); + } + System.arraycopy(b, off, buf, pos, len); + pos+=len; + count = newcount; + } + + /** + * Resets the count field of this byte array output + * stream to zero, so that all currently accumulated output in the + * output stream is discarded. The output stream can be used again, + * reusing the already allocated buffer space. + * + * @see java.io.ByteArrayInputStream#count + */ + @Override + public synchronized void reset() { + count = 0; + pos=0; + } + + /** + * Sets the current stream position to the desired location. The + * next read will occur at this location. The bit offset is set + * to 0. + * + *

An IndexOutOfBoundsException will be thrown if + * pos is smaller than the flushed position (as + * returned by getflushedPosition). + * + *

It is legal to seek past the end of the file; an + * EOFException will be thrown only if a read is + * performed. + * + * @param pos a long containing the desired file + * pointer position. + * + * @exception IndexOutOfBoundsException if pos is smaller + * than the flushed position. + * @exception IOException if any other I/O error occurs. + */ + public void seek(long pos) throws IOException { + this.pos = (int)pos; + } + + /** + * Returns the current byte position of the stream. The next write + * will take place starting at this offset. + * + * @return a long containing the position of the stream. + * + * @exception IOException if an I/O error occurs. + */ + public long getStreamPosition() throws IOException { + return pos; + } + + /** Writes the contents of the byte array into the specified output + * stream. + * @param out + */ + public void toOutputStream(OutputStream out) throws IOException { + out.write(buf, 0, count); + } + + /** Returns the underlying byte buffer. */ + public byte[] getBuffer() { + return buf; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java b/trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java new file mode 100644 index 000000000..89477a7df --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/io/SubImageOutputStream.java @@ -0,0 +1,151 @@ +/* + * @(#)SubImageOutputStream.java 1.0 2011-07-20 + * + * Copyright (c) 2011 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.io; + +import java.io.IOException; +import java.nio.ByteOrder; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.ImageOutputStreamImpl; + +/** + * {@code SubImageOutputStream}. + * + * @author Werner Randelshofer + * @version 1.0 2011-07-20 Created. + */ +public class SubImageOutputStream extends ImageOutputStreamImpl { + + private ImageOutputStream out; + private long offset; + private long length; + + /** Whether flush and close request shall be forwarded to underlying stream.*/ + private boolean forwardFlushAndClose; + + public SubImageOutputStream(ImageOutputStream out, ByteOrder bo,boolean forwardFlushAndClose) throws IOException { + this(out, out.getStreamPosition(),bo,forwardFlushAndClose); + } + + public SubImageOutputStream(ImageOutputStream out, long offset, ByteOrder bo,boolean forwardFlushAndClose) throws IOException { + this.out = out; + this.offset = offset; + this.forwardFlushAndClose=forwardFlushAndClose; + setByteOrder(bo); + out.seek(offset); + } + + private long available() throws IOException { + checkClosed(); + long pos = out.getStreamPosition(); + if (pos < offset) { + out.seek(offset); + pos = offset; + } + return offset + out.length() - pos; + } + + @Override + public int read() throws IOException { + if (available() <= 0) { + return -1; + } else { + return out.read(); + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + long av = available(); + if (av <= 0) { + return -1; + } else { + int result = out.read(b, off, (int) Math.min(len, av)); + return result; + } + } + + @Override + public long getStreamPosition() throws IOException { + return out.getStreamPosition() - offset; + } + + @Override + public void seek(long pos) throws IOException { + out.seek(pos + offset); + length=Math.max(pos-offset+1,length); + } + + @Override + public void flush() throws IOException { + if (forwardFlushAndClose) { + out.flush(); + } + } + + @Override + public void close() throws IOException { + if (forwardFlushAndClose) { + super.close(); + } + } + + @Override + public long getFlushedPosition() { + return out.getFlushedPosition() - offset; + } + + /** + * Default implementation returns false. Subclasses should + * override this if they cache data. + */ + @Override + public boolean isCached() { + return out.isCached(); + } + + /** + * Default implementation returns false. Subclasses should + * override this if they cache data in main memory. + */ + @Override + public boolean isCachedMemory() { + return out.isCachedMemory(); + } + + @Override + public boolean isCachedFile() { + return out.isCachedFile(); + } + + @Override + public long length() { + return length; + } + + @Override + public void write(int b) throws IOException { + out.write(b); + length = Math.max(out.getStreamPosition()-offset,length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b,off,len); + length = Math.max(out.getStreamPosition()-offset,length); + } + + public void dispose() throws IOException { + if (forwardFlushAndClose) { + checkClosed(); + } + out=null; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java b/trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java new file mode 100644 index 000000000..67ed7c1ba --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/jpeg/JPEGCodec.java @@ -0,0 +1,155 @@ +/* + * @(#)JPGCodec.java + * + * Copyright (c) 2011 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.jpeg; + +import org.monte.media.io.ByteArrayImageInputStream; +import javax.imageio.ImageReader; +import org.monte.media.Format; +import org.monte.media.AbstractVideoCodec; +import org.monte.media.Buffer; +import org.monte.media.io.ByteArrayImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.BufferFlag.*; + +/** + * {@code JPEGCodec} encodes a BufferedImage as a byte[] array. + *

+ * Supported input formats: + *

    + * {@code VideoFormat} with {@code BufferedImage.class}, any width, any height, + * any depth. + *
+ * Supported output formats: + *
    + * {@code VideoFormat} with {@code byte[].class}, same width and height as input + * format, depth=24. + *
+ * + * @author Werner Randelshofer + * @version $Id: JPEGCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public class JPEGCodec extends AbstractVideoCodec { + + public JPEGCodec() { + super(new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + EncodingKey, ENCODING_QUICKTIME_JPEG,// + CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_JPEG, // + DataClassKey, byte[].class, DepthKey, 24), // + // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_MJPG, DataClassKey, byte[].class, DepthKey, 24), // + }, + new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE), // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME,// + EncodingKey, ENCODING_QUICKTIME_JPEG,// + CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_JPEG, // + DataClassKey, byte[].class, DepthKey, 24), // + // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_MJPG, DataClassKey, byte[].class, DepthKey, 24), // + }// + ); + name = "JPEG Codec"; + } + + @Override + public int process(Buffer in, Buffer out) { + if (outputFormat.get(EncodingKey).equals(ENCODING_BUFFERED_IMAGE)) { + return decode(in, out); + } else { + return encode(in, out); + } + } + + public int encode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + BufferedImage image = getBufferedImage(in); + if (image == null) { + out.setFlag(DISCARD); + return CODEC_FAILED; + } + ByteArrayImageOutputStream tmp; + if (out.data instanceof byte[]) { + tmp = new ByteArrayImageOutputStream((byte[]) out.data); + } else { + tmp = new ByteArrayImageOutputStream(); + } + + try { + ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/jpeg").next(); + ImageWriteParam iwParam = iw.getDefaultWriteParam(); + iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + float quality = outputFormat.get(QualityKey, 1f); + iwParam.setCompressionQuality(quality); + iw.setOutput(tmp); + IIOImage img = new IIOImage(image, null, null); + iw.write(null, img, iwParam); + iw.dispose(); + + out.sampleCount = 1; + out.setFlag(KEYFRAME); + out.data = tmp.getBuffer(); + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } + + public int decode(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + byte[] data = (byte[]) in.data; + if (data == null) { + out.setFlag(DISCARD); + return CODEC_FAILED; + } + ByteArrayImageInputStream tmp = new ByteArrayImageInputStream(data); + + try { + // ImageReader ir = (ImageReader) ImageIO.getImageReadersByMIMEType("image/jpeg").next(); + ImageReader ir = new MJPGImageReader(new MJPGImageReaderSpi()); + ir.setInput(tmp); + out.data = ir.read(0); + ir.dispose(); + + out.sampleCount = 1; + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java new file mode 100644 index 000000000..f70ecc3f6 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReader.java @@ -0,0 +1,119 @@ +/* + * @(#)MJPGImageReader.java + * + * Copyright (c) 2010-2011 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.jpeg; + +import org.monte.media.avi.AVIBMPDIB; +import com.sun.imageio.plugins.jpeg.JPEGImageReader; +import java.awt.image.BufferedImage; +import java.awt.image.DirectColorModel; +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.LinkedList; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.MemoryCacheImageInputStream; + +/** + * Reads an image in the Motion JPEG (MJPG) format. + *

. + * This class can read Motion JPEG files with omitted Huffmann table. + *

+ * For more information see: + * Microsoft Windows Bitmap Format. + * Multimedia Technical Note: JPEG DIB Format. + * (c) 1993 Microsoft Corporation. All rights reserved. + * BMPDIB.txt + * + * @author Werner Randelshofer + * @version $Id: MJPGImageReader.java 299 2013-01-03 07:40:18Z werner $ + */ +public class MJPGImageReader extends ImageReader { + + private static DirectColorModel RGB = new DirectColorModel(24, 0xff0000, 0xff00, 0xff, 0x0); + /** When we read the header, we read the whole image. */ + private BufferedImage image; + + public MJPGImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return 1; + } + + @Override + public int getWidth(int imageIndex) throws IOException { + readHeader(); + return image.getWidth(); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + readHeader(); + return image.getHeight(); + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + readHeader(); + LinkedList l = new LinkedList(); + l.add(new ImageTypeSpecifier(RGB, RGB.createCompatibleSampleModel(image.getWidth(), image.getHeight()))); + return l.iterator(); + } + + @Override + public IIOMetadata getStreamMetadata() throws IOException { + return null; + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + if (imageIndex > 0) { + throw new IndexOutOfBoundsException(); + } + readHeader(); + + return image; + } + + /** Reads the image header. + * Does nothing if the header has already been loaded. + */ + private void readHeader() throws IOException { + if (image == null) { + ImageReader r = new JPEGImageReader(getOriginatingProvider()); + Object in = getInput(); + /*if (in instanceof Buffer) { + Buffer buffer = (Buffer) in; + in=buffer.getData(); + }*/ + if (in instanceof byte[]) { + r.setInput(new MemoryCacheImageInputStream(AVIBMPDIB.prependDHTSeg((byte[]) in))); + } else if (in instanceof ImageInputStream) { + r.setInput(AVIBMPDIB.prependDHTSeg((ImageInputStream) in)); + } else { + r.setInput(AVIBMPDIB.prependDHTSeg((InputStream) in)); + } + image = r.read(0); + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java new file mode 100644 index 000000000..df9428f99 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/jpeg/MJPGImageReaderSpi.java @@ -0,0 +1,88 @@ +/* + * @(#)MJPGImageReaderSpi.java + * + * Copyright (c) 2010-2011 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.jpeg; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + + +/** + * ImageIO service provider interface for images in the Motion JPEG (MJPG) + * format. + *

+ * The reader described by this class can read Motion JPEG files with omitted + * Huffmann table. + *

+ * For more information see: + * Microsoft Windows Bitmap Format. + * Multimedia Technical Note: JPEG DIB Format. + * (c) 1993 Microsoft Corporation. All rights reserved. + * BMPDIB.txt + + * + * @author Werner Randelshofer + * @version $Id: MJPGImageReaderSpi.java 299 2013-01-03 07:40:18Z werner $ + */ +public class MJPGImageReaderSpi extends ImageReaderSpi { + + public MJPGImageReaderSpi() { + super("Werner Randelshofer",//vendor name + "1.0",//version + new String[]{"MJPG"},//names + new String[]{"mjpg"},//suffixes, + new String[]{"image/mjpg"},// MIMETypes, + "org.monte.media.jmf.renderer.video.MJPGImageReader",// readerClassName, + new Class[]{ImageInputStream.class,InputStream.class,byte[].class/*,javax.media.Buffer.class*/},// inputTypes, + null,// writerSpiNames, + false,// supportsStandardStreamMetadataFormat, + null,// nativeStreamMetadataFormatName, + null,// nativeStreamMetadataFormatClassName, + null,// extraStreamMetadataFormatNames, + null,// extraStreamMetadataFormatClassNames, + false,// supportsStandardImageMetadataFormat, + null,// nativeImageMetadataFormatName, + null,// nativeImageMetadataFormatClassName, + null,// extraImageMetadataFormatNames, + null// extraImageMetadataFormatClassNames + ); + } + + @Override + public boolean canDecodeInput(Object source) throws IOException { + if (source instanceof ImageInputStream) { + ImageInputStream in = (ImageInputStream) source; + in.mark(); + + // Check if file starts with a JFIF SOI magic (0xffd8=-40) + if (in.readShort() != -40) { + in.reset(); + return false; + } + in.reset(); + return true; + } + return false; + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new MJPGImageReader(this); + } + + @Override + public String getDescription(Locale locale) { + return "MJPG Image Reader"; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/math/IntMath.java b/trunk/libsrc/avi/src/org/monte/media/math/IntMath.java new file mode 100644 index 000000000..c9cfb0d0d --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/math/IntMath.java @@ -0,0 +1,250 @@ +/* + * @(#)IntMath.java + * + * Copyright (c) 2002-2012 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.math; + +import java.math.BigInteger; + +/** + * Utility class for integer arithmetic. + * + * @author Werner Randelshofer + * @version $Id: IntMath.java 299 2013-01-03 07:40:18Z werner $ + */ +public class IntMath { + + /** Creates a new instance of IntMath */ + public IntMath() { + } + + /** + * Returns an int whose value is the greatest common divisor of + * abs(a) and abs(b). Returns 0 if + * a==0 && b==0. + * + * @param a value with with the GCD is to be computed. + * @param b value with with the GCD is to be computed. + * @return GCD(a, b) + */ + public static int gcd(int a, int b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // ggt6, Seite 63 + + a = Math.abs(a); + b = Math.abs(b); + + while (a > 0 && b > 0) { + a = a % b; + if (a > 0) b = b % a; + } + return a + b; + } + /** + * Returns a long whose value is the greatest common divisor of + * abs(a) and abs(b). Returns 0 if + * a==0 && b==0. + * + * @param a value with with the GCD is to be computed. + * @param b value with with the GCD is to be computed. + * @return GCD(a, b) + */ + public static long gcd(long a, long b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // ggt6, Seite 63 + + a = Math.abs(a); + b = Math.abs(b); + + while (a > 0 && b > 0) { + a = a % b; + if (a > 0) b = b % a; + } + return a + b; + } + /** + * Returns a long whose value is the greatest common divisor of + * abs(a) and abs(b). Returns 0 if + * a==0 && b==0. + * + * @param a value with with the GCD is to be computed. + * @param b value with with the GCD is to be computed. + * @return GCD(a, b) + */ + public static BigInteger gcd(BigInteger a, BigInteger b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // ggt6, Seite 63 + + a = a.abs(); + b = b.abs(); + + while (a.compareTo(BigInteger.ZERO) > 0 && b.compareTo(BigInteger.ZERO) > 0) { + a = a.mod(b); + if (a.compareTo(BigInteger.ZERO) > 0) b = b.mod(a); + } + return a.add(b); + } + + /** + * Returns an int whose value is the smallest common multiple of + * abs(a) and abs(b). Returns 0 if + * a==0 || b==0. + * + * @param a value with with the SCM is to be computed. + * @param b value with with the SCM is to be computed. + * @return SCM(a, b) + */ + public static int scm(int a, int b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // gill, Seite 141 + + if (a == 0 || b == 0) return 0; + + a = Math.abs(a); + b = Math.abs(b); + + int u = a; + int v = b; + + while (a != b) { + if (a < b) { + b -= a; + v += u; + } else { + a -= b; + u += v; + } + } + + + //return a; // gcd + return (u + v) / 2; // scm + } + /** + * Returns an int whose value is the smallest common multiple of + * abs(a) and abs(b). Returns 0 if + * a==0 || b==0. + * + * @param a value with with the SCM is to be computed. + * @param b value with with the SCM is to be computed. + * @return SCM(a, b) + */ + public static long scm(long a, long b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // gill, Seite 141 + + if (a == 0 || b == 0) return 0; + + a = Math.abs(a); + b = Math.abs(b); + if (b==1)return a; + if (a==1)return b; + + long u = a; + long v = b; + + // FIXME - Handle overflow + while (a != b) { + if (a < b) { + b -= a; + v += u; + } else { + a -= b; + u += v; + } + } + + + //return a; // gcd + return (u + v) / 2; // scm + } + /** + * Returns an int whose value is the smallest common multiple of + * abs(a) and abs(b). Returns 0 if + * a==0 || b==0. + * + * @param a value with with the SCM is to be computed. + * @param b value with with the SCM is to be computed. + * @return SCM(a, b) + */ + public static BigInteger scm(BigInteger a, BigInteger b) { + // Quelle: + // Herrmann, D. (1992). Algorithmen Arbeitsbuch. + // Bonn, München Paris: Addison Wesley. + // gill, Seite 141 + + if (a.compareTo(BigInteger.ZERO) == 0 || b.compareTo(BigInteger.ZERO) == 0) { + return BigInteger.ZERO; + } + + a = a.abs(); + b = b.abs(); + if (b.compareTo(BigInteger.ONE)==0)return a; + if (a.compareTo(BigInteger.ONE)==0)return b; + + BigInteger u = a; + BigInteger v = b; + + // FIXME - Handle overflow + while (a.compareTo(b) != 0) { + if (a .compareTo( b)<0) { + b = b.subtract(a); + v = v.add(u); + } else { + a = a.subtract(b); + u = u.add(v); + } + } + + + //return a; // gcd + return (u.add(v)).divide(BigInteger.valueOf(2)); // scm + } + + /** + * Reverses all 32 bits of the provided integer value. + */ + public static int reverseBits(int a) { + return reverseBits(a, 32); + } + /** + * Reverses specified number of bits of the provided integer value. + * @param a The number. + * @param numBits The number of bits (must be between 1 and 32). + */ + public static int reverseBits(int a, int numBits) { + int b = 0; + for (int i=0; i < numBits; i++) { + b <<= 1; + b |= (a & 1); + a >>>= 1; + } + return b; + + } + + public static void main(String[] args) { + for (int i=0; i < 8; i++) { + int a = 1< Two LONGs 32-bit (4-byte) unsigned + * integer: the first represents the numerator of a fraction; the second, the + * denominator.

Invariants:

  • denominator>=0, the + * denominator is always a positive integer
  • 0/1 is the unique + * representation of 0.
  • 1/0,-1/0 are the unique representations of + * infinity.
+ * + * @author Werner Randelshofer + * @version $Id: Rational.java 299 2013-01-03 07:40:18Z werner $ + */ +public class Rational extends Number { + + public static final Rational ONE = new Rational(1, 1,false); + public static final Rational ZERO = new Rational(0, 1,false); + public static final long serialVersionUID = 1L; + private final long num; + private final long den; + + public Rational(long numerator) { + this(numerator, 1); + } + + public Rational(long numerator, long denominator) { + this(numerator, denominator, true); + } + + private Rational(long numerator, long denominator, boolean reduceFraction) { + if (numerator == 0) { + // Invariant: 0/1 is unique representation of 0 + denominator = 1; + } + + if (denominator == 0) { + // Invariant: 1/0, -1/0 are unique representations of infinity + numerator = (numerator > 0) ? 1 : -1; + } else if (denominator < 0) { + // Invariant: denominator is always positive + denominator = -denominator; + numerator = -numerator; + } + + if (reduceFraction) { + long g = gcd(numerator, denominator); + num = numerator / g; + den = denominator / g; + } else { + num = numerator; + den = denominator; + } + } + + private Rational(BigInteger numerator, BigInteger denominator, boolean reduceFraction) { + if (numerator.equals(BigInteger.ZERO)) { + // Invariant: 0/1 is unique representation of 0 + denominator = BigInteger.ONE; + } + + if (denominator.equals(BigInteger.ZERO)) { + // Invariant: 1/0, -1/0 are unique representations of infinity + numerator = (numerator.compareTo(BigInteger.ZERO) > 0) ? BigInteger.ONE : BigInteger.ONE.negate(); + } else if (denominator.compareTo(BigInteger.ZERO) < 0) { + // Invariant: denominator is always positive + denominator = denominator.negate(); + numerator = numerator.negate(); + } + + BigInteger numB, denB; + if (reduceFraction) { + BigInteger g = gcd(numerator, denominator); + numB = numerator.divide(g); + denB = denominator.divide(g); + } else { + numB = numerator; + denB = denominator; + } + int bitLength = Math.max(numB.bitLength(), denB.bitLength()); + if (bitLength > 63) { + numB = numB.shiftRight(bitLength - 63); + denB = denB.shiftRight(bitLength - 63); + if (numB.equals(BigInteger.ZERO)) { + // Invariant: 0/1 is unique representation of 0 + denB = BigInteger.ONE; + } + + if (denB.equals(BigInteger.ZERO)) { + // Invariant: 1/0, -1/0 are unique representations of infinity + numB = (numB.compareTo(BigInteger.ZERO) > 0) ? BigInteger.ONE : BigInteger.ONE.negate(); + + } + } + num = numB.longValue(); + den = denB.longValue(); + } + + public Rational(Rational r) { + this(r.num, r.den); + } + + public long getNumerator() { + return num; + } + + public long getDenominator() { + return den; + } + + public Rational add(Rational that) { + return add(that, true); + } + + private Rational add(Rational that, boolean reduceFraction) { + if (this.den == that.den) { + // => same denominator: add numerators + return new Rational(this.num + that.num, this.den, reduceFraction); + } + + // FIXME - handle overflow + long s = scm(this.den, that.den); + Rational result = new Rational( + this.num * (s / this.den) + that.num * (s / that.den), + s, reduceFraction); + + return result; + } + + /** + * Warning. Rational is supposed to be immutable. * + * + * private Rational addAssign(Rational that) { if (this.den == that.den) { + * // => same denominator: add numerators this.num += that.num; return this; + * } + * + * // FIXME - handle overflow long s = scm(this.den, that.den); this.num = + * this.num * (s / this.den) + that.num * (s / that.den); this.den = s; + * + * + * return reduceAssign(); } + */ + public Rational subtract(Rational that) { + return add(that.negate()); + } + + public Rational negate() { + return valueOf(-num, den); + } + + public Rational inverse() { + return valueOf(den, num, false); + } + + /** + * Returns the closest rational with the specified denominator which is + * smaller or equal than this number. + */ + public Rational floor(long d) { + if (d == den) { + return valueOf(num, den); + } + long s = scm(this.den, d); + + if (s == d) { + return valueOf(num * s / den, d); + } else if (s == den) { + return valueOf(num * d / den, d); + } else { + return valueOf(num * d / den, d); + } + } + + /** + * Returns the closest rational with the specified denominator which is + * greater or equal than this number. + */ + public Rational ceil(long d) { + if (d == den) { + return valueOf(num, den); + } + long s = scm(this.den, d); + + if (s == d) { + return valueOf((num * s + den - 1) / den, d); + } else if (s == den) { + return valueOf((num * d + den - 1) / den, d); + } else { + return valueOf((num * d + den - 1) / den, d); + } + } + + public Rational multiply(Rational that) { + if (abs(this.num) < Integer.MAX_VALUE + && abs(this.den) < Integer.MAX_VALUE + && abs(that.num) < Integer.MAX_VALUE + && abs(that.den) < Integer.MAX_VALUE) { + return valueOf(this.num * that.num, + this.den * that.den); + } else { + return new Rational( + BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(that.num)), + BigInteger.valueOf(this.den).multiply(BigInteger.valueOf(that.den)), + true); + } + } + + public Rational multiply(long integer) { + if (integer==0) { + return ZERO; + } else if (this.den % integer == 0) { + return valueOf( + this.num, + this.den / integer); + } else if (abs(this.num) < Integer.MAX_VALUE + && abs(integer) < Integer.MAX_VALUE) { + return valueOf( + this.num * integer, + this.den); + } else { + return new Rational( + BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(integer)), + BigInteger.valueOf(this.den), true); + } + } + + public Rational divide(Rational that) { + if (abs(this.num) < Integer.MAX_VALUE + && abs(this.den) < Integer.MAX_VALUE + && abs(that.num) < Integer.MAX_VALUE + && abs(that.den) < Integer.MAX_VALUE) { + return valueOf(this.num * that.den, + this.den * that.num); + } else { + return valueOf( + BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(that.den)), + BigInteger.valueOf(this.den).multiply(BigInteger.valueOf(that.num)), + true); + } + } + + @Override + public String toString() { + //long gcd = IntMath.gcd(num, den); + if (num == 0) { + return "0"; + } else if (den == 1) { + return Long.toString(num); + } else { + return num + "/" + den; + /* + } else { + return Float.toString((float) num / den); + */ + } + } + + public String toDescriptiveString() { + long gcd = IntMath.gcd(num, den); + if (gcd == 0 || num == 0) { + return num + "/" + den + " = " + 0; + } else if (gcd == den) { + return num + "/" + den + " = " + Long.toString(num / den); + } else { + return num + "/" + den + " ≈ " + ((float) num / den); + } + } + + @Override + public int intValue() { + return (int) (num / den); + } + + @Override + public long longValue() { + return num / den; + } + + @Override + public float floatValue() { + return (float) num / (float) den; + } + + @Override + public double doubleValue() { + return (double) num / (double) den; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Rational that = (Rational) obj; + + return compareTo(that) == 0; + } + + /** + * return { -1, 0, +1 } if a < b, a = b, or a > b. + */ + public int compareTo(Rational that) { + // The following code avoids BigInteger allocation if the denominators + // are equal + if (this.den == that.den) { + if (this.num < that.num) { + return -1; + } else if (this.num > that.num) { + return 1; + } else { + return 0; + } + } + + // Work with longs if overflow can not occur + if (abs(this.num) < Integer.MAX_VALUE + && abs(this.den) < Integer.MAX_VALUE + && abs(that.num) < Integer.MAX_VALUE + && abs(that.den) < Integer.MAX_VALUE) { + long lhs = this.num * that.den; + long rhs = this.den * that.num; + if (lhs < rhs) { + return -1; + } else if (lhs > rhs) { + return 1; + } else { + return 0; + } + } + + // Use big integers to avoid overflows + BigInteger lhs; + BigInteger rhs; + lhs = BigInteger.valueOf(this.num).multiply(BigInteger.valueOf(that.den)); + rhs = BigInteger.valueOf(this.den).multiply(BigInteger.valueOf(that.num)); + + return lhs.compareTo(rhs); + } + + @Override + public int hashCode() { + return (int) ((num ^ (num >>> 32)) + ^ (den ^ (den >>> 32))); + + } + + public static Rational max(Rational a, Rational b) { + return (a.compareTo(b) >= 0) ? a : b; + } + + public static Rational min(Rational a, Rational b) { + return (a.compareTo(b) <= 0) ? a : b; + } + + public boolean isZero() { + return num == 0; + } + + public boolean isLessOrEqualZero() { + return num <= 0; + } + + public static Rational valueOf(double d) { + if (d == 0) { + return valueOf(0, 1); + } + if (abs(d) > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Value " + d + " is too big."); + } + if (Double.isInfinite(d)) { + return valueOf((long) signum(d), 0); + } + if (Double.isNaN(d)) { + return valueOf(0, 1); // no way to express a NaN :-( + } + return toRational(d, Integer.MAX_VALUE, 100); + } + + public static Rational valueOf(long num, long den) { + return valueOf(num, den, true); + } + + private static Rational valueOf(long num, long den, boolean reduceFraction) { + if (num == den) { + return ONE; + } + if (num == 0) { + return ZERO; + } + return new Rational(num, den, reduceFraction); + } + + public static Rational valueOf(BigInteger num, BigInteger den) { + return valueOf(num, den, true); + } + + private static Rational valueOf(BigInteger num, BigInteger den, boolean reduceFraction) { + if (num.equals(den)) { + return ONE; + } + if (num.equals(BigInteger.ZERO)) { + return ZERO; + } + return new Rational(num, den, reduceFraction); + } + + /** + * Iteratively computes rational from double.

Reference:
+ * http://www2.fz-juelich.de/video/cpp/html/exercises/exercise/Rational_cpp.html + *

+ */ + private static Rational toRational(double x, double limit, int iterations) { + double intpart = Math.floor(x); + double fractpart = x - intpart; + double d = 1.0 / fractpart; + long left = (long) intpart; + if (d > limit || iterations == 0) { + return valueOf(left, 1, false); + } else { + return valueOf(left, 1, false).add(toRational(d, limit * 0.1, iterations - 1).inverse(), false); + } + } + + public Rational round(long d) { + if (d == den) { + return valueOf(num, den); + } + + Rational fl = floor(d); + Rational diffFl = subtract(fl); + + if (diffFl.isZero()) { + return fl; + } + + Rational cl = ceil(d); + Rational diffCl = subtract(cl); + if (diffCl.isZero()) { + return cl; + } + + if (diffFl.isNegative()) { + diffFl = diffFl.negate(); + } + if (diffCl.isNegative()) { + diffCl = diffCl.negate(); + } + return diffFl.compareTo(diffCl) <= 0 ? fl : cl; + } + + private boolean isNegative() { + return num < 0; + } + + /** + * Parses a string. + * + * A rational can be represented in the following ways:
  • As a long + * number
  • As a double number
  • As an integer/integer + * rational number
  • + * + * @throws NumberFormatException if str can not be parsed. + */ + public static Rational valueOf(String str) { + int p = str.indexOf('/'); + if (p != -1) { + return valueOf(Long.valueOf(str.substring(0, p)), Long.valueOf(str.substring(p + 1))); + } + try { + return valueOf(Long.valueOf(str)); + } catch (NumberFormatException e) { + return valueOf(Double.valueOf(str)); + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java b/trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java new file mode 100644 index 000000000..4da926542 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/png/PNGCodec.java @@ -0,0 +1,118 @@ +/* + * @(#)PNGCodec.java + * + * Copyright (c) 2011-2012 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.png; + +import org.monte.media.Format; +import org.monte.media.AbstractVideoCodec; +import org.monte.media.Buffer; +import org.monte.media.io.ByteArrayImageOutputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import static org.monte.media.VideoFormatKeys.*; +import static org.monte.media.BufferFlag.*; + +/** + * {@code PNGCodec} encodes a BufferedImage as a byte[] array.. + *

    + * Supported input formats: + *

      + * {@code VideoFormat} with {@code BufferedImage.class}, any width, any height, + * any depth. + *
    + * Supported output formats: + *
      + * {@code VideoFormat} with {@code byte[].class}, same width and height as input + * format, depth=24. + *
    + * + * @author Werner Randelshofer + * @version $Id: PNGCodec.java 299 2013-01-03 07:40:18Z werner $ + */ +public class PNGCodec extends AbstractVideoCodec { + + public PNGCodec() { + super(new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, + EncodingKey, ENCODING_BUFFERED_IMAGE), // + }, + new Format[]{ + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + DepthKey, 24, + EncodingKey, ENCODING_QUICKTIME_PNG, DataClassKey, byte[].class), // + // + new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + DepthKey, 24, + EncodingKey, ENCODING_AVI_PNG, DataClassKey, byte[].class), // + }); + } + + @Override + public Format setOutputFormat(Format f) { + String mimeType = f.get(MimeTypeKey, MIME_QUICKTIME); + if (mimeType != null && !mimeType.equals(MIME_AVI)) { + return super.setOutputFormat( + f.prepend(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, + EncodingKey, ENCODING_QUICKTIME_PNG, DataClassKey, + byte[].class, DepthKey, 24)); + } else { + return super.setOutputFormat( + f.prepend(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, + EncodingKey, ENCODING_AVI_PNG, DataClassKey, + byte[].class, DepthKey, 24)); + } + } + + @Override + public int process(Buffer in, Buffer out) { + out.setMetaTo(in); + out.format = outputFormat; + if (in.isFlag(DISCARD)) { + return CODEC_OK; + } + + BufferedImage image = getBufferedImage(in); + if (image == null) { + out.setFlag(DISCARD); + return CODEC_FAILED; + } + + ByteArrayImageOutputStream tmp; + if (out.data instanceof byte[]) { + tmp = new ByteArrayImageOutputStream((byte[]) out.data); + } else { + tmp = new ByteArrayImageOutputStream(); + } + + try { + ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next(); + ImageWriteParam iwParam = iw.getDefaultWriteParam(); + iw.setOutput(tmp); + IIOImage img = new IIOImage(image, null, null); + iw.write(null, img, iwParam); + iw.dispose(); + + out.setFlag(KEYFRAME); + out.header = null; + out.data = tmp.getBuffer(); + out.offset = 0; + out.length = (int) tmp.getStreamPosition(); + return CODEC_OK; + } catch (IOException ex) { + ex.printStackTrace(); + out.setFlag(DISCARD); + return CODEC_FAILED; + } + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java new file mode 100644 index 000000000..67843205b --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFChunk.java @@ -0,0 +1,188 @@ +/* + * @(#)RIFFChunk.java + * + * Copyright (c) 2005-2012 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.riff; + +import java.util.*; +/** + * RIFF Chunks form the building blocks of a RIFF file. + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version $Id: RIFFChunk.java 299 2013-01-03 07:40:18Z werner $ + */ +public class RIFFChunk { + private int id; + private int type; + private long size; + private long scan; + private byte[] data; + private Hashtable propertyChunks; + private ArrayList collectionChunks; + /** + * This is used to display parser messages, when the parser encounters and + * error while parsing the chunk. + */ + private String parserMessage; + + public RIFFChunk(int type, int id) { + this.id = id; + this.type = type; + size = -1; + scan = -1; + } + public RIFFChunk(int type, int id, long size, long scan) { + this.id = id; + this.type = type; + this.size = size; + this.scan = scan; + } + public RIFFChunk(int type, int id, long size, long scan, RIFFChunk propGroup) { + this.id = id; + this.type = type; + this.size = size; + this.scan = scan; + if (propGroup != null) { + if (propGroup.propertyChunks != null) { + propertyChunks = new Hashtable(propGroup.propertyChunks); + } + if (propGroup.collectionChunks != null) { + collectionChunks = new ArrayList(propGroup.collectionChunks); + } + } + } + + /** + * @return ID of chunk. + */ + public int getID() { + return id; + } + + /** + * @return Type of chunk. + */ + public int getType() { + return type; + } + + /** + * @return Size of chunk. + */ + public long getSize() { + return size; + } + + /** + * @return Scan position of chunk within the file. + */ + public long getScan() { + return scan; + } + + public void putPropertyChunk(RIFFChunk chunk) { + if (propertyChunks == null) { + propertyChunks = new Hashtable (); + } + propertyChunks.put(chunk,chunk); + } + + public RIFFChunk getPropertyChunk(int id) { + if (propertyChunks == null) { + return null; + } + RIFFChunk chunk = new RIFFChunk(type, id); + return propertyChunks.get(chunk); + } + + public Enumeration propertyChunks() { + if (propertyChunks == null) { + propertyChunks = new Hashtable (); + } + return propertyChunks.keys(); + } + + public void addCollectionChunk(RIFFChunk chunk) { + if (collectionChunks == null) { + collectionChunks = new ArrayList(); + } + collectionChunks.add(chunk); + } + + public RIFFChunk[] getCollectionChunks(int id) { + if (collectionChunks == null) { + return new RIFFChunk[0]; + } + Iterator enm = collectionChunks.iterator(); + int i = 0; + while ( enm.hasNext() ) { + RIFFChunk chunk = enm.next(); + if (chunk.id==id) { + i++; + } + } + RIFFChunk[] array = new RIFFChunk[i]; + i = 0; + enm = collectionChunks.iterator(); + while ( enm.hasNext() ) { + RIFFChunk chunk = enm.next(); + if (chunk.id==id) { + array[i++] = chunk; + } + } + return array; + } + public Iterator collectionChunks() { + if (collectionChunks == null) { + collectionChunks = new ArrayList(); + } + return collectionChunks.iterator(); + } + + /** + * Sets the data. + * Note: The array will not be cloned. + */ + public void setData(byte[] data) { + this.data = data; + } + /** + * Gets the data. + * Note: The array will not be cloned. + */ + public byte[] getData() { + return data; + } + + @Override + public boolean equals(Object another) { + if (another instanceof RIFFChunk) { + RIFFChunk that = (RIFFChunk) another; + return (that.id==this.id) && (that.type==this.type); + } + return false; + } + + @Override + public int hashCode() { + return id; + } + + public void setParserMessage(String newValue) { + this.parserMessage = newValue; + } + public String getParserMessage() { + return this.parserMessage; + } + + @Override + public String toString() { + return super.toString()+"{"+RIFFParser.idToString(getType())+","+RIFFParser.idToString(getID())+"}"; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java new file mode 100644 index 000000000..dd7529186 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFParser.java @@ -0,0 +1,841 @@ +/* + * @(#)RIFFParser.java 1.5 2012-08-03 + * + * Copyright (c) 2005-2012 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.riff; + +import org.monte.media.AbortException; +import org.monte.media.ParseException; +import org.monte.media.io.ImageInputStreamAdapter; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.text.NumberFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.WeakHashMap; +import javax.imageio.stream.ImageInputStream; + +/** + * Interprets Resource Interchange File Format (RIFF) streams. + * + *

    Abstract + *

    + * RIFF File Format + * A RIFF file consists of a RIFF header followed by zero or more lists and chunks. + *

    + * The RIFF header has the following form: + *

    + * 'RIFF' fileSize fileType (data)
    + * 
    + * where 'RIFF' is the literal FOURCC code 'RIFF', fileSize is a 4-byte value + * giving the size of the data in the file, and fileType is a FOURCC that + * identifies the specific file type. The value of fileSize includes the size + * of the fileType FOURCC plus the size of the data that follows, but does not + * include the size of the 'RIFF' FOURCC or the size of fileSize. The file data + * consists of chunks and lists, in any order. + *

    + * FOURCCs
    + * A FOURCC (four-character code) is a 32-bit unsigned integer created by + * concatenating four ASCII characters. For example, the FOURCC 'abcd' is + * represented on a Little-Endian system as 0x64636261. FOURCCs can contain + * space characters, so ' abc' is a valid FOURCC. The RIFF file format uses + * FOURCC codes to identify stream types, data chunks, index entries, and other + * information. + *

    + * A chunk has the following form: + *

    + * ckID ckSize ckData
    + * 
    + * where ckID is a FOURCC that identifies the data contained in the chunk, + * ckData is a 4-byte value giving the size of the data in ckData, and ckData is + * zero or more bytes of data. The data is always padded to nearest WORD boundary. + * ckSize gives the size of the valid data in the chunk; it does not include + * the padding, the size of ckID, or the size of ckSize. + *

    + * A list has the following form: + *

    + * 'LIST' listSize listType listData
    + * 
    + * where 'LIST' is the literal FOURCC code 'LIST', listSize is a 4-byte value + * giving the size of the list, listType is a FOURCC code, and listData consists + * of chunks or lists, in any order. The value of listSize includes the size of + * listType plus the size of listData; it does not include the 'LIST' FOURCC or + * the size of listSize. + *

    + * For more information see: + * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_c/directx/htm/avirifffilereference.asp + * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_c/directx/htm/aboutriff.asp + *

    + *

    Grammar for RIFF streams used by this parser + *

    + * RIFFFile    ::= 'RIFF' FormGroup
    + * 
    + * GroupChunk ::= FormGroup | ListGroup + * FormGroup ::= size GroupType [ ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] } + * ListGroup ::= size GroupType [ ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] } + *
    + * LocalChunk ::= DataChunk | CollectionChunk | PropertyChunk + * DataChunk ::= size [ struct ] + * CollectionChunk ::= size [ struct ] + * PropertyChunk ::= size [ struct ] + *
    + * size ::= ULONG + * GroupType ::= FourCC + * ChunkID ::= FourCC + * pad ::= (BYTE)0 + * struct ::= any C language struct built with primitive data types. + *
    + * + *

    Examples + * + *

    Traversing the raw structure of a RIFF file + *

    To traverse the file structure you must first set up a RIFFVisitor object + * that does something useful at each call to the visit method. Then create an + * instance of a RIFFParser and invoke the #interpret method. + * + *

    + * class RIFFRawTraversal
    + * .	{
    + * .	static class Visitor
    + * .	implements RIFFVisitor
    + * .		{
    + * .		...implement the visitor interface here...
    + * .		}
    + * .
    + * .	public static void main(String[] args)
    + * .		{
    + * .		try	{
    + * .			Visitor visitor = new Visitor();
    + * .			FileInputStream stream = new FileInputStream(args[0]);
    + * .			RIFFParser p = new RIFFParser();
    + * .			p.interpret(stream,visitor);
    + * .			stream.close();
    + * .			}
    + * .		catch (IOException e) { System.out.println(e); }
    + * .		catch (InterpreterException e)  { System.out.println(e); }
    + * .		catch (AbortedException e)  { System.out.println(e); }
    + * .		}
    + * .	}
    + * 
    + * + *

    Traversing the RIFF file and interpreting its content. + *

    Since RIFF files are not completely self describing (there is no information + * that helps differentiate between data chunks, property chunks and collection + * chunks) a reader must set up the interpreter with some contextual information + * before starting the interpreter. + *

    + * Once at least one chunk has been declared, the interpreter will only call the + * visitor for occurences of the declared group chunks and data chunks. The property + * chunks and the collection chunks can be obtained from the current group chunk + * by calling #getProperty or #getCollection. + *
    Note: All information the visitor can obtain during interpretation is only + * valid during the actual #visit... call. Dont try to get information about properties + * or collections for chunks that the visitor is not visiting right now. + * + *

    + * class InterpretingAnILBMFile
    + * .	{
    + * .	static class Visitor
    + * .	implements RIFFVisitor
    + * .		{
    + * .		...
    + * .		}
    + * .
    + * .	public static void main(String[] args)
    + * .		{
    + * .		try	{
    + * .			Visitor visitor = new Visitor();
    + * .			FileInputStream stream = new FileInputStream(args[0]);
    + * .			RIFFParser p = new RIFFParser();
    + * .			p.declareGroupChunk('FORM','ILBM');
    + * .			p.declarePropertyChunk('ILBM','BMHD');
    + * .			p.declarePropertyChunk('ILBM','CMAP');
    + * .			p.declareCollectionChunk('ILBM','CRNG');
    + * .			p.declareDataChunk('ILBM','BODY');
    + * .			p.interpret(stream,visitor);
    + * .			stream.close();
    + * .			}
    + * .		catch (IOException e) { System.out.println(e); }
    + * .		catch (InterpreterException e)  { System.out.println(e); }
    + * .		catch (AbortedException e)  { System.out.println(e); }
    + * .		}
    + * .	}
    + * 
    + * + * @see RIFFVisitor + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version 1.5 2012-08-03 Adds offset property. + *
    1.4 2011-08-26 Adds HashMap for stop chunks. + *
    1.3 2011-01-23 Use HashMap instead of Hashtable. + *
    1.2 2010-07-06 Use integer IDs for efficiency. Added support for + * stop chunks. + *
    1.0 2005-01-16 Created. + */ +public class RIFFParser extends Object { +private final static boolean DEBUG =false; + /** ID for FormGroupExpression. */ + public final static int RIFF_ID = stringToID("RIFF"); + /** ID for ListGroupExpression. */ + public final static int LIST_ID = stringToID("LIST"); + /** ID for NULL chunks. */ + public final static int NULL_ID = stringToID(" "); + /** ID for NULL chunks. */ + public final static int NULL_NUL_ID = stringToID("\0\0\0\0"); + /** ID for JUNK chunks. */ + public final static int JUNK_ID = stringToID("JUNK"); + /** The visitor traverses the parse tree. */ + private RIFFVisitor visitor; + /** List of data chunks the visitor is interested in. */ + private HashSet dataChunks; + /** List of property chunks the visitor is interested in. */ + private HashSet propertyChunks; + /** List of collection chunks the visitor is interested in. */ + private HashSet collectionChunks; + /** List of stop chunks the visitor is interested in. */ + private HashSet stopChunkTypes; + /** List of group chunks the visitor is interested in. */ + private HashSet groupChunks; + /** Reference to the input stream. */ + private RIFFPrimitivesInputStream in; + /** Reference to the image input stream. */ + private ImageInputStream iin; + + /** Whether we stop at all chunks. */ + private boolean isStopChunks; + + /** Stream offset. */ + private long streamOffset; + + /* ---- constructors ---- */ + /** + * Constructs a new RIFF parser. + */ + public RIFFParser() { + } + + public long getStreamOffset() { + return streamOffset; + } + + public void setStreamOffset(long offset) { + this.streamOffset = offset; + } + + + + /* ---- accessor methods ---- */ + /* ---- action methods ---- */ + /** + * Interprets the RIFFFile expression located at the + * current position of the indicated InputStream. + * Lets the visitor traverse the RIFF parse tree during + * interpretation. + * + *

    Pre condition + *

  • Data-, property- and collection chunks must have been + * declared prior to this call. + *
  • When the client never declared chunks, then all local + * chunks will be interpreted as data chunks. + *
  • The stream must be positioned at the beginning of the + * RIFFFileExpression. + * + *

    Post condition + *

  • When no exception was thrown then the stream is positioned + * after the RIFFFile expression. + * + *

    Obligation + * The visitor may throw an ParseException or an + * AbortException during tree traversal. + * + * @exception ParseException + * Is thrown when an interpretation error occured. + * The stream is positioned where the error occured. + * @exception AbortException + * Is thrown when the visitor decided to abort the + * interpretation. + */ + public long parse(InputStream in, RIFFVisitor v) + throws ParseException, AbortException, IOException { + this.in = new RIFFPrimitivesInputStream(in); + visitor = v; + parseFile(); + return getScan(this.in); + } + + public long parse(ImageInputStream in, RIFFVisitor v) + throws ParseException, AbortException, IOException { + return parse(new ImageInputStreamAdapter(in), v); + } + + /** + * Parses a RIFF file. + * + *

    +     * RIFF = 'RIFF' FormGroup
    +     * 
    + */ + private void parseFile() + throws ParseException, AbortException, IOException { + int id = in.readFourCC(); + + if (id == RIFF_ID) { + parseFORM(null); + } else if (id == JUNK_ID) { + parseLocalChunk(null,id); + } else { + if (iin!=null) { + throw new ParseException("Invalid RIFF File ID: \"" + idToString(id) +" 0x"+Integer.toHexString(id)+" near "+iin.getStreamPosition()+" 0x"+Long.toHexString(iin.getStreamPosition())); + } else { + throw new ParseException("Invalid RIFF File ID: \"" + idToString(id) +" 0x"+Integer.toHexString(id)); + } + } + } + + private long getScan(RIFFPrimitivesInputStream in) { + return in.getScan()+streamOffset; + } + + /** + * Parses a FORM group. + *
    +     * FormGroup ::= size GroupType { ChunkID LocalChunk [pad]
    +     * | 'FORM' FormGroup  [pad] }
    +     * | 'LIST' ListGroup  [pad] }
    +     * 
    + */ + private void parseFORM(HashMap props) + throws ParseException, AbortException, IOException { + long size = in.readULONG(); + long offset = getScan(in); + int type = in.readFourCC(); +if (DEBUG)System.out.println("RIFFParser.parseForm "+idToString(type)); + if (!isGroupType(type)) { + throw new ParseException("Invalid FORM Type: \"" + idToString(type) + "\""); + } + + RIFFChunk propGroup = (props == null) ? null : (RIFFChunk) props.get(type); + RIFFChunk chunk = new RIFFChunk(type, RIFF_ID, size, offset, propGroup); + + boolean visitorWantsToEnterGroup = false; + if (isGroupChunk(chunk) && (visitorWantsToEnterGroup = visitor.enteringGroup(chunk))) { + visitor.enterGroup(chunk); + } + + try { + long finish = offset + size; + while (getScan(in) < finish) { + long idscan = getScan(in); + int id = in.readFourCC(); + + if (id == RIFF_ID) { + parseFORM(props); + } else if (id == LIST_ID) { + parseLIST(props); + } else if (isLocalChunkID(id)) { + parseLocalChunk(chunk, id); + } else { + ParseException pex = new ParseException("Invalid Chunk: \"" + id + "\" at offset:" + idscan); + chunk.setParserMessage(pex.getMessage()); + throw pex; + } + + in.align(); + } + } catch (EOFException e) { + e.printStackTrace(); + chunk.setParserMessage( + "Unexpected EOF after " + + NumberFormat.getInstance().format(getScan(in) - offset) + + " bytes"); + } finally { + if (visitorWantsToEnterGroup) { + visitor.leaveGroup(chunk); + } + } + } + + /** + * Parses a LIST group. + *
    +     * ListGroup ::= size GroupType { ChunkID LocalChunk [pad] | 'LIST ' ListGroup  [pad] }
    +     * 
    + */ + private void parseLIST(HashMap props) + throws ParseException, AbortException, IOException { + long size = in.readULONG(); + long scan = getScan(in); + int type = in.readFourCC(); +if (DEBUG)System.out.println("RIFFParser.parseLIST "+idToString(type)); + + if (!isGroupType(type)) { + throw new ParseException("Invalid LIST Type: \"" + type + "\""); + } + + RIFFChunk propGroup = (props == null) ? null : (RIFFChunk) props.get(type); + RIFFChunk chunk = new RIFFChunk(type, LIST_ID, size, scan, propGroup); + + boolean visitorWantsToEnterGroup = false; + if (isGroupChunk(chunk) && (visitorWantsToEnterGroup = visitor.enteringGroup(chunk))) { + visitor.enterGroup(chunk); + } + try { + if (visitorWantsToEnterGroup) { + long finish = scan + size; + while (getScan(in) < finish) { + long idscan = getScan(in); + int id = in.readFourCC(); + if (id == LIST_ID) { + parseLIST(props); + } else if (isLocalChunkID(id)) { + parseLocalChunk(chunk, id); + } else { + parseGarbage(chunk, id, finish-getScan(in), getScan(in)); + ParseException pex = new ParseException("Invalid Chunk: \"" + id + "\" at offset:" + idscan); + chunk.setParserMessage(pex.getMessage()); + //throw pex; + } + + in.align(); + } + } else { + in.skipFully(size-4); + in.align(); + } + } finally { + if (visitorWantsToEnterGroup) { + visitor.leaveGroup(chunk); + } + } + } + + /** + * Parses a local chunk. + *
    +     * LocalChunk  ::= size { DataChunk | PropertyChunk | CollectionChunk }
    +     * DataChunk = PropertyChunk = CollectionChunk ::= { byte }...*size
    +     * 
    + */ + private void parseLocalChunk(RIFFChunk parent, int id) + throws ParseException, AbortException, IOException { + long size = in.readULONG(); + long scan = getScan(in); +if (DEBUG)System.out.println("RIFFParser.parseLocalChunk "+idToString(id)); + RIFFChunk chunk = new RIFFChunk(parent==null?0:parent.getType(), id, size, scan); + + if (isDataChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + visitor.visitChunk(parent, chunk); + } else if (isPropertyChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.putPropertyChunk(chunk); + } else if (isCollectionChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.addCollectionChunk(chunk); + } else { + in.skipFully((int) size); + if (isStopChunks) { + visitor.visitChunk(parent, chunk); + } + } + } + /** + * This method is invoked when we encounter a parsing problem. + *
    +     * LocalChunk  ::= size { DataChunk | PropertyChunk | CollectionChunk }
    +     * DataChunk = PropertyChunk = CollectionChunk ::= { byte }...*size
    +     * 
    + */ + private void parseGarbage(RIFFChunk parent, int id, long size, long scan) + throws ParseException, AbortException, IOException { + //long size = in.readULONG(); + //long scan = getScan(in); + + RIFFChunk chunk = new RIFFChunk(parent.getType(), id, size, scan); + + if (isDataChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + visitor.visitChunk(parent, chunk); + } else if (isPropertyChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.putPropertyChunk(chunk); + } else if (isCollectionChunk(chunk)) { + byte[] data = new byte[(int) size]; + in.read(data, 0, (int) size); + chunk.setData(data); + parent.addCollectionChunk(chunk); + } else { + in.skipFully((int) size); + if (isStopChunk(chunk)) { + visitor.visitChunk(parent, chunk); + } + } + } + + /** + * Checks whether the ID of the chunk has been declared as a + * data chunk. + * + *

    Pre condition + *

  • Data chunks must have been declared before the + * interpretation has been started. + *
  • This method will always return true when neither + * data chunks, property chunks nor collection chunks + * have been declared, + * + * @param chunk Chunk to be verified. + * @return True when the parameter is a data chunk. + */ + protected boolean isDataChunk(RIFFChunk chunk) { + if (dataChunks == null) { + if (collectionChunks == null && propertyChunks == null && (stopChunkTypes==null||!stopChunkTypes.contains(chunk.getType()))) { + return true; + } else { + return false; + } + } else { + return dataChunks.contains(chunk); + } + } + + /** + * Checks whether the ID of the chunk has been declared as + * a group chunk. + * + *

    Pre condition + *

  • Group chunks must have been declared before the + * interpretation has been started. + * (Otherwise the response is always true). + * + * @param chunk Chunk to be verified. + * @return True when the visitor is interested in this is a group chunk. + */ + protected boolean isGroupChunk(RIFFChunk chunk) { + if (groupChunks == null) { + return true; + } else { + return groupChunks.contains(chunk); + } + } + + /** + * Checks wether the ID of the chunk has been declared as a + * property chunk. + * + *

    Pre condition + *

  • Property chunks must have been declared before the + * interpretation has been started. + *
  • This method will always return false when neither + * data chunks, property chunks nor collection chunks + * have been declared, + */ + protected boolean isPropertyChunk(RIFFChunk chunk) { + if (propertyChunks == null) { + return false; + } else { + return propertyChunks.contains(chunk); + } + } + + /** + * Checks wether the ID of the chunk has been declared as a + * collection chunk. + * + *

    Pre condition + *

  • Collection chunks must have been declared before the + * interpretation has been started. + *
  • This method will always return true when neither + * data chunks, property chunks nor collection chunks + * have been declared, + * + * @param chunk Chunk to be verified. + * @return True when the parameter is a collection chunk. + */ + protected boolean isCollectionChunk(RIFFChunk chunk) { + if (collectionChunks == null) { + return false; + } else { + return collectionChunks.contains(chunk); + } + } + + /** + * Declares a data chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Data chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isLocalChunkID. + */ + public void declareDataChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (dataChunks == null) { + dataChunks = new HashSet(); + } + dataChunks.add(chunk); + } + + /** + * Declares a FORM group chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Group chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isContentsType. + */ + public void declareGroupChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (groupChunks == null) { + groupChunks = new HashSet(); + } + groupChunks.add(chunk); + } + + /** + * Declares a property chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Group chunk declared + * + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isLocalChunkID. + */ + public void declarePropertyChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (propertyChunks == null) { + propertyChunks = new HashSet(); + } + propertyChunks.add(chunk); + } + + /** + * Declares a collection chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Collection chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + * @param id + * ID of the chunk. Must be formulated as a ChunkID conforming + * to the method #isLocalChunkID. + */ + public void declareCollectionChunk(int type, int id) { + RIFFChunk chunk = new RIFFChunk(type, id); + if (collectionChunks == null) { + collectionChunks = new HashSet(); + } + collectionChunks.add(chunk); + } + + /** + * Declares a stop chunk. + * + *

    Pre condition + *

  • The chunk must not have already been declared as of a + * different type. + *
  • Declarations may not be done during interpretation + * of an RIFFFileExpression. + * + *

    Post condition + *

  • Stop chunk declared + * + * @param type + * Type of the chunk. Must be formulated as a TypeID conforming + * to the method #isFormType. + */ + public void declareStopChunkType(int type) { + if (stopChunkTypes == null) { + stopChunkTypes = new HashSet(); + } + stopChunkTypes.add(type); + } + + /** Whether the parse should stop at all chunks. + *

    + * The parser does not read the data body of stop chunks. + *

    + * By declaring stop chunks, and not declaring any data, group or + * property chunks, the file structure of a RIFF file can be quickly + * scanned through. + */ + public void declareStopChunks() { + isStopChunks = true; + } + + private boolean isStopChunk(RIFFChunk chunk) { + return isStopChunks||stopChunkTypes!=null&&stopChunkTypes.contains(chunk.getType()); + } + + /* ---- Class methods ---- */ + /** + * Checks wether the argument represents a valid RIFF GroupID. + * + *

    Validation + *

      + *
    • Group ID must be one of RIFF_ID, LIST_ID.
    • + *
    + * + * @param id Chunk ID to be checked. + * @return True when the chunk ID is a valid Group ID. + */ + public static boolean isGroupID(int id) { + return id == LIST_ID || id == RIFF_ID; + } + + /** + * Checks wether the argument represents a valid RIFF Group Type. + * + *

    Validation + *

      + *
    • Must be a valid ID.
    • + *
    • Must not be a group ID.
    • + *
    • Must not be a NULL_ID.
    • + *
    + * + * @param id Chunk ID to be checked. + * @return True when the chunk ID is a valid Group ID. + */ + public static boolean isGroupType(int id) { + return isID(id) && !isGroupID(id) && id != NULL_ID; + } + + /** + * Checks if the argument represents a valid RIFF ID. + * + *

    Validation + *

  • Every byte of an ID must be in the range of 0x20..0x7e + *
  • The id may not have leading spaces (unless the id is a NULL_ID). + * + * @param id Chunk ID to be checked. + * @return True when the ID is a valid IFF chunk ID. + */ + public static boolean isID(int id) { + int c0 = id >> 24; + int c1 = (id >> 16) & 0xff; + int c2 = (id >> 8) & 0xff; + int c3 = id & 0xff; + + return id == NULL_NUL_ID + || c0 >= 0x20 && c0 <= 0x7e + && c1 >= 0x20 && c1 <= 0x7e + && c2 >= 0x20 && c2 <= 0x7e + && c3 >= 0x20 && c3 <= 0x7e; + } + + /** + * Returns whether the argument is a valid Local Chunk ID. + * + *

    Validation + * + *

  • Must be valid ID.
  • + *
  • Local Chunk IDs may not collide with GroupIDs. + *
  • Must not be a NULL_ID.
  • + * + * + * @param id Chunk ID to be checked. + * @return True when the chunk ID is a Local Chunk ID. + */ + public static boolean isLocalChunkID(int id) { + if (isGroupID(id)) { + return false; + } + return id != NULL_ID && isID(id); + } + private WeakHashMap ids; + + /** + * Convert an integer IFF identifier to String. + * + * @param anInt ID to be converted. + * @return String representation of the ID. + */ + public static String idToString(int anInt) { + byte[] bytes = new byte[4]; + + bytes[0] = (byte) (anInt >>> 24); + bytes[1] = (byte) (anInt >>> 16); + bytes[2] = (byte) (anInt >>> 8); + bytes[3] = (byte) (anInt >>> 0); + + try { + return new String(bytes, "ASCII"); + } catch (UnsupportedEncodingException e) { + throw new InternalError(e.getMessage()); + } + } + + /** + * Converts the first four letters of the + * String into an IFF Identifier. + * + * @param aString String to be converted. + * @return ID representation of the String. + */ + public static int stringToID(String aString) { + byte[] bytes = aString.getBytes(); + + return ((int) bytes[0]) << 24 + | ((int) bytes[1]) << 16 + | ((int) bytes[2]) << 8 + | ((int) bytes[3]) << 0; + } +} diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java new file mode 100644 index 000000000..7a13dec80 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFPrimitivesInputStream.java @@ -0,0 +1,265 @@ +/* + * @(#)RIFFPrimitivesInputStream.java 1.0 2005-01-15 + * + * Copyright (c) 2005 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.riff; + +import java.io.*; + +/** + * A RIFF primitives input stream lets an application read primitive data + * types in the Microsoft Resource Interfache File Format (RIFF) format from an + * underlying input stream. + * + * Reference: + * AVI RIFF File Reference + * http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/directx9_c/directx/htm/avirifffilereference.asp + * + * @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland + * @version 1.0 2005-01-15 Created. + */ +public class RIFFPrimitivesInputStream extends FilterInputStream { + private long scan, mark; + + /** + * Creates a new instance. + * + * @param in the input stream. + */ + public RIFFPrimitivesInputStream(InputStream in) { + super(in); + } + + /** + * Read 1 byte from the input stream and interpret + * them as an 8 Bit unsigned UBYTE value. + */ + public int readUBYTE() + throws IOException { + int b0 = in.read(); + + if (b0 == -1) { + throw new EOFException(); + } + + scan += 1; + return b0 & 0xff; + } + /** + * Read 2 bytes from the input stream and interpret + * them as a 16 Bit signed WORD value. + */ + public short readWORD() + throws IOException { + int b0 = in.read(); + int b1 = in.read(); + + if (b1 == -1) { + throw new EOFException(); + } + scan += 2; + + return (short) (((b0 & 0xff) << 0) | ((b1 & 0xff) << 8)); + } + + /** + * Read 2 bytes from the input stream and interpret + * them as a 16 Bit unsigned UWORD value. + */ + public int readUWORD() + throws IOException { + return readWORD() & 0xffff; + } + + /** + * Read 4 bytes from the input stream and interpret + * them as a 32 Bit signed LONG value. + */ + public int readLONG() + throws IOException { + int b0 = in.read(); + int b1 = in.read(); + int b2 = in.read(); + int b3 = in.read(); + + if (b3 == -1) { + throw new EOFException(); + } + scan += 4; + + return ((b0&0xff) << 0) + + ((b1&0xff) << 8) + + ((b2&0xff) << 16) + + ((b3&0xff) << 24); + } + + /** + * Read 4 bytes from the input stream and interpret + * them as a four byte character code. + * + * Cited from Referenced "AVI RIFF File Reference": + * "A FOURCC (four-character code) is a 32-bit unsigned integer created by + * concatenating four ASCII characters. For example, the FOURCC 'abcd' is + * represented on a Little-Endian system as 0x64636261. FOURCCs can contain + * space characters, so ' abc' is a valid FOURCC. The AVI file format uses + * FOURCC codes to identify stream types, data chunks, index entries, and + * other information." + */ + public int readFourCC() + throws IOException { + int b3 = in.read(); + int b2 = in.read(); + int b1 = in.read(); + int b0 = in.read(); + + if (b0 == -1) { + throw new EOFException(); + } + scan += 4; + + return ((b0&0xff) << 0) + + ((b1&0xff) << 8) + + ((b2&0xff) << 16) + + ((b3&0xff) << 24); + } + /** + * Read 4 bytes from the input stream and interpret + * them as a four byte character code. + * + * Cited from Referenced "AVI RIFF File Reference": + * "A FOURCC (four-character code) is a 32-bit unsigned integer created by + * concatenating four ASCII characters. For example, the FOURCC 'abcd' is + * represented on a Little-Endian system as 0x64636261. FOURCCs can contain + * space characters, so ' abc' is a valid FOURCC. The AVI file format uses + * FOURCC codes to identify stream types, data chunks, index entries, and + * other information." + */ + public String readFourCCString() + throws IOException { + byte[] buf = new byte[4]; + readFully(buf, 0, 4); + //scan += 4; <- scan is updated by method readFully + return new String(buf, "ASCII"); + } + + /** + * Read 4 Bytes from the input Stream and interpret + * them as an unsigned Integer value of type ULONG. + */ + public long readULONG() + throws IOException { + return (long)(readLONG()) & 0x00ffffffff; + } + + /** + * Align to an even byte position in the input stream. + * This will skip one byte in the stream if the current + * read position is not even. + */ + public void align() + throws IOException { + if (scan % 2 == 1) { + in.skip(1); + scan++; + } + } + + /** + * Get the current read position within the file (as seen + * by this input stream filter). + */ + public long getScan() + { return scan; } + + /** + * Reads one byte. + */ + public int read() + throws IOException { + int data = in.read(); + if (data != -1) scan++; + return data; + } + /** + * Reads a sequence of bytes. + */ + public int readFully(byte[] b,int offset, int length) + throws IOException { + int count = read(b, offset, length); + if (count != length) { + throw new EOFException("readFully for "+length+" bytes, unexpected EOF after "+count+" bytes."); + } + //scan += count; <- scan is already counted by read method + return count; + } + /** + * Reads a sequence of bytes. + */ + public int read(byte[] b,int offset, int length) + throws IOException { + int count = 0; + while (count < length) { + int result = in.read(b,offset+count,length-count); + if (result == -1) break; + count += result; + } + scan += count; + return count; + } + /** + * Marks the input stream. + * @param readlimit The maximum limit of bytes that can be read before + * the mark position becomes invalid. + */ + public void mark(int readlimit) { + in.mark(readlimit); + mark = scan; + } + /** + * Repositions the stream at the previously marked position. + * + * @exception IOException If the stream has not been marked or if the + * mark has been invalidated. + */ + public void reset() + throws IOException { + in.reset(); + scan = mark; + } + /** + * Skips over and discards n bytes of data from this input stream. This skip + * method tries to skip the provided number of bytes. + */ + public long skip(long n) + throws IOException { + long skipped = in.skip(n); + scan += skipped; + return skipped; + } + /** + * Skips over and discards n bytes of data from this input stream. Throws + * + * @param n the number of bytes to be skipped. + * @exception EOFException if this input stream reaches the end before + * skipping all the bytes. + */ + public void skipFully(long n) + throws IOException { + if (n==0) return; + + int total = 0; + int cur = 0; + + while ((total 0)) { + total += cur; + } + if (cur == 0) throw new EOFException(); + scan += total; + } +} \ No newline at end of file diff --git a/trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java new file mode 100644 index 000000000..e4eaeabd4 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/riff/RIFFVisitor.java @@ -0,0 +1,43 @@ +/* + * @(#)RIFFVIsitor.java 1.0 2005-01-09 + * + * Copyright (c) 2005 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.riff; + +import org.monte.media.AbortException; +import org.monte.media.ParseException; + +/** + * RIFFVIsitor is notified each time the RIFFParser visits + * a data chunk and when a group is entered or leaved. + * + * @version 1.0 2005-01-09 Created. + */ +public interface RIFFVisitor { + /** This method is invoked when the parser attempts to enter a group. + * The visitor can return false, if the parse shall skip the group contents. + * + * @param group + * @return True to enter the group, false to skip over the group. + */ + public boolean enteringGroup(RIFFChunk group); + + /** This method is invoked when the parser enters a group chunk.*/ + public void enterGroup(RIFFChunk group) + throws ParseException, AbortException; + + /** This method is invoked when the parser leaves a group chunk.*/ + public void leaveGroup(RIFFChunk group) + throws ParseException, AbortException; + + /** This method is invoked when the parser has read a data chunk or + * has skipped a stop chunk.*/ + public void visitChunk(RIFFChunk group, RIFFChunk chunk) + throws ParseException, AbortException; +} diff --git a/trunk/libsrc/avi/src/org/monte/media/util/Methods.java b/trunk/libsrc/avi/src/org/monte/media/util/Methods.java new file mode 100644 index 000000000..f5cb2bd57 --- /dev/null +++ b/trunk/libsrc/avi/src/org/monte/media/util/Methods.java @@ -0,0 +1,463 @@ +/* + * @(#)Methods.java + * + * Copyright (c) 2011 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.util; + +import java.lang.reflect.*; + +/** + * Methods contains convenience methods for method invocations using + * java.lang.reflect. + * + * @author Werner Randelshofer + * @version $Id: Methods.java 299 2013-01-03 07:40:18Z werner $ + */ + +@SuppressWarnings("unchecked") +public class Methods { + /** + * Prevent instance creation. + */ + private Methods() { + } + + /** + * Invokes the specified accessible parameterless method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not + * accessible. + */ + public static Object invoke(Object obj, String methodName) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified accessible method with a string parameter if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param stringParameter The String parameter + * @return The return value of the method or METHOD_NOT_FOUND. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invoke(Object obj, String methodName, String stringParameter) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { String.class }); + Object result = method.invoke(obj, new Object[] { stringParameter }); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + + /** + * Invokes the specified accessible parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @return The return value of the method or METHOD_NOT_FOUND. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(Class clazz, String methodName) + throws NoSuchMethodException { + try { + Method method = clazz.getMethod(methodName, new Class[0]); + Object result = method.invoke(null, new Object[0]); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified accessible parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(String clazz, String methodName) + throws NoSuchMethodException { + try { + return invokeStatic(Class.forName(clazz), methodName); + } catch (ClassNotFoundException e) { + throw new NoSuchMethodException("class "+clazz+" not found"); + } + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param type The parameter type. + * @param value The parameter value. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(Class clazz, String methodName, Class type, Object value) + throws NoSuchMethodException { + return invokeStatic(clazz,methodName,new Class[]{type},new Object[]{value}); + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param types The parameter types. + * @param values The parameter values. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(Class clazz, String methodName, Class[] types, Object[] values) + throws NoSuchMethodException { + try { + Method method = clazz.getMethod(methodName, types); + Object result = method.invoke(null, values); + return result; + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param types The parameter types. + * @param values The parameter values. + * @return The return value of the method. + * @return NoSuchMethodException if the method does not exist or is not accessible. + */ + public static Object invokeStatic(String clazz, String methodName, + Class[] types, Object[] values) + throws NoSuchMethodException { + try { + return invokeStatic(Class.forName(clazz), methodName, types, values); + } catch (ClassNotFoundException e) { + throw new NoSuchMethodException("class "+clazz+" not found"); + } + } + /** + * Invokes the specified parameterless method if it exists. + * + * @param clazz The class on which to invoke the method. + * @param methodName The name of the method. + * @param types The parameter types. + * @param values The parameter values. + * @param defaultValue The default value. + * @return The return value of the method or the default value if the method + * does not exist or is not accessible. + */ + public static Object invokeStatic(String clazz, String methodName, + Class[] types, Object[] values, Object defaultValue) { + try { + return invokeStatic(Class.forName(clazz), methodName, types, values); + } catch (ClassNotFoundException e) { + return defaultValue; + } catch (NoSuchMethodException e) { + return defaultValue; + } + } + + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static int invokeGetter(Object obj, String methodName, int defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return ((Integer) result).intValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static long invokeGetter(Object obj, String methodName, long defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return ((Long) result).longValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static boolean invokeGetter(Object obj, String methodName, boolean defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return ((Boolean) result).booleanValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static Object invokeGetter(Object obj, String methodName, Object defaultValue) { + try { + Method method = obj.getClass().getMethod(methodName, new Class[0]); + Object result = method.invoke(obj, new Object[0]); + return result; + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified getter method if it exists. + * + * @param clazz The object on which to invoke the method. + * @param methodName The name of the method. + * @param defaultValue This value is returned, if the method does not exist. + * @return The value returned by the getter method or the default value. + */ + public static boolean invokeStaticGetter(Class clazz, String methodName, boolean defaultValue) { + try { + Method method = clazz.getMethod(methodName, new Class[0]); + Object result = method.invoke(null, new Object[0]); + return ((Boolean) result).booleanValue(); + } catch (NoSuchMethodException e) { + return defaultValue; + } catch (IllegalAccessException e) { + return defaultValue; + } catch (InvocationTargetException e) { + return defaultValue; + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, boolean newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { Boolean.TYPE} ); + return method.invoke(obj, new Object[] { newValue}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, int newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { Integer.TYPE} ); + return method.invoke(obj, new Object[] { newValue}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, float newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { Float.TYPE} ); + return method.invoke(obj, new Object[] { new Float(newValue)}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, Class clazz, Object newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, new Class[] { clazz } ); + return method.invoke(obj, new Object[] { newValue}); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + throw new InternalError(e.getMessage()); + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static Object invoke(Object obj, String methodName, Class[] clazz, Object... newValue) + throws NoSuchMethodException { + try { + Method method = obj.getClass().getMethod(methodName, clazz ); + return method.invoke(obj, newValue); + } catch (IllegalAccessException e) { + throw new NoSuchMethodException(methodName+" is not accessible"); + } catch (InvocationTargetException e) { + // The method is not supposed to throw exceptions + InternalError error = new InternalError(e.getMessage()); + error.initCause((e.getCause() != null) ? e.getCause() : e); + throw error; + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName) { + try { + invoke(obj, methodName); + } catch (NoSuchMethodException e) { + // ignore + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName, float newValue) { + try { + invoke(obj, methodName, newValue); + } catch (NoSuchMethodException e) { + // ignore + } + } + /** + * Invokes the specified method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName, boolean newValue) { + try { + invoke(obj, methodName, newValue); + } catch (NoSuchMethodException e) { + // ignore + } + } + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExists(Object obj, String methodName, Class clazz, Object newValue) { + try { + invoke(obj, methodName, clazz, newValue); + } catch (NoSuchMethodException e) { + // ignore + } + } + + /** + * Invokes the specified setter method if it exists. + * + * @param obj The object on which to invoke the method. + * @param methodName The name of the method. + */ + public static void invokeIfExistsWithEnum(Object obj, String methodName, String enumClassName, String enumValueName) { + try { + Class enumClass = Class.forName(enumClassName); + Object enumValue = invokeStatic("java.lang.Enum", "valueOf", new Class[] {Class.class, String.class}, + new Object[] {enumClass, enumValueName} + ); + invoke(obj, methodName, enumClass, enumValue); + } catch (ClassNotFoundException e) { + // ignore + e.printStackTrace(); + } catch (NoSuchMethodException e) { + // ignore + e.printStackTrace(); + } + } +} diff --git a/trunk/libsrc/gif/build.xml b/trunk/libsrc/gif/build.xml new file mode 100644 index 000000000..ece3c2dfc --- /dev/null +++ b/trunk/libsrc/gif/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + Builds, tests, and runs the project gif. + + + diff --git a/trunk/libsrc/gif/nbproject/build-impl.xml b/trunk/libsrc/gif/nbproject/build-impl.xml new file mode 100644 index 000000000..9a4d2773d --- /dev/null +++ b/trunk/libsrc/gif/nbproject/build-impl.xml @@ -0,0 +1,1407 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/trunk/libsrc/gif/nbproject/genfiles.properties b/trunk/libsrc/gif/nbproject/genfiles.properties new file mode 100644 index 000000000..d8a93fa60 --- /dev/null +++ b/trunk/libsrc/gif/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=c07cf2d0 +build.xml.script.CRC32=d45952bc +build.xml.stylesheet.CRC32=8064a381@1.68.1.46 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=c07cf2d0 +nbproject/build-impl.xml.script.CRC32=cf654385 +nbproject/build-impl.xml.stylesheet.CRC32=5a01deb7@1.68.1.46 diff --git a/trunk/libsrc/gif/nbproject/project.properties b/trunk/libsrc/gif/nbproject/project.properties new file mode 100644 index 000000000..bb6838723 --- /dev/null +++ b/trunk/libsrc/gif/nbproject/project.properties @@ -0,0 +1,71 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/gif.jar +dist.javadoc.dir=${dist.dir}/javadoc +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.7 +javac.target=1.7 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=true +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/trunk/libsrc/gif/nbproject/project.xml b/trunk/libsrc/gif/nbproject/project.xml new file mode 100644 index 000000000..fb4075c8c --- /dev/null +++ b/trunk/libsrc/gif/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + gif + + + + + + + + + diff --git a/trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java b/trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java new file mode 100644 index 000000000..dda40623d --- /dev/null +++ b/trunk/libsrc/gif/src/net/kroo/elliot/GifSequenceWriter.java @@ -0,0 +1,191 @@ +package net.kroo.elliot; + +/* + + Created by Elliot Kroo on 2009-04-25. + + This work is licensed under the Creative Commons Attribution 3.0 Unported + License. To view a copy of this license, visit + http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative + Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. +*/ + +import javax.imageio.*; +import javax.imageio.metadata.*; +import javax.imageio.stream.*; +import java.awt.image.*; +import java.io.*; +import java.util.Iterator; + +public class GifSequenceWriter { + protected ImageWriter gifWriter; + protected ImageWriteParam imageWriteParam; + protected IIOMetadata imageMetaData; + + /** + * Creates a new GifSequenceWriter + * + * @param outputStream the ImageOutputStream to be written to + * @param imageType one of the imageTypes specified in BufferedImage + * @param timeBetweenFramesMS the time between frames in miliseconds + * @param loopContinuously wether the gif should loop repeatedly + * @throws IIOException if no gif ImageWriters are found + * + * @author Elliot Kroo (elliot[at]kroo[dot]net) + */ + public GifSequenceWriter( + ImageOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) throws IIOException, IOException { + // my method to create a writer + gifWriter = getWriter(); + imageWriteParam = gifWriter.getDefaultWriteParam(); + ImageTypeSpecifier imageTypeSpecifier = + ImageTypeSpecifier.createFromBufferedImageType(imageType); + + imageMetaData = + gifWriter.getDefaultImageMetadata(imageTypeSpecifier, + imageWriteParam); + + String metaFormatName = imageMetaData.getNativeMetadataFormatName(); + + IIOMetadataNode root = (IIOMetadataNode) + imageMetaData.getAsTree(metaFormatName); + + IIOMetadataNode graphicsControlExtensionNode = getNode( + root, + "GraphicControlExtension"); + + graphicsControlExtensionNode.setAttribute("disposalMethod", "restoreToBackgroundColor"); //JPEXS: changed none to restoreToBackgroundColor + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute( + "transparentColorFlag", + "FALSE"); + graphicsControlExtensionNode.setAttribute( + "delayTime", + Integer.toString(timeBetweenFramesMS / 10)); + graphicsControlExtensionNode.setAttribute( + "transparentColorIndex", + "0"); + + IIOMetadataNode commentsNode = getNode(root, "CommentExtensions"); + commentsNode.setAttribute("CommentExtension", "Created by MAH"); + + IIOMetadataNode appEntensionsNode = getNode( + root, + "ApplicationExtensions"); + + IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); + + child.setAttribute("applicationID", "NETSCAPE"); + child.setAttribute("authenticationCode", "2.0"); + + int loop = loopContinuously ? 0 : 1; + + child.setUserObject(new byte[]{ 0x1, (byte) (loop & 0xFF), (byte) + ((loop >> 8) & 0xFF)}); + appEntensionsNode.appendChild(child); + + imageMetaData.setFromTree(metaFormatName, root); + + gifWriter.setOutput(outputStream); + + gifWriter.prepareWriteSequence(null); + } + + public void writeToSequence(RenderedImage img) throws IOException { + gifWriter.writeToSequence( + new IIOImage( + img, + null, + imageMetaData), + imageWriteParam); + } + + /** + * Close this GifSequenceWriter object. This does not close the underlying + * stream, just finishes off the GIF. + */ + public void close() throws IOException { + gifWriter.endWriteSequence(); + } + + /** + * Returns the first available GIF ImageWriter using + * ImageIO.getImageWritersBySuffix("gif"). + * + * @return a GIF ImageWriter object + * @throws IIOException if no GIF image writers are returned + */ + private static ImageWriter getWriter() throws IIOException { + Iterator iter = ImageIO.getImageWritersBySuffix("gif"); + if(!iter.hasNext()) { + throw new IIOException("No GIF Image Writers Exist"); + } else { + return iter.next(); + } + } + + /** + * Returns an existing child node, or creates and returns a new child node (if + * the requested node does not exist). + * + * @param rootNode the IIOMetadataNode to search for the child node. + * @param nodeName the name of the child node. + * + * @return the child node, if found or a new node created with the given name. + */ + private static IIOMetadataNode getNode( + IIOMetadataNode rootNode, + String nodeName) { + int nNodes = rootNode.getLength(); + for (int i = 0; i < nNodes; i++) { + if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) + == 0) { + return((IIOMetadataNode) rootNode.item(i)); + } + } + IIOMetadataNode node = new IIOMetadataNode(nodeName); + rootNode.appendChild(node); + return(node); + } + + /** + public GifSequenceWriter( + BufferedOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) { + + */ + + public static void main(String[] args) throws Exception { + if (args.length > 1) { + // grab the output image type from the first image in the sequence + BufferedImage firstImage = ImageIO.read(new File(args[0])); + + // create a new BufferedOutputStream with the last argument + ImageOutputStream output = + new FileImageOutputStream(new File(args[args.length - 1])); + + // create a gif sequence with the type of the first image, 1 second + // between frames, which loops continuously + GifSequenceWriter writer = + new GifSequenceWriter(output, firstImage.getType(), 1, false); + + // write out the first image to our sequence... + writer.writeToSequence(firstImage); + for(int i=1; i src - lib/LZMA.jar;lib/jna-3.5.1.jar;lib/jpproxy.jar;lib/jsyntaxpane-0.9.5.jar;lib/trident-6.2.jar;lib/substance-flamingo-6.2.jar;lib/flamingo-6.2.jar;lib/substance-6.2.jar;lib/jl1.0.1.jar;lib/nellymoser.jar + lib/LZMA.jar;lib/jna-3.5.1.jar;lib/jpproxy.jar;lib/jsyntaxpane-0.9.5.jar;lib/trident-6.2.jar;lib/substance-flamingo-6.2.jar;lib/flamingo-6.2.jar;lib/substance-6.2.jar;lib/jl1.0.1.jar;lib/nellymoser.jar;lib/gif.jar;lib/avi.jar build javadoc reports diff --git a/trunk/src/com/jpexs/decompiler/flash/SWF.java b/trunk/src/com/jpexs/decompiler/flash/SWF.java index 9f5ff2bb8..9285d05ff 100644 --- a/trunk/src/com/jpexs/decompiler/flash/SWF.java +++ b/trunk/src/com/jpexs/decompiler/flash/SWF.java @@ -55,7 +55,9 @@ import com.jpexs.decompiler.flash.ecma.Null; import com.jpexs.decompiler.flash.exporters.ExportRectangle; import com.jpexs.decompiler.flash.exporters.Matrix; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; +import com.jpexs.decompiler.flash.exporters.modes.MorphshapeExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; import com.jpexs.decompiler.flash.exporters.modes.ShapeExportMode; @@ -78,6 +80,7 @@ import com.jpexs.decompiler.flash.tags.DoInitActionTag; import com.jpexs.decompiler.flash.tags.ExportAssetsTag; import com.jpexs.decompiler.flash.tags.FileAttributesTag; import com.jpexs.decompiler.flash.tags.JPEGTablesTag; +import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag; import com.jpexs.decompiler.flash.tags.ShowFrameTag; import com.jpexs.decompiler.flash.tags.SoundStreamBlockTag; import com.jpexs.decompiler.flash.tags.SymbolClassTag; @@ -112,6 +115,7 @@ import com.jpexs.decompiler.flash.treenodes.ContainerNode; import com.jpexs.decompiler.flash.treenodes.FrameNode; import com.jpexs.decompiler.flash.treenodes.TagNode; import com.jpexs.decompiler.flash.treenodes.TreeNode; +import com.jpexs.decompiler.flash.types.CXFORMWITHALPHA; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.MATRIX; import com.jpexs.decompiler.flash.types.RECT; @@ -171,6 +175,12 @@ import java.util.logging.Logger; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import javax.imageio.ImageIO; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import net.kroo.elliot.GifSequenceWriter; +import org.monte.media.VideoFormatKeys; +import org.monte.media.avi.AVIWriter; /** * Class representing SWF file @@ -1006,7 +1016,7 @@ public final class SWF implements TreeItem, Timelined { TreeNode addNode = null; if (t instanceof ShowFrameTag) { // do not add PlaceObjects (+etc) to script nodes - FrameNode tti = new FrameNode(new FrameNodeItem(t.getSwf(), frame, parent, false), null); + FrameNode tti = new FrameNode(new FrameNodeItem(t.getSwf(), frame, parent, false), null, true); for (int r = ret.size() - 1; r >= 0; r--) { if (!(ret.get(r).getItem() instanceof DefineSpriteTag)) { @@ -1291,13 +1301,134 @@ public final class SWF implements TreeItem, Timelined { fos.write(chunkBytes); } - /*private static void createWavFromAdpcm(OutputStream fos, int soundRateHz, boolean soundSize, boolean soundType, byte[] data) throws IOException { - createWavFromPcmData(fos, soundRateHz, soundSize, soundType, AdpcmDecoder.decode(data, soundType)); - } - - private static void createWavFromNelly(OutputStream fos, int soundRateHz,byte[] data) throws IOException { - createWavFromPcmData(fos, soundRateHz, true, false, NellyMoserDecoder.decode(data)); - }*/ + //TODO: implement morphshape export. How to handle 65536 frames? + public List exportMorphShapes(AbortRetryIgnoreHandler handler, String outdir, List tags, final MorphshapeExportMode mode) throws IOException { + List ret = new ArrayList<>(); + if (tags.isEmpty()) { + return ret; + } + File foutdir = new File(outdir); + if (!foutdir.exists()) { + if (!foutdir.mkdirs()) { + if (!foutdir.exists()) { + throw new IOException("Cannot create directory " + outdir); + } + } + } + throw new UnsupportedOperationException("Not implemented"); + //return ret; + } + + private static void makeAVI(List images, int frameRate, File file) throws IOException { + if (images.isEmpty()) { + return; + } + AVIWriter out = new AVIWriter(file); + out.addVideoTrack(VideoFormatKeys.ENCODING_AVI_PNG, 1, frameRate, images.get(0).getWidth(), images.get(0).getHeight(), 0, 0); + try { + for (BufferedImage img : images) { + out.write(0, img, 1); + } + } finally { + out.close(); + } + + } + + private static void makeGIF(List images, int frameRate, File file) throws IOException { + if (images.isEmpty()) { + return; + } + try (ImageOutputStream output = new FileImageOutputStream(file)) { + GifSequenceWriter writer = new GifSequenceWriter(output, images.get(0).getType(), 1000 / frameRate, true); + + for (BufferedImage img : images) { + writer.writeToSequence(img); + } + + writer.close(); + } + } + + public List exportFrames(AbortRetryIgnoreHandler handler, String outdir, int containerId, List frames, final FramesExportMode mode) throws IOException { + List ret = new ArrayList<>(); + if (tags.isEmpty()) { + return ret; + } + Timeline tim = null; + String path = ""; + if (containerId == 0) { + tim = getTimeline(); + } else { + tim = ((Timelined) characters.get(containerId)).getTimeline(); + path = File.separator + characters.get(containerId).getExportFileName(); + } + if (frames == null) { + int frameCnt = tim.frames.size(); + frames = new ArrayList<>(); + for (int i = 0; i < frameCnt; i++) { + frames.add(i); + } + } + + final File foutdir = new File(outdir + path); + if (!foutdir.exists()) { + if (!foutdir.mkdirs()) { + if (!foutdir.exists()) { + throw new IOException("Cannot create directory " + outdir); + } + } + } + + final List fframes = frames; + + Color backgroundColor = null; + if (mode == FramesExportMode.AVI) { + for (Tag t : tags) { + if (t instanceof SetBackgroundColorTag) { + SetBackgroundColorTag sb = (SetBackgroundColorTag) t; + backgroundColor = sb.backgroundColor.toColor(); + } + } + } + + final List frameImages = new ArrayList<>(); + for (int frame : frames) { + frameImages.add(frameToImageGet(tim, frame, 0, null, 0, tim.displayRect, new Matrix(), new ColorTransform(), backgroundColor).getBufferedImage()); + } + switch (mode) { + case GIF: + new RetryTask(new RunnableIOEx() { + @Override + public void run() throws IOException { + makeGIF(frameImages, frameRate, new File(foutdir + File.separator + "frames.gif")); + } + }, handler).run(); + break; + case PNG: + for (int i = 0; i < frameImages.size(); i++) { + final int fi = i; + new RetryTask(new RunnableIOEx() { + @Override + public void run() throws IOException { + ImageIO.write(frameImages.get(fi), "PNG", new File(foutdir + File.separator + fframes.get(fi) + ".png")); + } + }, handler).run(); + } + break; + case AVI: + new RetryTask(new RunnableIOEx() { + @Override + public void run() throws IOException { + makeAVI(frameImages, frameRate, new File(foutdir + File.separator + "frames.avi")); + } + }, handler).run(); + break; + } + + return ret; + } + public List exportSounds(AbortRetryIgnoreHandler handler, String outdir, List tags, final SoundExportMode mode) throws IOException { List ret = new ArrayList<>(); if (tags.isEmpty()) { @@ -1518,7 +1649,7 @@ public final class SWF implements TreeItem, Timelined { exportTexts(handler, outdir, tags, mode); } - public static List exportShapes(AbortRetryIgnoreHandler handler, String outdir, List tags, ShapeExportMode mode) throws IOException { + public static List exportShapes(AbortRetryIgnoreHandler handler, String outdir, List tags, final ShapeExportMode mode) throws IOException { List ret = new ArrayList<>(); if (tags.isEmpty()) { return ret; @@ -1538,13 +1669,35 @@ public final class SWF implements TreeItem, Timelined { if (t instanceof CharacterTag) { characterID = ((CharacterTag) t).getCharacterId(); } - final File file = new File(outdir + File.separator + characterID + ".svg"); + String ext = "svg"; + if (mode == ShapeExportMode.PNG) { + ext = "png"; + } + + final File file = new File(outdir + File.separator + characterID + "." + ext); new RetryTask(new RunnableIOEx() { @Override public void run() throws IOException { - try (FileOutputStream fos = new FileOutputStream(file)) { - fos.write(Utf8Helper.getBytes(((ShapeTag) t).toSVG())); + ShapeTag st = (ShapeTag) t; + switch (mode) { + case SVG: + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(Utf8Helper.getBytes(st.toSVG())); + } + break; + case PNG: + RECT rect = st.getRect(); + int newWidth = (int) (rect.getWidth() / SWF.unitDivisor); + int newHeight = (int) (rect.getHeight() / SWF.unitDivisor); + SerializableImage img = new SerializableImage(newWidth, newHeight, SerializableImage.TYPE_INT_ARGB); + img.fillTransparent(); + Matrix m = new Matrix(); + m.translate(-rect.Xmin, -rect.Ymin); + st.toImage(0, 0, 0, null, 0, img, m, new CXFORMWITHALPHA()); + ImageIO.write(img.getBufferedImage(), "PNG", new FileOutputStream(file)); + break; } + } }, handler).run(); ret.add(file); @@ -2194,7 +2347,7 @@ public final class SWF implements TreeItem, Timelined { return ret; } - public static SerializableImage frameToImageGet(Timeline timeline, int frame, int time, DepthState stateUnderCursor, int mouseButton, RECT displayRect, Matrix transformation, ColorTransform colorTransform) { + public static SerializableImage frameToImageGet(Timeline timeline, int frame, int time, DepthState stateUnderCursor, int mouseButton, RECT displayRect, Matrix transformation, ColorTransform colorTransform, Color backGroundColor) { String key = "frame_" + frame + "_" + timeline.id + "_" + timeline.swf.hashCode(); SerializableImage image = getFromCache(key); if (image != null) { @@ -2208,7 +2361,14 @@ public final class SWF implements TreeItem, Timelined { RECT rect = displayRect; image = new SerializableImage((int) (rect.getWidth() / SWF.unitDivisor) + 1, (int) (rect.getHeight() / SWF.unitDivisor) + 1, SerializableImage.TYPE_INT_ARGB); - image.fillTransparent(); + if (backGroundColor == null) { + image.fillTransparent(); + } else { + Graphics2D g = (Graphics2D) image.getBufferedImage().getGraphics(); + g.setComposite(AlphaComposite.Src); + g.setColor(backGroundColor); + g.fill(new Rectangle(image.getWidth(), image.getHeight())); + } Matrix m = new Matrix(); m.translate(-rect.Xmin, -rect.Ymin); frameToImage(timeline, frame, time, stateUnderCursor, mouseButton, image, m, colorTransform); diff --git a/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index 2a67c1ec4..b163075ac 100644 --- a/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/trunk/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -27,6 +27,7 @@ import com.jpexs.decompiler.flash.abc.RenameType; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.configuration.ConfigurationItem; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; @@ -97,6 +98,7 @@ public class CommandLineArgumentParser { System.out.println(" image - Images (Default format: PNG/JPEG)"); System.out.println(" shape - Shapes (Default format: SVG)"); System.out.println(" movie - Movies (Default format: FLV without sound)"); + System.out.println(" frame - Frames (Default format: PNG)"); System.out.println(" sound - Sounds (Default format: MP3/WAV/FLV only sound)"); System.out.println(" binaryData - Binary data (Default format: Raw data)"); System.out.println(" text - Texts (Default format: Formatted text)"); @@ -116,6 +118,11 @@ public class CommandLineArgumentParser { System.out.println(" script:pcode - ActionScript P-code"); System.out.println(" script:pcodehex - ActionScript P-code with hex"); System.out.println(" script:hex - ActionScript Hex only"); + System.out.println(" shape:svg - SVG format for Shapes"); + System.out.println(" shape:png - PNG format for Shapes"); + System.out.println(" frame:png - PNG format for Frames"); + System.out.println(" frame:gif - GIF format for Frames"); + System.out.println(" frame:avi - AVI format for Frames"); System.out.println(" image:png_jpeg - PNG/JPEG format for Images"); System.out.println(" image:png - PNG format for Images"); System.out.println(" image:jpeg - JPEG format for Images"); @@ -532,6 +539,7 @@ public class CommandLineArgumentParser { "binarydata", "text", "all", + "frame", "fla", "xfl" }; @@ -629,25 +637,11 @@ public class CommandLineArgumentParser { break; case "image": System.out.println("Exporting images..."); - String imageFormat = formats.get("image"); - if (imageFormat == null) { - imageFormat = "png_jpeg"; - } - ImageExportMode iem = ImageExportMode.PNG_JPEG; - switch(imageFormat){ - case "png": - iem = ImageExportMode.PNG; - break; - case "jpeg": - iem = ImageExportMode.JPEG; - break; - } - - exfile.exportImages(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "images" : ""), iem); + exfile.exportImages(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "images" : ""), enumFromStr(formats.get("image"), ImageExportMode.class)); break; case "shape": System.out.println("Exporting shapes..."); - exfile.exportShapes(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "shapes" : ""), ShapeExportMode.SVG); + exfile.exportShapes(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "shapes" : ""), enumFromStr(formats.get("shape"), ShapeExportMode.class)); break; case "script": case "as": @@ -655,60 +649,37 @@ public class CommandLineArgumentParser { case "pcodehex": case "hex": System.out.println("Exporting scripts..."); - ScriptExportMode exportMode = ScriptExportMode.AS; - if (!exportFormat.equals("script")) { - exportMode = strToExportFormat(exportFormat); - } else if (formats.containsKey("script")) { - exportMode = strToExportFormat(formats.get("script")); - } boolean parallel = Configuration.parallelSpeedUp.get(); if (as3classes.isEmpty()) { as3classes = parseSelectClasses(args); } if (!as3classes.isEmpty()) { for (String as3class : as3classes) { - exportOK = exportOK && exfile.exportAS3Class(as3class, outDir.getAbsolutePath(), exportMode, parallel); + exportOK = exportOK && exfile.exportAS3Class(as3class, outDir.getAbsolutePath(), enumFromStr(formats.get("script"), ScriptExportMode.class), parallel); } } else { - exportOK = exportOK && exfile.exportActionScript(handler, outDir.getAbsolutePath(), exportMode, parallel) != null; + exportOK = exportOK && exfile.exportActionScript(handler, outDir.getAbsolutePath(), enumFromStr(formats.get("script"), ScriptExportMode.class), parallel) != null; } break; case "movie": System.out.println("Exporting movies..."); - exfile.exportMovies(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "movies" : ""), MovieExportMode.FLV); + exfile.exportMovies(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "movies" : ""), enumFromStr(formats.get("movie"), MovieExportMode.class)); + break; + case "frame": + System.out.println("Exporting frames..."); + exfile.exportFrames(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "frames" : ""), 0, null, enumFromStr(formats.get("frame"), FramesExportMode.class)); break; case "sound": System.out.println("Exporting sounds..."); - String soundFormat = formats.get("sound"); - if (soundFormat == null) { - soundFormat = "mp3_wav_flv"; - } - SoundExportMode sem=SoundExportMode.MP3_WAV_FLV; - switch(soundFormat){ - case "mp3_wav": - sem = SoundExportMode.MP3_WAV; - break; - case "wav": - sem = SoundExportMode.WAV; - break; - case "flv": - sem = SoundExportMode.FLV; - break; - - } - exfile.exportSounds(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "sounds" : ""), sem); + exfile.exportSounds(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "sounds" : ""), enumFromStr(formats.get("sound"), SoundExportMode.class)); break; case "binarydata": System.out.println("Exporting binaryData..."); - exfile.exportBinaryData(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "binaryData" : ""), BinaryDataExportMode.RAW); + exfile.exportBinaryData(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "binaryData" : ""), enumFromStr(formats.get("binarydata"), BinaryDataExportMode.class)); break; case "text": System.out.println("Exporting texts..."); - String textFormat = formats.get("text"); - if (textFormat == null) { - textFormat = "formatted"; - } - exfile.exportTexts(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "texts" : ""), textFormat.equals("formatted") ? TextExportMode.FORMATTED : TextExportMode.PLAIN); + exfile.exportTexts(handler, outDir.getAbsolutePath() + (exportFormats.length > 1 ? File.separator + "texts" : ""), enumFromStr(formats.get("text"), TextExportMode.class)); break; case "textplain": System.out.println("Exporting texts..."); @@ -961,4 +932,14 @@ public class CommandLineArgumentParser { } System.exit(0); } + + private static E enumFromStr(String str, Class cls) { + E[] vals = cls.getEnumConstants(); + for (E e : vals) { + if (e.toString().toLowerCase().equals(str.toLowerCase())) { + return e; + } + } + return vals[0]; + } } diff --git a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java new file mode 100644 index 000000000..66b2dd76e --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/FramesExportMode.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.exporters.modes; + +/** + * + * @author JPEXS + */ +public enum FramesExportMode { + + PNG, + GIF, + AVI +} diff --git a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java new file mode 100644 index 000000000..b83bdd378 --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/MorphshapeExportMode.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.exporters.modes; + +/** + * + * @author JPEXS + */ +public enum MorphshapeExportMode { + + //TODO: implement morphshape export + PNG, + GIF, + AVI +} diff --git a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java index aeb16b6d4..50002fb90 100644 --- a/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java +++ b/trunk/src/com/jpexs/decompiler/flash/exporters/modes/ShapeExportMode.java @@ -22,5 +22,6 @@ package com.jpexs.decompiler.flash.exporters.modes; */ public enum ShapeExportMode { - SVG + SVG, + PNG } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java b/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java index 2a7757f28..c6f64e359 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/ExportDialog.java @@ -19,6 +19,7 @@ package com.jpexs.decompiler.flash.gui; import com.jpexs.decompiler.flash.abc.ScriptPack; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; @@ -31,6 +32,7 @@ import com.jpexs.decompiler.flash.tags.base.ImageTag; import com.jpexs.decompiler.flash.tags.base.ShapeTag; import com.jpexs.decompiler.flash.tags.base.SoundTag; import com.jpexs.decompiler.flash.tags.base.TextTag; +import com.jpexs.decompiler.flash.treeitems.FrameNodeItem; import com.jpexs.decompiler.flash.treenodes.TreeNode; import java.awt.BorderLayout; import java.awt.Container; @@ -62,7 +64,9 @@ public class ExportDialog extends AppDialog { "movies", "sounds", "scripts", - "binaryData" + "binaryData", + "frames" + //,"morphshapes" }; //Display options only when these classes found @@ -73,7 +77,9 @@ public class ExportDialog extends AppDialog { {DefineVideoStreamTag.class}, {SoundTag.class}, {TreeNode.class, ScriptPack.class}, - {DefineBinaryDataTag.class} + {DefineBinaryDataTag.class}, + {FrameNodeItem.class} + //,{MorphShapeTag.class} }; //Enum classes for values @@ -84,7 +90,9 @@ public class ExportDialog extends AppDialog { MovieExportMode.class, SoundExportMode.class, ScriptExportMode.class, - BinaryDataExportMode.class + BinaryDataExportMode.class, + FramesExportMode.class + //MorphshapeExportMode.class }; private final JComboBox[] combos; diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java b/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java index 9b8736045..f15c27ee7 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/ImagePanel.java @@ -308,7 +308,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis if (stateUnderCursor != null) { ButtonTag b = (ButtonTag) swf.characters.get(stateUnderCursor.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar2 != 0) { //OverUpToOverDown + if (sounds != null && sounds.buttonSoundChar2 != 0) { //OverUpToOverDown playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar2)); } } @@ -322,7 +322,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis if (stateUnderCursor != null) { ButtonTag b = (ButtonTag) swf.characters.get(stateUnderCursor.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar3 != 0) { //OverDownToOverUp + if (sounds != null && sounds.buttonSoundChar3 != 0) { //OverDownToOverUp playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar3)); } } @@ -339,7 +339,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis //New mouse entered ButtonTag b = (ButtonTag) swf.characters.get(stateUnderCursor.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar1 != 0) { //IddleToOverUp + if (sounds != null && sounds.buttonSoundChar1 != 0) { //IddleToOverUp playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar1)); } } @@ -349,7 +349,7 @@ public final class ImagePanel extends JPanel implements ActionListener, MediaDis //Old mouse leave ButtonTag b = (ButtonTag) swf.characters.get(lastUnderCur.characterId); DefineButtonSoundTag sounds = b.getSounds(); - if (sounds!=null && sounds.buttonSoundChar0 != 0) { //OverUpToIddle + if (sounds != null && sounds.buttonSoundChar0 != 0) { //OverUpToIddle playSound((SoundTag) swf.characters.get(sounds.buttonSoundChar0)); } } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 0e9165986..19172234d 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -30,6 +30,7 @@ import com.jpexs.decompiler.flash.action.Action; import com.jpexs.decompiler.flash.action.parser.pcode.ASMParser; import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.exporters.modes.BinaryDataExportMode; +import com.jpexs.decompiler.flash.exporters.modes.FramesExportMode; import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; import com.jpexs.decompiler.flash.exporters.modes.MovieExportMode; import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; @@ -169,6 +170,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.concurrent.CancellationException; @@ -1443,29 +1446,7 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec private SearchDialog searchDialog; public boolean hasExportableNodes() { - List sel = getAllSelected(tagTree); - - for (TreeNode d : sel) { - if (d instanceof ContainerNode) { - ContainerNode n = (ContainerNode) d; - TreeNodeType nodeType = TagTree.getTreeNodeType(n.getItem()); - if (nodeType == TreeNodeType.IMAGE - || nodeType == TreeNodeType.SHAPE - || nodeType == TreeNodeType.AS - || nodeType == TreeNodeType.MOVIE - || nodeType == TreeNodeType.SOUND - || nodeType == TreeNodeType.BINARY_DATA - || nodeType == TreeNodeType.TEXT) { - return true; - } - } - if (d instanceof TreeElement) { - if (((TreeElement) d).isLeaf()) { - return true; - } - } - } - return false; + return !getSelection(getCurrentSwf()).isEmpty(); } private List getSelection(SWF swf) { @@ -1500,6 +1481,12 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec ret.add((Tag) n.getItem()); } } + if (d instanceof FrameNode) { + FrameNode fn = (FrameNode) d; + if (!fn.scriptsNode) { + ret.add(d.getItem()); + } + } if (d instanceof TreeElement) { if (((TreeElement) d).isLeaf()) { TreeElement treeElement = (TreeElement) d; @@ -1525,6 +1512,7 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec List texts = new ArrayList<>(); List as12scripts = new ArrayList<>(); List binaryData = new ArrayList<>(); + Map> frames = new HashMap<>(); for (TreeNode d : sel) { if (d.getItem().getSwf() != swf) { @@ -1555,6 +1543,23 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec texts.add((Tag) n.getItem()); } } + if (d instanceof FrameNode) { + FrameNode fn = (FrameNode) d; + if (!fn.scriptsNode) { + + FrameNodeItem fni = (FrameNodeItem) d.getItem(); + Tag par = fni.getParent(); + int frame = fni.getFrame(); + int parentId = 0; + if (par != null) { + parentId = ((CharacterTag) par).getCharacterId(); + } + if (!frames.containsKey(parentId)) { + frames.put(parentId, new ArrayList()); + } + frames.get(parentId).add(frame); + } + } if (d instanceof TreeElement) { if (((TreeElement) d).isLeaf()) { TreeElement treeElement = (TreeElement) d; @@ -1563,16 +1568,6 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec } } - List allnodes = new ArrayList<>(); - allnodes.addAll(as3scripts); - allnodes.addAll(as12scripts); - allnodes.addAll(images); - allnodes.addAll(shapes); - allnodes.addAll(movies); - allnodes.addAll(sounds); - allnodes.addAll(texts); - allnodes.addAll(binaryData); - if (selFile == null) { selFile = selectExportDir(); if (selFile == null) { @@ -1587,6 +1582,9 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec ret.addAll(swf.exportMovies(handler, selFile + File.separator + "movies", movies, export.getValue(MovieExportMode.class))); ret.addAll(swf.exportSounds(handler, selFile + File.separator + "sounds", sounds, export.getValue(SoundExportMode.class))); ret.addAll(SWF.exportBinaryData(handler, selFile + File.separator + "binaryData", binaryData, export.getValue(BinaryDataExportMode.class))); + for (Entry> entry : frames.entrySet()) { + ret.addAll(swf.exportFrames(handler, selFile + File.separator + "frames", entry.getKey(), entry.getValue(), export.getValue(FramesExportMode.class))); + } List abcList = swf.abcList; if (abcPanel != null) { for (int i = 0; i < as3scripts.size(); i++) { @@ -1991,6 +1989,12 @@ public final class MainPanel extends JPanel implements ActionListener, TreeSelec swf.exportMovies(errorHandler, selFile + File.separator + "movies", export.getValue(MovieExportMode.class)); swf.exportSounds(errorHandler, selFile + File.separator + "sounds", export.getValue(SoundExportMode.class)); swf.exportBinaryData(errorHandler, selFile + File.separator + "binaryData", export.getValue(BinaryDataExportMode.class)); + swf.exportFrames(errorHandler, selFile + File.separator + "frames", 0, null, export.getValue(FramesExportMode.class)); + for (CharacterTag c : swf.characters.values()) { + if (c instanceof DefineSpriteTag) { + swf.exportFrames(errorHandler, selFile + File.separator + "frames", c.getCharacterId(), null, export.getValue(FramesExportMode.class)); + } + } swf.exportActionScript(errorHandler, selFile, exportMode, Configuration.parallelSpeedUp.get()); } } catch (Exception ex) { diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java b/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java index be0112652..e3cdc1b49 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/PreviewImage.java @@ -165,7 +165,7 @@ public class PreviewImage extends JPanel { } else if (treeItem instanceof FrameNodeItem) { FrameNodeItem fn = (FrameNodeItem) treeItem; RECT rect = swf.displayRect; - imgSrc = SWF.frameToImageGet(swf.getTimeline(), fn.getFrame() - 1, 0, null, 0, rect, Matrix.getScaleInstance(1 / SWF.unitDivisor), new ColorTransform()); + imgSrc = SWF.frameToImageGet(swf.getTimeline(), fn.getFrame() - 1, 0, null, 0, rect, Matrix.getScaleInstance(1 / SWF.unitDivisor), new ColorTransform(), null); width = (imgSrc.getWidth()); height = (imgSrc.getHeight()); } else if (treeItem instanceof ImageTag) { diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java b/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java index 6815f8cd2..213e463de 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/TagTreeModel.java @@ -122,7 +122,7 @@ public class TagTreeModel implements TreeModel { switch (ttype) { case SHOW_FRAME: ShowFrameTag showFrameTag = (ShowFrameTag) t; - frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags)); + frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags, false)); break; case SHAPE: shapes.add(new TagNode(t)); @@ -277,11 +277,13 @@ public class TagTreeModel implements TreeModel { switch (ttype) { case SHOW_FRAME: ShowFrameTag showFrameTag = (ShowFrameTag) t; - frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags)); + frames.add(new FrameNode(new FrameNodeItem(t.getSwf(), ++frameCnt, parent, true), showFrameTag.innerTags, false)); break; default: if (!actionScriptTags.contains(t) && !ShowFrameTag.isNestedTagType(t.getId())) { - others.add(new TagNode(t)); + if (!(t instanceof SoundStreamHeadTypeTag)) { + others.add(new TagNode(t)); + } } break; } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties index a20f64366..a65ba6bbe 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties @@ -14,6 +14,7 @@ # along with this program. If not, see . shapes = Shapes shapes.svg = SVG +shapes.png = PNG texts = Texts texts.plain = Plain text @@ -46,3 +47,11 @@ dialog.title = Export... button.ok = OK button.cancel = Cancel + +morphshapes = Morphshapes +morphshapes.gif = GIF + +frames = Frames +frames.png = PNG +frames.gif = GIF +frames.avi = AVI diff --git a/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java b/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java index 838d832ae..cd46bdcbe 100644 --- a/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java +++ b/trunk/src/com/jpexs/decompiler/flash/tags/ShowFrameTag.java @@ -42,8 +42,8 @@ public class ShowFrameTag extends Tag { add(StartSound2Tag.ID); add(VideoFrameTag.ID); add(SoundStreamBlockTag.ID); - add(SoundStreamHeadTag.ID); - add(SoundStreamHead2Tag.ID); + /*add(SoundStreamHeadTag.ID); + add(SoundStreamHead2Tag.ID);*/ } }; public List innerTags; diff --git a/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java b/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java index 94ebe6241..d6ac8a90c 100644 --- a/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java +++ b/trunk/src/com/jpexs/decompiler/flash/treenodes/FrameNode.java @@ -26,8 +26,11 @@ import java.util.List; */ public class FrameNode extends TreeNode { - public FrameNode(FrameNodeItem item, List innerTags) { + public boolean scriptsNode = false; + + public FrameNode(FrameNodeItem item, List innerTags, boolean scriptsNode) { super(item); + this.scriptsNode = scriptsNode; if (innerTags != null) { for (Tag tag : innerTags) { subNodes.add(new TagNode(tag)); diff --git a/trunk/src/com/jpexs/helpers/SerializableImage.java b/trunk/src/com/jpexs/helpers/SerializableImage.java index 56de36420..e01c713eb 100644 --- a/trunk/src/com/jpexs/helpers/SerializableImage.java +++ b/trunk/src/com/jpexs/helpers/SerializableImage.java @@ -143,9 +143,9 @@ public class SerializableImage implements Serializable { } private void writeObject(ObjectOutputStream out) throws IOException { - try{ + try { ImageIO.write(image, "png", out); - }catch(Exception ex){ + } catch (Exception ex) { //ignore } }