/* * SWFEncoder.java * Transform * * Copyright (c) 2001-2010 Flagstone Software Ltd. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Flagstone Software Ltd. nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.flagstone.transform.coder; import java.io.IOException; import java.io.OutputStream; import java.util.Stack; import com.flagstone.transform.CharacterEncoding; /** * SWFEncoder wraps an OutputStream with a buffer to reduce the amount of * memory required to encode a movie and to improve efficiency by writing * data to a file or external source in blocks. */ @SuppressWarnings("PMD.TooManyMethods") public final class SWFEncoder { /** The default size, in bytes, for the internal buffer. */ public static final int BUFFER_SIZE = 4096; /** Bit mask applied to bytes when converting to unsigned integers. */ private static final int BYTE_MASK = 255; /** Number of bits in an int. */ private static final int BITS_PER_INT = 32; /** Number of bits in a byte. */ private static final int BITS_PER_BYTE = 8; /** Offset to add to number of bits when calculating number of bytes. */ private static final int ROUND_TO_BYTES = 7; /** Right shift to convert number of bits to number of bytes. */ private static final int BITS_TO_BYTES = 3; /** Left shift to convert number of bytes to number of bits. */ private static final int BYTES_TO_BITS = 3; /** Number of bits to shift when aligning a value to the second byte. */ private static final int TO_BYTE1 = 8; /** Number of bits to shift when aligning a value to the third byte. */ private static final int TO_BYTE2 = 16; /** Number of bits to shift when aligning a value to the fourth byte. */ private static final int TO_BYTE3 = 24; /** The underlying input stream. */ private final transient OutputStream stream; /** The buffer for data read from the stream. */ private final transient byte[] buffer; /** The index in bytes to the current location in the buffer. */ private transient int index; /** The offset in bits to the location in the current byte. */ private transient int offset; /** The character encoding used for strings. */ private transient String encoding; /** Stack for storing file locations. */ private final transient Stack<Integer>locations; /** The position of the buffer relative to the start of the stream. */ private transient int pos; /** * Create a new SWFEncoder for the underlying InputStream with the * specified buffer size. * * @param streamOut the stream from which data will be written. * @param length the size in bytes of the buffer. */ public SWFEncoder(final OutputStream streamOut, final int length) { stream = streamOut; buffer = new byte[length]; encoding = CharacterEncoding.UTF8.getEncoding(); locations = new Stack<Integer>(); } /** * Create a new SWFEncoder for the underlying InputStream using the * default buffer size. * * @param streamOut the stream from which data will be written. */ public SWFEncoder(final OutputStream streamOut) { stream = streamOut; buffer = new byte[BUFFER_SIZE]; encoding = CharacterEncoding.UTF8.getEncoding(); locations = new Stack<Integer>(); } /** * Sets the character encoding scheme used when encoding or decoding * strings. * * @param enc * the CharacterEncoding that identifies how strings are encoded. */ public void setEncoding(final CharacterEncoding enc) { encoding = enc.getEncoding(); } /** * Remember the current position. * @return the current position. */ public int mark() { return locations.push(pos + index); } /** * Discard the last saved position. */ public void unmark() { locations.pop(); } /** * Compare the number of bytes read with the expected number and throw an * exception if there is a difference. * * @param expected the expected number of bytes read. * * @throws CoderException if the number of bytes read is different from the * expected number. */ public void check(final int expected) throws CoderException { final int actual = (pos + index) - locations.peek(); if (actual != expected) { throw new CoderException(locations.peek(), expected, actual - expected); } } /** * Changes the location to the next byte boundary. */ public void alignToByte() { if (offset > 0) { index += 1; offset = 0; } } /** * Write the data currently stored in the buffer to the underlying stream. * @throws IOException if an error occurs while writing the data to the * stream. */ public void flush() throws IOException { stream.write(buffer, 0, index); stream.flush(); int diff; if (offset == 0) { diff = 0; } else { diff = 1; buffer[0] = buffer[index]; } for (int i = diff; i < buffer.length; i++) { buffer[i] = 0; } pos += index; index = 0; } /** * Write a value to bit field. * * @param value * the value. * @param numberOfBits * the (least significant) number of bits that will be written. * @throws IOException if there is an error writing data to the underlying * stream. */ public void writeBits(final int value, final int numberOfBits) throws IOException { final int ptr = (index << BYTES_TO_BITS) + offset + numberOfBits; if (ptr >= (buffer.length << BYTES_TO_BITS)) { flush(); } final int val = ((value << (BITS_PER_INT - numberOfBits)) >>> offset) | (buffer[index] << TO_BYTE3); int base = BITS_PER_INT - (((offset + numberOfBits + ROUND_TO_BYTES) >>> BITS_TO_BYTES) << BYTES_TO_BITS); base = base < 0 ? 0 : base; int pointer = (index << BYTES_TO_BITS) + offset; for (int i = 24; i >= base; i -= BITS_PER_BYTE) { buffer[index++] = (byte) (val >>> i); } if (offset + numberOfBits > BITS_PER_INT) { buffer[index] = (byte) (value << (BITS_PER_BYTE - (offset + numberOfBits - BITS_PER_INT))); } pointer += numberOfBits; index = pointer >>> BITS_TO_BYTES; offset = pointer & Coder.LOWEST3; } /** * Write a byte. * * @param value * the value to be written - only the least significant byte will * be written. * @throws IOException if there is an error writing data to the underlying * stream. */ public void writeByte(final int value) throws IOException { if (index == buffer.length) { flush(); } buffer[index++] = (byte) value; } /** * Write an array of bytes. * * @param bytes * the array to be written. * * @return the number of bytes written. * @throws IOException if there is an error reading data from the underlying * stream. */ public int writeBytes(final byte[] bytes) throws IOException { if (index + bytes.length < buffer.length) { System.arraycopy(bytes, 0, buffer, index, bytes.length); index += bytes.length; } else { flush(); stream.write(bytes, 0, bytes.length); pos += bytes.length; } return bytes.length; } /** * Write a string using the default character set defined in the encoder. * * @param str * the string. * @throws IOException if there is an error reading data from the underlying * stream. */ public void writeString(final String str) throws IOException { try { writeBytes(str.getBytes(encoding)); buffer[index++] = 0; } catch (final java.io.UnsupportedEncodingException e) { throw new AssertionError(e); } } /** * Write a 16-bit integer. * * @param value * an integer containing the value to be written. * @throws IOException if there is an error reading data from the underlying * stream. */ public void writeShort(final int value) throws IOException { if (index + 2 > buffer.length) { flush(); } buffer[index++] = (byte) value; buffer[index++] = (byte) (value >>> TO_BYTE1); } /** * Write a 32-bit integer. * * @param value * an integer containing the value to be written. * @throws IOException if there is an error reading data from the underlying * stream. */ public void writeInt(final int value) throws IOException { if (index + 4 > buffer.length) { flush(); } buffer[index++] = (byte) value; buffer[index++] = (byte) (value >>> TO_BYTE1); buffer[index++] = (byte) (value >>> TO_BYTE2); buffer[index++] = (byte) (value >>> TO_BYTE3); } /** * Write a 32-bit unsigned integer, encoded in a variable number of bytes. * * @param value * an integer containing the value to be written. * @throws IOException if there is an error reading data from the underlying * stream. */ public void writeVarInt(final int value) throws IOException { if (index + 5 > buffer.length) { flush(); } int val = value; while (val > Coder.VAR_INT_MAX) { buffer[index++] = (byte) ((val & Coder.LOWEST7) | Coder.BIT7); val = val >>> Coder.VAR_INT_SHIFT; } buffer[index++] = (byte) (val & Coder.LOWEST7); } /** * Number of bits to shift to obtain the exponent in a half-precision * floating-point value. */ private static final int HALF_EXP_SHIFT = 10; /** * The offset to apply to the exponent in a half-precision * floating-point value. */ private static final int HALF_EXP_OFFSET = 15; /** * The maximum value of the exponent in a half-precision * floating-point value. */ private static final int HALF_EXP_MAX = 31; /** * The bit pattern used to represent Infinity in a half-precision * floating-point value. */ private static final int HALF_INF = 0x7C00; /** * Number of bits to shift to obtain the exponent in a single-precision * floating-point value. */ private static final int EXP_SHIFT = 23; /** * The maximum value of the exponent in a single-precision * floating-point value. */ private static final int EXP_MAX = 127; /** * Number of bits to shift to obtain the mantissa in a single-precision * float-point value. */ private static final int MANT_SHIFT = 13; /** * Mask to obtain the mantissa in a single-precision floating-point value. */ private static final int LOWEST23 = 0x007fffff; /** * Mask to obtain the most significant bit of the mantissa in a * single-precision floating-point value. */ private static final int BIT23 = 0x00800000; /** * Write a single-precision floating point number. * * @param value * the value to be written. * @throws IOException if there is an error reading data from the underlying * stream. */ public void writeHalf(final float value) throws IOException { final int intValue = Float.floatToIntBits(value); final int sign = (intValue >> Coder.ALIGN_BYTE2) & Coder.BIT15; final int exponent = ((intValue >> EXP_SHIFT) & BYTE_MASK) - (EXP_MAX - HALF_EXP_OFFSET); int mantissa = intValue & LOWEST23; if (exponent <= 0) { if (exponent < -10) { writeShort(0); } else { mantissa = (mantissa | BIT23) >> (1 - exponent); writeShort((sign | (mantissa >> MANT_SHIFT))); } } else if (exponent == 0xff - (EXP_MAX - HALF_EXP_OFFSET)) { if (mantissa == 0) { // Inf writeShort(sign | HALF_INF); } else { // NAN mantissa >>= MANT_SHIFT; writeShort((sign | HALF_INF | mantissa | ((mantissa == 0) ? 1 : 0))); } } else { if (exponent >= HALF_EXP_MAX) { // Overflow writeShort((sign | HALF_INF)); } else { writeShort((sign | (exponent << HALF_EXP_SHIFT) | (mantissa >> MANT_SHIFT))); } } } }