trunk contents moved to root

This commit is contained in:
Jindra Petřík
2014-05-10 20:50:57 +02:00
parent 1b851e66a8
commit 199a4d0c2b
2296 changed files with 0 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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) {
ArrayList<Format>of=new ArrayList<Format>(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+'}';
}
}

View File

@@ -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);
}
}

View File

@@ -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<Integer> SampleSizeInBitsKey = new FormatKey<Integer>("sampleSizeInBits", Integer.class);
/**
* The numer of ChannelsKey.
*/
public final static FormatKey<Integer> ChannelsKey = new FormatKey<Integer>("channels", Integer.class);
/**
* The size of a frame.
*/
public final static FormatKey<Integer> FrameSizeKey = new FormatKey<Integer>("frameSize", Integer.class);
/**
* The compressor name.
*/
public final static FormatKey<ByteOrder> ByteOrderKey = new FormatKey<ByteOrder>("byteOrder", ByteOrder.class);
/**
* The number of frames per second.
*/
public final static FormatKey<Rational> SampleRateKey = new FormatKey<Rational>("sampleRate", Rational.class);
/**
* Whether values are signed.
*/
public final static FormatKey<Boolean> SignedKey = new FormatKey<Boolean>("signed", Boolean.class);
/**
* Whether silence is encoded as -128 instead of 0.
*/
public final static FormatKey<Boolean> SilenceBugKey = new FormatKey<Boolean>("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);
}
}

View File

@@ -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<BufferFlag> 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 &gt;= 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.
* <p>
* 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<BufferFlag> flags) {
if (flags == null) {
this.flags = EnumSet.noneOf(BufferFlag.class);
} else {
this.flags = EnumSet.copyOf(flags);
}
}
public void clearFlags() {
flags.clear();
}
}

View File

@@ -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.
* <p>
* 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.
* <p>
* 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;
}

View File

@@ -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.
* <p>
* 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();
}

View File

