/*
* JSwiff is an open source Java API for Macromedia Flash file generation
* and manipulation
*
* Copyright (C) 2004-2006 Ralf Terdic (contact@jswiff.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.jswiff.io;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/**
* Implements a bit stream used for writing SWF files.
*/
public final class OutputBitStream {
private OutputStream stream;
private ByteArrayOutputStream memoryStream;
private int bitBuffer;
private int bitCursor; // 0-7, when 7 is reached, bitBuffer is written
private boolean compressed = false;
private long offset;
private boolean isMemoryStream;
private boolean ansi;
private boolean shiftJIS;
/**
* Creates a new OutputBitStream instance.
*
* @param stream the internal stream used for output data.
*/
public OutputBitStream(OutputStream stream) {
this.stream = stream;
}
/**
* Creates a new memory OutputBitStream instance (with a
* ByteArrayOutputStream as internal stream, its data can be retrieved with
* the getData() method).
*/
public OutputBitStream() {
memoryStream = new ByteArrayOutputStream();
stream = memoryStream;
isMemoryStream = true;
}
/**
* Specifies whether ANSI encoding is to be used when encoding strings.
*
* @param ansi <code>true</code> for ANSI encoding
*/
public void setANSI(boolean ansi) {
this.ansi = ansi;
}
/**
* Checks whether ANSI encoding is to be used when encoding strings.
*
* @return <code>true</code> for ANSI encoding
*/
public boolean isANSI() {
return ansi;
}
/**
* Returns the stream data.
*
* @return byte array containing the output data
*
* @throws IllegalStateException if called on a non-memory stream
*/
public byte[] getData() {
if (!isMemoryStream) {
throw new IllegalStateException(
"Use this method only with memory streams!");
}
try {
stream.close();
} catch (IOException e) {
// nothing to do
}
return memoryStream.toByteArray();
}
/**
* Returns the number of bits needed for the representation of a given fixed
* point value. Returns values between 2 and 64 because fixed point values
* are represented as long numbers.
*
* @param value a double value (representing a fixed point number)
*
* @return number of bits needed for the representation of the value
*/
public static int getFPBitsLength(double value) {
if (value == 0.0) {
return 1;
}
long fpBits = (long) (value * 65536.0);
return getSignedBitsLength(fpBits);
}
/**
* Returns the stream offset, i.e. the number of bytes (fully) written.
*
* @return the stream offset
*/
public long getOffset() {
return offset;
}
/**
* Returns the number of bits needed for the representation of a given signed
* value. Signed values are represented using at least 2 bits, so the method
* returns at least a value of 2. The maximum returned value is 64.
*
* @param value a long value (representing a signed integer)
*
* @return number of bits needed for the representation of the value
*/
public static int getSignedBitsLength(long value) {
int nBits; // result is at least 2!
if (value == 0) {
nBits = 0; // zero is represented as two cleared bits
} else {
// floor(ld(abs(value)))+1 bits for the absolute value, +1 bit for the sign
nBits = (int) (Math.floor(Math.log(Math.abs(value)) / Math.log(2)) + 2);
}
return nBits;
}
/**
* Returns the number of bits needed for the representation of a given
* unsigned value. For negative values, the method returns 0. For 0 or 1 as
* value, a length of 1 bit is returned. The maximum returned value is 64.
*
* @param value a long value (representing an unsigned integer)
*
* @return number of bits needed for the representation of the value
*/
public static int getUnsignedBitsLength(long value) {
if (value < 1) {
return 0;
}
return (int) (Math.floor(Math.log(value) / Math.log(2)) + 1);
}
/**
* Specifies whether Shift-JIS encoding is to be used when encoding strings.
*
* @param shiftJIS <code>true</code> for Shift-JIS encoding
*/
public void setShiftJIS(boolean shiftJIS) {
this.shiftJIS = shiftJIS;
}
/**
* Checks whether Shift-JIS encoding is to be used when encoding strings.
*
* @return <code>true</code> for Shift-JIS encoding
*/
public boolean isShiftJIS() {
return shiftJIS;
}
/**
* When this method is called, the content of the bit buffer is written and
* the current byte is filled up with zero bits.
*
* @throws IOException if an I/O error has occurred
*/
public void align() throws IOException {
if (bitCursor > 0) {
stream.write(bitBuffer);
offset++;
bitCursor = 0;
bitBuffer = 0;
}
}
/**
* Closes the internal stream. After this, data cannot be written anymore.
*
* @throws IOException if an I/O error has occurred
*/
public void close() throws IOException {
align();
stream.close();
}
/**
* Enables stream compression (ZLIB, maximum compression level).
*/
public void enableCompression() {
if (!compressed) {
stream = new BufferedOutputStream(
new DeflaterOutputStream(
stream, new Deflater(Deflater.BEST_COMPRESSION)));
compressed = true;
}
}
/**
* Forces buffered data to be written out.
*
* @throws IOException if an I/O error has occurred
*/
public void flush() throws IOException {
stream.flush();
}
/**
* Writes a boolean byte as one bit. For <code>true</code>, a 1 is written,
* for <code>false</code>, a 0.
*
* @param value a boolean value
*
* @throws IOException if an I/O error has occurred
*/
public void writeBooleanBit(boolean value) throws IOException {
writeUnsignedBits(value ? 1 : 0, 1);
}
/**
* Writes a byte buffer.
*
* @param buffer buffer as byte array
*
* @throws IOException if an I/O error has occurred
*/
public void writeBytes(byte[] buffer) throws IOException {
align();
if (buffer == null) {
return;
}
stream.write(buffer);
offset += buffer.length;
}
/**
* Writes a double value.
*
* @param value a double
*
* @throws IOException if an I/O error has occurred
*/
public void writeDouble(double value) throws IOException {
long longBits = Double.doubleToLongBits(value);
byte[] buffer = new byte[8];
buffer[0] = (byte) (longBits >> 32);
buffer[1] = (byte) (longBits >> 40);
buffer[2] = (byte) (longBits >> 48);
buffer[3] = (byte) (longBits >> 56);
buffer[4] = (byte) longBits;
buffer[5] = (byte) (longBits >> 8);
buffer[6] = (byte) (longBits >> 16);
buffer[7] = (byte) (longBits >> 24);
writeBytes(buffer);
}
/**
* Writes a 16 bit (8.8) fixed point number.
*
* @param value fixed point number as double value
*
* @throws IOException if an I/O error occured
*/
public void writeFP16(double value) throws IOException {
writeSI16((short) (value * 256.0));
}
/**
* Writes a 32 bit (16.16) fixed point number.
*
* @param value fixed point number as double value
*
* @throws IOException if an I/O error occured
*/
public void writeFP32(double value) throws IOException {
writeSI32((int) (value * 65536.0));
}
/**
* Writes a fixed point value, using a given number of bits (e.g. computed by
* <code>getFPBitsLength()</code>).
*
* @param value a fixed point number as double value
* @param nBits number of bits to be written
*
* @throws IOException if an I/O error has occurred
*/
public void writeFPBits(double value, int nBits) throws IOException {
long fpBits = (long) (value * 65536.0);
writeSignedBits(fpBits, nBits);
}
/**
* Writes a float value.
*
* @param value a float
*
* @throws IOException if an I/O error has occurred
*/
public void writeFloat(float value) throws IOException {
writeSI32(Float.floatToIntBits(value));
}
/**
* Writes a float value as a 16 bit floating point number (half precision, or
* s10e5, i.e. 1 sign bit, 5 exponent bits and 10 mantissa bits).
*
* @param value float value
*
* @throws IOException if an I/O error occured
*/
public void writeFloat16(float value) throws IOException {
int bits32 = Float.floatToIntBits(value);
int sign = Math.abs((bits32 & 0x80000000) >> 31);
int exponent32 = (bits32 & 0x7f800000) >> 23;
int mantissa32 = bits32 & 0x7fffff;
int exponent16 = 0;
if (exponent32 != 0) {
if (exponent32 == 0xff) {
exponent16 = 0x1f;
} else {
exponent16 = exponent32 - 127 + 15;
}
}
int mantissa16 = 0;
if (exponent16 < 0) {
exponent16 = 0; // +-0
} else if (exponent16 > 0x1f) {
exponent16 = 0x1f; // +- Infinity
} else {
mantissa16 = mantissa32 >> 13;
}
int bits16 = sign << 15;
bits16 |= exponent16 << 10;
bits16 |= mantissa16;
writeUI16(bits16);
}
/**
* Writes a signed word value.
*
* @param value signed word as short value
*
* @throws IOException if an I/O error has occurred
*/
public void writeSI16(short value) throws IOException {
align();
stream.write(value & 0xFF);
stream.write(value >> 8);
offset += 2;
}
/**
* Writes a signed double word.
*
* @param value signed double word as int value
*
* @throws IOException if an I/O error has occurred
*/
public void writeSI32(int value) throws IOException {
align();
stream.write(value & 0xFF);
stream.write(value >> 8);
stream.write(value >> 16);
stream.write(value >> 24);
offset += 4;
}
/**
* Writes a signed byte.
*
* @param value signed byte as byte value
*
* @throws IOException if an I/O error has occurred
*/
public void writeSI8(byte value) throws IOException {
align();
stream.write(value);
offset++;
}
/**
* Writes a signed integer, using a given number of bits (e.g. computed by
* <code>getSignedBitsLength()</code>).
*
* @param value a signed integer as long value
* @param nBits number of bits to be written
*
* @throws IOException if an I/O error has occurred
*/
public void writeSignedBits(long value, int nBits) throws IOException {
int bitsNeeded = getSignedBitsLength(value);
if (nBits < bitsNeeded) {
throw new IOException(
"At least " + bitsNeeded + " bits needed for representation of " +
value);
}
writeInteger(value, nBits);
}
/**
* Writes an UTF-8 encoded, null-terminated string.
*
* @param string a string
*
* @throws IOException if an I/O error has occurred
*/
public void writeString(String string) throws IOException {
String encoding;
if (shiftJIS) {
encoding = "SJIS";
} else if (ansi) {
encoding = "cp1252";
} else {
encoding = "UTF-8";
}
writeBytes(string.getBytes(encoding));
stream.write(0);
offset++;
}
/**
* Writes an unsigned word.
*
* @param value an unsigned word as int value
*
* @throws IOException if an I/O error has occurred
*/
public void writeUI16(int value) throws IOException {
align();
stream.write(value & 0xFF);
stream.write(value >> 8);
offset += 2;
}
/**
* Writes an unsigned double word.
*
* @param value unsigned double word as long value
*
* @throws IOException if an I/O error has occurred
*/
public void writeUI32(long value) throws IOException {
align();
stream.write((int) (value & 0xFF));
stream.write((int) (value >> 8));
stream.write((int) (value >> 16));
stream.write((int) (value >> 24));
offset += 4;
}
/**
* Writes an unsigned byte.
*
* @param value unsigned byte as byte value
*
* @throws IOException if an I/O error has occurred
*/
public void writeUI8(short value) throws IOException {
align();
stream.write(value);
offset++;
}
/**
* Writes an unsigned integer, using a given number of bits (e.g. computed by
* <code>getUnsignedBitsLength()</code>).
*
* @param value an unsigned integer as long value
* @param nBits number of bits to be written
*
* @throws IOException if an I/O error has occurred
*/
public void writeUnsignedBits(long value, int nBits)
throws IOException {
int bitsNeeded = getUnsignedBitsLength(value);
if (nBits < bitsNeeded) {
throw new IOException(
"At least " + bitsNeeded + " bits needed for representation of " +
value + ". Used bits: " + nBits);
}
writeInteger(value, nBits);
}
private void writeInteger(long value, int nBits) throws IOException {
int bitsLeft = nBits;
while (bitsLeft > 0) {
bitCursor++;
// bit set?
if (((1L << (bitsLeft - 1)) & value) != 0) {
bitBuffer |= (1 << (8 - bitCursor));
}
if (bitCursor == 8) {
// write bit buffer
stream.write(bitBuffer);
offset++;
bitCursor = 0;
bitBuffer = 0;
}
bitsLeft--;
}
}
}