/* * Place3.java * Transform * * Copyright (c) 2009-2010 Flagstone Software Ltd. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Flagstone Software Ltd. nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.flagstone.transform; import java.io.IOException; import java.util.ArrayList; import java.util.List; import com.flagstone.transform.coder.Coder; import com.flagstone.transform.coder.Context; import com.flagstone.transform.coder.SWFDecoder; import com.flagstone.transform.coder.SWFEncoder; import com.flagstone.transform.coder.SWFFactory; import com.flagstone.transform.datatype.Blend; import com.flagstone.transform.datatype.ColorTransform; import com.flagstone.transform.datatype.CoordTransform; import com.flagstone.transform.exception.IllegalArgumentRangeException; import com.flagstone.transform.filter.Filter; /** * Place3 is used to update the display list. It extends Place2 by specifying * a Blend which controls how an object is composited with its background and * also Filters which can be used to create special effects such as drop shadows * etc. * * @see Place2 */ @SuppressWarnings({"PMD.TooManyFields", "PMD.TooManyMethods", "PMD.CyclomaticComplexity" }) public final class Place3 implements MovieTag { /** * Place a new object on the display list. * @param identifier the unique identifier for the object. * @param layer the layer where it will be displayed. * @param xCoord the x-coordinate where the object's origin will be. * @param yCoord the y-coordinate where the object's origin will be. * @return the Place3 object to update the display list. */ public static Place3 show(final int identifier, final int layer, final int xCoord, final int yCoord) { final Place3 object = new Place3(); object.setType(PlaceType.NEW); object.setLayer(layer); object.setIdentifier(identifier); object.setTransform(CoordTransform.translate(xCoord, yCoord)); return object; } /** * Change the position of a displayed object. * * @param layer the display list layer where the object is displayed. * @param xCoord the x-coordinate where the object's origin will be moved. * @param yCoord the y-coordinate where the object's origin will be moved. * @return the Place3 object to change the position of the object. */ public static Place3 move(final int layer, final int xCoord, final int yCoord) { final Place3 object = new Place3(); object.setType(PlaceType.MODIFY); object.setLayer(layer); object.setTransform(CoordTransform.translate(xCoord, yCoord)); return object; } /** * Replace an existing object with another. * * @param identifier the unique identifier of the new object. * @param layer the display list layer of the existing object. * @return the Place3 object to update the display list. */ public static Place3 replace(final int identifier, final int layer) { final Place3 object = new Place3(); object.setType(PlaceType.REPLACE); object.setLayer(layer); object.setIdentifier(identifier); return object; } /** * Replace an existing object with another. * * @param identifier the unique identifier of the new object. * @param layer the display list layer of the existing object. * @param xCoord the x-coordinate where the new object's origin will be. * @param yCoord the y-coordinate where the new object's origin will be. * @return the Place3 object to update the display list. */ public static Place3 replace(final int identifier, final int layer, final int xCoord, final int yCoord) { final Place3 object = new Place3(); object.setType(PlaceType.REPLACE); object.setLayer(layer); object.setIdentifier(identifier); object.setTransform(CoordTransform.translate(xCoord, yCoord)); return object; } /** Format string used in toString() method. */ private static final String FORMAT = "PlaceObject3: { type=%s; layer=%d;" + " bitmapCache=%d; identifier=%d; transform=%s;" + " colorTransform=%s; ratio=%d; clippingDepth=%d;" + " name=%s; className=%s;" + " filters=%s; blend=%s; clipEvents=%s}"; /** How the display list will be updated. */ private PlaceType type; /** The display list layer number. */ private int layer; /** The actionscript 3 class that will render the object to be displayed. */ private String className; /** Whether the displayed object will be cached as a bitmap. */ private Integer bitmapCache; /** The unique identifier of the object that will be displayed. */ private int identifier; /** The coordinate transform applied to the displayed object. */ private CoordTransform transform; /** The color transform applied to the displayed object. */ private ColorTransform colorTransform; /** The progression of the morphing process. */ private Integer ratio; /** The name assigned to an object. */ private String name; /** The number of layers to clip. */ private Integer depth; /** The set of effects filters applied to the object. */ private List<Filter> filters; /** How the object is blended with its background. */ private Integer blend; /** The set of event handlers for movie clips. */ private List<EventHandler> events; /** The length of the object, minus the header, when it is encoded. */ private transient int length; /** Indicates whether the encoded object contains a blend. */ private transient boolean hasBlend; /** Indicates whether the encoded object contains filters. */ private transient boolean hasFilters; /** Indicates whether the encoded object contains an image. */ private transient boolean hasImage; /** * Creates and initialises a Place3 object using values encoded * in the Flash binary format. * * @param coder * an SWFDecoder object that contains the encoded Flash data. * * @param context * a Context object used to manage the decoders for different * type of object and to pass information on how objects are * decoded. * * @throws IOException * if an error occurs while decoding the data. */ @SuppressWarnings({"PMD.AssignmentInOperand", "PMD.ExcessiveMethodLength" }) public Place3(final SWFDecoder coder, final Context context) throws IOException { context.put(Context.TRANSPARENT, 1); length = coder.readUnsignedShort() & Coder.LENGTH_FIELD; if (length == Coder.IS_EXTENDED) { length = coder.readInt(); } coder.mark(); int bits = coder.readByte(); final boolean hasEvents = (bits & Coder.BIT7) != 0; final boolean hasDepth = (bits & Coder.BIT6) != 0; final boolean hasName = (bits & Coder.BIT5) != 0; final boolean hasRatio = (bits & Coder.BIT4) != 0; final boolean hasColorTransform = (bits & Coder.BIT3) != 0; final boolean hasTransform = (bits & Coder.BIT2) != 0; switch (bits & Coder.PAIR0) { case 0: type = PlaceType.MODIFY; break; case 1: type = PlaceType.MODIFY; break; case 2: type = PlaceType.NEW; break; default: type = PlaceType.REPLACE; break; } bits = coder.readByte(); hasImage = (bits & Coder.BIT4) != 0; final boolean hasClassName = (bits & Coder.BIT3) != 0; final boolean hasBitmapCache = (bits & Coder.BIT2) != 0; hasBlend = (bits & Coder.BIT1) != 0; hasFilters = (bits & Coder.BIT0) != 0; layer = coder.readUnsignedShort(); /* The following line implements the logic as described in the SWF 9 * specification but it appears to be incorrect. The class name is not * given when hasImage is set. * * if (hasClassName || ((type == PlaceType.NEW * || type == PlaceType.REPLACE) && hasImage)) { */ if (hasClassName) { className = coder.readString(); } if ((type == PlaceType.NEW) || (type == PlaceType.REPLACE)) { identifier = coder.readUnsignedShort(); } if (hasTransform) { transform = new CoordTransform(coder); } if (hasColorTransform) { colorTransform = new ColorTransform(coder, context); } if (hasRatio) { ratio = coder.readUnsignedShort(); } if (hasName) { name = coder.readString(); } if (hasDepth) { depth = coder.readUnsignedShort(); } filters = new ArrayList<Filter>(); if (hasFilters) { final SWFFactory<Filter> decoder = context.getRegistry() .getFilterDecoder(); final int count = coder.readByte(); for (int i = 0; i < count; i++) { decoder.getObject(filters, coder, context); } } if (hasBlend) { blend = coder.readByte(); } if (hasBitmapCache) { bitmapCache = coder.readByte(); } events = new ArrayList<EventHandler>(); if (hasEvents) { int event; coder.readUnsignedShort(); coder.readInt(); while ((event = coder.readInt()) != 0) { events.add(new EventHandler(event, coder, context)); } } context.remove(Context.TRANSPARENT); coder.check(length); coder.unmark(); } /** * Creates an uninitialised Place3 object. */ public Place3() { filters = new ArrayList<Filter>(); events = new ArrayList<EventHandler>(); } /** * Creates and initialises a Place3 object using the values copied * from another Place3 object. * * @param object * a Place3 object from which the values will be * copied. */ public Place3(final Place3 object) { type = object.type; layer = object.layer; bitmapCache = object.bitmapCache; className = object.className; identifier = object.identifier; if (object.transform != null) { transform = object.transform; } if (object.colorTransform != null) { colorTransform = object.colorTransform; } ratio = object.ratio; depth = object.depth; name = object.name; filters = new ArrayList<Filter>(object.filters); blend = object.blend; events = new ArrayList<EventHandler>(object.events.size()); for (final EventHandler event : object.events) { events.add(event.copy()); } } /** * Get the type of place operation being performed, either adding a new * object, replacing an existing one with another or modifying an existing * object. * * @return the way the object will be placed. */ public PlaceType getType() { return type; } /** * Sets the type of placement. * * @param aType * the type of operation to be performed, either New, Modify or * Replace. * * @return this object. */ public Place3 setType(final PlaceType aType) { type = aType; return this; } /** * Get the Layer on which the object will be displayed in the display * list. * * @return the layer where the object will be displayed. */ public int getLayer() { return layer; } /** * Sets the layer at which the object will be placed. * * @param aLayer * the layer number on which the object is being displayed. Must * be in the range 1..65535. * * @return this object. */ public Place3 setLayer(final int aLayer) { if ((aLayer < 1) || (aLayer > Coder.USHORT_MAX)) { throw new IllegalArgumentRangeException(1, Coder.USHORT_MAX, aLayer); } layer = aLayer; return this; } /** * Get the identifier of the object to be placed. This is only required * when placing an object for the first time. Subsequent references to the * object on this layer can simply use the layer number. * * @return the unique identifier of the object to be displayed. */ public int getIdentifier() { return identifier; } /** * Sets the identifier of the object. * * @param uid * the identifier of a new object to be displayed. Must be in the * range 1..65535. * * @return this object. */ public Place3 setIdentifier(final int uid) { if ((uid < 1) || (uid > Coder.USHORT_MAX)) { throw new IllegalArgumentRangeException( 1, Coder.USHORT_MAX, uid); } identifier = uid; return this; } /** * Get the coordinate transform. May be null if no coordinate transform * was defined. * * @return the coordinate transform that will be applied to the displayed * object. */ public CoordTransform getTransform() { return transform; } /** * Sets the coordinate transform that defines the position where the object * will be displayed. The argument may be null if the location of the object * is not being changed. * * @param matrix * an CoordTransform object that will be applied to the object * displayed. * * @return this object. */ public Place3 setTransform(final CoordTransform matrix) { transform = matrix; return this; } /** * Get the colour transform. May be null if no colour transform was * defined. * * @return the colour transform that will be applied to the displayed * object. */ public ColorTransform getColorTransform() { return colorTransform; } /** * Sets the colour transform that defines the colour effects applied to the * object. The argument may be null if the color of the object is not being * changed. * * @param cxform * an ColorTransform object that will be applied to the object * displayed. * * @return this object. */ public Place3 setColorTransform(final ColorTransform cxform) { colorTransform = cxform; return this; } /** * Get the morph ratio, in the range 0..65535 that defines the progress * in the morphing process performed by the Flash Player from the defined * start and end shapes. A value of 0 indicates the start of the process and * 65535 the end. Returns null if no ratio was specified. * * @return the morphing ratio. */ public Integer getRatio() { return ratio; } /** * Sets point of the morphing process for a morph shape in the range * 0..65535. May be set to null if the shape being placed is not being * morphed. * * @param aNumber * the progress in the morphing process. * * @return this object. */ public Place3 setRatio(final Integer aNumber) { if ((aNumber != null) && ((aNumber < 0) || (aNumber > Coder.USHORT_MAX))) { throw new IllegalArgumentRangeException( 1, Coder.USHORT_MAX, aNumber); } ratio = aNumber; return this; } /** * Get the number of layers that will be clipped by the object placed on * the layer specified in this object. * * @return the number of layers to be clipped. */ public Integer getDepth() { return depth; } /** * Sets the number of layers that this object will mask. May be set to zero * if the shape being placed does not define a clipping area. * * @param aNumber * the number of layers clipped. * * @return this object. */ public Place3 setDepth(final Integer aNumber) { if ((aNumber != null) && ((aNumber < 1) || (aNumber > Coder.USHORT_MAX))) { throw new IllegalArgumentRangeException( 1, Coder.USHORT_MAX, aNumber); } depth = aNumber; return this; } /** * Get the name of the object. May be null if a name was not assigned to * the object. * * @return the name of the object. */ public String getName() { return name; } /** * Set the name of an object to be displayed. If a shape is not being * assigned a name then setting the argument to null will omit the attribute * when the object is encoded. * * @param aString * the name assigned to the object. * * @return this object. */ public Place3 setName(final String aString) { name = aString; return this; } /** * Get the value indicating whether the display object will be cached as a * bitmap (non-zero) or not cached (zero). * * @return a non-zero value if the object is cached or zer oif not cached. */ public Integer getBitmapCache() { return bitmapCache; } /** * Set whether the displayed object should be cached as a bitmap. * * @param cache set to a non-zero value if the object should be cached as * a bitmap or to zero to disable caching. * * @return this object. */ public Place3 setBitmapCache(final Integer cache) { bitmapCache = cache; return this; } /** * Get the name of the Actionscript 3 class which will be used to render * the object to be displayed. * * @return the name of the Actionscript class. */ public String getClassName() { return className; } /** * Set the name of the Actionscript 3 class which will be used to render * the object to be displayed. * * @param aName the name of the Actionscript class. * @return this object. */ public Place3 setClassName(final String aName) { className = aName; return this; } /** * Get the list of filters that will be applied to the object when it is * displayed as a bitmap. * @return the list of bitmap filters. */ public List<Filter> getFilters() { return filters; } /** * Set the list of filters that will be applied to the object when it is * displayed as a bitmap. * @param list the list of bitmap filters. */ public void setFilters(final List<Filter> list) { if (list == null) { throw new IllegalArgumentException(); } filters = list; } /** * Get the blend that describes how the object will be rendered in relation * to the background. * @return the Blend that describes how the object is composited. */ public Blend getBlend() { return Blend.fromInt(blend); } /** * Set the blend that describes how the object will be rendered in relation * to the background. * @param mode the Blend that describes how the object is composited. * @return this object. */ public Place3 setBlend(final Blend mode) { blend = mode.getValue(); return this; } /** * Adds a clip event to the list of clip events. * * @param aClipEvent * a clip event object. * * @return this object. */ public Place3 add(final EventHandler aClipEvent) { if (aClipEvent == null) { throw new IllegalArgumentException(); } events.add(aClipEvent); return this; } /** * Get the list of event handlers that define the actions that will * be executed in response to events that occur in the movie clip being * placed. * * @return the set of event handlers for the movie clip. */ public List<EventHandler> getEvents() { return events; } /** * Set the list of Clip events. Clip Events are only valid for movie clips * and the argument should be set to null when placing other types of * object. * * If the object already contains a set of encoded clip event objects they * will be deleted. * * @param list * a list of ClipEvent objects. */ public void setEvents(final List<EventHandler> list) { if (list == null) { throw new IllegalArgumentException(); } events = list; } /** * Adds a Filter to the list of filters. * * @param filter * a Filter object. * * @return this object. */ public Place3 add(final Filter filter) { if (filter == null) { throw new IllegalArgumentException(); } filters.add(filter); return this; } /** {@inheritDoc} */ public Place3 copy() { return new Place3(this); } /** {@inheritDoc} */ @Override public String toString() { return String.format(FORMAT, type, layer, bitmapCache, identifier, transform, colorTransform, ratio, depth, name, className, filters, blend, events); } /** {@inheritDoc} */ @SuppressWarnings("PMD.NPathComplexity") public int prepareToEncode(final Context context) { // CHECKSTYLE:OFF context.put(Context.TRANSPARENT, 1); hasBlend = blend != null; hasFilters = true ^ filters.isEmpty(); length = 4; length += ((type == PlaceType.NEW) || (type == PlaceType.REPLACE)) ? 2 : 0; length += transform == null ? 0 : transform.prepareToEncode(context); length += colorTransform == null ? 0 : colorTransform.prepareToEncode( context); length += ratio == null ? 0 : 2; length += depth == null ? 0 : 2; length += name == null ? 0 : context.strlen(name); length += className == null ? 0 : context.strlen(className); if (hasFilters) { length += 1; for (final Filter filter : filters) { length += filter.prepareToEncode(context); } } if (hasBlend) { length += 1; } if (bitmapCache != null) { length += 1; } if (!events.isEmpty()) { length += 6; for (final EventHandler handler : events) { length += handler.prepareToEncode(context); } length += 4; } context.remove(Context.TRANSPARENT); return (length > Coder.HEADER_LIMIT ? Coder.LONG_HEADER : Coder.SHORT_HEADER) + length; // CHECKSTYLE:ON } /** {@inheritDoc} */ @SuppressWarnings({"PMD.NPathComplexity", "PMD.ExcessiveMethodLength" }) public void encode(final SWFEncoder coder, final Context context) throws IOException { if (length > Coder.HEADER_LIMIT) { coder.writeShort((MovieTypes.PLACE_3 << Coder.LENGTH_FIELD_SIZE) | Coder.IS_EXTENDED); coder.writeInt(length); } else { coder.writeShort((MovieTypes.PLACE_3 << Coder.LENGTH_FIELD_SIZE) | length); } if (Constants.DEBUG) { coder.mark(); } context.put(Context.TRANSPARENT, 1); int bits = 0; bits |= events.isEmpty() ? 0 : Coder.BIT7; bits |= depth == null ? 0 : Coder.BIT6; bits |= name == null ? 0 : Coder.BIT5; bits |= ratio == null ? 0 : Coder.BIT4; bits |= colorTransform == null ? 0 : Coder.BIT3; bits |= transform == null ? 0 : Coder.BIT2; switch (type) { case MODIFY: bits |= Coder.BIT0; break; case NEW: bits |= Coder.BIT1; break; default: bits |= Coder.BIT0; bits |= Coder.BIT1; break; } coder.writeByte(bits); bits = 0; bits |= hasImage ? Coder.BIT4 : 0; bits |= className == null ? 0 : Coder.BIT3; bits |= bitmapCache == null ? 0 : Coder.BIT2; bits |= hasBlend ? Coder.BIT1 : 0; bits |= hasFilters ? Coder.BIT0 : 0; coder.writeByte(bits); coder.writeShort(layer); if (className != null) { coder.writeString(className); } if ((type == PlaceType.NEW) || (type == PlaceType.REPLACE)) { coder.writeShort(identifier); } if (transform != null) { transform.encode(coder, context); } if (colorTransform != null) { colorTransform.encode(coder, context); } if (ratio != null) { coder.writeShort(ratio); } if (name != null) { coder.writeString(name); } if (depth != null) { coder.writeShort(depth); } if (hasFilters) { coder.writeByte(filters.size()); for (Filter filter : filters) { filter.encode(coder, context); } } if (hasBlend) { coder.writeByte(blend); } if (bitmapCache != null) { coder.writeByte(bitmapCache); } if (!events.isEmpty()) { int eventMask = 0; coder.writeShort(0); for (final EventHandler handler : events) { eventMask |= handler.getEventCode(); } coder.writeInt(eventMask); for (final EventHandler handler : events) { handler.encode(coder, context); } coder.writeInt(0); } context.remove(Context.TRANSPARENT); if (Constants.DEBUG) { coder.check(length); coder.unmark(); } } }