@@ -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}.
* <p>
* 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<String, LinkedList<RegistryEntry>> codecMap;
private HashMap<String, LinkedList<RegistryEntry>> readerMap;
private HashMap<String, LinkedList<RegistryEntry>> writerMap;
private HashMap<String, Format> 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<String, LinkedList<RegistryEntry>>();
readerMap = new HashMap<String, LinkedList<RegistryEntry>>();
writerMap = new HashMap<String, LinkedList<RegistryEntry>>();
fileFormatMap = new HashMap<String, Format>();
// 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<RegistryEntry> list = codecMap.get(key);
if (list == null) {
list = new LinkedList<RegistryEntry>();
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<RegistryEntry> list = readerMap.get(key);
if (list == null) {
list = new LinkedList<RegistryEntry>();
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<RegistryEntry> list = writerMap.get(key);
if (list == null) {
list = new LinkedList<RegistryEntry>();
writerMap.put(key, list);
}
list.add(entry);
}
@Override
public String[] getCodecClasses(Format inputFormat, Format outputFormat) {
HashSet<String> classNames = new HashSet<String>();
HashSet<RegistryEntry> entries = new HashSet<RegistryEntry>();
if (inputFormat != null) {
LinkedList<RegistryEntry> re;
if (inputFormat.get(EncodingKey) == null) {
re = new LinkedList<RegistryEntry>();
for (Map.Entry<String, LinkedList<RegistryEntry>> 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<RegistryEntry> re;
if (outputFormat.get(EncodingKey) == null) {
re = new LinkedList<RegistryEntry>();
for (Map.Entry<String, LinkedList<RegistryEntry>> 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<RegistryEntry> 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<RegistryEntry> 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<String, Format> 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<String, LinkedList<RegistryEntry>> i:codecMap.entrySet()) {
LinkedList<RegistryEntry> ll=i.getValue();
for (Iterator<RegistryEntry> j=ll.iterator();j.hasNext();) {
RegistryEntry e=j.next();
if (e.className.equals(codecClass)) {
j.remove();
}
}
}
}
}

View File

@@ -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<FormatKey, Object> properties;
/**
* Creates a new format onlyWith the specified properties.
*/
public Format(Map<FormatKey, Object> properties) {
this(properties, true);
}
/**
* Creates a new format onlyWith the specified properties.
*/
private Format(Map<FormatKey, Object> properties, boolean copy) {
if (copy || ! (properties instanceof HashMap)) {
for (Map.Entry<FormatKey, Object> 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> T get(FormatKey<T> key) {
return (T) properties.get(key);
}
@SuppressWarnings("unchecked")
public <T> T get(FormatKey<T> 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<FormatKey, Object> getProperties() {
return Collections.unmodifiableMap(properties);
}
/**
* Gets the keys of the format as an unmodifiable set.
*/
public Set<FormatKey> 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<FormatKey, Object> 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<FormatKey, Object> 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. <p> If a property is specified in
* both formats, then the property value from this format is used. It
* overwrites that format. <p> 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<FormatKey, Object> m = new HashMap<FormatKey, Object>(this.properties);
for (Map.Entry<FormatKey, Object> 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. <p> If a property is specified in both
* formats, then the property value from this format is used. It overwrites
* that format. <p> 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<FormatKey, Object> m = new HashMap<FormatKey, Object>(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.
* <p> If a property is specified in both formats, then the property value
* from this format is used. It overwrites that format.
* <p> 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<FormatKey, Object> m = new HashMap<FormatKey, Object>(that.properties);
for (Map.Entry<FormatKey, Object> 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.
* <p> If a property is specified in both formats, then the property value
* from this format is used. It overwrites that format.
* <p> 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<FormatKey, Object> m = 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());
}
m.put(key, p[i + 1]);
}
for (Map.Entry<FormatKey, Object> 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). <p> If
* the keys are reduced, then the new format is less specific than this
* format.
*/
public Format intersectKeys(FormatKey... keys) {
HashMap<FormatKey, Object> m = new HashMap<FormatKey, Object>();
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. <p> 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<FormatKey, Object> m = new HashMap<FormatKey, Object>(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<FormatKey, Object> m = new HashMap<FormatKey, Object>(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<FormatKey, Object> 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);
}
}
}
}
}

View File

@@ -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 <em>FormatKey</em> provides type-safe access to an attribute of
* a {@link Format}.
* <p>
* 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<T> 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<T> 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<T> 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<T> 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<T> 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);
}
}

View File

@@ -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<MediaType> MediaTypeKey = new FormatKey<MediaType>("mediaType", MediaType.class);
/**
* The EncodingKey.
*/
public final static FormatKey<String> EncodingKey = new FormatKey<String>("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<String> MimeTypeKey = new FormatKey<String>("mimeType", String.class);
/**
* The number of frames per second.
*/
public final static FormatKey<Rational> FrameRateKey = new FormatKey<Rational>("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<Integer> KeyFrameIntervalKey = new FormatKey<Integer>("keyFrameInterval", Integer.class);
}

View File

@@ -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.
*
* <p>
* 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 &gt;= 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;
}

View File

@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* This limit is imposed by data structures of the movie file
* which will overflow if more samples are added to the movie.
* <p>
* 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);
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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<Codec> codecs = new ArrayList<Codec>(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<Format> suggestOutputFormats(Format inputMediaFormat, Format outputFileFormat) {
ArrayList<Format> formats = new ArrayList<Format>();
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);
}

View File

@@ -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<Integer> WidthKey = new FormatKey<Integer>("dimX","width", Integer.class);
/** The HeightKey of a video frame. */
public final static FormatKey<Integer> HeightKey = new FormatKey<Integer>("dimY","height", Integer.class);
/** The number of bits per pixel. */
public final static FormatKey<Integer> DepthKey = new FormatKey<Integer>("dimZ","depth", Integer.class);
/** The data class. */
public final static FormatKey<Class> DataClassKey = new FormatKey<Class>("dataClass", Class.class);
/** The compressor name. */
public final static FormatKey<String> CompressorNameKey = new FormatKey<String>("compressorName", "compressorName",String.class, true);
/** The pixel aspect ratio WidthKey : HeightKey;
*/
public final static FormatKey<Rational> PixelAspectRatioKey = new FormatKey<Rational>("pixelAspectRatio", Rational.class);
/** Whether the frame rate must be fixed. False means variable frame rate. */
public final static FormatKey<Boolean> FixedFrameRateKey = new FormatKey<Boolean>("fixedFrameRate", Boolean.class);
/** Whether the video is interlaced. */
public final static FormatKey<Boolean> InterlaceKey = new FormatKey<Boolean>("interlace", Boolean.class);
/** Encoding quality. Value between 0 and 1. */
public final static FormatKey<Float> QualityKey = new FormatKey<Float>("quality", Float.class);
}

