org.apache.flex.swf.io.SWFWriter.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.flex.swf.io.SWFWriter.java

Source

/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.flex.swf.io;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import org.apache.commons.io.output.CountingOutputStream;

import org.apache.flex.swc.io.ISWFWriterFactory;
import org.apache.flex.swf.Header;
import org.apache.flex.swf.Header.Compression;
import org.apache.flex.swf.ISWF;
import org.apache.flex.swf.SWF;
import org.apache.flex.swf.SWFFrame;
import org.apache.flex.swf.TagType;
import org.apache.flex.swf.io.SWFReader.CurrentStyles;
import org.apache.flex.swf.tags.CSMTextSettingsTag;
import org.apache.flex.swf.tags.CharacterTag;
import org.apache.flex.swf.tags.DefineBinaryDataTag;
import org.apache.flex.swf.tags.DefineBitsJPEG2Tag;
import org.apache.flex.swf.tags.DefineBitsJPEG3Tag;
import org.apache.flex.swf.tags.DefineBitsLossless2Tag;
import org.apache.flex.swf.tags.DefineBitsLosslessTag;
import org.apache.flex.swf.tags.DefineBitsTag;
import org.apache.flex.swf.tags.DefineButton2Tag;
import org.apache.flex.swf.tags.DefineButtonSoundTag;
import org.apache.flex.swf.tags.DefineButtonTag;
import org.apache.flex.swf.tags.DefineEditTextTag;
import org.apache.flex.swf.tags.DefineFont2Tag;
import org.apache.flex.swf.tags.DefineFont3Tag;
import org.apache.flex.swf.tags.DefineFont4Tag;
import org.apache.flex.swf.tags.DefineFontAlignZonesTag;
import org.apache.flex.swf.tags.DefineFontInfo2Tag;
import org.apache.flex.swf.tags.DefineFontNameTag;
import org.apache.flex.swf.tags.DefineFontTag;
import org.apache.flex.swf.tags.DefineMorphShape2Tag;
import org.apache.flex.swf.tags.DefineMorphShapeTag;
import org.apache.flex.swf.tags.DefineScalingGridTag;
import org.apache.flex.swf.tags.DefineShape2Tag;
import org.apache.flex.swf.tags.DefineShape3Tag;
import org.apache.flex.swf.tags.DefineShape4Tag;
import org.apache.flex.swf.tags.DefineShapeTag;
import org.apache.flex.swf.tags.DefineSoundTag;
import org.apache.flex.swf.tags.DefineSpriteTag;
import org.apache.flex.swf.tags.DefineText2Tag;
import org.apache.flex.swf.tags.DefineTextTag;
import org.apache.flex.swf.tags.DefineVideoStreamTag;
import org.apache.flex.swf.tags.DoABCTag;
import org.apache.flex.swf.tags.EnableDebugger2Tag;
import org.apache.flex.swf.tags.EndTag;
import org.apache.flex.swf.tags.ExportAssetsTag;
import org.apache.flex.swf.tags.FileAttributesTag;
import org.apache.flex.swf.tags.FrameLabelTag;
import org.apache.flex.swf.tags.IAlwaysLongTag;
import org.apache.flex.swf.tags.ICharacterTag;
import org.apache.flex.swf.tags.IFontInfo;
import org.apache.flex.swf.tags.ITag;
import org.apache.flex.swf.tags.JPEGTablesTag;
import org.apache.flex.swf.tags.MetadataTag;
import org.apache.flex.swf.tags.PlaceObject2Tag;
import org.apache.flex.swf.tags.PlaceObject3Tag;
import org.apache.flex.swf.tags.PlaceObjectTag;
import org.apache.flex.swf.tags.ProductInfoTag;
import org.apache.flex.swf.tags.RawTag;
import org.apache.flex.swf.tags.RemoveObject2Tag;
import org.apache.flex.swf.tags.RemoveObjectTag;
import org.apache.flex.swf.tags.ScriptLimitsTag;
import org.apache.flex.swf.tags.SetBackgroundColorTag;
import org.apache.flex.swf.tags.SetTabIndexTag;
import org.apache.flex.swf.tags.SoundStreamBlockTag;
import org.apache.flex.swf.tags.SoundStreamHead2Tag;
import org.apache.flex.swf.tags.SoundStreamHeadTag;
import org.apache.flex.swf.tags.StartSound2Tag;
import org.apache.flex.swf.tags.StartSoundTag;
import org.apache.flex.swf.tags.SymbolClassTag;
import org.apache.flex.swf.tags.VideoFrameTag;
import org.apache.flex.swf.types.ARGB;
import org.apache.flex.swf.types.BevelFilter;
import org.apache.flex.swf.types.BlurFilter;
import org.apache.flex.swf.types.ButtonRecord;
import org.apache.flex.swf.types.CXForm;
import org.apache.flex.swf.types.CXFormWithAlpha;
import org.apache.flex.swf.types.ConvolutionFilter;
import org.apache.flex.swf.types.CurvedEdgeRecord;
import org.apache.flex.swf.types.DropShadowFilter;
import org.apache.flex.swf.types.EndShapeRecord;
import org.apache.flex.swf.types.FillStyle;
import org.apache.flex.swf.types.FillStyleArray;
import org.apache.flex.swf.types.Filter;
import org.apache.flex.swf.types.FocalGradient;
import org.apache.flex.swf.types.GlowFilter;
import org.apache.flex.swf.types.GlyphEntry;
import org.apache.flex.swf.types.GradRecord;
import org.apache.flex.swf.types.Gradient;
import org.apache.flex.swf.types.GradientBevelFilter;
import org.apache.flex.swf.types.GradientGlowFilter;
import org.apache.flex.swf.types.IFillStyle;
import org.apache.flex.swf.types.ILineStyle;
import org.apache.flex.swf.types.KerningRecord;
import org.apache.flex.swf.types.LineStyle;
import org.apache.flex.swf.types.LineStyle2;
import org.apache.flex.swf.types.LineStyleArray;
import org.apache.flex.swf.types.Matrix;
import org.apache.flex.swf.types.MorphFillStyle;
import org.apache.flex.swf.types.MorphGradRecord;
import org.apache.flex.swf.types.MorphGradient;
import org.apache.flex.swf.types.MorphLineStyle;
import org.apache.flex.swf.types.MorphLineStyle2;
import org.apache.flex.swf.types.RGB;
import org.apache.flex.swf.types.RGBA;
import org.apache.flex.swf.types.Rect;
import org.apache.flex.swf.types.Shape;
import org.apache.flex.swf.types.ShapeRecord;
import org.apache.flex.swf.types.ShapeWithStyle;
import org.apache.flex.swf.types.SoundEnvelope;
import org.apache.flex.swf.types.SoundInfo;
import org.apache.flex.swf.types.StraightEdgeRecord;
import org.apache.flex.swf.types.StyleChangeRecord;
import org.apache.flex.swf.types.Styles;
import org.apache.flex.swf.types.TextRecord;
import org.apache.flex.swf.types.ZoneRecord;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Ints;

/**
 * The implementation of SWF tag, type encoding logic. The SWF file body are
 * buffered in memory using {@code IOutputBitStream}. ZLIB compression is
 * optional. If enabled, compression is on-the-fly via a filtered output stream.
 */
public class SWFWriter implements ISWFWriter {
    /**
     * Default SWF writer factory.
     */
    private static class SWFWriterFactory implements ISWFWriterFactory {

        @Override
        public ISWFWriter createSWFWriter(ISWF swf, Compression useCompression, boolean enableDebug) {
            return new SWFWriter(swf, useCompression, enableDebug);
        }

    }

    /**
     * A factory for default SWF writers. These SWF writers just write SWFs
     * without any additional features.
     */
    public static final ISWFWriterFactory DEFAULT_SWF_WRITER_FACTORY = new SWFWriterFactory();

    private static final int RESERVED = 0;
    private static final int SHORT_TAG_MAX_LENGTH = 62;

    /**
     * Compares the absolute values of 4 signed integers and returns the
     * unsigned magnitude of the number with the greatest absolute value.
     */
    public static int maxNum(int a, int b, int c, int d) {
        // take the absolute values of the given numbers
        a = Math.abs(a);
        b = Math.abs(b);
        c = Math.abs(c);
        d = Math.abs(d);

        // compare the numbers and return the unsigned value of the one with the greatest magnitude
        return Ints.max(a, b, c, d);
    }

    /**
     * Compares the absolute values of 4 signed doubles and returns the unsigned
     * magnitude of the number with the greatest absolute value.
     */
    public static double maxNum(double a, double b, double c, double d) {
        // take the absolute values of the given numbers
        a = Math.abs(a);
        b = Math.abs(b);
        c = Math.abs(c);
        d = Math.abs(d);

        // compare the numbers and return the unsigned value of the one with the greatest magnitude
        return Doubles.max(a, b, c, d);
    }

    /**
     * Calculate number of bits required to represent the given value in double
     * bit value.
     * 
     * @param value signed integer
     * @return number of bits required for SB[]
     */
    public static int requireFBCount(double value) {
        return requireSBCount((int) (value * 0x10000));
    }

    /**
     * Calculate number of bits required to represent the given value in signed
     * bit values.
     * 
     * @param value signed integer
     * @return number of bits required for SB[]
     */
    public static int requireSBCount(int value) {
        return minBits(Math.abs(value), 1);
    }

    public static int requireSBCount(int... values) {
        final int array[] = new int[values.length];
        for (int i = 0; i < values.length; i++)
            array[i] = requireSBCount(values[i]);
        Arrays.sort(array); // ascending order: last one is the biggest
        return array[array.length - 1];
    }

    /**
     * Calculate number of bits required to represent the given value in
     * unsigned bit values.
     * 
     * @param value signed integer
     * @return number of bits required for SB[]
     */
    public static int requireUBCount(int value) {
        assert (value >= 0) : "requireUBCount called with negative number";
        return minBits(value, 0);
    }

    /**
     * Calculates the minimum number of bits necessary to represent the given
     * number. The number should be given in its unsigned form with the starting
     * bits equal to 1 if it is signed. Repeatedly compares number to another
     * unsigned int called x. x is initialized to 1. The value of x is shifted
     * left i times until x is greater than number. Now i is equal to the number
     * of bits the UNSIGNED value of number needs. The signed value will need
     * one more bit for the sign so i+1 is returned if the number is signed, and
     * i is returned if the number is unsigned.
     * 
     * @param number the number to compute the size of
     * @param bits 1 if number is signed, 0 if unsigned
     */
    private static int minBits(int number, int bits) {
        int val = 1;
        for (int x = 1; val <= number && !(bits > 32); x <<= 1) {
            val = val | x;
            bits++;
        }

        assert (bits <= 32) : ("minBits " + bits + " must not exceed 32");

        return bits;
    }

    private void writeLengthString(String name) {
        try {
            assert (tagBuffer.getBitPos() == 8);
            byte[] b = swf.getVersion() >= 6 ? name.getBytes("UTF8") : name.getBytes();

            // [paul] Flash Authoring and the player expect the String
            // to be null terminated.
            tagBuffer.writeUI8(b.length + 1);
            tagBuffer.write(b);
            tagBuffer.writeUI8(0);
        } catch (UnsupportedEncodingException e) {
            assert false;
        }
    }

    // Tag buffer
    protected IOutputBitStream tagBuffer;

    // SWF model
    private final ISWF swf;

    // This buffer contains the SWF data after FileLength field. 
    protected final IOutputBitStream outputBuffer;

    // True if the encoded SWF file is compressed.
    private final Header.Compression useCompression;

    private final boolean enableDebug; // if true enable debugging of the SWF.

    // Current frame index. Updated in writeFrames().
    private int currentFrameIndex;

    // Prevent writing out the same tag twice.
    private Set<ITag> writtenTags;

    /**
     * Create a SWF writer.
     * 
     * @param swf the SWF model to be encoded
     * @param useCompression use ZLIB compression if true
     */
    public SWFWriter(ISWF swf, Header.Compression useCompression) {
        this(swf, useCompression, false);
    }

    /**
     * Create a SWF writer.
     * 
     * @param swf the SWF model to be encoded
     * @param useCompression use ZLIB compression if true
     * @param enableDebug enable debugging of the SWF if true
     */
    public SWFWriter(ISWF swf, Header.Compression useCompression, boolean enableDebug) {
        this.swf = swf;
        this.useCompression = useCompression;
        this.enableDebug = enableDebug;
        this.outputBuffer = new OutputBitStream(false);
        this.tagBuffer = new OutputBitStream(false);

        computeCharacterID();
    }

    /**
     * Compute the character ID for all the {@code ICharacterTag}s.
     */
    private void computeCharacterID() {
        int id = 1; // need to start at 1, as index 0 has special meaning
        for (int frameIndex = 0; frameIndex < swf.getFrameCount(); frameIndex++) {
            final SWFFrame frame = swf.getFrameAt(frameIndex);
            for (final ITag tag : frame) {
                if (tag instanceof CharacterTag) {
                    final CharacterTag characterTag = (CharacterTag) tag;
                    characterTag.setCharacterID(id);
                    id++;
                }
            }
        }
    }

    /**
     * Compute the tag length for the tag header, then write the header and the
     * buffered tag body to target output stream.
     * 
     * @param tag tag object
     * @param tagData serialized tag body
     * @param out target output stream
     */
    protected void finishTag(final ITag tag, final IOutputBitStream tagData, final IOutputBitStream out) {
        tagData.flush();
        final int tagLength = tagData.size();

        // write tag header
        if (tag instanceof IAlwaysLongTag || tagLength > SHORT_TAG_MAX_LENGTH) {
            // use long tag header
            out.writeUI16((tag.getTagType().getValue() << 6) | 0x3F);
            out.writeSI32(tagLength);
        } else {
            // use short tag header
            out.writeUI16((tag.getTagType().getValue() << 6) | tagLength);
        }
        out.write(tagData.getBytes(), 0, tagLength);
    }

    public void writeARGB(ARGB argb) {
        tagBuffer.writeUI8(argb.getAlpha());
        tagBuffer.writeUI8(argb.getRed());
        tagBuffer.writeUI8(argb.getGreen());
        tagBuffer.writeUI8(argb.getBlue());
    }

    protected void writeCompressibleHeader() {
        // Frame size
        final Rect rect = swf.getFrameSize();
        tagBuffer.reset();
        writeRect(rect);
        outputBuffer.write(tagBuffer.getBytes(), 0, tagBuffer.size());

        // Frame rate
        outputBuffer.writeFIXED8(swf.getFrameRate());

        // Frame count
        outputBuffer.writeUI16(swf.getFrameCount());
    }

    /**
     * @see SWFReader#readCurvedEdgeRecord
     */
    private void writeCurvedEdgeRecord(CurvedEdgeRecord shape) {
        tagBuffer.writeBit(true); // This is an edge. Always 1.
        tagBuffer.writeBit(false); // StraightFlag is always false.
        int numBits = shape.getNumBits();
        tagBuffer.writeUB(numBits, 4);
        tagBuffer.writeSB(shape.getControlDeltaX(), numBits + 2);
        tagBuffer.writeSB(shape.getControlDeltaY(), numBits + 2);
        tagBuffer.writeSB(shape.getAnchorDeltaX(), numBits + 2);
        tagBuffer.writeSB(shape.getAnchorDeltaY(), numBits + 2);
    }

    private void writeDefineBinaryData(DefineBinaryDataTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUI32(0); // Reserved, always zero.
        tagBuffer.write(tag.getData());
    }

    /**
     * This method treats the bytes after the color table as a binary blob so
     * both the lossless and lossless2 tags can be written using this method.
     * 
     * @param tag
     */
    private void writeDefineBitsLossless(DefineBitsLosslessTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUI8(tag.getBitmapFormat());
        tagBuffer.writeUI16(tag.getBitmapWidth());
        tagBuffer.writeUI16(tag.getBitmapHeight());
        if (DefineBitsLossless2Tag.BF_8BIT_COLORMAPPED_IMAGE == tag.getBitmapFormat()) {
            tagBuffer.writeUI8(tag.getBitmapColorTableSize() - 1);
        }
        tagBuffer.write(tag.getZlibBitmapData());
    }

    /**
     * @see SWFReader#readDefineShape
     */
    private void writeDefineShape(DefineShapeTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        writeRect(tag.getShapeBounds());
        writeShapeWithStyle(tag.getShapes(), tag.getTagType());
    }

    /**
     * @see SWFReader#readDefineShape2
     */
    private void writeDefineShape2(DefineShape2Tag tag) {
        writeDefineShape(tag);
    }

    /**
     * @see SWFReader#readDefineShape3
     */
    private void writeDefineShape3(DefineShape3Tag tag) {
        writeDefineShape2(tag);
    }

    /**
     * @see SWFReader#readDefineShape4
     */
    private void writeDefineShape4(DefineShape4Tag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        writeRect(tag.getShapeBounds());
        writeRect(tag.getEdgeBounds());
        tagBuffer.byteAlign();
        tagBuffer.writeUB(0, 5); // Reserved. Must be 0.
        tagBuffer.writeBit(tag.isUsesFillWindingRule());
        tagBuffer.writeBit(tag.isUsesNonScalingStrokes());
        tagBuffer.writeBit(tag.isUsesScalingStrokes());
        tagBuffer.byteAlign();
        writeShapeWithStyle(tag.getShapes(), tag.getTagType());
    }

    /**
     * @see SWFReader#readDefineMorphShape2
     * @see SWFWriter#writeDefineMorphShape
     */
    private void writeDefineMorphShape2(DefineMorphShape2Tag tag) {
        writeDefineMorphShape(tag);
    }

    /**
     * @see SWFReader#readDefineMorphShape
     */
    private void writeDefineMorphShape(DefineMorphShapeTag tag) {
        // Write to another buffer to calculate offset to EndEdges field.
        final IOutputBitStream originalTagBuffer = tagBuffer;
        tagBuffer = new OutputBitStream();

        // fields before "offset"
        tagBuffer.writeUI16(tag.getCharacterID());
        writeRect(tag.getStartBounds());
        writeRect(tag.getEndBounds());
        if (TagType.DefineMorphShape2 == tag.getTagType()) {
            final DefineMorphShape2Tag tag2 = (DefineMorphShape2Tag) tag;
            writeRect(tag2.getStartEdgeBounds());
            writeRect(tag2.getEndEdgeBounds());
            tagBuffer.writeUB(0, 6);
            tagBuffer.writeBit(tag2.isUsesNonScalingStrokes());
            tagBuffer.writeBit(tag2.isUsesScalingStrokes());
            tagBuffer.byteAlign();
        }
        final int sizeBeforeOffset = tagBuffer.size();

        // fields after "offset"

        Shape startEdges = tag.getStartEdges();
        writeShapeWithStyle((ShapeWithStyle) startEdges, tag.getTagType());

        final int sizeAfterOffsetToEnd = tagBuffer.size() - sizeBeforeOffset;

        // put together
        originalTagBuffer.write(tagBuffer.getBytes(), 0, sizeBeforeOffset);
        originalTagBuffer.writeUI32(sizeAfterOffsetToEnd);
        originalTagBuffer.write(tagBuffer.getBytes(), sizeBeforeOffset, sizeAfterOffsetToEnd);

        // recover current tag buffer
        tagBuffer = originalTagBuffer;

        writeShape(tag.getEndEdges(), tag.getTagType(), 0, 0);
    }

    /**
     * @see SWFReader#readShape
     */
    private void writeShape(Shape shape, TagType tagType, int fillStyleCount, int lineStyleCount) {
        CurrentStyles currentStyles = new CurrentStyles();
        currentStyles.numFillBits = requireUBCount(fillStyleCount);
        currentStyles.numLineBits = requireUBCount(lineStyleCount);
        writeShape(shape, tagType, currentStyles);
    }

    /**
     * @see SWFReader#readShape
     */
    private void writeShape(Shape shape, TagType tagType, CurrentStyles currentStyles) {
        tagBuffer.writeUB(currentStyles.numFillBits, 4);
        tagBuffer.writeUB(currentStyles.numLineBits, 4);
        for (final ShapeRecord shapeRecord : shape.getShapeRecords()) {
            writeShapeRecord(shapeRecord, tagType, currentStyles);
        }

        writeShapeRecord(new EndShapeRecord(), tagType, currentStyles);
        tagBuffer.byteAlign();
    }

    /**
     * @see SWFReader#readMorphLineStyleArray
     */
    //    private void writeMorphLineStyleArray(MorphLineStyleArray lineStyles)
    //    {
    //        writeExtensibleCount(lineStyles.size());
    //        for (MorphLineStyle lineStyle : lineStyles)
    //        {
    //            writeMorphLineStyle(lineStyle);
    //        }
    //    }

    /**
     * @see SWFReader#readMorphLineStyle
     */
    private void writeMorphLineStyle(MorphLineStyle lineStyle) {
        tagBuffer.writeUI16(lineStyle.getStartWidth());
        tagBuffer.writeUI16(lineStyle.getEndWidth());
        writeRGBA(lineStyle.getStartColor());
        writeRGBA(lineStyle.getEndColor());
    }

    private void writeMorphLineStyle2(MorphLineStyle2 lineStyle, TagType tagType) {
        // widths
        tagBuffer.writeUI16(lineStyle.getStartWidth());
        tagBuffer.writeUI16(lineStyle.getEndWidth());

        // misc fields byte
        tagBuffer.writeUB(lineStyle.getStartCapStyle(), 2);
        tagBuffer.writeUB(lineStyle.getJoinStyle(), 2);
        tagBuffer.writeBit(lineStyle.isHasFillFlag());
        tagBuffer.writeBit(lineStyle.isNoHScaleFlag());
        tagBuffer.writeBit(lineStyle.isNoVScaleFlag());
        tagBuffer.writeBit(lineStyle.isPixelHintingFlag());

        // next mixed byte
        tagBuffer.writeUB(0, 5); // reserved
        tagBuffer.writeBit(lineStyle.isNoClose());
        tagBuffer.writeUB(lineStyle.getEndCapStyle(), 2);

        //
        if (lineStyle.getJoinStyle() == LineStyle2.JS_MITER_JOIN) {
            tagBuffer.writeUI16(lineStyle.getMiterLimitFactor());
        }

        if (!lineStyle.isHasFillFlag()) {
            writeRGBA(lineStyle.getStartColor());
            writeRGBA(lineStyle.getEndColor());
        } else {
            writeMorphFillStyle(lineStyle.getFillType(), tagType);
        }
    }

    /**
     * @see SWFReader#readMorphFillStyle
     */
    private void writeMorphFillStyle(MorphFillStyle fillStyle, TagType tagType) {
        int fillStyleType = fillStyle.getFillStyleType();
        tagBuffer.writeUI8(fillStyleType);
        switch (fillStyle.getFillStyleType()) {
        case FillStyle.SOLID_FILL:
            writeRGBA(fillStyle.getStartColor());
            writeRGBA(fillStyle.getEndColor());
            break;
        case FillStyle.LINEAR_GRADIENT_FILL:
        case FillStyle.RADIAL_GRADIENT_FILL:
        case FillStyle.FOCAL_RADIAL_GRADIENT_FILL:
            writeMatrix(fillStyle.getStartGradientMatrix());
            writeMatrix(fillStyle.getEndGradientMatrix());
            writeMorphGradient(fillStyle.getGradient());
            if (fillStyleType == FillStyle.FOCAL_RADIAL_GRADIENT_FILL
                    && tagType.getValue() == TagType.DefineMorphShape2.getValue()) {
                tagBuffer.writeSI16(fillStyle.getRatio1());
                tagBuffer.writeSI16(fillStyle.getRatio2());
            }
            break;
        case FillStyle.REPEATING_BITMAP_FILL:
        case FillStyle.CLIPPED_BITMAP_FILL:
        case FillStyle.NON_SMOOTHED_CLIPPED_BITMAP:
        case FillStyle.NON_SMOOTHED_REPEATING_BITMAP:
            tagBuffer.writeUI16(fillStyle.getBitmap().getCharacterID());
            writeMatrix(fillStyle.getStartBitmapMatrix());
            writeMatrix(fillStyle.getEndBitmapMatrix());
            break;
        }
    }

    /**
     * @see SWFReader#readMorphGradient
     */
    private void writeMorphGradient(MorphGradient gradient) {
        tagBuffer.writeUI8(gradient.size());
        for (MorphGradRecord morphGradRecord : gradient) {
            writeMorphGradRecord(morphGradRecord);
        }
    }

    /**
     * @see SWFReader#readMorphGradRecord
     */
    private void writeMorphGradRecord(MorphGradRecord morphGradRecord) {
        tagBuffer.writeUI8(morphGradRecord.getStartRatio());
        writeRGBA(morphGradRecord.getStartColor());
        tagBuffer.writeUI8(morphGradRecord.getEndRatio());
        writeRGBA(morphGradRecord.getEndColor());
    }

    /**
     * @see SWFReader#readExtensibleCount
     * @param count
     */
    private void writeExtensibleCount(int count) {
        if (count < 0xFF) {
            tagBuffer.writeUI8(count);
        } else {
            tagBuffer.writeUI8(0xFF);
            tagBuffer.writeUI16(count);
        }
    }

    public void writeDoABC(DoABCTag tag) {
        assert swf.getUseAS3() : "DoABC tag requires FileAttributes.Actionscript3=true.";
        tagBuffer.writeUI32(tag.getFlags());
        tagBuffer.writeString(tag.getName());
        tagBuffer.write(tag.getABCData());
    }

    private void writeEnableDebugger2(EnableDebugger2Tag tag) {
        tagBuffer.writeUI16(0); // reserved always zero
        tagBuffer.writeString(tag.getPassword());
    }

    private void writeEnd() {
        // End tag has no tag body.
    }

    private void writeEndShapeRecord(EndShapeRecord shape) {
        tagBuffer.writeBit(shape.getTypeFlag());
        tagBuffer.writeUB(0, 5); // EndOfShape always 0.
    }

    public void writeFileAttributes(FileAttributesTag tag) {
        tagBuffer.writeBit(false);
        tagBuffer.writeBit(tag.isUseDirectBlit());
        tagBuffer.writeBit(tag.isUseGPU());
        tagBuffer.writeBit(tag.isHasMetadata());
        tagBuffer.writeBit(tag.isAS3());
        tagBuffer.writeUB(RESERVED, 2);
        tagBuffer.writeBit(tag.isUseNetwork());
        tagBuffer.writeUB(RESERVED, 24);
        tagBuffer.byteAlign();
    }

    private void writeFillStyle(IFillStyle fillStyle, TagType tagType) {
        if (fillStyle instanceof FillStyle)
            writeFillStyle((FillStyle) fillStyle, tagType);
        else if (fillStyle instanceof MorphFillStyle)
            writeMorphFillStyle((MorphFillStyle) fillStyle, tagType);
        else
            assert false;
    }

    private void writeFillStyle(FillStyle fillStyle, TagType tagType) {
        assert fillStyle != null;

        final int fillStyleType = fillStyle.getFillStyleType();
        tagBuffer.writeUI8(fillStyleType);

        switch (fillStyleType) {
        case FillStyle.SOLID_FILL:
            switch (tagType) {
            case DefineShape3:
            case DefineShape4:
                writeRGBA((RGBA) fillStyle.getColor());
                break;
            case DefineShape:
            case DefineShape2:
                writeRGB(fillStyle.getColor());
                break;
            default:
                throw new IllegalArgumentException("Invalid tag: " + tagType);
            }
            break;
        case FillStyle.LINEAR_GRADIENT_FILL:
        case FillStyle.RADIAL_GRADIENT_FILL:
            writeMatrix(fillStyle.getGradientMatrix());
            writeGradient(fillStyle.getGradient(), tagType);
            break;
        case FillStyle.FOCAL_RADIAL_GRADIENT_FILL:
            writeMatrix(fillStyle.getGradientMatrix());
            writeFocalGradient((FocalGradient) fillStyle.getGradient(), tagType);
            break;
        case FillStyle.REPEATING_BITMAP_FILL:
        case FillStyle.CLIPPED_BITMAP_FILL:
        case FillStyle.NON_SMOOTHED_REPEATING_BITMAP:
        case FillStyle.NON_SMOOTHED_CLIPPED_BITMAP:
            tagBuffer.writeUI16(fillStyle.getBitmapCharacter().getCharacterID());
            writeMatrix(fillStyle.getBitmapMatrix());
            break;
        default:
            throw new IllegalArgumentException("Invalid FillStyleType: " + fillStyleType);
        }
    }

    private void writeFillStyles(FillStyleArray fillStyles, TagType tagType) {
        assert fillStyles != null;

        final int fillStyleCount = fillStyles.size();
        writeExtensibleCount(fillStyleCount);
        for (final IFillStyle fillStyle : fillStyles) {
            writeFillStyle(fillStyle, tagType);
        }
    }

    private void writeFocalGradient(FocalGradient gradient, TagType tagType) {
        assert TagType.DefineShape4 == tagType;
        writeGradient(gradient, tagType);
        tagBuffer.writeFIXED8(gradient.getFocalPoint());
    }

    private void writeFrames() {
        for (currentFrameIndex = 0; currentFrameIndex < swf.getFrameCount(); currentFrameIndex++) {
            final SWFFrame frame = swf.getFrameAt(currentFrameIndex);

            // If the SWF has a top level class name, the first frame must contain a SymbolClass tag.
            if (0 == currentFrameIndex && null != swf.getTopLevelClass()) {
                SWFFrame.forceSymbolClassTag(frame);
            }

            for (final ITag tag : frame) {
                writeTag(tag);
            }
        }
    }

    private void writeGradient(Gradient gradient, TagType tagType) {
        assert gradient != null;
        assert gradient.getGradientRecords() != null;

        tagBuffer.writeUB(gradient.getSpreadMode(), 2);
        tagBuffer.writeUB(gradient.getInterpolationMode(), 2);
        tagBuffer.writeUB(gradient.getGradientRecords().size(), 4);
        tagBuffer.byteAlign();

        for (final GradRecord gradRecord : gradient.getGradientRecords()) {
            writeGradRecord(gradRecord, tagType);
        }
    }

    private void writeGradRecord(GradRecord gradRecord, TagType tagType) {
        assert gradRecord != null;

        tagBuffer.writeUI8(gradRecord.getRatio());

        switch (tagType) {
        case DefineShape:
        case DefineShape2:
            writeRGB(gradRecord.getColor());
            break;
        case DefineShape3:
        case DefineShape4:
            writeRGBA((RGBA) gradRecord.getColor());
            break;
        default:
            throw new IllegalArgumentException("Invalid tag: " + tagType);
        }
    }

    private void writeLineStyle(LineStyle lineStyle, TagType tagType) {
        assert lineStyle != null;

        tagBuffer.writeUI16(lineStyle.getWidth());

        switch (tagType) {
        case DefineShape:
        case DefineShape2:
            writeRGB(lineStyle.getColor());
            break;
        case DefineShape3:
            writeRGBA((RGBA) lineStyle.getColor());
            break;
        default:
            throw new IllegalArgumentException("Invalid tag: " + tagType);
        }
    }

    private void writeLineStyle2(LineStyle2 lineStyle, TagType tagType) {
        assert lineStyle != null;

        tagBuffer.writeUI16(lineStyle.getWidth());
        tagBuffer.writeUB(lineStyle.getStartCapStyle(), 2);
        tagBuffer.writeUB(lineStyle.getJoinStyle(), 2);
        tagBuffer.writeBit(lineStyle.isHasFillFlag());
        tagBuffer.writeBit(lineStyle.isNoHScaleFlag());
        tagBuffer.writeBit(lineStyle.isNoVScaleFlag());
        tagBuffer.writeBit(lineStyle.isPixelHintingFlag());
        tagBuffer.writeUB(0, 5); // Reserved
        tagBuffer.writeBit(lineStyle.isNoClose());
        tagBuffer.writeUB(lineStyle.getEndCapStyle(), 2);
        tagBuffer.byteAlign();

        if (LineStyle2.JS_MITER_JOIN == lineStyle.getJoinStyle()) {
            tagBuffer.writeFIXED8(lineStyle.getMiterLimitFactor());
        }

        if (lineStyle.isHasFillFlag()) {
            writeFillStyle(lineStyle.getFillType(), tagType);
        } else {
            writeRGBA((RGBA) lineStyle.getColor());
        }

    }

    private void writeLineStyles(LineStyleArray lineStyles, TagType tagType) {
        assert lineStyles != null;
        final int lineStyleCount = lineStyles.size();
        writeExtensibleCount(lineStyleCount);
        for (final ILineStyle lineStyle : lineStyles) {
            switch (tagType) {
            case DefineShape:
            case DefineShape2:
            case DefineShape3:
                writeLineStyle((LineStyle) lineStyle, tagType);
                break;
            case DefineShape4:
                writeLineStyle2((LineStyle2) lineStyle, tagType);
                break;
            case DefineMorphShape2:
                writeMorphLineStyle2((MorphLineStyle2) lineStyle, tagType);
                break;
            case DefineMorphShape:
                writeMorphLineStyle((MorphLineStyle) lineStyle);
                break;
            default:
                throw new IllegalArgumentException("Invalid tag: " + tagType);
            }
        }
    }

    /**
     * @param gradientMatrix
     */
    private void writeMatrix(Matrix matrix) {
        // scale (optional)
        tagBuffer.writeBit(matrix.hasScale());
        if (matrix.hasScale()) {
            final double scaleX = matrix.getScaleX();
            final double scaleY = matrix.getScaleY();
            final int nScaleBits = requireFBCount(maxNum(scaleX, scaleY, 0, 0));
            tagBuffer.writeUB(nScaleBits, 5);
            tagBuffer.writeFB(scaleX, nScaleBits);
            tagBuffer.writeFB(scaleY, nScaleBits);
        }

        // rotate-skew (optional)
        tagBuffer.writeBit(matrix.hasRotate());
        if (matrix.hasRotate()) {
            final double rotateSkew0 = matrix.getRotateSkew0();
            final double rotateSkew1 = matrix.getRotateSkew1();
            final int nRotateBits = requireFBCount(maxNum(rotateSkew0, rotateSkew1, 0, 0));
            tagBuffer.writeUB(nRotateBits, 5);
            tagBuffer.writeFB(rotateSkew0, nRotateBits);
            tagBuffer.writeFB(rotateSkew1, nRotateBits);
        }

        // translate (always)
        final int translateX = matrix.getTranslateX();
        final int translateY = matrix.getTranslateY();
        final int nTranslateBits = requireSBCount(maxNum(translateX, translateY, 0, 0));
        tagBuffer.writeUB(nTranslateBits, 5);
        tagBuffer.writeSB(translateX, nTranslateBits);
        tagBuffer.writeSB(translateY, nTranslateBits);

        tagBuffer.byteAlign();
    }

    /* Tag Encoders */

    private void writeMetadata(MetadataTag tag) {
        tagBuffer.writeString(tag.getMetadata());
    }

    private void writeProductInfo(ProductInfoTag tag) {
        tagBuffer.writeUI32(tag.getProduct().getCode());
        tagBuffer.writeUI32(tag.getEdition().getCode());
        tagBuffer.writeUI8(tag.getMajorVersion());
        tagBuffer.writeUI8(tag.getMinorVersion());
        tagBuffer.writeSI64(tag.getBuild());
        tagBuffer.writeSI64(tag.getCompileDate());
    }

    public void writeRect(Rect rect) {
        int maxRectNum = maxNum(rect.xMin(), rect.xMax(), rect.yMin(), rect.yMax());
        final int nbits = requireSBCount(maxRectNum);
        tagBuffer.writeUB(nbits, 5);
        tagBuffer.writeSB(rect.xMin(), nbits);
        tagBuffer.writeSB(rect.xMax(), nbits);
        tagBuffer.writeSB(rect.yMin(), nbits);
        tagBuffer.writeSB(rect.yMax(), nbits);
        tagBuffer.byteAlign();
    }

    public void writeRGB(RGB rgb) {
        tagBuffer.writeUI8(rgb.getRed());
        tagBuffer.writeUI8(rgb.getGreen());
        tagBuffer.writeUI8(rgb.getBlue());
    }

    public void writeRGBA(RGBA rgba) {
        tagBuffer.writeUI8(rgba.getRed());
        tagBuffer.writeUI8(rgba.getGreen());
        tagBuffer.writeUI8(rgba.getBlue());
        tagBuffer.writeUI8(rgba.getAlpha());
    }

    private void writeScriptLimits(ScriptLimitsTag tag) {
        tagBuffer.writeUI16(tag.getMaxRecursionDepth());
        tagBuffer.writeUI16(tag.getScriptTimeoutSeconds());
    }

    private void writeSetBackgroundColor(SetBackgroundColorTag tag) {
        writeRGB(tag.getColor());
    }

    private void writeShapeRecord(final ShapeRecord shape, final TagType tagType,
            final CurrentStyles currentStyles) {
        switch (shape.getShapeRecordType()) {
        case END_SHAPE:
            writeEndShapeRecord((EndShapeRecord) shape);
            break;
        case CURVED_EDGE:
            writeCurvedEdgeRecord((CurvedEdgeRecord) shape);
            break;
        case STRAIGHT_EDGE:
            writeStraightEdgeRecord((StraightEdgeRecord) shape);
            break;
        case STYLE_CHANGE:
            writeStyleChangeRecord((StyleChangeRecord) shape, tagType, currentStyles);
            break;
        }
    }

    private void writeShapeWithStyle(ShapeWithStyle shape, TagType tagType) {
        writeFillStyles(shape.getFillStyles(), tagType);
        writeLineStyles(shape.getLineStyles(), tagType);
        CurrentStyles currentStyles = new CurrentStyles();
        currentStyles.styles = new Styles(shape.getFillStyles(), shape.getLineStyles());
        currentStyles.numFillBits = requireUBCount(shape.getFillStyles().size());
        currentStyles.numLineBits = requireUBCount(shape.getLineStyles().size());
        writeShape(shape, tagType, currentStyles);
    }

    private void writeShowFrame() {
        // ShowFrame tag has no tag body.
    }

    /**
     * @see SWFReader#readStraightEdgeRecord
     */
    private void writeStraightEdgeRecord(StraightEdgeRecord shape) {
        tagBuffer.writeBit(true); // This is an edge. Always 1.
        tagBuffer.writeBit(true); // StraightFlag is always true.
        int numBits = shape.getNumBits();
        tagBuffer.writeUB(numBits, 4);
        switch (shape.getLineType()) {
        case GENERAL:
            tagBuffer.writeBit(true);
            tagBuffer.writeSB(shape.getDeltaX(), numBits + 2);
            tagBuffer.writeSB(shape.getDeltaY(), numBits + 2);
            break;
        case VERTICAL:
            tagBuffer.writeBit(false);
            tagBuffer.writeBit(true);
            tagBuffer.writeSB(shape.getDeltaY(), numBits + 2);
            break;
        case HORIZONTAL:
            tagBuffer.writeBit(false);
            tagBuffer.writeBit(false);
            tagBuffer.writeSB(shape.getDeltaX(), numBits + 2);
            break;
        }
    }

    /**
     * @see SWFReader#readStyleChangeRecord
     */
    private void writeStyleChangeRecord(StyleChangeRecord shape, TagType tagType, CurrentStyles currentStyles) {
        tagBuffer.writeBit(shape.getTypeFlag());
        tagBuffer.writeBit(shape.isStateNewStyles());
        tagBuffer.writeBit(shape.isStateLineStyle());
        tagBuffer.writeBit(shape.isStateFillStyle1());
        tagBuffer.writeBit(shape.isStateFillStyle0());
        tagBuffer.writeBit(shape.isStateMoveTo());

        if (shape.isStateMoveTo()) {
            final int moveBits = requireSBCount(maxNum(shape.getMoveDeltaX(), shape.getMoveDeltaY(), 0, 0));
            tagBuffer.writeUB(moveBits, 5);
            tagBuffer.writeSB(shape.getMoveDeltaX(), moveBits);
            tagBuffer.writeSB(shape.getMoveDeltaY(), moveBits);
        }

        // there shouldn't be any styles on a shape for fonts, as the
        // tag is a Shape, not ShapeWithStyle, but the fillStyle0 can be 1 because
        // of the following from the SWF spec:
        // "The first STYLECHANGERECORD of each SHAPE in the GlyphShapeTable does not use
        // the LineStyle and LineStyles fields. In addition, the first STYLECHANGERECORD of each
        // shape must have both fields StateFillStyle0 and FillStyle0 set to 1."
        boolean ignoreStyle = tagType == TagType.DefineFont || tagType == TagType.DefineFont2
                || tagType == TagType.DefineFont3;

        if (shape.isStateFillStyle0()) {
            int fs0;
            if (ignoreStyle)
                fs0 = 1;
            else
                fs0 = currentStyles.styles.getFillStyles().indexOf(shape.getFillstyle0()) + 1;

            tagBuffer.writeUB(fs0, currentStyles.numFillBits);
        }

        if (shape.isStateFillStyle1()) {
            int fs1;
            if (ignoreStyle)
                fs1 = 1;
            else
                fs1 = currentStyles.styles.getFillStyles().indexOf(shape.getFillstyle1()) + 1;

            tagBuffer.writeUB(fs1, currentStyles.numFillBits);
        }

        if (shape.isStateLineStyle()) {
            int ls;
            if (ignoreStyle)
                ls = 0;
            else
                ls = currentStyles.styles.getLineStyles().indexOf(shape.getLinestyle()) + 1;

            tagBuffer.writeUB(ls, currentStyles.numLineBits);
        }

        if (shape.isStateNewStyles()) {
            tagBuffer.byteAlign();

            // encode
            writeFillStyles(shape.getStyles().getFillStyles(), tagType);
            writeLineStyles(shape.getStyles().getLineStyles(), tagType);
            final int nFillBits = shape.getNumFillBits();
            final int nLineBits = shape.getNumLineBits();
            tagBuffer.writeUB(nFillBits, 4);
            tagBuffer.writeUB(nLineBits, 4);

            // update context
            currentStyles.styles = shape.getStyles();
            currentStyles.numFillBits = nFillBits;
            currentStyles.numLineBits = nLineBits;
        }
    }

    /**
     * @see SWFReader#readSymbolClass
     */
    private void writeSymbolClass(SymbolClassTag tag) {
        final boolean writeRootClass = currentFrameIndex == 0 && swf.getTopLevelClass() != null;

        // number of symbols
        final int count = writeRootClass ? tag.size() + 1 : tag.size();
        tagBuffer.writeUI16(count);

        // export symbols
        for (String symbolName : tag.getSymbolNames()) {
            final ICharacterTag characterTag = tag.getSymbol(symbolName);
            tagBuffer.writeUI16(characterTag.getCharacterID());
            tagBuffer.writeString(symbolName);
        }

        // root class name
        if (writeRootClass) {
            tagBuffer.writeUI16(0);
            tagBuffer.writeString(swf.getTopLevelClass());
        }

    }

    /**
     * This is the entry-point for encoding a SWF tag. In order to calculate the
     * size of a tag, each tag data is buffered on a OutputBitStream object.
     * This method initialize the buffer, select encoding method according to
     * the tag type, encode the tag body and then write the tag header and tag
     * body onto the target output stream.
     * 
     * @param tag tag to encode
     */

    private void writeTag(ITag tag) {
        if (!writtenTags.contains(tag)) {
            tagBuffer.reset();
            writeTag(tag, tagBuffer, outputBuffer);

            writtenTags.add(tag);
        }
    }

    /**
     * Encode {@code tag}'s body onto buffer {@code tagData}. Then compute the
     * tag header and length. Finally, write the tag header and tag body to
     * {@code out}.
     * <p>
     * This method assumes that {@code tagData} is a clean, initialized
     * {@code IOutputBitStream} object.
     * 
     * @param tag tag object
     * @param tagData tag buffer
     * @param out target output
     */
    private void writeTag(ITag tag, IOutputBitStream tagData, IOutputBitStream out) {
        assert tag != null;
        assert tagData != null;
        assert out != null;
        assert tagData != out;

        if (tag == SWFReader.INVALID_TAG)
            return;

        // redirect tag buffer to "tagData"
        IOutputBitStream swfTagBuffer = null;
        if (tagData != this.tagBuffer) {
            swfTagBuffer = this.tagBuffer;
            this.tagBuffer = tagData;
        }

        boolean skipRawTag = false;
        Collection<ITag> extraTags = new LinkedList<ITag>();
        if (tag instanceof RawTag) {
            skipRawTag = writeRawTag((RawTag) tag);
        } else {
            switch (tag.getTagType()) {
            case DoABC:
                writeDoABC((DoABCTag) tag);
                break;
            case FileAttributes:
                writeFileAttributes((FileAttributesTag) tag);
                break;
            case SymbolClass:
                writeSymbolClass((SymbolClassTag) tag);
                break;
            case ShowFrame:
                writeShowFrame();
                break;
            case SetBackgroundColor:
                writeSetBackgroundColor((SetBackgroundColorTag) tag);
                break;
            case EnableDebugger2:
                writeEnableDebugger2((EnableDebugger2Tag) tag);
                break;
            case ScriptLimits:
                writeScriptLimits((ScriptLimitsTag) tag);
                break;
            case ProductInfo:
                writeProductInfo((ProductInfoTag) tag);
                break;
            case Metadata:
                writeMetadata((MetadataTag) tag);
                break;
            case DefineBits:
                writeDefineBits((DefineBitsTag) tag);
                break;
            case DefineBitsJPEG2:
                writeDefineBitsJPEG2((DefineBitsJPEG2Tag) tag);
                break;
            case DefineBitsJPEG3:
                writeDefineBitsJPEG3((DefineBitsJPEG3Tag) tag);
                break;
            case DefineBitsLossless:
            case DefineBitsLossless2:
                writeDefineBitsLossless((DefineBitsLosslessTag) tag);
                break;
            case DefineBinaryData:
                writeDefineBinaryData((DefineBinaryDataTag) tag);
                break;
            case DefineShape:
                writeDefineShape((DefineShapeTag) tag);
                break;
            case DefineShape2:
                writeDefineShape2((DefineShape2Tag) tag);
                break;
            case DefineShape3:
                writeDefineShape3((DefineShape3Tag) tag);
                break;
            case DefineShape4:
                writeDefineShape4((DefineShape4Tag) tag);
                break;
            case DefineSprite:
                writeDefineSprite((DefineSpriteTag) tag);
                break;
            case ExportAssets:
                writeExportAssets((ExportAssetsTag) tag);
                break;
            case DefineScalingGrid:
                writeDefineScalingGrid((DefineScalingGridTag) tag);
                break;
            case DefineFont:
                writeDefineFont((DefineFontTag) tag, extraTags);
                break;
            case DefineFont2:
                writeDefineFont2((DefineFont2Tag) tag, extraTags);
                break;
            case DefineFont3:
                writeDefineFont3((DefineFont3Tag) tag, extraTags);
                break;
            case DefineFont4:
                writeDefineFont4((DefineFont4Tag) tag, extraTags);
                break;
            case DefineFontInfo:
                writeDefineFontInfo((IFontInfo) tag);
                break;
            case DefineFontInfo2:
                writeDefineFontInfo2((DefineFontInfo2Tag) tag);
                break;
            case DefineFontAlignZones:
                writeDefineFontAlignZones((DefineFontAlignZonesTag) tag);
                break;
            case DefineFontName:
                writeDefineFontName((DefineFontNameTag) tag);
                break;
            case DefineText:
                writeDefineText((DefineTextTag) tag, extraTags);
                break;
            case DefineText2:
                writeDefineText2((DefineText2Tag) tag, extraTags);
                break;
            case DefineEditText:
                writeDefineEditText((DefineEditTextTag) tag, extraTags);
                break;
            case DefineSound:
                writeDefineSound((DefineSoundTag) tag);
                break;
            case DefineVideoStream:
                writeDefineVideoStream((DefineVideoStreamTag) tag);
                break;
            case VideoFrame:
                writeVideoFrame((VideoFrameTag) tag);
                break;
            case StartSound:
                writeStartSound((StartSoundTag) tag);
                break;
            case StartSound2:
                writeStartSound2((StartSound2Tag) tag);
                break;
            case SoundStreamHead:
                writeSoundStreamHead((SoundStreamHeadTag) tag);
                break;
            case SoundStreamHead2:
                writeSoundStreamHead((SoundStreamHead2Tag) tag);
                break;
            case SoundStreamBlock:
                writeSoundStreamBlock((SoundStreamBlockTag) tag);
                break;
            case DefineButton:
                writeDefineButton((DefineButtonTag) tag);
                break;
            case DefineButton2:
                writeDefineButton2((DefineButton2Tag) tag);
                break;
            case DefineButtonSound:
                writeDefineButtonSound((DefineButtonSoundTag) tag);
                break;
            case CSMTextSettings:
                writeCSMTextSettings((CSMTextSettingsTag) tag);
                break;
            case End:
                writeEnd();
                break;
            case JPEGTables:
                writeJPEGTables(((JPEGTablesTag) tag));
                break;
            case DefineMorphShape:
                writeDefineMorphShape((DefineMorphShapeTag) tag);
                break;
            case DefineMorphShape2:
                writeDefineMorphShape2((DefineMorphShape2Tag) tag);
                break;
            case PlaceObject:
                writePlaceObject((PlaceObjectTag) tag);
                break;
            case PlaceObject2:
                writePlaceObject2((PlaceObject2Tag) tag);
                break;
            case PlaceObject3:
                writePlaceObject3((PlaceObject3Tag) tag);
                break;
            case RemoveObject:
                writeRemoveObject((RemoveObjectTag) tag);
                break;
            case RemoveObject2:
                writeRemoveObject2((RemoveObject2Tag) tag);
                break;
            case SetTabIndex:
                writeSetTabIndex((SetTabIndexTag) tag);
                break;
            case FrameLabel:
                writeFrameLabel((FrameLabelTag) tag);
                break;
            default:
                throw new RuntimeException("Tag not supported: " + tag);
            }
        }

        // reset tag buffer
        if (swfTagBuffer != null) {
            this.tagBuffer = swfTagBuffer;
        }

        if (!skipRawTag)
            finishTag(tag, tagData, out);

        for (ITag extraTag : extraTags)
            writeTag(extraTag);
    }

    private boolean writeRawTag(RawTag tag) {
        boolean skipTag = false;
        // if writing out an AS3 swf, there are a number of
        // tags which need to be ignored as they're not valid
        // in AS3.  These can get in when embedding tags from an
        // old SWF into a AS3 type SWF.
        if (swf.getUseAS3()) {
            switch (tag.getTagType()) {
            case DoAction:
            case DoInitAction:
                skipTag = true;
                break;
            }
        }

        if (!skipTag) {
            tagBuffer.write(tag.getTagBody());
        }

        return skipTag;
    }

    private void writeSetTabIndex(SetTabIndexTag tag) {
        tagBuffer.writeUI16(tag.getDepth());
        tagBuffer.writeUI16(tag.getTabIndex());
    }

    private void writeRemoveObject2(RemoveObject2Tag tag) {
        tagBuffer.writeUI16(tag.getDepth());
    }

    private void writeRemoveObject(RemoveObjectTag tag) {
        tagBuffer.writeUI16(tag.getCharacter().getCharacterID());
        tagBuffer.writeUI16(tag.getDepth());
    }

    private void writePlaceObject(PlaceObjectTag tag) {
        tagBuffer.writeUI16(tag.getCharacter().getCharacterID());
        tagBuffer.writeUI16(tag.getDepth());
        writeMatrix(tag.getMatrix());
        final CXForm colorTransform = tag.getColorTransform();
        if (colorTransform != null)
            writeColorTransform(colorTransform);
    }

    private void writeColorTransform(CXForm cx) {
        final int nbits = requireSBCount(cx.getRedMultTerm(), cx.getGreenMultTerm(), cx.getBlueMultTerm(),
                cx.getRedAddTerm(), cx.getGreenAddTerm(), cx.getBlueAddTerm());

        tagBuffer.writeBit(cx.hasAdd());
        tagBuffer.writeBit(cx.hasMult());
        tagBuffer.writeUB(nbits, 4);

        if (cx.hasMult()) {
            tagBuffer.writeSB(cx.getRedMultTerm(), nbits);
            tagBuffer.writeSB(cx.getGreenMultTerm(), nbits);
            tagBuffer.writeSB(cx.getBlueMultTerm(), nbits);
        }

        if (cx.hasAdd()) {
            tagBuffer.writeSB(cx.getRedAddTerm(), nbits);
            tagBuffer.writeSB(cx.getGreenAddTerm(), nbits);
            tagBuffer.writeSB(cx.getBlueAddTerm(), nbits);
        }
        tagBuffer.byteAlign();
    }

    private void writePlaceObject2(PlaceObject2Tag tag) {
        tagBuffer.writeBit(tag.isHasClipActions());
        tagBuffer.writeBit(tag.isHasClipDepth());
        tagBuffer.writeBit(tag.isHasName());
        tagBuffer.writeBit(tag.isHasRatio());
        tagBuffer.writeBit(tag.isHasColorTransform());
        tagBuffer.writeBit(tag.isHasMatrix());
        tagBuffer.writeBit(tag.isHasCharacter());
        tagBuffer.writeBit(tag.isMove());

        tagBuffer.writeUI16(tag.getDepth());

        if (tag.isHasCharacter())
            tagBuffer.writeUI16(tag.getCharacter().getCharacterID());
        if (tag.isHasMatrix())
            writeMatrix(tag.getMatrix());
        if (tag.isHasColorTransform())
            writeColorTransformWithAlpha(tag.getColorTransform());
        if (tag.isHasRatio())
            tagBuffer.writeUI16(tag.getRatio());
        if (tag.isHasName())
            tagBuffer.writeString(tag.getName());
        if (tag.isHasClipDepth())
            tagBuffer.writeUI16(tag.getClipDepth());
        if (tag.isHasClipActions())
            tagBuffer.write(tag.getClipActions().data);
    }

    private void writePlaceObject3(PlaceObject3Tag tag) {
        tagBuffer.writeBit(tag.isHasClipActions());
        tagBuffer.writeBit(tag.isHasClipDepth());
        tagBuffer.writeBit(tag.isHasName());
        tagBuffer.writeBit(tag.isHasRatio());
        tagBuffer.writeBit(tag.isHasColorTransform());
        tagBuffer.writeBit(tag.isHasMatrix());
        tagBuffer.writeBit(tag.isHasCharacter());
        tagBuffer.writeBit(tag.isMove());

        tagBuffer.writeUB(0, 3); // reserved
        tagBuffer.writeBit(tag.isHasImage());
        tagBuffer.writeBit(tag.isHasClassName());
        tagBuffer.writeBit(tag.isHasCacheAsBitmap());
        tagBuffer.writeBit(tag.isHasBlendMode());
        tagBuffer.writeBit(tag.isHasFilterList());

        tagBuffer.writeUI16(tag.getDepth());

        if (tag.isHasClassName())
            tagBuffer.writeString(tag.getClassName());
        if (tag.isHasCharacter())
            tagBuffer.writeUI16(tag.getCharacter().getCharacterID());
        if (tag.isHasMatrix())
            writeMatrix(tag.getMatrix());
        if (tag.isHasColorTransform())
            writeColorTransformWithAlpha(tag.getColorTransform());
        if (tag.isHasRatio())
            tagBuffer.writeUI16(tag.getRatio());
        if (tag.isHasName())
            tagBuffer.writeString(tag.getName());
        if (tag.isHasClipDepth())
            tagBuffer.writeUI16(tag.getClipDepth());
        if (tag.isHasFilterList()) {
            tagBuffer.writeUI8(tag.getSurfaceFilterList().length);
            for (final Filter filter : tag.getSurfaceFilterList())
                writeFilter(filter);
        }
        if (tag.isHasBlendMode())
            tagBuffer.writeUI8(tag.getBlendMode());

        if (tag.isHasCacheAsBitmap())
            tagBuffer.writeUI8(tag.getBitmapCache());

        if (tag.isHasClipActions())
            tagBuffer.write(tag.getClipActions().data);
    }

    private void writeVideoFrame(VideoFrameTag tag) {
        tagBuffer.writeUI16(tag.getStreamTag().getCharacterID());
        tagBuffer.writeUI16(tag.getFrameNum());
        tagBuffer.write(tag.getVideoData());
    }

    private void writeDefineVideoStream(DefineVideoStreamTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUI16(tag.getNumFrames());
        tagBuffer.writeUI16(tag.getWidth());
        tagBuffer.writeUI16(tag.getHeight());
        tagBuffer.writeUB(0, 4); // reserved
        tagBuffer.writeUB(tag.getDeblocking(), 3);
        tagBuffer.writeBit(tag.isSmoothing());
        tagBuffer.writeUI8(tag.getCodecID());
    }

    private void writeDefineButtonSound(DefineButtonSoundTag tag) {
        tagBuffer.writeUI16(tag.getButtonTag().getCharacterID());
        for (int i = 0; i < DefineButtonSoundTag.TOTAL_SOUND_STYLE; i++) {
            if (tag.getSoundChar()[i] == null) {
                // write out a zero sound id if there is no sound info.
                tagBuffer.writeUI16(0);
                continue;
            }

            assert tag.getSoundChar()[i].getTagType() == TagType.DefineSound;

            tagBuffer.writeUI16(tag.getSoundChar()[i].getCharacterID());
            writeSoundInfo(tag.getSoundInfo()[i]);
        }
    }

    private void writeDefineButton2(DefineButton2Tag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUB(0, 7); // reserved
        tagBuffer.writeBit(tag.isTrackAsMenu());
        tagBuffer.writeUI16(tag.getActionOffset()); // TODO: need calculation
        for (final ButtonRecord r : tag.getCharacters())
            writeButtonRecord(r, tag.getTagType());
        tagBuffer.writeUI8(0); // end of character tag
        tagBuffer.write(tag.getActions());
    }

    private void writeDefineButton(DefineButtonTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        for (final ButtonRecord record : tag.getCharacters()) {
            writeButtonRecord(record, tag.getTagType());
        }
        tagBuffer.writeUI8(0); // end of characters
        tagBuffer.write(tag.getActions());
        tagBuffer.writeUI8(0); // end of actions
    }

