/* * * 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.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.*; 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, 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<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, 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}. * <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 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<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")); } // 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(); } }