View File

@@ -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.
* <p>
* Source:
* Microsoft Windows Bitmap Format.
* Multimedia Technical Note: JPEG DIB Format.
* (c) 1993 Microsoft Corporation. All rights reserved.
* <a href="http://www.fileformat.info/format/bmp/spec/b7c72ebab8064da48ae5ed0c053c67a4/BMPDIB.TXT">BMPDIB.txt</a>
*
* @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<InputStream> v = new Vector<InputStream>();
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<InputStream> v = new Vector<InputStream>();
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<InputStream> v = new Vector<InputStream>();
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()));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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. <p> 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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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.
* <p>
* The DIB codec only works with the AVI file format. Other file formats, such
* as QuickTime, use a different encoding for uncompressed video.
* <p>
* This codec currently only supports encoding from a {@code BufferedImage} into
* the file format. Decoding support may be added in the future.
* <p>
* This codec does not encode the color palette of an image. This must be done
* separately.
* <p>
* 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.
* <p>
* Supported input formats:
* <ul>
* {@code Format} with {@code BufferedImage.class}, any width, any height,
* depth=4.
* </ul>
* Supported output formats:
* <ul>
* {@code Format} with {@code byte[].class}, same width and height as input
* format, depth=4.
* </ul>
*
* @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<h;y++) {
System.arraycopy(in,i,out,j,w);
i+=w;
j-=scanlineStride;
}
}
public void readKey24(int[] in, int offset, int length, BufferedImage img) {
DataBufferInt buf = (DataBufferInt) 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;
int[] out=buf.getData();
for (int y=0;y<h;y++) {
System.arraycopy(in,i,out,j,w);
i+=w;
j-=scanlineStride;
}
}
/** Encodes a 4-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 writeKey4(OutputStream out, byte[] pixels, int width, int height, int offset, int scanlineStride)
throws IOException {
byte[] bytes = new byte[width];
for (int y = (height - 1) * scanlineStride; 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);
}
}
}

View File

@@ -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.
*
* <p>
* References:<br>
* <a href="http://paulbourke.net/texture_colour/colourramp/">http://paulbourke.net/texture_colour/colourramp/</a>
*
* @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));
}
}

View File

@@ -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.
* <p>
* 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 <code>buf[0]</code>
* through <code>buf[count-1]</code> are the
* only bytes that can ever be read from the
* stream; element <code>buf[streamPos]</code> 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 <code>buf</code>.
* It is one greater than the position of
* the last byte within <code>buf</code> 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 <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned.
* <p>
* This <code>read</code> method
* cannot block.
*
* @return the next byte of data, or <code>-1</code> 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 <code>len</code> bytes of data into an array of bytes
* from this input stream.
* If <code>streamPos</code> equals <code>count</code>,
* then <code>-1</code> is returned to indicate
* end of file. Otherwise, the number <code>k</code>
* of bytes read is equal to the smaller of
* <code>len</code> and <code>count-streamPos</code>.
* If <code>k</code> is positive, then bytes
* <code>buf[streamPos]</code> through <code>buf[streamPos+k-1]</code>
* are copied into <code>b[off]</code> through
* <code>b[off+k-1]</code> in the manner performed
* by <code>System.arraycopy</code>. The
* value <code>k</code> is added into <code>streamPos</code>
* and <code>k</code> is returned.
* <p>
* This <code>read</code> method cannot block.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
*/
@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 <code>n</code> bytes of input from this input stream. Fewer
* bytes might be skipped if the end of the input stream is reached.
* The actual number <code>k</code>
* of bytes to be skipped is equal to the smaller
* of <code>n</code> and <code>count-streamPos</code>.
* The value <code>k</code> is added into <code>streamPos</code>
* and <code>k</code> 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.
* <p>
* The value returned is <code>count&nbsp;- streamPos</code>,
* 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 <tt>ByteArrayInputStream</tt> has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an <tt>IOException</tt>.
* <p>
*/
@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;
}
}

View File

@@ -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()}.
* <p>
* 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.
* <br>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 <code>buf[0]</code>
* through <code>buf[count-1]</code> are the
* only bytes that can ever be read from the
* stream; element <code>buf[streamPos]</code> 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 <code>buf</code>.
* It is one greater than the position of
* the last byte within <code>buf</code> 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 <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned.
* <p>
* This <code>read</code> method
* cannot block.
*
* @return the next byte of data, or <code>-1</code> 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 <code>len</code> bytes of data into an array of bytes
* from this input stream.
* If <code>streamPos</code> equals <code>count</code>,
* then <code>-1</code> is returned to indicate
* end of file. Otherwise, the number <code>k</code>
* of bytes read is equal to the smaller of
* <code>len</code> and <code>count-streamPos</code>.
* If <code>k</code> is positive, then bytes
* <code>buf[streamPos]</code> through <code>buf[streamPos+k-1]</code>
* are copied into <code>b[off]</code> through
* <code>b[off+k-1]</code> in the manner performed
* by <code>System.arraycopy</code>. The
* value <code>k</code> is added into <code>streamPos</code>
* and <code>k</code> is returned.
* <p>
* This <code>read</code> method cannot block.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
*/
@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 <code>n</code> bytes of input from this input stream. Fewer
* bytes might be skipped if the end of the input stream is reached.
* The actual number <code>k</code>
* of bytes to be skipped is equal to the smaller
* of <code>n</code> and <code>count-streamPos</code>.
* The value <code>k</code> is added into <code>streamPos</code>
* and <code>k</code> 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.
* <p>
* The value returned is <code>count&nbsp;- streamPos</code>,
* 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 <tt>ByteArrayInputStream</tt> has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an <tt>IOException</tt>.
* <p>
*/
@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 <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> 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 <code>count</code> 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;
}
}

View File

@@ -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 <code>int</code> in the range
* <code>0</code> to <code>255</code>. If no byte is available
* because the end of the stream has been reached, the value
* <code>-1</code> is returned. This method blocks until input data
* is available, the end of the stream is detected, or an exception
* is thrown.
* <p>
* This method
* simply performs <code>in.read()</code> and returns the result.
*
* @return the next byte of data, or <code>-1</code> 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 <code>len</code> bytes of data from this input stream
* into an array of bytes. If <code>len</code> is not zero, the method
* blocks until some input is available; otherwise, no
* bytes are read and <code>0</code> is returned.
* <p>
* This method simply performs <code>in.read(b, off, len)</code>
* and returns the result.
*
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @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}
* <p>
* This method simply performs <code>in.skip(n)</code>.
*/
@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.
* <p>
* 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 <code>in.close()</code>.
*
* @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 <code>reset</code> method repositions this stream at
* the last marked position so that subsequent reads re-read the same bytes.
* <p>
* The <code>readlimit</code> argument tells this input stream to
* allow that many bytes to be read before the mark position gets
* invalidated.
* <p>
* This method simply performs <code>in.mark(readlimit)</code>.
*
* @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
* <code>mark</code> method was last called on this input stream.
* <p>
* This method
* simply performs <code>in.reset()</code>.
* <p>
* 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 <code>mark</code>
* and <code>reset</code> methods.
* This method
* simply performs <code>in.markSupported()</code>.
*
* @return <code>true</code> if this stream type supports the
* <code>mark</code> and <code>reset</code> method;
* <code>false</code> otherwise.
* @see java.io.FilterInputStream#in
* @see java.io.InputStream#mark(int)
* @see java.io.InputStream#reset()
*/
@Override
public boolean markSupported() {
return true;
}
}

View File

@@ -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.
* <p>
* 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));
}
}
}

View File

@@ -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 <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> 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 <code>count</code> 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.
*
* <p> An <code>IndexOutOfBoundsException</code> will be thrown if
* <code>pos</code> is smaller than the flushed position (as
* returned by <code>getflushedPosition</code>).
*
* <p> It is legal to seek past the end of the file; an
* <code>EOFException</code> will be thrown only if a read is
* performed.
*
* @param pos a <code>long</code> containing the desired file
* pointer position.
*
* @exception IndexOutOfBoundsException if <code>pos</code> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.
* <p>
* Supported input formats:
* <ul>
* {@code VideoFormat} with {@code BufferedImage.class}, any width, any height,
* any depth.
* </ul>
* Supported output formats:
* <ul>
* {@code VideoFormat} with {@code byte[].class}, same width and height as input
* format, depth=24.
* </ul>
*
* @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;
}
}
}

View File

@@ -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.
* <p>.
* This class can read Motion JPEG files with omitted Huffmann table.
* <p>
* For more information see:
* Microsoft Windows Bitmap Format.
* Multimedia Technical Note: JPEG DIB Format.
* (c) 1993 Microsoft Corporation. All rights reserved.
* <a href="http://www.fileformat.info/format/bmp/spec/b7c72ebab8064da48ae5ed0c053c67a4/BMPDIB.TXT">BMPDIB.txt</a>
*
* @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<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
readHeader();
LinkedList<ImageTypeSpecifier> l = new LinkedList<ImageTypeSpecifier>();
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);
}
}
}

View File

@@ -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.
* <p>
* The reader described by this class can read Motion JPEG files with omitted
* Huffmann table.
* <p>
* For more information see:
* Microsoft Windows Bitmap Format.
* Multimedia Technical Note: JPEG DIB Format.
* (c) 1993 Microsoft Corporation. All rights reserved.
* <a href="http://www.fileformat.info/format/bmp/spec/b7c72ebab8064da48ae5ed0c053c67a4/BMPDIB.TXT">BMPDIB.txt</a>
*
* @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";
}
}

View File

@@ -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
* <tt>abs(a)</tt> and <tt>abs(b)</tt>. Returns 0 if
* <tt>a==0 &amp;&amp; b==0</tt>.
*
* @param a value with with the GCD is to be computed.
* @param b value with with the GCD is to be computed.
* @return <tt>GCD(a, b)</tt>
*/
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
* <tt>abs(a)</tt> and <tt>abs(b)</tt>. Returns 0 if
* <tt>a==0 &amp;&amp; b==0</tt>.
*
* @param a value with with the GCD is to be computed.
* @param b value with with the GCD is to be computed.
* @return <tt>GCD(a, b)</tt>
*/
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
* <tt>abs(a)</tt> and <tt>abs(b)</tt>. Returns 0 if
* <tt>a==0 &amp;&amp; b==0</tt>.
*
* @param a value with with the GCD is to be computed.
* @param b value with with the GCD is to be computed.
* @return <tt>GCD(a, b)</tt>
*/
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
* <tt>abs(a)</tt> and <tt>abs(b)</tt>. Returns 0 if
* <tt>a==0 || b==0</tt>.
*
* @param a value with with the SCM is to be computed.
* @param b value with with the SCM is to be computed.
* @return <tt>SCM(a, b)</tt>
*/
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
* <tt>abs(a)</tt> and <tt>abs(b)</tt>. Returns 0 if
* <tt>a==0 || b==0</tt>.
*
* @param a value with with the SCM is to be computed.
* @param b value with with the SCM is to be computed.
* @return <tt>SCM(a, b)</tt>
*/
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
* <tt>abs(a)</tt> and <tt>abs(b)</tt>. Returns 0 if
* <tt>a==0 || b==0</tt>.
*
* @param a value with with the SCM is to be computed.
* @param b value with with the SCM is to be computed.
* @return <tt>SCM(a, b)</tt>
*/
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<<i;
int b = reverseBits(a, 3);
System.out.println(a+" - "+b);
}
}
}

View File

@@ -0,0 +1,492 @@
/*
* @(#)Rational.java
*
* Copyright (c) 2009-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.math;
import static java.lang.Math.*;
import java.math.BigInteger;
import static org.monte.media.math.IntMath.*;
/**
* Represents a TIFF RATIONAL number. <p> Two LONGs 32-bit (4-byte) unsigned
* integer: the first represents the numerator of a fraction; the second, the
* denominator. </p> <p> Invariants: </p> <ul> <li>denominator>=0, the
* denominator is always a positive integer</li> <li>0/1 is the unique
* representation of 0.</li> <li>1/0,-1/0 are the unique representations of
* infinity.</li> </ul>
*
* @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. <p>Reference:<br> <a
* href="http://www2.fz-juelich.de/video/cpp/html/exercises/exercise/Rational_cpp.html">
* http://www2.fz-juelich.de/video/cpp/html/exercises/exercise/Rational_cpp.html</a>
* </p>
*/
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: <li>As a long
* number</li> <li>As a double number</li> <li>As an integer/integer
* rational number</li>
*
* @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));
}
}
}

