All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.royale.swf.io.SWFWriter Maven / Gradle / Ivy

There is a newer version: 0.9.10
Show newest version
/*
 *
 *  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.royale.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.royale.swf.Header;
import org.apache.royale.swf.Header.Compression;
import org.apache.royale.swf.ISWF;
import org.apache.royale.swf.SWF;
import org.apache.royale.swf.SWFFrame;
import org.apache.royale.swf.TagType;
import org.apache.royale.swf.io.SWFReader.CurrentStyles;
import org.apache.royale.swf.tags.*;
import org.apache.royale.swf.types.ARGB;
import org.apache.royale.swf.types.BevelFilter;
import org.apache.royale.swf.types.BlurFilter;
import org.apache.royale.swf.types.ButtonRecord;
import org.apache.royale.swf.types.CXForm;
import org.apache.royale.swf.types.CXFormWithAlpha;
import org.apache.royale.swf.types.ConvolutionFilter;
import org.apache.royale.swf.types.CurvedEdgeRecord;
import org.apache.royale.swf.types.DropShadowFilter;
import org.apache.royale.swf.types.EndShapeRecord;
import org.apache.royale.swf.types.FillStyle;
import org.apache.royale.swf.types.FillStyleArray;
import org.apache.royale.swf.types.Filter;
import org.apache.royale.swf.types.FocalGradient;
import org.apache.royale.swf.types.GlowFilter;
import org.apache.royale.swf.types.GlyphEntry;
import org.apache.royale.swf.types.GradRecord;
import org.apache.royale.swf.types.Gradient;
import org.apache.royale.swf.types.GradientBevelFilter;
import org.apache.royale.swf.types.GradientGlowFilter;
import org.apache.royale.swf.types.IFillStyle;
import org.apache.royale.swf.types.ILineStyle;
import org.apache.royale.swf.types.KerningRecord;
import org.apache.royale.swf.types.LineStyle;
import org.apache.royale.swf.types.LineStyle2;
import org.apache.royale.swf.types.LineStyleArray;
import org.apache.royale.swf.types.Matrix;
import org.apache.royale.swf.types.MorphFillStyle;
import org.apache.royale.swf.types.MorphGradRecord;
import org.apache.royale.swf.types.MorphGradient;
import org.apache.royale.swf.types.MorphLineStyle;
import org.apache.royale.swf.types.MorphLineStyle2;
import org.apache.royale.swf.types.RGB;
import org.apache.royale.swf.types.RGBA;
import org.apache.royale.swf.types.Rect;
import org.apache.royale.swf.types.Shape;
import org.apache.royale.swf.types.ShapeRecord;
import org.apache.royale.swf.types.ShapeWithStyle;
import org.apache.royale.swf.types.SoundEnvelope;
import org.apache.royale.swf.types.SoundInfo;
import org.apache.royale.swf.types.StraightEdgeRecord;
import org.apache.royale.swf.types.StyleChangeRecord;
import org.apache.royale.swf.types.Styles;
import org.apache.royale.swf.types.TextRecord;
import org.apache.royale.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, boolean enableTelemetry)
        {
            return new SWFWriter(swf, useCompression, enableDebug, enableTelemetry);
        }

    }

    /**
     * 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;

    // True if debugging of the SWF is enabled.
    private final boolean enableDebug;

    // True if telemetry features of the SWF are enabled.
    private final boolean enableTelemetry;

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

    // Prevent writing out the same tag twice.
    private Set 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, 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
     * @param enableTelemetry enable telemetry
     */
    public SWFWriter(ISWF swf, Header.Compression useCompression, boolean enableDebug, boolean enableTelemetry)
    {
        this.swf = swf;
        this.useCompression = useCompression;
        this.enableDebug = enableDebug;
        this.enableTelemetry = enableTelemetry;
        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 writeEnableTelemetry(EnableTelemetryTag tag)
    {
        // Tag Code (Upper 10 bits = tag type, lower 16 bit = tag length)
        tagBuffer.writeUI16(0); // reserved always zero
        // PasswordHash: Optional SHA-256 hash of the UTF-8 representation of the password.
        // If not present telemetry clients can connect without using a password, if set they
        // have to authenticate.
        if(tag.getPassword() != null) {
            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 matrix
     */
    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}.
     * 

* 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 extraTags = new LinkedList(); 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 EnableTelemetry: writeEnableTelemetry((EnableTelemetryTag) 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 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 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 extraTags) { writeDefineText(tag, extraTags); } private void writeDefineText(DefineTextTag tag, Collection 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 extraTags) { DefineFontAlignZonesTag zones = tag.getZones(); if (zones != null) extraTags.add(zones); writeDefineFont2(tag, extraTags); } /** * @see SWFReader#readDefineFont2 */ private void writeDefineFont2(DefineFont2Tag tag, Collection 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 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(); // 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")); } // EnableTelemetry tag if (enableTelemetry) { writeTag(new EnableTelemetryTag()); } // 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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy