/*
* Movie.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;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import com.flagstone.transform.coder.Coder;
import com.flagstone.transform.coder.Context;
import com.flagstone.transform.coder.Copyable;
import com.flagstone.transform.coder.DecoderRegistry;
import com.flagstone.transform.coder.SWFDecoder;
import com.flagstone.transform.coder.SWFEncoder;
import com.flagstone.transform.coder.SWFFactory;
/**
* Movie is a container class for the objects that represents the data
* structures in a Flash file.
*
* <p>
* Movie is the core class of the Transform package. It is used to parse and
* generate Flash files, translating the binary format of the Flash file into an
* list objects that can be inspected and updated.
* </p>
*
* <p>
* A Movie object also contains the attributes that make up the header
* information of the Flash file, identifying the version support, size of the
* Flash Player screen, etc.
* </p>
*
* <p>
* Movie is also used to generate the unique identifiers that are used to
* reference objects. Each call to newIdentifier() returns a unique number for
* the current. The identifiers are generated using a simple counter. When a
* movie is decoded this counter is updated each time an object definition is
* decoded. This allows new objects to be added and ensures that the identifier
* does not conflict with an existing object.
* </p>
*/
public final class Movie implements Copyable<Movie> {
/** The version of Flash supported. */
public static final int VERSION = 10;
/** Length in bytes of the magic number used to identify the file type. */
private static final int SIGNATURE_LENGTH = 3;
/** Length in bytes of the signature and length fields. */
private static final int HEADER_LENGTH = 8;
/** Signature identifying Flash (SWF) files. */
public static final byte[] FWS = new byte[] {0x46, 0x57, 0x53 };
/** Signature identifying Compressed Flash (SWF) files. */
public static final byte[] CWS = new byte[] {0x43, 0x57, 0x53 };
/** Format string used in toString() method. */
private static final String FORMAT = "Movie: { objects=%s}";
/** The registry for the different types of decoder. */
private transient DecoderRegistry registry;
/** The character encoding used for strings. */
private transient CharacterEncoding encoding;
/** The list of objects that make up the movie. */
private List<MovieTag> objects;
/**
* Creates a new Movie.
*/
public Movie() {
registry = DecoderRegistry.getDefault();
encoding = CharacterEncoding.UTF8;
objects = new ArrayList<MovieTag>();
}
/**
* Creates a complete copy of this movie.
*
* @param movie the Movie to copy.
*/
public Movie(final Movie movie) {
if (movie.registry != null) {
registry = movie.registry.copy();
}
encoding = movie.encoding;
objects = new ArrayList<MovieTag>(movie.objects.size());
for (final MovieTag tag : movie.objects) {
objects.add(tag.copy());
}
}
/**
* Sets the registry containing the object used to decode the different
* types of object found in a movie.
*
* @param decoderRegistry a central registry to decoders of different types
* of object.
*/
public void setRegistry(final DecoderRegistry decoderRegistry) {
registry = decoderRegistry;
}
/**
* Sets the encoding scheme for strings encoded and decoded from Flash
* files.
*
* @param enc the character encoding used for strings.
*/
public void setEncoding(final CharacterEncoding enc) {
encoding = enc;
}
/**
* Get the list of objects contained in the Movie.
*
* @return the list of objects that make up the movie.
*/
public List<MovieTag> getObjects() {
return objects;
}
/**
* Sets the list of objects contained in the Movie.
*
* @param list
* the list of objects that describe a coder. Must not be null.
*/
public void setObjects(final List<MovieTag> list) {
if (list == null) {
throw new IllegalArgumentException();
}
objects = list;
}
/**
* Adds the object to the Movie.
*
* @param anObject
* the object to be added to the movie. Must not be null.
* @return this object.
*/
public Movie add(final MovieTag anObject) {
if (anObject == null) {
throw new IllegalArgumentException();
}
objects.add(anObject);
return this;
}
/** {@inheritDoc} */
@Override
public Movie copy() {
return new Movie(this);
}
/** {@inheritDoc} */
@Override
public String toString() {
return String.format(FORMAT, objects);
}
/**
* Decodes the contents of the specified file.
*
* @param file
* the Flash file that will be parsed.
* @throws DataFormatException
* - if the file does not contain Flash data.
* @throws IOException
* - if an I/O error occurs while reading the file.
*/
public void decodeFromFile(final File file) throws DataFormatException,
IOException {
decodeFromStream(new FileInputStream(file));
}
/**
* Decodes a Flash file referenced by a URL.
*
* @param url
* the Uniform Resource Locator referencing the file.
*
* @throws IOException
* if there is an error reading the file.
*
* @throws DataFormatException
* if there is a problem decoding the font, either it is in an
* unsupported format or an error occurred while decoding the
* font data.
*/
public void decodeFromUrl(final URL url) throws DataFormatException,
IOException {
final URLConnection connection = url.openConnection();
if (connection.getContentLength() < 0) {
throw new FileNotFoundException(url.getFile());
}
decodeFromStream(connection.getInputStream());
}
/**
* Decodes the binary Flash data from an input stream. If an error occurs
* while the data is being decoded an exception is thrown. The list of
* objects in the Movie will contain the last tag successfully decoded.
*
* @param stream
* an InputStream from which the objects will be decoded.
*
* @throws DataFormatException
* if the file does not contain Flash data.
* @throws IOException
* if an I/O error occurs while reading the file.
*/
public void decodeFromStream(final InputStream stream)
throws DataFormatException, IOException {
InputStream streamIn = null;
try {
final Context context = new Context();
context.setRegistry(registry);
context.setEncoding(encoding.getEncoding());
final byte[] signature = new byte[SIGNATURE_LENGTH];
if (stream.read(signature) != signature.length) {
throw new DataFormatException("Could not read file signature");
}
if (Arrays.equals(CWS, signature)) {
streamIn = new InflaterInputStream(stream);
context.put(Context.COMPRESSED, 1);
} else if (Arrays.equals(FWS, signature)) {
streamIn = stream;
context.put(Context.COMPRESSED, 0);
} else {
throw new DataFormatException();
}
context.put(Context.VERSION, stream.read());
int length = stream.read();
length |= stream.read() << Coder.ALIGN_BYTE1;
length |= stream.read() << Coder.ALIGN_BYTE2;
length |= stream.read() << Coder.ALIGN_BYTE3;
/*
* If the file is shorter than the default buffer size then set the
* buffer size to be the file size - this gets around a bug in Java
* where the end of ZLIB streams are not detected correctly.
*/
SWFDecoder decoder;
if (length < SWFDecoder.BUFFER_SIZE) {
decoder = new SWFDecoder(streamIn, length - HEADER_LENGTH);
} else {
decoder = new SWFDecoder(streamIn);
}
decoder.setEncoding(encoding);
objects.clear();
final SWFFactory<MovieTag> factory = registry.getMovieDecoder();
final MovieHeader header = new MovieHeader(decoder, context);
objects.add(header);
while (decoder.scanUnsignedShort() >>> Coder.LENGTH_FIELD_SIZE
!= MovieTypes.END) {
factory.getObject(objects, decoder, context);
}
decoder.readUnsignedShort();
header.setVersion(context.get(Context.VERSION));
header.setCompressed(context.get(Context.COMPRESSED) == 1);
} finally {
if (streamIn != null) {
streamIn.close();
}
}
}
/**
* Encodes the list of objects and writes the data to the specified file.
* If an error occurs while encoding the file then an exception is thrown.
*
* @param file
* the Flash file that the movie will be encoded to.
*
* @throws IOException
* - if an I/O error occurs while writing the file.
* @throws DataFormatException
* if an error occurs when compressing the flash file.
*/
public void encodeToFile(final File file) throws IOException,
DataFormatException {
encodeToStream(new FileOutputStream(file));
}
/**
* Returns the encoded representation of the list of objects that this
* Movie contains. If an error occurs while encoding the file then an
* exception is thrown.
*
* @param stream
* the output stream that the video will be encoded to.
* @throws IOException
* - if an I/O error occurs while encoding the file.
* @throws DataFormatException
* if an error occurs when compressing the flash file.
*/
public void encodeToStream(final OutputStream stream)
throws DataFormatException, IOException {
OutputStream streamOut = null;
try {
final MovieHeader header = (MovieHeader) objects.get(0);
final Context context = new Context();
context.setEncoding(encoding.getEncoding());
context.put(Context.VERSION, header.getVersion());
// length of signature, version, length and end
// CHECKSTYLE IGNORE MagicNumberCheck FOR NEXT 1 LINES
int length = 10;
int frameCount = 0;
for (final MovieTag tag : objects) {
length += tag.prepareToEncode(context);
if (tag instanceof ShowFrame) {
frameCount++;
}
}
header.setFrameCount(frameCount);
if (header.isCompressed()) {
stream.write(CWS);
} else {
stream.write(FWS);
}
stream.write(header.getVersion());
stream.write(length);
stream.write(length >>> Coder.ALIGN_BYTE1);
stream.write(length >>> Coder.ALIGN_BYTE2);
stream.write(length >>> Coder.ALIGN_BYTE3);
if (header.isCompressed()) {
streamOut = new DeflaterOutputStream(stream);
} else {
streamOut = stream;
}
final SWFEncoder coder = new SWFEncoder(streamOut);
coder.setEncoding(encoding);
for (final MovieTag tag : objects) {
tag.encode(coder, context);
}
coder.writeShort(0);
coder.flush();
} finally {
if (streamOut != null) {
streamOut.close();
}
}
}
}