View File

@@ -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..
* <p>
* Supported input formats:
* <ul>
* {@code VideoFormat} with {@code BufferedImage.class}, any width, any height,
* any depth.
* </ul>
* Supported output formats:
* <ul>
* {@code VideoFormat} with {@code byte[].class}, same width and height as input
* format, depth=24.
* </ul>
*
* @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;
}
}
}

View File

@@ -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<RIFFChunk,RIFFChunk> propertyChunks;
private ArrayList<RIFFChunk> 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<RIFFChunk,RIFFChunk>(propGroup.propertyChunks);
}
if (propGroup.collectionChunks != null) {
collectionChunks = new ArrayList<RIFFChunk>(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<RIFFChunk,RIFFChunk> ();
}
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<RIFFChunk,RIFFChunk> ();
}
return propertyChunks.keys();
}
public void addCollectionChunk(RIFFChunk chunk) {
if (collectionChunks == null) {
collectionChunks = new ArrayList<RIFFChunk>();
}
collectionChunks.add(chunk);
}
public RIFFChunk[] getCollectionChunks(int id) {
if (collectionChunks == null) {
return new RIFFChunk[0];
}
Iterator<RIFFChunk> 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<RIFFChunk>();
}
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())+"}";
}
}

View File

@@ -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.
*
* <p><b>Abstract</b>
* <p>
* <b>RIFF File Format</b>
* A RIFF file consists of a RIFF header followed by zero or more lists and chunks.
* <p>
* The RIFF header has the following form:
* <pre>
* 'RIFF' fileSize fileType (data)
* </pre>
* 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.
* <p>
* <b>FOURCCs</b><br>
* 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.
* <p>
* A chunk has the following form:
* <pre>
* ckID ckSize ckData
* </pre>
* 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.
* <p>
* A list has the following form:
* <pre>
* 'LIST' listSize listType listData
* </pre>
* 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.
* <p>
* 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
* <p>
* <p><b>Grammar for RIFF streams used by this parser</b>
* <pre>
* RIFFFile ::= 'RIFF' FormGroup
* <br>
* GroupChunk ::= FormGroup | ListGroup
* FormGroup ::= size GroupType [ ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] }
* ListGroup ::= size GroupType [ ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] }
* <br>
* LocalChunk ::= DataChunk | CollectionChunk | PropertyChunk
* DataChunk ::= size [ struct ]
* CollectionChunk ::= size [ struct ]
* PropertyChunk ::= size [ struct ]
* <br>
* size ::= ULONG
* GroupType ::= FourCC
* ChunkID ::= FourCC
* pad ::= (BYTE)0
* struct ::= any C language struct built with primitive data types.
* </pre>
*
* <p><b>Examples</b>
*
* <p><b>Traversing the raw structure of a RIFF file</b>
* <p>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.
*
* <pre>
* 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); }
* . }
* . }
* </pre>
*
* <p><b>Traversing the RIFF file and interpreting its content.</b>
* <p>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.
* <p>
* 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.
* <br>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.
*
* <pre>
* 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); }
* . }
* . }
* </pre>
*
* @see RIFFVisitor
*
* @author Werner Randelshofer, Hausmatt 10, CH-6405 Goldau, Switzerland
* @version 1.5 2012-08-03 Adds offset property.
* <br>1.4 2011-08-26 Adds HashMap for stop chunks.
* <br>1.3 2011-01-23 Use HashMap instead of Hashtable.
* <br>1.2 2010-07-06 Use integer IDs for efficiency. Added support for
* stop chunks.
* <br>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<RIFFChunk> dataChunks;
/** List of property chunks the visitor is interested in. */
private HashSet<RIFFChunk> propertyChunks;
/** List of collection chunks the visitor is interested in. */
private HashSet<RIFFChunk> collectionChunks;
/** List of stop chunks the visitor is interested in. */
private HashSet<Integer> stopChunkTypes;
/** List of group chunks the visitor is interested in. */
private HashSet<RIFFChunk> 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.
*
* <p>Pre condition
* <li> Data-, property- and collection chunks must have been
* declared prior to this call.
* <li> When the client never declared chunks, then all local
* chunks will be interpreted as data chunks.
* <li> The stream must be positioned at the beginning of the
* RIFFFileExpression.
*
* <p>Post condition
* <li> When no exception was thrown then the stream is positioned
* after the RIFFFile expression.
*
* <p>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.
*
* <pre>
* RIFF = 'RIFF' FormGroup
* </pre>
*/
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.
* <pre>
* FormGroup ::= size GroupType { ChunkID LocalChunk [pad]
* | 'FORM' FormGroup [pad] }
* | 'LIST' ListGroup [pad] }
* </pre>
*/
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.
* <pre>
* ListGroup ::= size GroupType { ChunkID LocalChunk [pad] | 'LIST ' ListGroup [pad] }
* </pre>
*/
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.
* <pre>
* LocalChunk ::= size { DataChunk | PropertyChunk | CollectionChunk }
* DataChunk = PropertyChunk = CollectionChunk ::= { byte }...*size
* </pre>
*/
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.
* <pre>
* LocalChunk ::= size { DataChunk | PropertyChunk | CollectionChunk }
* DataChunk = PropertyChunk = CollectionChunk ::= { byte }...*size
* </pre>
*/
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.
*
* <p>Pre condition
* <li> Data chunks must have been declared before the
* interpretation has been started.
* <li> 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.
*
* <p>Pre condition
* <li> 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.
*
* <p>Pre condition
* <li> Property chunks must have been declared before the
* interpretation has been started.
* <li> 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.
*
* <p>Pre condition
* <li> Collection chunks must have been declared before the
* interpretation has been started.
* <li> 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.
*
* <p>Pre condition
* <li> The chunk must not have already been declared as of a
* different type.
* <li> Declarations may not be done during interpretation
* of an RIFFFileExpression.
*
* <p>Post condition
* <li> 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<RIFFChunk>();
}
dataChunks.add(chunk);
}
/**
* Declares a FORM group chunk.
*
* <p>Pre condition
* <li> The chunk must not have already been declared as of a
* different type.
* <li> Declarations may not be done during interpretation
* of an RIFFFileExpression.
*
* <p>Post condition
* <li> 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<RIFFChunk>();
}
groupChunks.add(chunk);
}
/**
* Declares a property chunk.
*
* <p>Pre condition
* <li> The chunk must not have already been declared as of a
* different type.
* <li> Declarations may not be done during interpretation
* of an RIFFFileExpression.
*
* <p>Post condition
* <li> 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<RIFFChunk>();
}
propertyChunks.add(chunk);
}
/**
* Declares a collection chunk.
*
* <p>Pre condition
* <li> The chunk must not have already been declared as of a
* different type.
* <li> Declarations may not be done during interpretation
* of an RIFFFileExpression.
*
* <p>Post condition
* <li> 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<RIFFChunk>();
}
collectionChunks.add(chunk);
}
/**
* Declares a stop chunk.
*
* <p>Pre condition
* <li> The chunk must not have already been declared as of a
* different type.
* <li> Declarations may not be done during interpretation
* of an RIFFFileExpression.
*
* <p>Post condition
* <li> 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<Integer>();
}
stopChunkTypes.add(type);
}
/** Whether the parse should stop at all chunks.
* <p>
* The parser does not read the data body of stop chunks.
* <p>
* 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.
*
* <p>Validation
* <ul>
* <li> Group ID must be one of RIFF_ID, LIST_ID.</li>
* </ul>
*
* @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.
*
* <p>Validation
* <ul>
* <li> Must be a valid ID.</li>
* <li> Must not be a group ID.</li>
* <li> Must not be a NULL_ID.</li>
* </ul>
*
* @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.
*
* <p>Validation
* <li> Every byte of an ID must be in the range of 0x20..0x7e
* <li> 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.
*
* <p>Validation
* <ud>
* <li> Must be valid ID.</li>
* <li> Local Chunk IDs may not collide with GroupIDs.</id>
* <li> Must not be a NULL_ID.</li>
* </ud>
*
* @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<String, String> 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;
}
}

View File

@@ -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<n) && ((cur = (int) in.skip(n-total)) > 0)) {
total += cur;
}
if (cur == 0) throw new EOFException();
scan += total;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}
}