    private void writeButtonRecord(ButtonRecord record, TagType tagType) {
        tagBuffer.writeUB(0, 2); // reserved
        tagBuffer.writeBit(record.isHasBlendMode());
        tagBuffer.writeBit(record.isHasFilterList());
        tagBuffer.writeBit(record.isStateHitTest());
        tagBuffer.writeBit(record.isStateDown());
        tagBuffer.writeBit(record.isStateOver());
        tagBuffer.writeBit(record.isStateUp());
        tagBuffer.writeUI16(record.getCharacterID());
        tagBuffer.writeUI16(record.getPlaceDepth());
        writeMatrix(record.getPlaceMatrix());
        if (tagType == TagType.DefineButton2) {
            writeColorTransformWithAlpha(record.getColorTransform());

            if (record.isHasFilterList()) {
                tagBuffer.writeUI8(record.getFilterList().length);
                for (final Filter filter : record.getFilterList())
                    writeFilter(filter);
            }

            if (record.isHasBlendMode())
                tagBuffer.writeUI8(record.getBlendMode());
        }
    }

    private void writeFilter(Filter filter) {
        tagBuffer.writeUI8(filter.getFilterID());
        switch (filter.getFilterID()) {
        case Filter.DROP_SHADOW:
            writeDropShadowFilter(filter.getDropShadowFilter());
            break;
        case Filter.BLUR:
            writeBlurFilter(filter.getBlurFilter());
            break;
        case Filter.GLOW:
            writeGlowFilter(filter.getGlowFilter());
            break;
        case Filter.BEVEL:
            writeBevelFilter(filter.getBevelFilter());
            break;
        case Filter.GRADIENT_GLOW:
            writeGradientGlowFilter(filter.getGradientGlowFilter());
            break;
        case Filter.CONVOLUTION:
            writeConvolutionFilter(filter.getConvolutionFilter());
            break;
        case Filter.COLOR_MATRIX:
            writeColorMatrixFilter(filter.getColorMatrixFilter());
            break;
        case Filter.GRADIENT_BEVEL:
            writeGradientBevelFilter(filter.getGradientBevelFilter());
            break;
        }
    }

    private void writeGradientBevelFilter(GradientBevelFilter filter) {
        assert filter.getNumColors() == filter.getGradientColors().length;
        assert filter.getNumColors() == filter.getGradientRatio().length;

        tagBuffer.writeUI8(filter.getNumColors());
        for (RGBA color : filter.getGradientColors())
            writeRGBA(color);
        for (int ratio : filter.getGradientRatio())
            tagBuffer.writeUI8(ratio);

        tagBuffer.writeFIXED(filter.getBlurX());
        tagBuffer.writeFIXED(filter.getBlurY());
        tagBuffer.writeFIXED(filter.getAngle());
        tagBuffer.writeFIXED(filter.getDistance());
        tagBuffer.writeFIXED8(filter.getStrength());
        tagBuffer.writeBit(filter.isInnerShadow());
        tagBuffer.writeBit(filter.isKnockout());
        tagBuffer.writeBit(filter.isCompositeSource());
        tagBuffer.writeBit(filter.isOnTop());
        tagBuffer.writeUB(filter.getPasses(), 4);
    }

    private void writeGradientGlowFilter(GradientGlowFilter filter) {
        assert filter.getNumColors() == filter.getGradientColors().length;
        assert filter.getNumColors() == filter.getGradientRatio().length;

        tagBuffer.writeUI8(filter.getNumColors());
        for (RGBA color : filter.getGradientColors())
            writeRGBA(color);
        for (int ratio : filter.getGradientRatio())
            tagBuffer.writeUI8(ratio);

        tagBuffer.writeFIXED(filter.getBlurX());
        tagBuffer.writeFIXED(filter.getBlurY());
        tagBuffer.writeFIXED(filter.getAngle());
        tagBuffer.writeFIXED(filter.getDistance());
        tagBuffer.writeFIXED8(filter.getStrength());
        tagBuffer.writeBit(filter.isInnerGlow());
        tagBuffer.writeBit(filter.isKnockout());
        tagBuffer.writeBit(filter.isCompositeSource());
        tagBuffer.writeBit(filter.isOnTop());
        tagBuffer.writeUB(filter.getPasses(), 4);
    }

    private void writeBevelFilter(BevelFilter filter) {
        //Note: The SWF File Format Specifications Version 10 switches these two colors (it writes ShadowColor before HighlightColor).
        //A bug has been logged in JIRA against the specs for this issue
        writeRGBA(filter.getHighlightColor());
        writeRGBA(filter.getShadowColor());
        tagBuffer.writeFIXED(filter.getBlurX());
        tagBuffer.writeFIXED(filter.getBlurY());
        tagBuffer.writeFIXED(filter.getAngle());
        tagBuffer.writeFIXED(filter.getDistance());
        tagBuffer.writeFIXED8(filter.getStrength());
        tagBuffer.writeBit(filter.isInnerShadow());
        tagBuffer.writeBit(filter.isKnockout());
        tagBuffer.writeBit(filter.isCompositeSource());
        tagBuffer.writeBit(filter.isOnTop());
        tagBuffer.writeUB(filter.getPasses(), 4);
    }

    private void writeGlowFilter(GlowFilter filter) {
        writeRGBA(filter.getGlowColor());
        tagBuffer.writeFIXED(filter.getBlurX());
        tagBuffer.writeFIXED(filter.getBlurY());
        tagBuffer.writeFIXED8(filter.getStrength());
        tagBuffer.writeBit(filter.isInnerGlow());
        tagBuffer.writeBit(filter.isKnockout());
        tagBuffer.writeBit(filter.isCompositeSource());
        tagBuffer.writeUB(filter.getPasses(), 5);
    }

    private void writeDropShadowFilter(DropShadowFilter filter) {
        writeRGBA(filter.getDropShadowColor());
        tagBuffer.writeFIXED(filter.getBlurX());
        tagBuffer.writeFIXED(filter.getBlurY());
        tagBuffer.writeFIXED(filter.getAngle());
        tagBuffer.writeFIXED(filter.getDistance());
        tagBuffer.writeFIXED8(filter.getStrength());
        tagBuffer.writeBit(filter.isInnerShadow());
        tagBuffer.writeBit(filter.isKnockout());
        tagBuffer.writeBit(filter.isCompositeSource());
        tagBuffer.writeUB(filter.getPasses(), 5);
    }

    private void writeBlurFilter(BlurFilter filter) {
        tagBuffer.writeFIXED(filter.getBlurX());
        tagBuffer.writeFIXED(filter.getBlurY());
        tagBuffer.writeUB(filter.getPasses(), 5);
        tagBuffer.writeUB(0, 3); // reserved
    }

    private void writeConvolutionFilter(ConvolutionFilter filter) {
        assert filter.getMatrixX() * filter.getMatrixY() == filter.getMatrix().length;

        tagBuffer.writeUI8(filter.getMatrixX());
        tagBuffer.writeUI8(filter.getMatrixY());
        tagBuffer.writeFLOAT(filter.getDivisor());
        tagBuffer.writeFLOAT(filter.getBias());
        for (final float f : filter.getMatrix())
            tagBuffer.writeFLOAT(f);
        writeRGBA(filter.getDefaultColor());
        tagBuffer.writeUB(0, 6); // reserved
        tagBuffer.writeBit(filter.isClamp());
        tagBuffer.writeBit(filter.isPreserveAlpha());
        tagBuffer.byteAlign();
    }

    private void writeColorMatrixFilter(float[] filter) {
        assert filter.length == 20;
        for (float f : filter)
            tagBuffer.writeFLOAT(f);
    }

    private void writeColorTransformWithAlpha(CXFormWithAlpha cx) {
        final int nbits = requireSBCount(cx.getRedMultTerm(), cx.getGreenMultTerm(), cx.getBlueMultTerm(),
                cx.getAlphaMultTerm(), cx.getRedAddTerm(), cx.getGreenAddTerm(), cx.getBlueAddTerm(),
                cx.getAlphaAddTerm());

        tagBuffer.writeBit(cx.hasAdd());
        tagBuffer.writeBit(cx.hasMult());
        tagBuffer.writeUB(nbits, 4);

        if (cx.hasMult()) {
            tagBuffer.writeSB(cx.getRedMultTerm(), nbits);
            tagBuffer.writeSB(cx.getGreenMultTerm(), nbits);
            tagBuffer.writeSB(cx.getBlueMultTerm(), nbits);
            tagBuffer.writeSB(cx.getAlphaMultTerm(), nbits);
        }

        if (cx.hasAdd()) {
            tagBuffer.writeSB(cx.getRedAddTerm(), nbits);
            tagBuffer.writeSB(cx.getGreenAddTerm(), nbits);
            tagBuffer.writeSB(cx.getBlueAddTerm(), nbits);
            tagBuffer.writeSB(cx.getAlphaAddTerm(), nbits);
        }
        tagBuffer.byteAlign();
    }

    private void writeSoundStreamBlock(SoundStreamBlockTag tag) {
        tagBuffer.write(tag.getStreamSoundData());
    }

    private void writeSoundStreamHead(SoundStreamHeadTag tag) {
        tagBuffer.byteAlign();
        tagBuffer.writeUB(0, 4); // reserved
        tagBuffer.writeUB(tag.getPlaybackSoundRate(), 2);
        tagBuffer.writeUB(tag.getPlaybackSoundSize(), 1);
        tagBuffer.writeUB(tag.getPlaybackSoundType(), 1);
        tagBuffer.writeUB(tag.getStreamSoundCompression(), 4);
        tagBuffer.writeUB(tag.getStreamSoundRate(), 2);
        tagBuffer.writeUB(tag.getStreamSoundSize(), 1);
        tagBuffer.writeUB(tag.getStreamSoundType(), 1);
        tagBuffer.writeUI16(tag.getStreamSoundSampleCount());
        if (tag.getStreamSoundCompression() == SoundStreamHeadTag.SSC_MP3)
            tagBuffer.writeSI16(tag.getLatencySeek());

    }

    private void writeStartSound2(StartSound2Tag tag) {
        tagBuffer.writeString(tag.getSoundClassName());
        writeSoundInfo(tag.getSoundInfo());
    }

    private void writeStartSound(StartSoundTag tag) {
        tagBuffer.writeUI16(tag.getSoundTag().getCharacterID());
        writeSoundInfo(tag.getSoundInfo());
    }

    private void writeSoundInfo(SoundInfo soundInfo) {
        tagBuffer.byteAlign();
        tagBuffer.writeUB(0, 2); // reserved
        tagBuffer.writeBit(soundInfo.isSyncStop());
        tagBuffer.writeBit(soundInfo.isSyncNoMultiple());
        tagBuffer.writeBit(soundInfo.isHasEnvelope());
        tagBuffer.writeBit(soundInfo.isHasLoops());
        tagBuffer.writeBit(soundInfo.isHasOutPoint());
        tagBuffer.writeBit(soundInfo.isHasInPoint());
        if (soundInfo.isHasInPoint())
            tagBuffer.writeUI32(soundInfo.getInPoint());
        if (soundInfo.isHasOutPoint())
            tagBuffer.writeUI32(soundInfo.getOutPoint());
        if (soundInfo.isHasLoops())
            tagBuffer.writeUI16(soundInfo.getLoopCount());
        if (soundInfo.isHasEnvelope()) {
            tagBuffer.writeUI8(soundInfo.getEnvPoints());
            for (final SoundEnvelope env : soundInfo.getEnvelopeRecords()) {
                tagBuffer.writeUI32(env.getPos44());
                tagBuffer.writeUI16(env.getLeftLevel());
                tagBuffer.writeUI16(env.getRightLevel());
            }
        }
    }

    private void writeDefineSound(DefineSoundTag tag) {
        tagBuffer.byteAlign();
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUB(tag.getSoundFormat(), 4);
        tagBuffer.writeUB(tag.getSoundRate(), 2);
        tagBuffer.writeUB(tag.getSoundSize(), 1);
        tagBuffer.writeUB(tag.getSoundType(), 1);
        tagBuffer.writeUI32(tag.getSoundSampleCount());
        tagBuffer.write(tag.getSoundData());
    }

    private void writeDefineFont4(DefineFont4Tag tag, Collection<ITag> extraTags) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUB(0, 5); // reserved
        tagBuffer.writeBit(tag.isFontFlagsHasFontData());
        tagBuffer.writeBit(tag.isFontFlagsItalic());
        tagBuffer.writeBit(tag.isFontFlagsBold());
        // 8 bits - no need to align

        tagBuffer.writeString(tag.getFontName());
        tagBuffer.write(tag.getFontData());

        DefineFontNameTag license = tag.getLicense();
        if (license != null)
            extraTags.add(license);
    }

    private void writeCSMTextSettings(CSMTextSettingsTag tag) {
        tagBuffer.writeUI16(tag.getTextTag().getCharacterID());
        tagBuffer.writeUB(tag.getUseFlashType(), 2);
        tagBuffer.writeUB(tag.getGridFit(), 3);
        tagBuffer.writeUB(0, 3);
        // 8 bits - no need to align

        tagBuffer.writeFLOAT(tag.getThickness());
        tagBuffer.writeFLOAT(tag.getSharpness());
        tagBuffer.writeUI8(0); // reserved
    }

    private void writeDefineEditText(DefineEditTextTag tag, Collection<ITag> extraTags) {
        tagBuffer.writeUI16(tag.getCharacterID());
        writeRect(tag.getBounds());

        tagBuffer.writeBit(tag.isHasText());
        tagBuffer.writeBit(tag.isWordWrap());
        tagBuffer.writeBit(tag.isMultiline());
        tagBuffer.writeBit(tag.isPassword());
        tagBuffer.writeBit(tag.isReadOnly());
        tagBuffer.writeBit(tag.isHasTextColor());
        tagBuffer.writeBit(tag.isHasMaxLength());
        tagBuffer.writeBit(tag.isHasFont());
        tagBuffer.writeBit(tag.isHasFontClass());
        tagBuffer.writeBit(tag.isAutoSize());
        tagBuffer.writeBit(tag.isHasLayout());
        tagBuffer.writeBit(tag.isNoSelect());
        tagBuffer.writeBit(tag.isBorder());
        tagBuffer.writeBit(tag.isWasStatic());
        tagBuffer.writeBit(tag.isHtml());
        tagBuffer.writeBit(tag.isUseOutlines());

        // Both HasFont and HasFontClass requires a Height field.
        if (tag.isHasFont()) {
            tagBuffer.writeUI16(tag.getFontTag().getCharacterID());
            tagBuffer.writeUI16(tag.getFontHeight());
        } else if (tag.isHasFontClass()) {
            tagBuffer.writeString(tag.getFontClass());
            tagBuffer.writeUI16(tag.getFontHeight());
        }

        if (tag.isHasTextColor())
            writeRGBA(tag.getTextColor());

        if (tag.isHasMaxLength())
            tagBuffer.writeUI16(tag.getMaxLength());

        if (tag.isHasLayout()) {
            tagBuffer.writeUI8(tag.getAlign());
            tagBuffer.writeUI16(tag.getLeftMargin());
            tagBuffer.writeUI16(tag.getRightMargin());
            tagBuffer.writeUI16(tag.getIndent());
            tagBuffer.writeSI16(tag.getLeading());
        }

        tagBuffer.writeString(tag.getVariableName());

        if (tag.isHasText())
            tagBuffer.writeString(tag.getInitialText());

        CSMTextSettingsTag textSettings = tag.getCSMTextSettings();
        if (textSettings != null)
            extraTags.add(textSettings);
    }

    private void writeDefineText2(DefineText2Tag tag, Collection<ITag> extraTags) {
        writeDefineText(tag, extraTags);
    }

    private void writeDefineText(DefineTextTag tag, Collection<ITag> extraTags) {
        tagBuffer.writeUI16(tag.getCharacterID());
        writeRect(tag.getTextBounds());
        writeMatrix(tag.getTextMatrix());
        tagBuffer.writeUI8(tag.getGlyphBits());
        tagBuffer.writeUI8(tag.getAdvanceBits());
        for (TextRecord textRecord : tag.getTextRecords()) {
            writeTextRecord(textRecord, tag);
        }
        tagBuffer.byteAlign();
        tagBuffer.writeUI8(0); // end of records

        CSMTextSettingsTag textSettings = tag.getCSMTextSettings();
        if (textSettings != null)
            extraTags.add(textSettings);
    }

    private void writeTextRecord(TextRecord textRecord, DefineTextTag tag) {
        tagBuffer.byteAlign();
        tagBuffer.writeBit(true); // TextRecordType always 1.
        tagBuffer.writeUB(0, 3); // reserved
        tagBuffer.writeBit(textRecord.isStyleFlagsHasFont());
        tagBuffer.writeBit(textRecord.isStyleFlagsHasColor());
        tagBuffer.writeBit(textRecord.isStyleFlagsHasYOffset());
        tagBuffer.writeBit(textRecord.isStyleFlagsHasXOffset());
        // 8 bits - no need to align

        if (textRecord.isStyleFlagsHasFont()) {
            tagBuffer.writeUI16(textRecord.getFontTag().getCharacterID());
        }

        if (textRecord.isStyleFlagsHasColor()) {
            if (tag.getTagType() == TagType.DefineText2) {
                assert textRecord.getTextColor() instanceof RGBA;
                writeRGBA((RGBA) textRecord.getTextColor());
            } else {
                writeRGB(textRecord.getTextColor());
            }
        }

        if (textRecord.isStyleFlagsHasXOffset()) {
            tagBuffer.writeSI16(textRecord.getxOffset());
        }

        if (textRecord.isStyleFlagsHasYOffset()) {
            tagBuffer.writeSI16(textRecord.getyOffset());
        }

        if (textRecord.isStyleFlagsHasFont()) {
            tagBuffer.writeUI16(textRecord.getTextHeight());
        }

        tagBuffer.writeUI8(textRecord.getGlyphCount());

        assert textRecord.getGlyphCount() == textRecord.getGlyphEntries().length;

        for (final GlyphEntry entry : textRecord.getGlyphEntries()) {
            writeGlyphEntry(entry, tag);
        }
    }

    /**
     * @param entry
     * @param tag
     */
    private void writeGlyphEntry(GlyphEntry entry, DefineTextTag tag) {
        tagBuffer.writeUB(entry.getGlyphIndex(), tag.getGlyphBits());
        tagBuffer.writeSB(entry.getGlyphAdvance(), tag.getAdvanceBits());
    }

    private void writeDefineFontName(DefineFontNameTag tag) {
        tagBuffer.writeUI16(tag.getFontTag().getCharacterID());
        tagBuffer.writeString(tag.getFontName());
        tagBuffer.writeString(tag.getFontCopyright());
    }

    private void writeDefineFontAlignZones(DefineFontAlignZonesTag tag) {
        tagBuffer.writeUI16(tag.getFontTag().getCharacterID());
        tagBuffer.writeUB(tag.getCsmTableHint(), 2);
        tagBuffer.writeUB(0, 6); // reserved
        tagBuffer.byteAlign();
        for (final ZoneRecord zoneRecord : tag.getZoneTable()) {
            writeZoneRecord(zoneRecord);
        }
    }

    /**
     * @param zoneRecord
     */
    private void writeZoneRecord(ZoneRecord zoneRecord) {
        assert zoneRecord.getNumZoneData() == 2;
        tagBuffer.writeUI8(2); // always 2
        tagBuffer.writeUI32(zoneRecord.getZoneData0().getData());
        tagBuffer.writeUI32(zoneRecord.getZoneData1().getData());
        tagBuffer.writeUB(0, 6); // reserved
        tagBuffer.writeBit(zoneRecord.isZoneMaskY());
        tagBuffer.writeBit(zoneRecord.isZoneMaskX());

    }

    private void writeDefineFont3(DefineFont3Tag tag, Collection<ITag> extraTags) {
        DefineFontAlignZonesTag zones = tag.getZones();
        if (zones != null)
            extraTags.add(zones);

        writeDefineFont2(tag, extraTags);
    }

    /**
     * @see SWFReader#readDefineFont2
     */
    private void writeDefineFont2(DefineFont2Tag tag, Collection<ITag> extraTags) {
        // need to write the glyphTable to a buffer first, so as to work out
        // size size of the table, so we know whether wide offsets are needed
        final int numGlyphs = tag.getNumGlyphs();
        int[] shapeSizes = new int[numGlyphs];
        IOutputBitStream shapeBuffer = writeGlyphTableToBuffer(numGlyphs, tag, shapeSizes);

        // if the shape table is bigger that 65535 bytes, we need to use
        // wide offsets if we're not already
        if (!tag.isFontFlagsWideOffsets() && shapeBuffer.size() > 65535) {
            tag.setFontFlagsWideOffsets(true);
        }

        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeBit(tag.isFontFlagsHasLayout());
        tagBuffer.writeBit(tag.isFontFlagsShiftJIS());
        tagBuffer.writeBit(tag.isFontFlagsSmallText());
        tagBuffer.writeBit(tag.isFontFlagsANSI());
        tagBuffer.writeBit(tag.isFontFlagsWideOffsets());
        tagBuffer.writeBit(tag.isFontFlagsWideCodes());
        tagBuffer.writeBit(tag.isFontFlagsItalic());
        tagBuffer.writeBit(tag.isFontFlagsBold());
        // 8bits - no need to align
        tagBuffer.writeUI8(tag.getLanguageCode());
        writeLengthString(tag.getFontName());
        tagBuffer.writeUI16(numGlyphs);

        writeFontOffsetAndGlyphTable(shapeBuffer, shapeSizes, numGlyphs, tag.getTagType(),
                tag.isFontFlagsWideOffsets());

        assert tag.getCodeTable().length == tag.getNumGlyphs();
        for (int code : tag.getCodeTable()) {
            if (tag.isFontFlagsWideCodes()) {
                tagBuffer.writeUI16(code);
            } else {
                tagBuffer.writeUI8(code);
            }
        }

        if (tag.isFontFlagsHasLayout()) {
            assert tag.getFontAdvanceTable().length == tag.getNumGlyphs();

            tagBuffer.writeSI16(tag.getFontAscent());
            tagBuffer.writeSI16(tag.getFontDescent());
            tagBuffer.writeSI16(tag.getFontLeading());

            for (int fontAdvance : tag.getFontAdvanceTable()) {
                tagBuffer.writeSI16(fontAdvance);
            }

            assert tag.getFontBoundsTable().length == tag.getNumGlyphs();
            for (Rect bound : tag.getFontBoundsTable()) {
                writeRect(bound);
            }

            tagBuffer.writeUI16(tag.getKerningCount());

            assert tag.getKerningCount() == tag.getFontKerningTable().length;
            for (KerningRecord kerning : tag.getFontKerningTable()) {
                writeKerningRecord(kerning, tag.isFontFlagsWideCodes());
            }
        }

        DefineFontNameTag license = tag.getLicense();
        if (license != null)
            extraTags.add(license);
    }

    /**
     * @param kerning
     * @param fontFlagsWideCodes
     */
    private void writeKerningRecord(KerningRecord kerning, boolean fontFlagsWideCodes) {
        if (fontFlagsWideCodes) {
            tagBuffer.writeUI16(kerning.getCode1());
            tagBuffer.writeUI16(kerning.getCode2());
        } else {
            tagBuffer.writeUI32(kerning.getCode1());
            tagBuffer.writeUI32(kerning.getCode2());
        }
        tagBuffer.writeSI16(kerning.getAdjustment());
    }

    private void writeDefineFontInfo2(DefineFontInfo2Tag tag) {
        tagBuffer.writeUI16(tag.getFontTag().getCharacterID());
        writeLengthString(tag.getFontName());
        tagBuffer.writeUB(tag.getFontFlagsReserved(), 2);
        tagBuffer.writeBit(tag.isFontFlagsSmallText());
        tagBuffer.writeBit(tag.isFontFlagsShiftJIS());
        tagBuffer.writeBit(tag.isFontFlagsANSI());
        tagBuffer.writeBit(tag.isFontFlagsItalic());
        tagBuffer.writeBit(tag.isFontFlagsBold());
        tagBuffer.writeBit(tag.isFontFlagsWideCodes());
        // 8 bits - no need to align
        tagBuffer.writeUI8(tag.getLanguageCode());
        for (final int code : tag.getCodeTable()) {
            if (tag.isFontFlagsWideCodes()) {
                tagBuffer.writeUI16(code);
            } else {
                tagBuffer.writeUI8(code);
            }
        }
    }

    /**
     * @see SWFReader#readDefineFontInfo
     */
    private void writeDefineFontInfo(IFontInfo tag) {
        tagBuffer.writeUI16(tag.getFontTag().getCharacterID());
        writeLengthString(tag.getFontName());
        tagBuffer.writeUB(tag.getFontFlagsReserved(), 2);
        tagBuffer.writeBit(tag.isFontFlagsSmallText());
        tagBuffer.writeBit(tag.isFontFlagsShiftJIS());
        tagBuffer.writeBit(tag.isFontFlagsANSI());
        tagBuffer.writeBit(tag.isFontFlagsItalic());
        tagBuffer.writeBit(tag.isFontFlagsBold());
        tagBuffer.writeBit(tag.isFontFlagsWideCodes());
        // 8 bits - no need to align
        for (final int code : tag.getCodeTable()) {
            if (tag.isFontFlagsWideCodes()) {
                tagBuffer.writeUI16(code);
            } else {
                tagBuffer.writeUI8(code);
            }
        }
    }

    private IOutputBitStream writeGlyphTableToBuffer(int numGlyphs, DefineFontTag tag, int[] shapeSizes) {
        // create a separate buffer for the glyph table to calculate offsets
        // and then write it out at the end
        final IOutputBitStream currentTagBuffer = tagBuffer;
        final IOutputBitStream shapeBuffer = new OutputBitStream();
        tagBuffer = shapeBuffer;

        int currentOffset = 0;
        int previousOffset = 0;
        Shape[] shapes = tag.getGlyphShapeTable();
        for (int i = 0; i < numGlyphs; i++) {
            /**
             * The first STYLECHANGERECORD of each SHAPE in the GlyphShapeTable
             * does not use the LineStyle and LineStyles fields. In addition,
             * the first STYLECHANGERECORD of each shape must have both fields
             * StateFillStyle0 and FillStyle0 set to 1.
             */
            writeShape(shapes[i], tag.getTagType(), 1, 0);
            currentOffset = shapeBuffer.size();
            shapeSizes[i] = currentOffset - previousOffset;
            previousOffset = currentOffset;
        }

        // restore the original tag buffer;
        tagBuffer = currentTagBuffer;

        return shapeBuffer;
    }

    private void writeFontOffsetAndGlyphTable(IOutputBitStream shapeBuffer, int[] shapeSizes, int numGlyphs,
            TagType tagType, boolean wideOffsets) {
        int offsetTableElementSize = wideOffsets ? 4 : 2;

        int baseOffset = numGlyphs * offsetTableElementSize;
        if (tagType != TagType.DefineFont) {
            // baseOffset is now at the end of the GlyphShapeTable,
            // so add space for the CodeTableOffset value (2 or 4 bytes)
            // and that gets us to the start of the CodeTable
            if (wideOffsets)
                baseOffset += 4;
            else
                baseOffset += 2;
        }

        // Write offset table
        int currentOffset = baseOffset;
        for (int i = 0; i < numGlyphs; i++) {
            if (wideOffsets)
                tagBuffer.writeUI32(currentOffset);
            else
                tagBuffer.writeUI16(currentOffset);

            currentOffset += shapeSizes[i];
        }

        // Only write the CodeTableOffset if numGlyphs is > 0
        if (tagType != TagType.DefineFont && numGlyphs > 0) {
            assert (currentOffset == (baseOffset
                    + shapeBuffer.size())) : "offset mismatch writing font glyph table";

            if (wideOffsets)
                tagBuffer.writeUI32(currentOffset);
            else
                tagBuffer.writeUI16(currentOffset);
        }

        // Write GlyphShapeTable from the already created buffer
        tagBuffer.write(shapeBuffer.getBytes(), 0, shapeBuffer.size());
        try {
            shapeBuffer.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @see SWFReader#readDefineFont
     */
    private void writeDefineFont(DefineFontTag tag, Collection<ITag> extraTags) {
        tagBuffer.writeUI16(tag.getCharacterID());
        final int numGlyphs = tag.getGlyphShapeTable().length;
        int[] shapeSizes = new int[numGlyphs];
        IOutputBitStream shapeBuffer = writeGlyphTableToBuffer(numGlyphs, tag, shapeSizes);
        writeFontOffsetAndGlyphTable(shapeBuffer, shapeSizes, numGlyphs, tag.getTagType(), false);

        DefineFontNameTag license = tag.getLicense();
        if (license != null)
            extraTags.add(license);
    }

    /**
     * @see SWFReader#readDefineBitsJPEG3
     */
    private void writeDefineBitsJPEG3(DefineBitsJPEG3Tag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUI32(tag.getAlphaDataOffset());
        tagBuffer.write(tag.getImageData());
        tagBuffer.write(tag.getBitmapAlphaData());
    }

    /**
     * @see SWFReader#readDefineBitsJPEG2
     */
    private void writeDefineBitsJPEG2(DefineBitsJPEG2Tag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.write(tag.getImageData());
    }

    /**
     * @see SWFReader#readJPEGTables
     */
    private void writeJPEGTables(JPEGTablesTag tag) {
        tagBuffer.write(tag.getJpegData());
    }

    /**
     * @see SWFReader#readDefineBits
     */
    private void writeDefineBits(DefineBitsTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.write(tag.getImageData());
    }

    /**
     * @see SWFReader#readDefineScalingGrid
     */
    private void writeDefineScalingGrid(DefineScalingGridTag tag) {
        tagBuffer.writeUI16(tag.getCharacter().getCharacterID());
        writeRect(tag.getSplitter());
    }

    /**
     * @see SWFReader#readExportAssets
     */
    private void writeExportAssets(ExportAssetsTag tag) {
        tagBuffer.writeUI16(tag.size());
        for (final String name : tag.getCharacterNames()) {
            final ICharacterTag characterTag = tag.getCharacterTagByName(name);
            tagBuffer.writeUI16(characterTag.getCharacterID());
            tagBuffer.writeString(name);
        }
    }

    /**
     * @see SWFReader#readDefineSprite
     */
    protected void writeDefineSprite(DefineSpriteTag tag) {
        tagBuffer.writeUI16(tag.getCharacterID());
        tagBuffer.writeUI16(tag.getFrameCount());

        // Tag buffer for embedded control tags.
        final IOutputBitStream controlTagBuffer = new OutputBitStream();
        for (final ITag controlTag : tag.getControlTags()) {
            controlTagBuffer.reset();
            // DefineSprite's tagBuffer is the target output for the embedded
            // tags.
            writeTag(controlTag, controlTagBuffer, tagBuffer);
        }

        // write end marker
        tagBuffer.writeUI16(0);
    }

    /**
     * This method does not close the {@code output} stream.
     */
    @Override
    public void writeTo(OutputStream output) {
        assert output != null;

        writtenTags = new HashSet<ITag>();

        // The SWF data after the first 8 bytes can be compressed. At this
        // moment, we only encode the "compressible" part.
        writeCompressibleHeader();

        // FileAttributes must be the first tag.
        writeTag(SWF.getFileAttributes(swf));

        // Raw Metadata
        String metadata = swf.getMetadata();

        if (metadata != null)
            writeTag(new MetadataTag(metadata));

        // SetBackgroundColor tag
        final RGB backgroundColor = swf.getBackgroundColor();
        if (backgroundColor != null)
            writeTag(new SetBackgroundColorTag(backgroundColor));

        // EnableDebugger2 tag        
        if (enableDebug)
            writeTag(new EnableDebugger2Tag("NO-PASSWORD"));

        // ProductInfo tag for Flex compatibility
        ProductInfoTag productInfo = swf.getProductInfo();
        if (productInfo != null)
            writeTag(productInfo);

        // ScriptLimits tag
        final ScriptLimitsTag scriptLimitsTag = swf.getScriptLimits();
        if (scriptLimitsTag != null)
            writeTag(scriptLimitsTag);

        // Frames and enclosed tags.
        writeFrames();

        // End of SWF
        writeTag(new EndTag());

        writtenTags = null;

        // Compute the size of the SWF file.
        long length = outputBuffer.size() + 8;
        try {
            // write the first 8 bytes
            switch (useCompression) {
            case LZMA:
                output.write('Z');
                break;
            case ZLIB:
                output.write('C');
                break;
            case NONE:
                output.write('F');
                break;
            default:
                assert false;
            }

            output.write('W');
            output.write('S');
            output.write(swf.getVersion());

            writeInt(output, (int) length);

            // write the "compressible" part
            switch (useCompression) {
            case LZMA: {
                LZMACompressor compressor = new LZMACompressor();
                compressor.compress(outputBuffer);
                // now write the compressed length
                final long compressedLength = compressor.getLengthOfCompressedPayload();
                assert compressedLength <= 0xffffffffl;

                writeInt(output, (int) compressedLength);

                // now write the LZMA props
                compressor.writeLZMAProperties(output);

                // Normally LZMA (7zip) would write an 8 byte length here, but we don't, because the
                // SWF header already has this info

                // now write the n bytes of LZMA data, followed by the 6 byte EOF
                compressor.writeDataAndEnd(output);
                output.flush();
            }
                break;
            case ZLIB: {
                int compressionLevel = enableDebug ? Deflater.BEST_SPEED : Deflater.BEST_COMPRESSION;
                Deflater deflater = new Deflater(compressionLevel);
                DeflaterOutputStream deflaterStream = new DeflaterOutputStream(output, deflater);
                deflaterStream.write(outputBuffer.getBytes(), 0, outputBuffer.size());
                deflaterStream.finish();
                deflater.end();
                deflaterStream.flush();
                break;
            }
            case NONE: {
                output.write(outputBuffer.getBytes(), 0, outputBuffer.size());
                output.flush();
                break;
            }
            default:
                assert false;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * write a 32 bit integer into an output stream, in SWF byte ordering, which
     * is little-endian.
     */
    private void writeInt(OutputStream output, int theInt) throws IOException {
        output.write(theInt);
        output.write((theInt >> 8));
        output.write((theInt >> 16));
        output.write((theInt >> 24));
    }

    @Override
    public int writeTo(File outputFile) throws FileNotFoundException, IOException {
        // Ensure that the directory for the SWF exists.
        final File outputDirectory = new File(outputFile.getAbsoluteFile().getParent());
        outputDirectory.mkdirs();

        // Write out the SWF, counting how many bytes were written.
        final CountingOutputStream output = new CountingOutputStream(
                new BufferedOutputStream(new FileOutputStream(outputFile)));
        writeTo(output);
        output.flush();
        output.close();
        close();

        final int swfSize = output.getCount();
        return swfSize;
    }

    private void writeFrameLabel(FrameLabelTag tag) {
        tagBuffer.writeString(tag.getName());
    }

    /**
     * Close the internal output buffer that stores the encoded SWF tags and
     * part of the SWF header. It does not close the {@link OutputStream}
     * argument in {@link #writeTo(OutputStream)}.
     */
    @Override
    public void close() throws IOException {
        outputBuffer.close();
    }
}