change folder structure and control files to newest concept

This commit is contained in:
Daniel Stock
2018-09-20 16:38:35 +02:00
parent 224fd33794
commit e25c30f942
2747 changed files with 21 additions and 80 deletions
@@ -0,0 +1,42 @@
/*
* @(#)AbstractTransferable.java 1.0 22. August 2007
*
* Copyright (c) 2007 Werner Randelshofer, Immensee, 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 ch.randelshofer.gui.datatransfer;
import java.awt.datatransfer.*;
/**
* Base class for transferable objects.
*
* @author Werner Randelshofer
* @version 1.0 22. August 2007 Created.
*/
public abstract class AbstractTransferable implements Transferable {
private DataFlavor[] flavors;
/** Creates a new instance. */
public AbstractTransferable(DataFlavor[] flavors) {
this.flavors = flavors;
}
public DataFlavor[] getTransferDataFlavors() {
return flavors.clone();
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
for (DataFlavor f : flavors) {
if (f.equals(flavor)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,75 @@
/*
* @(#)CompositeTransferable.java 1.0 2002-04-07
*
* Copyright (c) 2001 Werner Randelshofer, Immensee, 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 ch.randelshofer.gui.datatransfer;
import java.awt.datatransfer.*;
import java.util.*;
import java.io.*;
/**
*
*
* @author Werner Randelshofer
*/
public class CompositeTransferable implements java.awt.datatransfer.Transferable {
private HashMap<DataFlavor, Transferable> transferables = new HashMap<DataFlavor, Transferable>();
private LinkedList<DataFlavor> flavors = new LinkedList<DataFlavor>();
/** Creates a new instance of CompositeTransferable */
public CompositeTransferable() {
}
public void add(Transferable t) {
DataFlavor[] f = t.getTransferDataFlavors();
for (int i=0; i < f.length; i++) {
if (! transferables.containsKey(f[i])) flavors.add(f[i]);
transferables.put(f[i], t);
}
}
/**
* Returns an object which represents the data to be transferred. The class
* of the object returned is defined by the representation class of the flavor.
*
* @param flavor the requested flavor for the data
* @see DataFlavor#getRepresentationClass
* @exception IOException if the data is no longer available
* in the requested flavor.
* @exception UnsupportedFlavorException if the requested data flavor is
* not supported.
*/
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
Transferable t = transferables.get(flavor);
if (t == null) throw new UnsupportedFlavorException(flavor);
return t.getTransferData(flavor);
}
/**
* Returns an array of DataFlavor objects indicating the flavors the data
* can be provided in. The array should be ordered according to preference
* for providing the data (from most richly descriptive to least descriptive).
* @return an array of data flavors in which this data can be transferred
*/
public DataFlavor[] getTransferDataFlavors() {
return flavors.toArray(new DataFlavor[transferables.size()]);
}
/**
* Returns whether or not the specified data flavor is supported for
* this object.
* @param flavor the requested flavor for the data
* @return boolean indicating wjether or not the data flavor is supported
*/
public boolean isDataFlavorSupported(DataFlavor flavor) {
return transferables.containsKey(flavor);
}
}
@@ -0,0 +1,363 @@
/*
* @(#)FileTextFieldTransferHandler.java 1.2 2010-10-02
*
* Copyright (c) 2007-2010 Werner Randelshofer
* Staldenmattweg 2, CH-6405 Immensee, Switzerland
* All rights reserved.
*
* This software is the confidential and proprietary information of
* Werner Randelshofer. ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Werner Randelshofer.
*/
package ch.randelshofer.gui.datatransfer;
import java.awt.datatransfer.*;
import java.awt.im.InputContext;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;
/**
* The FileTextFieldTransferHandler can be used to add drag and drop
* support for JTextFields, which contain the path to a file.
*
* @author Werner Randelshofer
* @version 1.2 2010-10-03 Adds support for file filter.
* <br>1.1 2008-12-03 Added file selection mode.
* <br>1.0 September 8, 2007 Created.
*/
public class FileTextFieldTransferHandler extends TransferHandler {
private boolean shouldRemove;
private JTextComponent exportComp;
private int p0;
private int p1;
private int fileSelectionMode;
private FileFilter fileFilter;
/** Creates a new instance. */
public FileTextFieldTransferHandler() {
this(JFileChooser.FILES_ONLY);
}
/** Creates a new instance.
* @param fileSelectionMode JFileChooser file selection mode.
*/
public FileTextFieldTransferHandler(int fileSelectionMode) {
this(fileSelectionMode, null);
}
/** Creates a new instance.
* @param fileSelectionMode JFileChooser file selection mode.
*/
public FileTextFieldTransferHandler(int fileSelectionMode, FileFilter filter) {
this.fileFilter = filter;
if (fileSelectionMode != JFileChooser.FILES_AND_DIRECTORIES
&& fileSelectionMode != JFileChooser.FILES_ONLY
&& fileSelectionMode != JFileChooser.DIRECTORIES_ONLY) {
throw new IllegalArgumentException("illegal file selection mode:" + fileSelectionMode);
}
this.fileSelectionMode = fileSelectionMode;
}
@Override
public boolean importData(JComponent comp, Transferable t) {
JTextComponent c = (JTextComponent) comp;
// if we are importing to the same component that we exported from
// then don't actually do anything if the drop location is inside
// the drag location and set shouldRemove to false so that exportDone
// knows not to remove any data
if (c == exportComp && c.getCaretPosition() >= p0 && c.getCaretPosition() <= p1) {
shouldRemove = false;
return true;
}
boolean imported = false;
if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
InputContext ic = c.getInputContext();
if (ic != null) {
ic.endComposition();
}
try {
List<?> list = (List<?>)
t.getTransferData(DataFlavor.javaFileListFlavor);
if (list.size() > 0) {
File file = (File) list.get(0);
switch (fileSelectionMode) {
case JFileChooser.FILES_AND_DIRECTORIES:
break;
case JFileChooser.FILES_ONLY:
if (file.isDirectory()) {
return false;
}
break;
case JFileChooser.DIRECTORIES_ONLY:
if (!file.isDirectory()) {
return false;
}
break;
}
if (fileFilter != null && !fileFilter.accept(file)) {
return false;
}
c.setText(file.getPath());
}
imported = true;
} catch (UnsupportedFlavorException | IOException ex) {
// ex.printStackTrace();
}
}
if (!imported) {
DataFlavor importFlavor = getImportFlavor(t.getTransferDataFlavors(), c);
if (importFlavor != null) {
InputContext ic = c.getInputContext();
if (ic != null) {
ic.endComposition();
}
try {
// String text = (String) t.getTransferData(DataFlavor.stringFlavor);
Reader r = importFlavor.getReaderForText(t);
boolean useRead = false;
handleReaderImport(r, c, useRead);
imported = true;
} catch (UnsupportedFlavorException | BadLocationException | IOException ex) {
// ex.printStackTrace();
}
}
}
return imported;
}
@Override
protected Transferable createTransferable(JComponent comp) {
CompositeTransferable t;
JTextComponent c = (JTextComponent) comp;
shouldRemove = true;
p0 = c.getSelectionStart();
p1 = c.getSelectionEnd();
if (p0 != p1) {
t = new CompositeTransferable();
String text = c.getSelectedText();
//LinkedList fileList = new LinkedList();
//fileList.add(new File(text));
//t.add(new JVMLocalObjectTransferable(DataFlavor.javaFileListFlavor, fileList));
t.add(new StringTransferable(text));
t.add(new PlainTextTransferable(text));
} else {
t = null;
}
return t;
}
@Override
public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
JTextComponent c = (JTextComponent) comp;
if (!(c.isEditable() && c.isEnabled())) {
return false;
}
for (DataFlavor flavor : transferFlavors) {
if (flavor.isFlavorJavaFileListType()
|| flavor.isFlavorTextType()) {
return true;
}
}
return false;
}
/**
* Try to find a flavor that can be used to import a Transferable.
* The set of usable flavors are tried in the following order:
* <ol>
* <li>First, an attempt to find a text/plain flavor is made.
* <li>Second, an attempt to find a flavor representing a String reference
* in the same VM is made.
* <li>Lastly, DataFlavor.stringFlavor is searched for.
* </ol>
*/
protected DataFlavor getImportFlavor(DataFlavor[] flavors, JTextComponent c) {
// DataFlavor plainFlavor = null;
DataFlavor refFlavor = null;
DataFlavor stringFlavor = null;
for (int i = 0; i < flavors.length; i++) {
String mime = flavors[i].getMimeType();
if (mime.startsWith("text/plain")) {
return flavors[i];
} else if (refFlavor == null && mime.startsWith("application/x-java-jvm-local-objectref") && flavors[i].getRepresentationClass() == java.lang.String.class) {
refFlavor = flavors[i];
} else if (stringFlavor == null && flavors[i].equals(DataFlavor.stringFlavor)) {
stringFlavor = flavors[i];
}
}
if (refFlavor != null) {
return refFlavor;
} else if (stringFlavor != null) {
return stringFlavor;
}
return null;
}
/**
* Import the given stream data into the text component.
*/
protected void handleReaderImport(Reader in, JTextComponent c, boolean useRead)
throws BadLocationException, IOException {
if (useRead) {
int startPosition = c.getSelectionStart();
int endPosition = c.getSelectionEnd();
int length = endPosition - startPosition;
EditorKit kit = c.getUI().getEditorKit(c);
Document doc = c.getDocument();
if (length > 0) {
doc.remove(startPosition, length);
}
kit.read(in, doc, startPosition);
} else {
char[] buff = new char[1024];
int nch;
boolean lastWasCR = false;
int last;
StringBuilder sb = null;
// Read in a block at a time, mapping \r\n to \n, as well as single
// \r to \n.
while ((nch = in.read(buff, 0, buff.length)) != -1) {
if (sb == null) {
sb = new StringBuilder(nch);
}
last = 0;
for (int counter = 0; counter < nch; counter++) {
switch (buff[counter]) {
case '\r':
if (lastWasCR) {
if (counter == 0) {
sb.append('\n');
} else {
buff[counter - 1] = '\n';
}
} else {
lastWasCR = true;
}
break;
case '\n':
if (lastWasCR) {
if (counter > (last + 1)) {
sb.append(buff, last, counter - last - 1);
}
// else nothing to do, can skip \r, next write will
// write \n
lastWasCR = false;
last = counter;
}
break;
default:
if (lastWasCR) {
if (counter == 0) {
sb.append('\n');
} else {
buff[counter - 1] = '\n';
}
lastWasCR = false;
}
break;
}
}
if (last < nch) {
if (lastWasCR) {
if (last < (nch - 1)) {
sb.append(buff, last, nch - last - 1);
}
} else {
sb.append(buff, last, nch - last);
}
}
}
if (lastWasCR) {
sb.append('\n');
}
System.out.println("FileTextTransferHandler " + c.getSelectionStart() + ".." + c.getSelectionEnd());
c.replaceSelection(sb != null ? sb.toString() : "");
}
}
// --- TransferHandler methods ------------------------------------
/**
* This is the type of transfer actions supported by the source. Some models are
* not mutable, so a transfer operation of COPY only should
* be advertised in that case.
*
* @param comp The component holding the data to be transfered. This
* argument is provided to enable sharing of TransferHandlers by
* multiple components.
* @return This is implemented to return NONE if the component is a JPasswordField
* since exporting data via user gestures is not allowed. If the text component is
* editable, COPY_OR_MOVE is returned, otherwise just COPY is allowed.
*/
@Override
public int getSourceActions(JComponent comp) {
JTextComponent c = (JTextComponent) comp;
if (c instanceof JPasswordField
&& c.getClientProperty("JPasswordField.cutCopyAllowed")
!= Boolean.TRUE) {
return NONE;
}
return c.isEditable() ? COPY_OR_MOVE : COPY;
}
/**
* This method is called after data has been exported. This method should remove
* the data that was transfered if the action was MOVE.
*
* @param comp The component that was the source of the data.
* @param data The data that was transferred or possibly null
* if the action is <code>NONE</code>.
* @param action The actual action that was performed.
*/
@Override
protected void exportDone(JComponent comp, Transferable data, int action) {
JTextComponent c = (JTextComponent) comp;
// only remove the text if shouldRemove has not been set to
// false by importData and only if the action is a move
if (shouldRemove && action == MOVE) {
try {
Document doc = c.getDocument();
doc.remove(p0, p1 - p0);
} catch (BadLocationException e) {
}
}
exportComp = null;
}
/**
* @return the fileFilter
*/
public FileFilter getFileFilter() {
return fileFilter;
}
/**
* @param fileFilter the fileFilter to set
*/
public void setFileFilter(FileFilter fileFilter) {
this.fileFilter = fileFilter;
}
}
@@ -0,0 +1,72 @@
/*
* @(#)PlainTextTransferable.java 1.1 2009-09-01
*
* Copyright (c) 2007-2009 Werner Randelshofer, Immensee, 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 ch.randelshofer.gui.datatransfer;
import java.awt.datatransfer.*;
import java.io.*;
/**
* PlainTextTransferable.
* <p>
* Note: This transferable should (almost) always be used in conjunction with
* PlainTextTransferable.
*
* @author Werner Randelshofer
* @version 1.1 2009-09-01 Replaced use of deprecated class StringBufferInputStream.
* <br>1.0 22. August 2007 Created.
*/
public class PlainTextTransferable extends AbstractTransferable {
private String plainText;
public PlainTextTransferable(String plainText) {
this(getDefaultFlavors(), plainText);
}
public PlainTextTransferable(DataFlavor flavor, String plainText) {
this(new DataFlavor[] { flavor }, plainText);
}
public PlainTextTransferable(DataFlavor[] flavors, String plainText) {
super(flavors);
this.plainText = plainText;
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
if (! isDataFlavorSupported(flavor)) {
throw new UnsupportedFlavorException(flavor);
}
plainText = (plainText == null) ? "" : plainText;
if (String.class.equals(flavor.getRepresentationClass())) {
return plainText;
} else if (Reader.class.equals(flavor.getRepresentationClass())) {
return new StringReader(plainText);
} else if (InputStream.class.equals(flavor.getRepresentationClass())) {
String charsetName = flavor.getParameter("charset");
return new ByteArrayInputStream(plainText.getBytes(charsetName==null?"UTF-8":charsetName));
//return new StringBufferInputStream(plainText);
} // fall through to unsupported
throw new UnsupportedFlavorException(flavor);
}
protected static DataFlavor[] getDefaultFlavors() {
try {
return new DataFlavor[] {
new DataFlavor("text/plain;class=java.lang.String"),
new DataFlavor("text/plain;class=java.io.Reader"),
new DataFlavor("text/plain;charset=unicode;class=java.io.InputStream")
};
} catch (ClassNotFoundException cle) {
InternalError ie = new InternalError(
"error initializing PlainTextTransferable");
ie.initCause(cle);
throw ie;
}
}
}
@@ -0,0 +1,67 @@
/*
* @(#)StringTransferable.java 1.0 22. August 2007
*
* Copyright (c) 2007 Werner Randelshofer, Immensee, 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 ch.randelshofer.gui.datatransfer;
import java.awt.datatransfer.*;
import java.io.IOException;
/**
* StringTransferable.
* <p>
* Note: This transferable should always be used in conjunction with
* PlainTextTransferable.
* <p>
* Usage:
* <pre>
* String text = "bla";
* CompositeTransfer t = new CompositeTransferable();
* t.add(new StringTransferable(text));
* t.add(new PlainTextTransferable(text));
* </pre>
*
* @author Werner Randelshofer
* @version 1.0 22. August 2007 Created.
*/
public class StringTransferable extends AbstractTransferable {
private String string;
public StringTransferable(String string) {
this(getDefaultFlavors(), string);
}
public StringTransferable(DataFlavor flavor, String string) {
this(new DataFlavor[] { flavor }, string);
}
public StringTransferable(DataFlavor[] flavors, String string) {
super(flavors);
this.string = string;
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
if (! isDataFlavorSupported(flavor)) {
throw new UnsupportedFlavorException(flavor);
}
return string;
}
protected static DataFlavor[] getDefaultFlavors() {
try {
return new DataFlavor[] {
new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType+";class=java.lang.String"),
DataFlavor.stringFlavor
};
} catch (ClassNotFoundException cle) {
InternalError ie = new InternalError(
"error initializing StringTransferable");
ie.initCause(cle);
throw ie;
}
}
}
@@ -0,0 +1,143 @@
/*
* @(#)ImageOutputStreamAdapter.java 1.1 2011-01-07
*
* Copyright © 2010 Werner Randelshofer, Immensee, 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 ch.randelshofer.io;
import java.io.IOException;
import java.io.OutputStream;
import javax.imageio.stream.ImageOutputStream;
/**
* Adapts an {@code ImageOutputStream} for classes requiring an
* {@code OutputStream}.
*
* @author Werner Randelshofer
* @version 1.1 2011-01-07 Fixes performance.
* <br>1.0 2010-12-26 Created.
*/
public class ImageOutputStreamAdapter extends OutputStream {
/**
* The underlying output stream to be filtered.
*/
protected ImageOutputStream out;
/**
* Creates an output stream filter built on top of the specified
* underlying output stream.
*
* @param out the underlying output stream to be assigned to
* the field <tt>this.out</tt> for later use, or
* <code>null</code> if this instance is to be
* created without an underlying stream.
*/
public ImageOutputStreamAdapter(ImageOutputStream out) {
this.out = out;
}
/**
* Writes the specified <code>byte</code> to this output stream.
* <p>
* The <code>write</code> method of <code>FilterOutputStream</code>
* calls the <code>write</code> method of its underlying output stream,
* that is, it performs <tt>out.write(b)</tt>.
* <p>
* Implements the abstract <tt>write</tt> method of <tt>OutputStream</tt>.
*
* @param b the <code>byte</code>.
* @exception IOException if an I/O error occurs.
*/
@Override
public void write(int b) throws IOException {
out.write(b);
}
/**
* Writes <code>b.length</code> bytes to this output stream.
* <p>
* The <code>write</code> method of <code>FilterOutputStream</code>
* calls its <code>write</code> method of three arguments with the
* arguments <code>b</code>, <code>0</code>, and
* <code>b.length</code>.
* <p>
* Note that this method does not call the one-argument
* <code>write</code> method of its underlying stream with the single
* argument <code>b</code>.
*
* @param b the data to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#write(byte[], int, int)
*/
@Override
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
/**
* Writes <code>len</code> bytes from the specified
* <code>byte</code> array starting at offset <code>off</code> to
* this output stream.
* <p>
* The <code>write</code> method of <code>FilterOutputStream</code>
* calls the <code>write</code> method of one argument on each
* <code>byte</code> to output.
* <p>
* Note that this method does not call the <code>write</code> method
* of its underlying input stream with the same arguments. Subclasses
* of <code>FilterOutputStream</code> should provide a more efficient
* implementation of this method.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#write(int)
*/
@Override
public void write(byte b[], int off, int len) throws IOException {
out.write(b,off,len);
}
/**
* Flushes this output stream and forces any buffered output bytes
* to be written out to the stream.
* <p>
* The <code>flush</code> method of <code>FilterOutputStream</code>
* calls the <code>flush</code> method of its underlying output stream.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
@Override
public void flush() throws IOException {
out.flush();
}
/**
* Closes this output stream and releases any system resources
* associated with the stream.
* <p>
* The <code>close</code> method of <code>FilterOutputStream</code>
* calls its <code>flush</code> method, and then calls the
* <code>close</code> method of its underlying output stream.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#flush()
* @see java.io.FilterOutputStream#out
*/
@Override
public void close() throws IOException {
try {
flush();
} finally {
out.close();
}
}
}
@@ -0,0 +1,153 @@
/*
* @(#)SeekableByteArrayOutputStream.java 1.0 2010-12-27
*
* Copyright © 2010 Werner Randelshofer, Immensee, 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 ch.randelshofer.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 1.0 2010-12-27 Created.
*/
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];
}
/**
* 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);
}
}
@@ -0,0 +1,149 @@
/*
* @(#)MP3AudioInputStream.java 1.0 2011-01-01
*
* Copyright © 2010 Werner Randelshofer, Immensee, 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 ch.randelshofer.media.mp3;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
/**
* {@code AudioInputStream} adapter for {@link MP3ElementaryInputStream}.
* <p>
* Unlike a regular audio input stream, an MP3 audio input stream can have a
* variable frame size and can change its encoding method in mid-stream.
* Therefore method getFormat can return different values for each frame,
* and mark/reset is not supported, and method getFrameLength can not return
* the total number of frames in the stream.
*
* @author Werner Randelshofer
* @version 1.0 2011-01-01 Created.
*/
public class MP3AudioInputStream extends AudioInputStream {
private MP3ElementaryInputStream in;
/** Creates an MP3AudioInputStream and reads the stream until the first
* frame is reached.
*
* @param file A File.
* @throws IOException if the file does not contain an MP3 elementary stream.
*/
public MP3AudioInputStream(File file) throws IOException {
this(new BufferedInputStream(new FileInputStream(file)));
}
/** Creates an MP3AudioInputStream and reads the stream until the first
* frame is reached.
*
* @param in An InputStream.
* @throws IOException if the stream does not contain an MP3 elementary stream.
*/
public MP3AudioInputStream(InputStream in) throws IOException {
// Feed superclass with nonsense - we override all methods anyway.
super(null, new AudioFormat(MP3ElementaryInputStream.MP3, 44100, 16, 2, 626, 44100f / 1152f, true), -1);
this.in = new MP3ElementaryInputStream(in);
if (this.in.getNextFrame() == null) {
throw new IOException("Stream is not an MP3 elementary stream");
}
}
@Override
public int available() throws IOException {
return in.available();
}
/** Returns the format of the <i>next</i> frame. Returns null if the stream
* is not positioned inside a frame.
*/
@Override
public AudioFormat getFormat() {
return in.getFormat();
}
/** Returns -1 because we don't know how many frames the stream has. */
@Override
public long getFrameLength() {
return -1;
}
@Override
public void close() throws IOException {
in.close();
}
/** Throws an IOException, because the frame size is greater than 1. */
@Override
public int read() throws IOException {
throw new IOException("cannot read a single byte if frame size > 1");
}
/** Reads some number of bytes from the audio input stream and stores them
* into the buffer array b. The number of bytes actually read is returned as
* an integer. This method blocks until input data is available, the end of
* the stream is detected, or an exception is thrown.
* This method will always read an integral number of frames. If the length
* of the array is not an integral number of frames, a maximum of
* {@code b.length - (b.length % frameSize)} bytes will be read.
*
* @return Returns the total number of bytes read into the buffer, or -1 if there is
* no more data because the end of the stream has been reached.
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (in.getFrame() == null && in.getNextFrame() == null) {
return -1;
}
if (in.getStreamPosition() != in.getFrame().getFrameOffset()) {
if (in.getNextFrame() == null) {
return -1;
}
}
int bytesRead = 0;
int frameSize = in.getFrame().getFrameSize();
while (len >= frameSize) {
in.readFully(b, off, frameSize);
len -= frameSize;
bytesRead += frameSize;
off += frameSize;
if (in.getNextFrame() == null) {
break;
}
frameSize = in.getFrame().getFrameSize();
}
return bytesRead;
}
@Override
public long skip(long n) throws IOException {
return in.skip(n);
}
@Override
public void mark(int readlimit) {
// can't do anything
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
}
@@ -0,0 +1,546 @@
/*
* @(#)MP3ElementaryInputStream.java 1.0 2011-01-03
*
* Copyright © 2010 Werner Randelshofer, Immensee, 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 ch.randelshofer.media.mp3;
import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.util.HashMap;
import javax.sound.sampled.AudioFormat;
/**
* Facilitates reading of an MP3 elementary stream frame by frame.
* <p>
* An MP3 frame has a 32-bit header with the following contents in big endian
* order:
* <ul>
* <li>bit 31-21, MP3 Sync Word, all bits must be set</li>
* <li>bit 20-19, Version, 00=MPEG 2.5,01=reserved,10=MPEG 2, 11=MPEG 1</li>
* <li>bit 18-17, Layer, 00=reserved, 01=layer 3, 10=layer 2, 11=layer 1</li>
* <li>bit 16, Error protection, 0=16 bit CRC follows header, 1=Not protected</li>
* <li>bit 15-12, Bit Rate in kbps, interpretation depends on version and layer</li>
* <li>bit 11-10, Frequency, interpretation depends on version</li>
* <li>bit 9, Pad Bit, 0=frame is not padded, 1=frame is padded to exactly fit the bit rate</li>
* <li>bit 8, Private bit, only informative</li>
* <li>bit 7-6, Channel Mode, 00=stereo, 01=joint stereo, 10=dual channel (2 mono channels), 11=single channel (mono)</li>
* <li>bit 5-4, Mode Extension (only used with Joint Stereo), interpretation depends on version and layer</li>
* <li>bit 3, Copyright, 0=not copyrighted, 1=copyrighted</li>
* <li>bit 2, Original, 0=Copy of original media,1=original media</li>
* <li>bit 1-0, Emphasis, 00=none,01=50/15ms,10=reserved,11=CCIT J.17</li>
* </ul>
* <p>
* Reference:<br>
* <a href="http://en.wikipedia.org/wiki/MP3">http://en.wikipedia.org/wiki/MP3</a><br>
* <a href="http://www.datavoyage.com/mpgscript/mpeghdr.htm">http://www.datavoyage.com/mpgscript/mpeghdr.htm</a><br>
* <a href="http://www.mp3-tech.org/programmer/frame_header.html">http://www.mp3-tech.org/programmer/frame_header.html</a><br>
* <a href="http://lame.sourceforge.net/tech-FAQ.txt">http://lame.sourceforge.net/tech-FAQ.txt</a><br>
* <a href0"http://www.altera.com/literature/dc/1.4-2005_Taiwan_2nd_SouthernTaiwanU-web.pdf">http://www.altera.com/literature/dc/1.4-2005_Taiwan_2nd_SouthernTaiwanU-web.pdf</a><br>
*
* @author Werner Randelshofer
* @version 1.0 2011-01-03 Created.
*/
public class MP3ElementaryInputStream extends FilterInputStream {
/** Defines the "MP3" encoding. */
public final static AudioFormat.Encoding MP3 = new AudioFormat.Encoding("MP3");
private Frame frame;
private long pos;
private final static int[][] BIT_RATES = { // All values are in kbps
// V1 - MPEG Version 1
// V2 - MPEG Version 2 and Version 2.5
// L1 - Layer I
// L2 - Layer II
// L3 - Layer III
//
// V1L1, V1L2, V1L3, V2L1, V2L2&L3
{-1, -1, -1, -1, -1}, // free
{32, 32, 32, 32, 8},
{64, 48, 40, 48, 16},
{96, 56, 48, 56, 24},
{128, 64, 56, 64, 32},
{160, 80, 64, 80, 40},
{192, 96, 80, 96, 48},
{224, 112, 96, 112, 56},
{256, 128, 112, 128, 64},
{288, 160, 128, 144, 80},
{320, 192, 160, 160, 96},
{352, 224, 192, 176, 112},
{384, 256, 224, 192, 128},
{416, 320, 256, 224, 144},
{448, 384, 320, 256, 160},
{-2, -2, -2, -2, -2}, // bad
};
private final static int[][] SAMPLE_RATES = { // All values are in Hz
// V1 - MPEG Version 1
// V2 - MPEG Version 2
// V25 - MPEG Version 2.5
//
// V1, V2, V25
{44100, 22050, 11025},
{48000, 24000, 12000},
{32000, 16000, 8000},
{-1, -1, -1}, // reserved
};
/** An elementary frame. */
public static class Frame {
/** 32-bit header. */
private int header;
/** 16-bit CRC. */
private int crc;
/** The size of the data in the frame. */
private int bodySize;
/** The offset of the data in the frame. */
private long bodyOffset;
/** Creates a new frame.
*
* @param header The 32-bit Frame header
*/
public Frame(int header) {
this.header = header;
}
public int getHeader() {
return header;
}
/** Returns the version number: 1=MPEG 1, 2=MPEG 2, 25=MPEG 2.5; -1=unknown. */
public int getVersion() {
switch (getVersionCode()) {
case 0:
return 25;
case 2:
return 2;
case 3:
return 1;
default:
return -1;
}
}
/** Returns the raw version code as it is stored in the
* header. 3=MPEG 1, 2=MPEG 2, 1=reserved, 0=MPEG 2.5. */
public int getVersionCode() {
return (header >>> 19) & 3;
}
/** Returns the layer number.
* 1=Layer I, 2=Layer II, 3=Layer III, -1=unknown. */
public int getLayer() {
switch (getLayerCode()) {
case 1:
return 3;
case 2:
return 2;
case 3:
return 1;
default:
return -1;
}
}
/** Returns the raw layer code as it is stored in the header.
* 3=Layer I, 2=Layer II, 1=Layer III, 0=reserved. */
public int getLayerCode() {
return (header >>> 17) & 3;
}
/** Returns the bitrate of the frame. Returns -1 if unknown.
*/
public int getBitRate() {
if (getVersion() < 0 || getLayer() < 0) {
return -1;
}
int v = getVersion() == 1 ? 0 : 3;
int l = getVersion() == 1 ? getLayer() - 1 : (getLayer() == 1 ? 0 : 1);
return BIT_RATES[getBitRateCode()][v + l];
}
/** Returns the raw bitrate code as it is stored in the header.
*/
public int getBitRateCode() {
return (header >>> 12) & 15;
}
/** Returns true if this frame has a CRC. */
public boolean hasCRC() {
return ((header >>> 16) & 1) == 0;
}
/** Returns the CRC of this frame. The value is only valid if hasCRC() returns true. */
public int getCRC() {
return crc;
}
public boolean hasPadding() {
return ((header >>> 9) & 1) == 1;
}
/** Returns the sample rate in Hz.
* Returns -1 if unknown.
*/
public int getSampleRate() {
if (getVersion() < 0 || getLayer() < 0) {
return -1;
}
int v = getVersion() == 25 ? 2 : getVersion() - 1;
return SAMPLE_RATES[getSampleRateCode()][v];
}
/** Returns the raw sample rate code as it is stored in the header.
*/
public int getSampleRateCode() {
return (header >>> 10) & 3;
}
/** Returns the number of samples in the frame.
* It is constant and always 384 samples for Layer I and 1152 samples
* for Layer II and Layer III.
* Returns -1 if unknown.
*/
public int getSampleCount() {
if (getLayer() < 0) {
return -1;
}
return (getLayer() == 1 ? 192 : 576) * getChannelCount();
}
/** Returns the number of channels.
* @return 1=mono, 2=stereo, joint stereo or dual channel.
*/
public int getChannelCount() {
return getChannelModeCode() == 3 ? 1 : 2;
}
/** Returns the sample size in bits. Always 16 bit per sample. */
public int getSampleSize() {
return 16;
}
/** Returns the raw channel mode as stored in the header.
*
* @return 0=stereo, 1=joint stereo, 2=dual channel, 3=single channel (mono).
*/
public int getChannelModeCode() {
return (header >>> 6) & 3;
}
/** Returns the frame header as a byte array. */
public byte[] headerToByteArray() {
byte[] data = new byte[hasCRC() ? 6 : 4];
headerToByteArray(data, 0);
return data;
}
/** Writes the frame header into the specified byte array.
* Returns the number of bytes written.
*/
public int headerToByteArray(byte[] data, int offset) {
if (data.length - offset < getHeaderSize()) {
throw new IllegalArgumentException("data array is too small");
}
data[offset + 0] = (byte) (header >>> 24);
data[offset + 1] = (byte) (header >>> 16);
data[offset + 2] = (byte) (header >>> 8);
data[offset + 3] = (byte) (header >>> 0);
if (hasCRC()) {
data[offset + 4] = (byte) (crc >>> 8);
data[offset + 5] = (byte) (crc >>> 0);
}
return getHeaderSize();
}
/** Writes the frame header into the specified output stream. */
public void writeHeader(OutputStream out) throws IOException {
out.write((header >>> 24));
out.write((header >>> 16));
out.write((header >>> 8));
out.write((header >>> 0));
if (hasCRC()) {
out.write((crc >>> 8));
out.write((crc >>> 0));
}
}
/** Returns the offset of the frame in the input stream. */
public long getFrameOffset() {
return getBodyOffset() - getHeaderSize();
}
/** Returns the size of the frame in bytes.
* This size includes the header, the data and the padding.
*/
public int getFrameSize() {
return getHeaderSize() + getBodySize();
}
/** Returns the offset of the header in the input stream. */
public long getHeaderOffset() {
return getFrameOffset();
}
/** Returns the size of the header in bytes. */
public int getHeaderSize() {
return hasCRC() ? 6 : 4;
}
/** Returns the offset of the side info in the input stream. */
public long getSideInfoOffset() {
return bodyOffset;
}
/** Returns the size of the side info in bytes.
* It is 17 bytes long in a single channel frame and 32 bytes in dual
* channel or stereo channel.
*/
public int getSideInfoSize() {
return getChannelCount() == 1 ? 17 : 32;
}
/** Returns the offset of the frame body in the input stream. */
public long getBodyOffset() {
return bodyOffset;
}
/** Returns the size of the frame body in bytes.
* The body includes the side info, the audio data, and the padding.
*/
public int getBodySize() {
return bodySize;
}
/** Padding is used to fit the bit rates exactly.
* For an example: 128k 44.1kHz layer II uses a lot of 418 bytes and
* some of 417 bytes long frames to get the exact 128k bitrate. For
* Layer I slot is 32 bits long, for Layer II and Layer III slot is 8
* bits long. */
public int getPaddingSize() {
if (hasPadding()) {
return getLayer() == 1 ? 4 : 1;
}
return 0;
}
private float getFrameRate() {
return (float) getSampleRate() / getSampleCount();
}
}
public MP3ElementaryInputStream(File file) throws IOException {
super(new PushbackInputStream(new BufferedInputStream(new FileInputStream(file)), 6));
}
public MP3ElementaryInputStream(InputStream in) {
super(new PushbackInputStream(in, 6));
}
/** Gets the next frame from the input stream.
* Positions the stream in front of the frame header.
*/
public Frame getNextFrame() throws IOException {
while (frame != null && pos < frame.getBodyOffset() + frame.getBodySize()) {
long skipped = skip(frame.getBodyOffset() + frame.getBodySize() - pos);
if (skipped < 0) {
break;
}
}
while (true) {
int b = read0();
if (b == -1) {
frame = null;
break;
} else if (b == 255) {
int h0 = b;
int h1 = read0();
if (h1 != -1 && (h1 & 0xe0) == 0xe0) {
int h2 = read0();
int h3 = read0();
if (h3 != -1) {
frame = new Frame((h0 << 24) | (h1 << 16) | (h2 << 8) | h3);
if (frame.getBitRate() == -1 || frame.getLayer() == -1 || frame.getSampleRate() == -1) {
// => the header is corrupt: push back 3 bytes
PushbackInputStream pin = (PushbackInputStream) in;
pin.unread(h3);
pin.unread(h2);
pin.unread(h1);
pos -= 3;
continue;
}
int crc0 = -1, crc1 = -1;
if (frame.hasCRC()) {
crc0 = read0();
crc1 = read0();
if (crc1 == -1) {
throw new EOFException();
}
frame.crc = (crc0 << 8) | crc1;
}
frame.bodyOffset = pos;
if (frame.getBitRate() <= 0 || frame.getSampleRate() <= 0) {
frame.bodySize = 0;
} else if (frame.getLayer() == 1) {
frame.bodySize = (int) ((12000L * frame.getBitRate() / frame.getSampleRate()) * 4) - frame.getHeaderSize() + frame.getPaddingSize();
} else if (frame.getLayer() == 2 || frame.getLayer() == 3) {
if (frame.getChannelCount() == 1) {
frame.bodySize = (int) (72000L * frame.getBitRate() / (frame.getSampleRate() + frame.getPaddingSize())) - frame.getHeaderSize() + frame.getPaddingSize();
} else {
frame.bodySize = (int) (144000L * frame.getBitRate() / (frame.getSampleRate() + frame.getPaddingSize())) - frame.getHeaderSize() + frame.getPaddingSize();
}
}
PushbackInputStream pin = (PushbackInputStream) in;
if (frame.hasCRC()) {
pin.unread(crc1);
pin.unread(crc0);
pos -= 2;
}
pin.unread(h3);
pin.unread(h2);
pin.unread(h1);
pin.unread(h0);
pos -= 4;
assert pos == frame.getFrameOffset() : pos + "!=" + frame.getFrameOffset();
break;
}
}
}
}
return frame;
}
/** Returns the current frame. */
public Frame getFrame() {
return frame;
}
/** Gets the format of the current frame.
* Returns null if the input stream is not positioned on a frame, or the frame
* is not valid.
* @return AudioFormat of current frame or null.
*/
public AudioFormat getFormat() {
if (frame == null) {
return null;
} else {
HashMap<String, Object> properties = new HashMap<String, Object>();
properties.put("vbr", true);
return new AudioFormat(MP3, //
frame.getSampleRate(), frame.getSampleSize(), frame.getChannelCount(),//
frame.getFrameSize(), frame.getFrameRate(), true, properties);
}
}
private int read0() throws IOException {
int b = super.read();
if (b != -1) {
pos++;
}
return b;
}
/** Reads a byte from the current frame (its header and its data).
* Returns -1 on an attempt to read past the end of the frame.
*/
@Override
public int read() throws IOException {
if (frame == null || pos >= frame.getBodyOffset() + frame.getBodySize()) {
return -1;
}
return read0();
}
/** Reads up to {@code len} bytes from the current frame (its header and its data).
* May read less then {@code len} bytes. Returns the actual number of bytes read.
* Returns -1 on an attempt to read past the end of the frame.
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (frame == null) {
return -1;
}
int maxlen = (int) (frame.getBodyOffset() + frame.getBodySize() - pos);
if (maxlen < 1) {
return -1;
}
len = Math.min(maxlen, len);
int count = super.read(b, off, len);
if (count != -1) {
pos += count;
}
return count;
}
/**
* Reads {@code b.length} bytes from the current frame (its header and its data).
* @throws {@code IOException} on an attempt to read past the end of the frame.
*/
public final void readFully(byte b[]) throws IOException {
readFully(b, 0, b.length);
}
/**
* Reads {@code len} bytes from the current frame (its header and its data).
* @throws {@code IOException} on an attempt to read past the end of the frame.
*/
public final void readFully(byte b[], int off, int len) throws IOException {
if (len < 0) {
throw new IndexOutOfBoundsException();
}
int n = 0;
while (n < len) {
int count = in.read(b, off + n, len - n);
if (count < 0) {
throw new EOFException();
}
n += count;
pos += count;
}
}
/** Skips up to {@code n} bytes from the current frame (its header and its data).
* Returns the actual number of bytes that have been skipped.
* Returns -1 on an attempt to skip past the end of the frame.
*/
@Override
public long skip(long n) throws IOException {
if (frame == null) {
return -1;
}
int maxlen = (int) (frame.getBodyOffset() + frame.getBodySize() - pos);
if (maxlen < 1) {
return -1;
}
n = Math.min(maxlen, n);
long skipped = in.skip(n);
if (skipped > 0) {
pos += skipped;
}
return skipped;
}
/**
* Returns the current position in the stream.
* @return The stream position.
*/
public long getStreamPosition() {
return pos;
}
}
@@ -0,0 +1,997 @@
/*
* @(#)AppleRLEEncoder.java 1.3 2011-01-16
*
* Copyright © 2011 Werner Randelshofer, Immensee, 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 ch.randelshofer.media.quicktime;
import java.io.IOException;
import java.io.OutputStream;
import ch.randelshofer.io.SeekableByteArrayOutputStream;
/**
* Implements the run length encoding of the Apple QuickTime Animation (RLE)
* format.
* <p>
* An RLE-encoded frame has the following format:
* <p>
* <pre>
* Header:
* uint32 chunkSize
*
* uint16 header 0x0000 => decode entire image
* 0x0008 => starting line and number of lines follows
* if header==0x0008 {
* uint16 startingLine at which to begin updating frame
* uint16 reserved 0x0000
* uint16 numberOfLines to update
* uint16 reserved 0x0000
* }
* n-bytes compressed lines
* </pre>
*
* The first 4 bytes defines the chunk length. This field also carries some
* other unknown flags, since at least one of the high bits is sometimes set.<br>
*
* If the overall length of the chunk is less than 8, treat the frame as a
* NOP, which means that the frame is the same as the one before it.<br>
*
* Next, there is a header of either 0x0000 or 0x0008. A header value with
* bit 3 set (header &amp; 0x0008) indicates that information follows revealing
* at which line the decode process is to begin:<br>
*
* <pre>
* 2 bytes starting line at which to begin updating frame
* 2 bytes unknown
* 2 bytes the number of lines to update
* 2 bytes unknown
* </pre>
*
* If the header is 0x0000, then the decode begins from the first line and
* continues through the entire height of the image.<br>
*
* After the header comes the individual RLE-compressed lines. An individual
* compressed line is comprised of a skip code, followed by a series of RLE
* codes and pixel data:<br>
* <pre>
* 1 byte skip code
* 1 byte RLE code
* n bytes pixel data
* 1 byte RLE code
* n bytes pixel data
* </pre>
* Each line begins with a byte that defines the number of pixels to skip in
* a particular line in the output line before outputting new pixel
* data. Actually, the skip count is set to one more than the number of
* pixels to skip. For example, a skip byte of 15 means "skip 14 pixels",
* while a skip byte of 1 means "don't skip any pixels". If the skip byte is
* 0, then the frame decode is finished. Therefore, the maximum skip byte
* value of 255 allows for a maximum of 254 pixels to be skipped.
* <p>
* After the skip byte is the first RLE code, which is a single signed
* byte. The RLE code can have the following meanings:<br>
* <ul>
* <li>equal to 0: There is another single-byte skip code in the stream.
* Again, the actual number of pixels to skip is 1 less
* than the skip code. Therefore, the maximum skip byte
* value of 255 allows for a maximum of 254 pixels to be
* skipped.</li>
*
* <li>equal to -1: End of the RLE-compressed line</li>
*
* <li>greater than 0: Run of pixel data is copied directly from the
* encoded stream to the output frame.</li>
*
* <li>less than -1: Repeat pixel data -(RLE code) times.</li>
* </ul>
* <p>
* The pixel data has the following format:
* <ul>
* <li>8-bit data: Pixels are handled in groups of four. Each pixel is a palette
* index (the palette is determined by the Quicktime file transporting the
* data).<br>
* If (code &gt; 0), copy (4 * code) pixels from the encoded stream to the
* output.<br>
* If (code &lt; -1), extract the next 4 pixels from the encoded stream
* and render the entire group -(code) times to the output frame. </li>
*
* <li>16-bit data: Each pixel is represented by a 16-bit RGB value with 5 bits
* used for each of the red, green, and blue color components and 1 unused bit
* to round the value tmp to 16 bits: {@code xrrrrrgg gggbbbbb}. Pixel data is
* rendered to the output frame one pixel at a time.<br>
* If (code &gt; 0), copy the run of (code) pixels from the encoded stream to
* the output.<br>
* If (code &lt; -1), unpack the next 16-bit RGB value from the encoded stream
* and render it to the output frame -(code) times.</li>
*
* <li>24-bit data: Each pixel is represented by a 24-bit RGB value with 8 bits
* (1 byte) used for each of the red, green, and blue color components:
* {@code rrrrrrrr gggggggg bbbbbbbb}. Pixel data is rendered to the output
* frame one pixel at a time.<br>
* If (code &gt; 0), copy the run of (code) pixels from the encoded stream to
* the output.<br>
* If (code &lt; -1), unpack the next 24-bit RGB value from the encoded stream
* and render it to the output frame -(code) times.</li>
*
* <li>32-bit data: Each pixel is represented by a 32-bit ARGB value with 8 bits
* (1 byte) used for each of the alpha, red, green, and blue color components:
* {@code aaaaaaaa rrrrrrrr gggggggg bbbbbbbb}. Pixel data is rendered to the
* output frame one pixel at a time.<br>
* If (code &gt; 0), copy the run of (code) pixels from the encoded stream to
* the output.<br>
* If (code &lt; -1), unpack the next 32-bit ARGB value from the encoded stream
* and render it to the output frame -(code) times.</li>
* </ul>
*
* References:<br/>
* <a href="http://multimedia.cx/qtrle.txt">http://multimedia.cx/qtrle.txt</a><br>
*
* @author Werner Randelshofer
* @version 1.3 2011-01-17 Fixes an index out of bounds exception when a
* sub-image is compressed.
* <br>1.2 2011-01-07 Improves compression rate.
* <br>1.1 2011-01-07 Reduces seeking operations on output stream by using
* a seekable output stream internally.
* <br>1.0 2011-01-05 Created.
*/
public class AppleRLEEncoder {
private SeekableByteArrayOutputStream tmpSeek = new SeekableByteArrayOutputStream();
private DataAtomOutputStream tmp = new DataAtomOutputStream(tmpSeek);
/** Encodes a 16-bit key frame.
*
* @param tmp The output stream. Must be set to Big-Endian.
* @param data 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 add to offset to get to the next scanline.
*/
public void writeKey16(OutputStream out, short[] data, int width, int height, int offset, int scanlineStride)
throws IOException {
tmpSeek.reset();
long headerPos = tmpSeek.getStreamPosition();
// Reserve space for the header:
tmp.writeInt(0);
tmp.writeShort(0x0000);
// Encode each scanline
int ymax = offset + height * scanlineStride;
for (int y = offset; y < ymax; y += scanlineStride) {
int xy = y;
int xymax = y + width;
tmp.write(1); // this is a key-frame, there is nothing to skip at the start of line
int literalCount = 0;
int repeatCount = 0;
for (; xy < xymax; ++xy) {
// determine repeat count
short v = data[xy];
for (repeatCount = 0; xy < xymax && repeatCount < 127; ++xy, ++repeatCount) {
if (data[xy] != v) {
break;
}
}
xy -= repeatCount;
if (repeatCount < 2) {
literalCount++;
if (literalCount == 127) {
tmp.write(literalCount); // Literal OP-code
tmp.writeShorts(data, xy - literalCount + 1, literalCount);
literalCount = 0;
}
} else {
if (literalCount > 0) {
tmp.write(literalCount); // Literal OP-code
tmp.writeShorts(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-repeatCount); // Repeat OP-code
tmp.writeShort(v);
xy += repeatCount - 1;
}
}
// flush literal run
if (literalCount > 0) {
tmp.write(literalCount);
tmp.writeShorts(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-1);// End of line OP-code
}
// Complete the header
long pos = tmpSeek.getStreamPosition();
tmpSeek.seek(headerPos);
tmp.writeInt((int) (pos - headerPos));
tmpSeek.seek(pos);
tmpSeek.toOutputStream(out);
}
/** Encodes a 16-bit delta frame.
*
* @param tmp The output stream. Must be set to Big-Endian.
* @param data The image data.
* @param prev The image data of the previous frame.
* @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 add to offset to get to the next scanline.
*/
public void writeDelta16(OutputStream out, short[] data, short[] prev, int width, int height, int offset, int scanlineStride)
throws IOException {
tmpSeek.reset();
// Determine whether we can skip lines at the beginning
int ymin;
int ymax = offset + height * scanlineStride;
scanline:
for (ymin = offset; ymin < ymax; ymin += scanlineStride) {
int xy = ymin;
int xymax = ymin + width;
for (; xy < xymax; ++xy) {
if (data[xy] != prev[xy]) {
break scanline;
}
}
}
if (ymin == ymax) {
// => Frame is identical to previous one
tmp.writeInt(4);
tmpSeek.toOutputStream(out);
return;
}
// Determine whether we can skip lines at the end
scanline:
for (; ymax > ymin; ymax -= scanlineStride) {
int xy = ymax - scanlineStride;
int xymax = ymax - scanlineStride + width;
for (; xy < xymax; ++xy) {
if (data[xy] != prev[xy]) {
break scanline;
}
}
}
//System.out.println("AppleRLEEncoder ymin:" + ymin / step + " ymax" + ymax / step);
// Reserve space for the header
long headerPos = tmpSeek.getStreamPosition();
tmp.writeInt(0);
if (ymin == offset && ymax == offset + height * scanlineStride) {
// => we can't skip any lines:
tmp.writeShort(0x0000);
} else {
// => we can skip lines:
tmp.writeShort(0x0008);
tmp.writeShort(ymin / scanlineStride);
tmp.writeShort(0);
tmp.writeShort((ymax - ymin + 1) / scanlineStride);
tmp.writeShort(0);
}
// Encode each scanline
for (int y = ymin; y < ymax; y += scanlineStride) {
int xy = y;
int xymax = y + width;
// determine skip count
int skipCount = 0;
for (; xy < xymax; ++xy, ++skipCount) {
if (data[xy] != prev[xy]) {
break;
}
}
if (skipCount == width) {
// => the entire line can be skipped
tmp.write(1); // don't skip any pixels
tmp.write(-1); // end of line
continue;
}
tmp.write(Math.min(255, skipCount + 1));
if (skipCount > 254) {
skipCount -= 254;
while (skipCount > 254) {
tmp.write(0); // Skip OP-code
tmp.write(255);
skipCount -= 254;
}
tmp.write(0); // Skip OP-code
tmp.write(skipCount + 1);
}
int literalCount = 0;
int repeatCount = 0;
for (; xy < xymax; ++xy) {
// determine skip count
for (skipCount = 0; xy < xymax; ++xy, ++skipCount) {
if (data[xy] != prev[xy]) {
break;
}
}
xy -= skipCount;
// determine repeat count
short v = data[xy];
for (repeatCount = 0; xy < xymax && repeatCount < 127; ++xy, ++repeatCount) {
if (data[xy] != v) {
break;
}
}
xy -= repeatCount;
if (skipCount < 2 && xy + skipCount < xymax && repeatCount < 2) {
literalCount++;
if (literalCount == 127) {
tmp.write(literalCount); // Literal OP-code
tmp.writeShorts(data, xy - literalCount + 1, literalCount);
literalCount = 0;
}
} else {
if (literalCount > 0) {
tmp.write(literalCount); // Literal OP-code
tmp.writeShorts(data, xy - literalCount, literalCount);
literalCount = 0;
}
if (xy + skipCount == xymax) {
// => we can skip until the end of the line without
// having to write an op-code
xy += skipCount - 1;
} else if (skipCount >= repeatCount) {
while (skipCount > 254) {
tmp.write(0); // Skip OP-code
tmp.write(255);
xy += 254;
skipCount -= 254;
}
tmp.write(0); // Skip OP-code
tmp.write(skipCount + 1);
xy += skipCount - 1;
} else {
tmp.write(-repeatCount); // Repeat OP-code
tmp.writeShort(v);
xy += repeatCount - 1;
}
}
}
// flush literal run
if (literalCount > 0) {
tmp.write(literalCount);
tmp.writeShorts(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-1);// End of line OP-code
}
// Complete the header
long pos = tmpSeek.getStreamPosition();
tmpSeek.seek(headerPos);
tmp.writeInt((int) (pos - headerPos));
tmpSeek.seek(pos);
tmpSeek.toOutputStream(out);
}
/** Encodes a 24-bit key frame.
*
* @param tmp The output stream. Must be set to Big-Endian.
* @param data 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 add to offset to get to the next scanline.
*/
public void writeKey24(OutputStream out, int[] data, int width, int height, int offset, int scanlineStride)
throws IOException {
tmpSeek.reset();
long headerPos = tmpSeek.getStreamPosition();
// Reserve space for the header:
tmp.writeInt(0);
tmp.writeShort(0x0000);
// Encode each scanline
int ymax = offset + height * scanlineStride;
for (int y = offset; y < ymax; y += scanlineStride) {
int xy = y;
int xymax = y + width;
tmp.write(1); // this is a key-frame, there is nothing to skip at the start of line
int literalCount = 0;
int repeatCount = 0;
for (; xy < xymax; ++xy) {
// determine repeat count
int v = data[xy];
for (repeatCount = 0; xy < xymax && repeatCount < 127; ++xy, ++repeatCount) {
if (data[xy] != v) {
break;
}
}
xy -= repeatCount;
if (repeatCount < 2) {
literalCount++;
if (literalCount > 126) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts24(data, xy - literalCount + 1, literalCount);
literalCount = 0;
}
} else {
if (literalCount > 0) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts24(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-repeatCount); // Repeat OP-code
tmp.writeInt24(v);
xy += repeatCount - 1;
}
}
// flush literal run
if (literalCount > 0) {
tmp.write(literalCount);
tmp.writeInts24(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-1);// End of line OP-code
}
// Complete the header
long pos = tmpSeek.getStreamPosition();
tmpSeek.seek(headerPos);
tmp.writeInt((int) (pos - headerPos));
tmpSeek.seek(pos);
tmpSeek.toOutputStream(out);
}
/** Encodes a 24-bit delta frame.
*
* @param tmp The output stream. Must be set to Big-Endian.
* @param data The image data.
* @param prev The image data of the previous frame.
* @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 add to offset to get to the next scanline.
*/
public void writeDelta24(OutputStream out, int[] data, int[] prev, int width, int height, int offset, int scanlineStride)
throws IOException {
tmpSeek.reset();
// Determine whether we can skip lines at the beginning
int ymin;
int ymax = offset + height * scanlineStride;
scanline:
for (ymin = offset; ymin < ymax; ymin += scanlineStride) {
int xy = ymin;
int xymax = ymin + width;
for (; xy < xymax; ++xy) {
if (data[xy] != prev[xy]) {
break scanline;
}
}
}
if (ymin == ymax) {
// => Frame is identical to previous one
tmp.writeInt(4);
tmpSeek.toOutputStream(out);
return;
}
// Determine whether we can skip lines at the end
scanline:
for (; ymax > ymin; ymax -= scanlineStride) {
int xy = ymax - scanlineStride;
int xymax = ymax - scanlineStride + width;
for (; xy < xymax; ++xy) {
if (data[xy] != prev[xy]) {
break scanline;
}
}
}
//System.out.println("AppleRLEEncoder ymin:" + ymin / step + " ymax" + ymax / step);
// Reserve space for the header
long headerPos = tmpSeek.getStreamPosition();
tmp.writeInt(0);
if (ymin == offset && ymax == offset + height * scanlineStride) {
// => we can't skip any lines:
tmp.writeShort(0x0000);
} else {
// => we can skip lines:
tmp.writeShort(0x0008);
tmp.writeShort(ymin / scanlineStride);
tmp.writeShort(0);
tmp.writeShort((ymax - ymin + 1) / scanlineStride);
tmp.writeShort(0);
}
// Encode each scanline
for (int y = ymin; y < ymax; y += scanlineStride) {
int xy = y;
int xymax = y + width;
// determine skip count
int skipCount = 0;
for (; xy < xymax; ++xy, ++skipCount) {
if (data[xy] != prev[xy]) {
break;
}
}
if (skipCount == width) {
// => the entire line can be skipped
tmp.write(1); // don't skip any pixels
tmp.write(-1); // end of line
continue;
}
tmp.write(Math.min(255, skipCount + 1));
if (skipCount > 254) {
skipCount -= 254;
while (skipCount > 254) {
tmp.write(0); // Skip OP-code
tmp.write(255);
skipCount -= 254;
}
tmp.write(0); // Skip OP-code
tmp.write(skipCount + 1);
}
int literalCount = 0;
int repeatCount = 0;
for (; xy < xymax; ++xy) {
// determine skip count
for (skipCount = 0; xy < xymax; ++xy, ++skipCount) {
if (data[xy] != prev[xy]) {
break;
}
}
xy -= skipCount;
// determine repeat count
int v = data[xy];
for (repeatCount = 0; xy < xymax && repeatCount < 127; ++xy, ++repeatCount) {
if (data[xy] != v) {
break;
}
}
xy -= repeatCount;
if (skipCount < 1 && xy + skipCount < xymax && repeatCount < 2) {
literalCount++;
if (literalCount == 127) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts24(data, xy - literalCount + 1, literalCount);
literalCount = 0;
}
} else {
if (literalCount > 0) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts24(data, xy - literalCount, literalCount);
literalCount = 0;
}
if (xy + skipCount == xymax) {
// => we can skip until the end of the line without
// having to write an op-code
xy += skipCount - 1;
} else if (skipCount >= repeatCount) {
while (skipCount > 254) {
tmp.write(0); // Skip OP-code
tmp.write(255);
xy += 254;
skipCount -= 254;
}
tmp.write(0); // Skip OP-code
tmp.write(skipCount + 1);
xy += skipCount - 1;
} else {
tmp.write(-repeatCount); // Repeat OP-code
tmp.writeInt24(v);
xy += repeatCount - 1;
}
}
}
// flush literal run
if (literalCount > 0) {
tmp.write(literalCount);
tmp.writeInts24(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-1);// End of line OP-code
}
// Complete the header
long pos = tmpSeek.getStreamPosition();
tmpSeek.seek(headerPos);
tmp.writeInt((int) (pos - headerPos));
tmpSeek.seek(pos);
tmpSeek.toOutputStream(out);
}
/** Encodes a 32-bit key frame.
*
* @param tmp The output stream. Must be set to Big-Endian.
* @param data 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 add to offset to get to the next scanline.
*/
public void writeKey32(OutputStream out, int[] data, int width, int height, int offset, int scanlineStride)
throws IOException {
tmpSeek.reset();
long headerPos = tmpSeek.getStreamPosition();
// Reserve space for the header:
tmp.writeInt(0);
tmp.writeShort(0x0000);
// Encode each scanline
int ymax = offset + height * scanlineStride;
for (int y = offset; y < ymax; y += scanlineStride) {
int xy = y;
int xymax = y + width;
tmp.write(1); // this is a key-frame, there is nothing to skip at the start of line
int literalCount = 0;
int repeatCount = 0;
for (; xy < xymax; ++xy) {
// determine repeat count
int v = data[xy];
for (repeatCount = 0; xy < xymax && repeatCount < 127; ++xy, ++repeatCount) {
if (data[xy] != v) {
break;
}
}
xy -= repeatCount;
if (repeatCount < 2) {
literalCount++;
if (literalCount > 126) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts(data, xy - literalCount + 1, literalCount);
literalCount = 0;
}
} else {
if (literalCount > 0) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-repeatCount); // Repeat OP-code
tmp.writeInt(v);
xy += repeatCount - 1;
}
}
// flush literal run
if (literalCount > 0) {
tmp.write(literalCount);
tmp.writeInts(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-1);// End of line OP-code
}
// Complete the header
long pos = tmpSeek.getStreamPosition();
tmpSeek.seek(headerPos);
tmp.writeInt((int) (pos - headerPos));
tmpSeek.seek(pos);
tmpSeek.toOutputStream(out);
}
/** Encodes a 32-bit delta frame.
*
* @param tmp The output stream. Must be set to Big-Endian.
* @param data The image data.
* @param prev The image data of the previous frame.
* @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 add to offset to get to the next scanline.
*/
public void writeDelta32(OutputStream out, int[] data, int[] prev, int width, int height, int offset, int scanlineStride)
throws IOException {
tmpSeek.reset();
// Determine whether we can skip lines at the beginning
int ymin;
int ymax = offset + height * scanlineStride;
scanline:
for (ymin = offset; ymin < ymax; ymin += scanlineStride) {
int xy = ymin;
int xymax = ymin + width;
for (; xy < xymax; ++xy) {
if (data[xy] != prev[xy]) {
break scanline;
}
}
}
if (ymin == ymax) {
// => Frame is identical to previous one
tmp.writeInt(4);
tmpSeek.toOutputStream(out);
return;
}
// Determine whether we can skip lines at the end
scanline:
for (; ymax > ymin; ymax -= scanlineStride) {
int xy = ymax - scanlineStride;
int xymax = ymax - scanlineStride + width;
for (; xy < xymax; ++xy) {
if (data[xy] != prev[xy]) {
break scanline;
}
}
}
//System.out.println("AppleRLEEncoder ymin:" + ymin / step + " ymax" + ymax / step);
// Reserve space for the header
long headerPos = tmpSeek.getStreamPosition();
tmp.writeInt(0);
if (ymin == offset && ymax == offset + height * scanlineStride) {
// => we can't skip any lines:
tmp.writeShort(0x0000);
} else {
// => we can skip lines:
tmp.writeShort(0x0008);
tmp.writeShort(ymin / scanlineStride);
tmp.writeShort(0);
tmp.writeShort((ymax - ymin + 1) / scanlineStride);
tmp.writeShort(0);
}
// Encode each scanline
for (int y = ymin; y < ymax; y += scanlineStride) {
int xy = y;
int xymax = y + width;
// determine skip count
int skipCount = 0;
for (; xy < xymax; ++xy, ++skipCount) {
if (data[xy] != prev[xy]) {
break;
}
}
if (skipCount == width) {
// => the entire line can be skipped
tmp.write(1); // don't skip any pixels
tmp.write(-1); // end of line
continue;
}
tmp.write(Math.min(255, skipCount + 1));
if (skipCount > 254) {
skipCount -= 254;
while (skipCount > 254) {
tmp.write(0); // Skip OP-code
tmp.write(255);
skipCount -= 254;
}
tmp.write(0); // Skip OP-code
tmp.write(skipCount + 1);
}
int literalCount = 0;
int repeatCount = 0;
for (; xy < xymax; ++xy) {
// determine skip count
for (skipCount = 0; xy < xymax; ++xy, ++skipCount) {
if (data[xy] != prev[xy]) {
break;
}
}
xy -= skipCount;
// determine repeat count
int v = data[xy];
for (repeatCount = 0; xy < xymax && repeatCount < 127; ++xy, ++repeatCount) {
if (data[xy] != v) {
break;
}
}
xy -= repeatCount;
if (skipCount < 1 && xy + skipCount < xymax && repeatCount < 2) {
literalCount++;
if (literalCount == 127) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts(data, xy - literalCount + 1, literalCount);
literalCount = 0;
}
} else {
if (literalCount > 0) {
tmp.write(literalCount); // Literal OP-code
tmp.writeInts(data, xy - literalCount, literalCount);
literalCount = 0;
}
if (xy + skipCount == xymax) {
// => we can skip until the end of the line without
// having to write an op-code
xy += skipCount - 1;
} else if (skipCount >= repeatCount) {
while (skipCount > 254) {
tmp.write(0); // Skip OP-code
tmp.write(255);
xy += 254;
skipCount -= 254;
}
tmp.write(0); // Skip OP-code
tmp.write(skipCount + 1);
xy += skipCount - 1;
} else {
tmp.write(-repeatCount); // Repeat OP-code
tmp.writeInt(v);
xy += repeatCount - 1;
}
}
}
// flush literal run
if (literalCount > 0) {
tmp.write(literalCount);
tmp.writeInts(data, xy - literalCount, literalCount);
literalCount = 0;
}
tmp.write(-1);// End of line OP-code
}
// Complete the header
long pos = tmpSeek.getStreamPosition();
tmpSeek.seek(headerPos);
tmp.writeInt((int) (pos - headerPos));
tmpSeek.seek(pos);
tmpSeek.toOutputStream(out);
}
/*
public static void main(String[] args) {
BufferedImage img=new BufferedImage(640,400,BufferedImage.TYPE_INT_RGB);
BufferedImage subImg = img.getSubimage(10, 20, 320, 200);
BufferedImage subsubImg = subImg.getSubimage(10, 10, 160, 100);
Graphics2D g = img.createGraphics();
g.setBackground(Color.WHITE);
g.clearRect(0, 0, 800, 600);
g.setColor(Color.BLUE);
g.drawRect(0, 0, 320-1, 400-1);
g.setColor(Color.RED);
g.drawRect(10, 20, 320-1, 200-1);
QuickTimeWriter qtr=null;
try {
qtr = new QuickTimeWriter(new File("RLE SubImg.mov"));
qtr.addVideoTrack("rle ","Animation", 600, 160, 100,24,30);
qtr.writeFrame(0, subsubImg, 100);
g.setColor(Color.GREEN);
g.drawRect(20,30, 160-1, 100-1);
qtr.writeFrame(0, subsubImg, 100);
qtr.close();
qtr=null;
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (qtr!=null) {
try {
qtr.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
g.dispose();
}
}
public static void main1(String[] args) {
int w = 800, h = 600;
int[] data = new int[w * h];
int[] prev = new int[w * h];
Random rnd = new Random();
int repeat = 0;
for (int i = 0; i < data.length; ++i) {
if (--repeat > 0) {
data[i] = data[i - 1];
} else {
repeat = rnd.nextInt(10);
data[i] = rnd.nextInt(1 << 24);
}
}
ArrayIndexOutOfBoundsException ai;
ByteArrayOutputStream buf = new ByteArrayOutputStream();
AppleRLEEncoder enc = new AppleRLEEncoder();
try {
enc.writeKey24(buf, data, w-2, h, 1, w);
buf.close();
byte[] result = buf.toByteArray();
int fullSize=(w-2)*h*3;
System.out.println("full size:" + fullSize);
System.out.println("compressed size:" + result.length);
System.out.println("compression percentage:" + (100 * (result.length / (float) fullSize)));
/* System.out.println(Arrays.toString(result));
System.out.print("0x [");
for (int i = 0; i < result.length; ++i) {
if (i != 0) {
System.out.print(',');
}
String hex = "00" + Integer.toHexString(result[i]);
System.out.print(hex.substring(hex.length() - 2));
}
System.out.println(']');
* /
} catch (IOException ex) {
ex.printStackTrace();
}
}
public static void main2(String[] args) {
short[] data = {//
8, 1, 1, 1, 1, 2, 8,//
8, 0, 2, 0, 0, 0, 8,//
8, 2, 3, 4, 4, 3, 8,//
8, 2, 2, 3, 4, 4, 8,//
8, 1, 4, 4, 4, 5, 8};
short[] prev = {//
8, 1, 1, 1, 1, 1, 8, //
8, 5, 5, 5, 5, 0, 8,//
8, 3, 3, 3, 3, 3, 8,//
8, 2, 2, 0, 0, 0, 8,//
8, 2, 0, 0, 0, 5, 8};
ByteArrayOutputStream buf = new ByteArrayOutputStream();
AppleRLEEncoder enc = new AppleRLEEncoder();
try {
// enc.writeDelta16(buf, data, prev, 1, 5, 7);
enc.writeKey16(buf, data, 5, 5, 1, 7);
buf.close();
byte[] result = buf.toByteArray();
System.out.println("size:" + result.length);
System.out.println(Arrays.toString(result));
System.out.print("0x [");
for (int i = 0; i < result.length; i++) {
if (i != 0) {
System.out.print(',');
}
String hex = "00" + Integer.toHexString(result[i]);
System.out.print(hex.substring(hex.length() - 2));
}
System.out.println(']');
} catch (IOException ex) {
ex.printStackTrace();
}
}
*/
}
@@ -0,0 +1,408 @@
/**
* @(#)DataAtomOutputStream.java 1.3 2011-01-07
*
* Copyright (c) 2008-2011 Werner Randelshofer, Immensee, 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 ch.randelshofer.media.quicktime;
import java.io.*;
import java.util.Date;
import java.util.GregorianCalendar;
import javax.imageio.stream.ImageOutputStreamImpl;
/**
* This output stream filter supports common data types used inside
* of QuickTime Data Atoms.
*
* @author Werner Randelshofer
* @version 1.3 2011-01-07 Adds writeShort/s and writeInt24/s methods.
* <br>1.2.1 2010-10-03 Fixes writing of empty P-Strings.
* <br>1.2 2009-08-29 Added writePString(String, int) method.
* <br>1.1 2008-08-11 Streamlined API and source code with AVI DataChunkOutputStream.
* <br>1.0.1 2008-06-22 Use ASCII instead of MacRoman for encoding
* type strings.
* <br>1.0 Jun 15, 2008 Created.
*/
public class DataAtomOutputStream extends FilterOutputStream {
ImageOutputStreamImpl impl;
protected static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, GregorianCalendar.JANUARY, 1).getTimeInMillis();
/**
* The number of bytes written to the data output stream so far.
* If this counter overflows, it will be wrapped to Integer.MAX_VALUE.
*/
protected long written;
public DataAtomOutputStream(OutputStream out) {
super(out);
}
/**
* Writes an Atom Type identifier (4 bytes).
* @param s A string with a length of 4 characters.
*/
public void writeType(String s) throws IOException {
if (s.length() != 4) {
throw new IllegalArgumentException("type string must have 4 characters");
}
try {
out.write(s.getBytes("ASCII"), 0, 4);
incCount(4);
} catch (UnsupportedEncodingException e) {
throw new InternalError(e.toString());
}
}
/**
* Writes out a <code>byte</code> to the underlying output stream as
* a 1-byte value. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>1</code>.
*
* @param v a <code>byte</code> value to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public final void writeByte(int v) throws IOException {
out.write(v);
incCount(1);
}
/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to the underlying output stream.
* If no exception is thrown, the counter <code>written</code> is
* incremented by <code>len</code>.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
@Override
public synchronized void write(byte b[], int off, int len)
throws IOException {
out.write(b, off, len);
incCount(len);
}
/**
* Writes the specified byte (the low eight bits of the argument
* <code>b</code>) to the underlying output stream. If no exception
* is thrown, the counter <code>written</code> is incremented by
* <code>1</code>.
* <p>
* Implements the <code>write</code> method of <code>OutputStream</code>.
*
* @param b the <code>byte</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
@Override
public synchronized void write(int b) throws IOException {
out.write(b);
incCount(1);
}
/**
* Writes an <code>int</code> to the underlying output stream as four
* bytes, high byte first. If no exception is thrown, the counter
* <code>written</code> is incremented by <code>4</code>.
*
* @param v an <code>int</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public void writeInt(int v) throws IOException {
out.write((v >>> 24) & 0xff);
out.write((v >>> 16) & 0xff);
out.write((v >>> 8) & 0xff);
out.write((v >>> 0) & 0xff);
incCount(4);
}
/**
* Writes an unsigned 32 bit integer value.
*
* @param v The value
* @throws java.io.IOException
*/
public void writeUInt(long v) throws IOException {
out.write((int) ((v >>> 24) & 0xff));
out.write((int) ((v >>> 16) & 0xff));
out.write((int) ((v >>> 8) & 0xff));
out.write((int) ((v >>> 0) & 0xff));
incCount(4);
}
/**
* Writes a signed 16 bit integer value.
*
* @param v The value
* @throws java.io.IOException
*/
public void writeShort(int v) throws IOException {
out.write((v >> 8) & 0xff);
out.write((v >>> 0) & 0xff);
incCount(2);
}
/**
* Writes a <code>BCD2</code> to the underlying output stream.
*
* @param v an <code>int</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public void writeBCD2(int v) throws IOException {
out.write(((v % 100 / 10) << 4) | (v % 10));
incCount(1);
}
/**
* Writes a <code>BCD4</code> to the underlying output stream.
*
* @param v an <code>int</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public void writeBCD4(int v) throws IOException {
out.write(((v % 10000 / 1000) << 4) | (v % 1000 / 100));
out.write(((v % 100 / 10) << 4) | (v % 10));
incCount(2);
}
/**
* Writes a 32-bit Mac timestamp (seconds since 1902).
* @param date
* @throws java.io.IOException
*/
public void writeMacTimestamp(Date date) throws IOException {
long millis = date.getTime();
long qtMillis = millis - MAC_TIMESTAMP_EPOCH;
long qtSeconds = qtMillis / 1000;
writeUInt(qtSeconds);
}
/**
* Writes 32-bit fixed-point number divided as 16.16.
*
* @param f an <code>int</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public void writeFixed16D16(double f) throws IOException {
double v = (f >= 0) ? f : -f;
int wholePart = (int) Math.floor(v);
int fractionPart = (int) ((v - wholePart) * 65536);
int t = (wholePart << 16) + fractionPart;
if (f < 0) {
t = t - 1;
}
writeInt(t);
}
/**
* Writes 32-bit fixed-point number divided as 2.30.
*
* @param f an <code>int</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public void writeFixed2D30(double f) throws IOException {
double v = (f >= 0) ? f : -f;
int wholePart = (int) v;
int fractionPart = (int) ((v - wholePart) * 1073741824);
int t = (wholePart << 30) + fractionPart;
if (f < 0) {
t = t - 1;
}
writeInt(t);
}
/**
* Writes 16-bit fixed-point number divided as 8.8.
*
* @param f an <code>int</code> to be written.
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#out
*/
public void writeFixed8D8(float f) throws IOException {
float v = (f >= 0) ? f : -f;
int wholePart = (int) v;
int fractionPart = (int) ((v - wholePart) * 256);
int t = (wholePart << 8) + fractionPart;
if (f < 0) {
t = t - 1;
}
writeUShort(t);
}
/**
* Writes a Pascal String.
*
* @param s
* @throws java.io.IOException
*/
public void writePString(String s) throws IOException {
if (s.length() > 0xffff) {
throw new IllegalArgumentException("String too long for PString");
}
if (s.length() != 0 && s.length() < 256) {
out.write(s.length());
} else {
out.write(0);
writeShort(s.length()); // increments +2
}
for (int i = 0; i < s.length(); i++) {
out.write(s.charAt(i));
}
incCount(1 + s.length());
}
/**
* Writes a Pascal String padded to the specified fixed size in bytes
*
* @param s
* @param length the fixed size in bytes
* @throws java.io.IOException
*/
public void writePString(String s, int length) throws IOException {
if (s.length() > length) {
throw new IllegalArgumentException("String too long for PString of length " + length);
}
if (s.length() != 0 && s.length() < 256) {
out.write(s.length());
} else {
out.write(0);
writeShort(s.length()); // increments +2
}
for (int i = 0; i < s.length(); i++) {
out.write(s.charAt(i));
}
// write pad bytes
for (int i = 1 + s.length(); i < length; i++) {
out.write(0);
}
incCount(length);
}
public void writeLong(long v) throws IOException {
out.write((int) (v >>> 56) & 0xff);
out.write((int) (v >>> 48) & 0xff);
out.write((int) (v >>> 40) & 0xff);
out.write((int) (v >>> 32) & 0xff);
out.write((int) (v >>> 24) & 0xff);
out.write((int) (v >>> 16) & 0xff);
out.write((int) (v >>> 8) & 0xff);
out.write((int) (v >>> 0) & 0xff);
incCount(8);
}
public void writeUShort(int v) throws IOException {
out.write((v >> 8) & 0xff);
out.write((v >>> 0) & 0xff);
incCount(2);
}
/**
* Increases the written counter by the specified value
* until it reaches Long.MAX_VALUE.
*/
protected void incCount(int value) {
long temp = written + value;
if (temp < 0) {
temp = Long.MAX_VALUE;
}
written = temp;
}
public void writeShorts(short[] s, int off, int len) throws IOException {
// Fix 4430357 - if off + len < 0, overflow occurred
if (off < 0 || len < 0 || off + len > s.length || off + len < 0) {
throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > s.length!");
}
byte[] b = new byte[len * 2];
int boff = 0;
for (int i = 0; i < len; i++) {
short v = s[off + i];
b[boff++] = (byte) (v >>> 8);
b[boff++] = (byte) (v >>> 0);
}
write(b, 0, len * 2);
}
public void writeInts(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 * 4];
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);
}
write(b, 0, len * 4);
}
private byte[] byteBuf = new byte[3];
public void writeInt24(int v) throws IOException {
byteBuf[0] = (byte) (v >>> 16);
byteBuf[1] = (byte) (v >>> 8);
byteBuf[2] = (byte) (v >>> 0);
write(byteBuf, 0, 3);
}
public void writeInts24(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);
}
write(b, 0, len * 3);
}
/**
* Returns the current value of the counter <code>written</code>,
* the number of bytes written to this data output stream so far.
* If the counter overflows, it will be wrapped to Integer.MAX_VALUE.
*
* @return the value of the <code>written</code> field.
* @see java.io.DataOutputStream#written
*/
public final long size() {
return written;
}
}
@@ -0,0 +1,243 @@
package processing.app.tools;
import java.awt.EventQueue;
import java.awt.FileDialog;
import java.awt.Frame;
import java.io.File;
import javax.swing.JFileChooser;
/** File chooser additions, cannibalized from PApplet. */
class Chooser {
static final boolean useNativeSelect = true;
static abstract class Callback {
//abstract void select(File file);
void handle(final File file) {
EventQueue.invokeLater(new Runnable() {
// new Thread(new Runnable() {
public void run() {
select(file);
}
});
// }).start();
}
abstract void select(File file);
}
// Frame parent;
//
// public Chooser(Frame parent) {
// this.parent = parent;
// }
/**
* Open a platform-specific file chooser dialog to select a file for input.
* After the selection is made, the selected File will be passed to the
* 'callback' function. If the dialog is closed or canceled, null will be
* sent to the function, so that the program is not waiting for additional
* input. The callback is necessary because of how threading works.
*
* <pre>
* void setup() {
* selectInput("Select a file to process:", "fileSelected");
* }
*
* void fileSelected(File selection) {
* if (selection == null) {
* println("Window was closed or the user hit cancel.");
* } else {
* println("User selected " + fileSeleted.getAbsolutePath());
* }
* }
* </pre>
*
* For advanced users, the method must be 'public', which is true for all
* methods inside a sketch when run from the PDE, but must explicitly be
* set when using Eclipse or other development environments.
*
* @webref input:files
* @param prompt message to the user
* @param callback name of the method to be called when the selection is made
*/
// public void selectInput(String prompt, String callback) {
// selectInput(prompt, callback, null);
// }
// public void selectInput(String prompt, String callback, File file) {
// selectInput(prompt, callback, file, this);
// }
// public void selectInput(String prompt, String callback,
// File file, Object callbackObject) {
// selectInput(prompt, callback, file, callbackObject, selectFrame());
// }
static public void selectInput(Frame parent, String prompt, File file,
Callback callback) {
selectImpl(parent, prompt, file, callback, FileDialog.LOAD);
}
/**
* See selectInput() for details.
*
* @webref output:files
* @param prompt message to the user
* @param callback name of the method to be called when the selection is made
*/
// public void selectOutput(String prompt, String callback) {
// selectOutput(prompt, callback, null);
// }
//
// public void selectOutput(String prompt, String callback, File file) {
// selectOutput(prompt, callback, file, this);
// }
//
//
// public void selectOutput(String prompt, String callback,
// File file, Object callbackObject) {
// selectOutput(prompt, callback, file, callbackObject, selectFrame());
// }
static public void selectOutput(Frame parent, String prompt, File file,
Callback callback) {
selectImpl(parent, prompt, file, callback, FileDialog.SAVE);
}
static protected void selectImpl(final Frame parentFrame,
final String prompt,
final File defaultSelection,
final Callback callback,
final int mode) {
// EventQueue.invokeLater(new Runnable() {
// public void run() {
File selectedFile = null;
if (useNativeSelect) {
FileDialog dialog = new FileDialog(parentFrame, prompt, mode);
if (defaultSelection != null) {
dialog.setDirectory(defaultSelection.getParent());
dialog.setFile(defaultSelection.getName());
}
dialog.setVisible(true);
String directory = dialog.getDirectory();
String filename = dialog.getFile();
if (filename != null) {
selectedFile = new File(directory, filename);
}
} else {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle(prompt);
if (defaultSelection != null) {
chooser.setSelectedFile(defaultSelection);
}
int result = -1;
if (mode == FileDialog.SAVE) {
result = chooser.showSaveDialog(parentFrame);
} else if (mode == FileDialog.LOAD) {
result = chooser.showOpenDialog(parentFrame);
}
if (result == JFileChooser.APPROVE_OPTION) {
selectedFile = chooser.getSelectedFile();
}
}
//selectCallback(selectedFile, callbackMethod, callbackObject);
callback.handle(selectedFile);
// }
// });
}
/**
* See selectInput() for details.
*
* @webref input:files
* @param prompt message to the user
* @param callback name of the method to be called when the selection is made
*/
// public void selectFolder(String prompt, String callback) {
// selectFolder(prompt, callback, null);
// }
//
//
// public void selectFolder(String prompt, String callback, File file) {
// selectFolder(prompt, callback, file, this);
// }
//
//
// public void selectFolder(String prompt, String callback,
// File file, Object callbackObject) {
// selectFolder(prompt, callback, file, callbackObject, selectFrame());
// }
static public void selectFolder(final Frame parentFrame,
final String prompt,
final File defaultSelection,
final Callback callback) {
// EventQueue.invokeLater(new Runnable() {
// public void run() {
File selectedFile = null;
if (System.getProperty("os.name").contains("Mac") && useNativeSelect) {
FileDialog fileDialog =
new FileDialog(parentFrame, prompt, FileDialog.LOAD);
System.setProperty("apple.awt.fileDialogForDirectories", "true");
fileDialog.setVisible(true);
System.setProperty("apple.awt.fileDialogForDirectories", "false");
String filename = fileDialog.getFile();
if (filename != null) {
selectedFile = new File(fileDialog.getDirectory(), fileDialog.getFile());
}
} else {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle(prompt);
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (defaultSelection != null) {
fileChooser.setSelectedFile(defaultSelection);
}
int result = fileChooser.showOpenDialog(parentFrame);
if (result == JFileChooser.APPROVE_OPTION) {
selectedFile = fileChooser.getSelectedFile();
}
}
//selectCallback(selectedFile, callbackMethod, callbackObject);
callback.handle(selectedFile);
// }
// });
}
// static private void selectCallback(File selectedFile,
// String callbackMethod,
// Object callbackObject) {
// try {
// Class<?> callbackClass = callbackObject.getClass();
// Method selectMethod =
// callbackClass.getMethod(callbackMethod, new Class[] { File.class });
// selectMethod.invoke(callbackObject, new Object[] { selectedFile });
//
// } catch (IllegalAccessException iae) {
// System.err.println(callbackMethod + "() must be public");
//
// } catch (InvocationTargetException ite) {
// ite.printStackTrace();
//
// } catch (NoSuchMethodException nsme) {
// System.err.println(callbackMethod + "() could not be found");
// }
// }
}