/* * * 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.builders; import org.apache.flex.swf.ISWFConstants; import org.apache.flex.swf.types.CurvedEdgeRecord; import org.apache.flex.swf.types.EdgeRecord; import org.apache.flex.swf.types.FillStyleArray; import org.apache.flex.swf.types.LineStyleArray; import org.apache.flex.swf.types.Shape; 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.utils.Point; import org.apache.flex.utils.Trace; /** * A utility class to help construct a SWF Shape from Java2D AWT Shapes. By * default, all co-ordinates are coverted to twips (1/20th of a pixel). */ public final class ShapeBuilder { /** * Constructor. * <p> * Creates an empty Flash Shape with the pen starting at [0.0, 0.0]. */ public ShapeBuilder() { this(new Styles(new FillStyleArray(), new LineStyleArray())); } /** * Constructor. * <p> * Creates an empty Flash Shape with the pen starting at [0.0,0.0]. * * @param useTwips <code>true</code> to convert to twips. */ public ShapeBuilder(boolean useTwips) { this(); convertToTwips = useTwips; } /** * Constructor. * <p> * Creates an empty Flash Shape with the pen starting at [0.0, 0.0]. */ public ShapeBuilder(Styles styles) { this(styles, new Point()); } /** * Constructor. * <p> * Use this constructor to specify whether co-ordinates will be * converted to twips (1/20th of a pixel). The default is to use this * conversion. * * @param useTwips <code>true</code> to convert to twips. */ public ShapeBuilder(Styles styles, boolean useTwips) { this(styles); convertToTwips = useTwips; } /** * Constructor. * <p> * Creates an empty Flash Shape. <code>ShapeRecord</code>s can * be added manually using the process shape method. * * @param origin the pen starting point, typically [0.0,0.0] * @see #processShape(IShapeIterator) */ public ShapeBuilder(Styles styles, Point origin) { shape = new Shape(); if (origin == null) origin = new Point(); this.start = new Point(origin.x, origin.y); this.lastMoveTo = new Point(origin.x, origin.y); this.pen = new Point(this.lastMoveTo.x, this.lastMoveTo.y); this.styles = styles; } private boolean convertToTwips = true; private boolean font123 = false; private Shape shape; private Point pen; private Point lastMoveTo; private Point start; private int dxSumTwips = 0; private int dySumTwips = 0; private int lineStyle = -1; private int fillStyle0 = -1; private int fillStyle1 = -1; private Styles styles; private boolean useFillStyle1 = true; private boolean useFillStyle0 = true; private boolean lineStyleHasChanged; private boolean fillStyle1HasChanged; private boolean fillStyle0HasChanged; private boolean closed; public Shape build() { return shape; } /** * Processes a <code>Shape</code> by converting its general path to a series * of <code>ShapeRecord</code>s. The records are not terminated with an * <code>EndShapeRecord</code> so that subsequent calls can be made to this * method to concatenate more shape paths. * <p/> * For closed shapes (including character glyphs) there exists a high * possibility of rounding errors caused by the conversion of * double-precision pixel co-ordinates to integer twips (1 twip = 1/20th * pixel at 72 dpi). As such, at each move command this error is checked and * corrected. * <p/> * * @param si A IShapeIterator who's path will be converted to records and * added to the collection */ public void processShape(IShapeIterator si) { while (!si.isDone()) { double[] coords = new double[6]; short code = si.currentSegment(coords); switch (code) { case IShapeIterator.MOVE_TO: { correctRoundingErrors(); move(coords[0], coords[1]); closed = false; //reset closed flag after move break; } case IShapeIterator.LINE_TO: { straight(coords[0], coords[1]); break; } case IShapeIterator.QUAD_TO: { curved(coords[0], coords[1], coords[2], coords[3]); break; } case IShapeIterator.CUBIC_TO: { approximateCubicBezier(new Point(pen.x, pen.y), new Point(coords[0], coords[1]), new Point(coords[2], coords[3]), new Point(coords[4], coords[5])); break; } case IShapeIterator.CLOSE: { closed = true; close(); break; } } si.next(); } correctRoundingErrors(); } /** * If a shape is closed then the start and finish records must match exactly * to the nearest twip. This method attempts to close the shape by adding a * <code>StraightEdgeRecord</code> with a delta equal to the accumulated * rounding errors, if such errors exists. */ public void correctRoundingErrors() { if ((dxSumTwips != 0 || dySumTwips != 0) && (closed || fillStyle0 > 0 || fillStyle1 > 0)) { addLineSubdivideAware(-dxSumTwips, -dySumTwips); dxSumTwips = 0; dySumTwips = 0; } } /** * Moves the current pen position to a new location without drawing. In SWF, * this requires a style change record and a delta is calculated between the * current position and the new position. * <p/> * If fill or line style information has changed since the last move * command, the new style information is also included in the record. * * @param x The new horizontal location. * @param y The new vertical location. */ public void move(double x, double y) { double dx = x - start.x; //dx is delta from origin-x to final-x double dy = y - start.y; //dy is delta from origin-y to final-y if (convertToTwips) { dx *= ISWFConstants.TWIPS_PER_PIXEL; dy *= ISWFConstants.TWIPS_PER_PIXEL; } StyleChangeRecord scr = new StyleChangeRecord(); scr.setMove((int)Math.rint(dx), (int)Math.rint(dy)); //Reset rounding counters, as this info is only useful per sub-shape/fill closure dxSumTwips = 0; dySumTwips = 0; int fillStyle0Index = -1; int fillStyle1Index = -1; int lineStyleIndex = -1; //Check styles if (lineStyleHasChanged) { lineStyleIndex = lineStyle; lineStyleHasChanged = false; } if (fillStyle0HasChanged && useFillStyle0) { fillStyle0Index = fillStyle0; fillStyle0HasChanged = false; } if (fillStyle1HasChanged && useFillStyle1) { fillStyle1Index = fillStyle1; fillStyle1HasChanged = false; } if (font123) scr.setDefinedFontStyles(fillStyle0Index, fillStyle1Index, lineStyleIndex, styles); else scr.setDefinedStyles(fillStyle0Index, fillStyle1Index, lineStyleIndex, styles); lastMoveTo.x = x; lastMoveTo.y = y; pen.x = x; pen.y = y; shape.addShapeRecord(scr); } /** * Calculates the change or 'delta' in position between the current pen * location and a given co-ordinate pair. This delta is used to create a * straight-edge shape record in SWF, i.e. a simple line. * * @param x The new horizontal location. * @param y The new vertical location. */ public void straight(double x, double y) { double dx = x - pen.x; //dx is delta from origin-x to final-x double dy = y - pen.y; //dy is delta from origin-y to final-y if (convertToTwips) { dx *= ISWFConstants.TWIPS_PER_PIXEL; dy *= ISWFConstants.TWIPS_PER_PIXEL; } if (dx == 0 && dy == 0) { return; //For now, we ignore zero length lines } else { int intdx = (int)Math.rint(dx); int intdy = (int)Math.rint(dy); addLineSubdivideAware(intdx, intdy); pen.x = x; pen.y = y; dxSumTwips += intdx; dySumTwips += intdy; } } /** * Creates a quadratic spline in SWF as a curved-edge record. The current * pen position is used as the first anchor point, and is used with the two * other points supplied to calculate a delta between the origin and the * control point, and the control point and the final anchor point. * * @param cx - control point-x * @param cy - control point-y * @param ax - anchor point-x * @param ay - anchor point-y */ public void curved(double cx, double cy, double ax, double ay) { double[] points = new double[] {pen.x, pen.y, cx, cy, ax, ay}; int[] deltas = addCurveSubdivideAware(points); pen.x = ax; pen.y = ay; dxSumTwips += (deltas[2] + deltas[0]); dySumTwips += (deltas[3] + deltas[1]); } /** * Creates a straight-edge record (i.e a straight line) from the current * drawing position to the last move-to position. If the delta for the x and * y co-ordinates is zero, a line is not necessary and the method does * nothing. */ public void close() { double dx = lastMoveTo.x - pen.x; //dx is delta from lastMoveTo-x to pen-x double dy = lastMoveTo.y - pen.y; //dy is delta from lastMoveTo-y to pen-y if (convertToTwips) { dx *= ISWFConstants.TWIPS_PER_PIXEL; dy *= ISWFConstants.TWIPS_PER_PIXEL; } pen.x = lastMoveTo.x; pen.y = lastMoveTo.y; if (dx == 0 && dy == 0) { return; //No action required } else { int intdx = (int)Math.rint(dx); int intdy = (int)Math.rint(dy); addLineSubdivideAware(intdx, intdy); dxSumTwips += intdx; dySumTwips += intdy; } } private void addLineSubdivideAware(int x, int y) { int limit = EdgeRecord.MAX_DELTA_IN_TWIPS; if (Math.abs(x) > limit || Math.abs(y) > limit) { int midXLeft = (int)Math.rint(Math.floor(x / 2.0)); int midYLeft = (int)Math.rint(Math.floor(y / 2.0)); int midXRight = (int)Math.rint(Math.ceil(x / 2.0)); int midYRight = (int)Math.rint(Math.ceil(y / 2.0)); if (Math.abs(midXLeft) > limit || Math.abs(midYLeft) > limit) addLineSubdivideAware(midXLeft, midYLeft); else shape.addShapeRecord(new StraightEdgeRecord(midXLeft, midYLeft)); if (Math.abs(midXRight) > limit || Math.abs(midYRight) > limit) addLineSubdivideAware(midXRight, midYRight); else shape.addShapeRecord(new StraightEdgeRecord(midXRight, midYRight)); } else { shape.addShapeRecord(new StraightEdgeRecord(x, y)); } } /** * Recursively draws smaller sub-sections of a curve until the * control-anchor point delta values fit into a SWF EdgeRecord. * * @param curve An array of 6 values representing the origin x-y, control * x-y, anchor x-y * @return int[] The four x-y delta values between the two anchor points and * one control point. * @see EdgeRecord#MAX_DELTA_IN_TWIPS */ private int[] addCurveSubdivideAware(double[] curve) { int[] delta = curveDeltas(curve); if (exceedsEdgeRecordLimit(delta)) { double[] left = new double[6]; double[] right = new double[6]; divideQuad(curve, 0, left, 0, right, 0); int[] deltaLeft = curveDeltas(left); int[] deltaRight = curveDeltas(right); if (exceedsEdgeRecordLimit(deltaLeft)) addCurveSubdivideAware(left); else curveRecord(deltaLeft); if (exceedsEdgeRecordLimit(deltaRight)) addCurveSubdivideAware(right); else curveRecord(deltaRight); } else { curveRecord(delta); } return delta; } /** * From java.awt.geom.QuadCurve2D */ public static void divideQuad(double src[], int srcoff, double left[], int loff, double right[], int roff) { double x1 = src[srcoff + 0]; double y1 = src[srcoff + 1]; double ctrlx = src[srcoff + 2]; double ctrly = src[srcoff + 3]; double x2 = src[srcoff + 4]; double y2 = src[srcoff + 5]; if (left != null) { left[loff + 0] = x1; left[loff + 1] = y1; } if (right != null) { right[roff + 4] = x2; right[roff + 5] = y2; } x1 = (x1 + ctrlx) / 2.0; y1 = (y1 + ctrly) / 2.0; x2 = (x2 + ctrlx) / 2.0; y2 = (y2 + ctrly) / 2.0; ctrlx = (x1 + x2) / 2.0; ctrly = (y1 + y2) / 2.0; if (left != null) { left[loff + 2] = x1; left[loff + 3] = y1; left[loff + 4] = ctrlx; left[loff + 5] = ctrly; } if (right != null) { right[roff + 0] = ctrlx; right[roff + 1] = ctrly; right[roff + 2] = x2; right[roff + 3] = y2; } } private void curveRecord(int[] delta) { CurvedEdgeRecord cer = new CurvedEdgeRecord(); cer.setControlDeltaX(delta[0]); cer.setControlDeltaY(delta[1]); cer.setAnchorDeltaX(delta[2]); cer.setAnchorDeltaY(delta[3]); shape.addShapeRecord(cer); } private int[] curveDeltas(double[] curve) { int[] deltas = new int[4]; double dcx = curve[2] - curve[0]; //dcx is delta from origin-x to control point-x double dcy = curve[3] - curve[1]; //dcy is delta from origin-y to control point-y double dax = curve[4] - curve[2]; //dax is delta from control point-x to anchor point-x double day = curve[5] - curve[3]; //day is delta from control point-y to anchor point-y if (convertToTwips) { dcx *= ISWFConstants.TWIPS_PER_PIXEL; dcy *= ISWFConstants.TWIPS_PER_PIXEL; dax *= ISWFConstants.TWIPS_PER_PIXEL; day *= ISWFConstants.TWIPS_PER_PIXEL; } deltas[0] = (int)Math.rint(dcx); deltas[1] = (int)Math.rint(dcy); deltas[2] = (int)Math.rint(dax); deltas[3] = (int)Math.rint(day); return deltas; } private boolean exceedsEdgeRecordLimit(int[] values) { for (int i = 0; i < values.length; i++) { if (Math.abs(values[i]) > EdgeRecord.MAX_DELTA_IN_TWIPS) return true; } return false; } /** * Set whether the shape is part of a DefineFont1, DefineFont2 or * DefineFont3 tag. * * @param b true if shape part of a font tag */ public void setFont12or3(boolean b) { this.font123 = b; } /** * Gets the current line style index. Note that a value of zero represents * the empty stroke. * * @return index to the current line style in the * <code>LineStyleArray</code> */ public int getCurrentLineStyle() { return lineStyle; } /** * Sets the current line style index. Note that a value of zero represents * the empty stroke. * * @param index index to a line style in the <code>LineStyleArray</code> */ public void setCurrentLineStyle(int index) { if (index != lineStyle) { lineStyleHasChanged = true; lineStyle = index; } } /** * Gets the current fill style index. Note that a value of zero represents a * blank fill. * * @return index to the current fill style in the * <code>FillStyleArray</code> */ public int getCurrentFillStyle0() { return fillStyle0; } /** * Sets the current fill style index. Note that a value of zero represents a * blank fill. * * @param index The index of a fill style. */ public void setCurrentFillStyle0(int index) { if (index != fillStyle0) { fillStyle0HasChanged = true; fillStyle0 = index; } } /** * Gets the current fill style 1 index. A fill style 1 record represents the * fill of intersecting shape areas. Note that a value of zero represents a * blank fill. * * @return index to the current fill style in the * <code>FillStyleArray</code> */ public int getCurrentFillStyle1() { return fillStyle1; } /** * Sets the current fill style 1 index. A fill style 1 record represents the * fill of intersecting shape areas. Note that a value of zero represents a * blank fill. * * @param index The index of a fill style. */ public void setCurrentFillStyle1(int index) { if (index != fillStyle1) { fillStyle1HasChanged = true; fillStyle1 = index; } } /** * Gets whether the current paint method should include fill style 1 * information, which controls how intersecting shape fills are drawn. */ public boolean getUseFillStyle1() { return useFillStyle1; } /** * Sets the paint method to include fill style 1 information, which controls * how intersecting shape fills are drawn. * * @param b if set to true, fill style 1 information will be used for * intersecting shapes */ public void setUseFillStyle1(boolean b) { useFillStyle1 = b; } /** * Sets the paint method to include fill style 0 information, which is for * filling simple shapes. * * @param b if set to true, fill style 0 information will be used for normal * shapes */ public void setUseFillStyle0(boolean b) { useFillStyle0 = b; } /** * Return a point on a segment [P0, P1] which distance from P0 is ratio of * the length [P0, P1] */ public static Point getPointOnSegment(Point P0, Point P1, double ratio) { return new Point((P0.x + ((P1.x - P0.x) * ratio)), (P0.y + ((P1.y - P0.y) * ratio))); } /** * Based on Timothee Groleau's public ActionScript library (which is based * on Helen Triolo's approach) using Casteljau's approximation for drawing * 3rd-order Cubic curves as a collection of 2nd-order Quadratic curves - * with a fixed level of accuracy using just 4 quadratic curves. * <p/> * The reason this fixed-level approach was chosen is because it is very * fast and should provide us with a reasonable approximation for small * curves involved in fonts. * <p/> * "This function will trace a cubic approximation of the cubic Bezier * It will calculate a series of [control point/Destination point] which * will be used to draw quadratic Bezier starting from P0" * <p/> * * @see <a href="http://timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm">Cubic Bezier in Flash</a> */ private void approximateCubicBezier(final Point P0, final Point P1, final Point P2, final Point P3) { // calculates the useful base points Point PA = getPointOnSegment(P0, P1, 3.0 / 4.0); Point PB = getPointOnSegment(P3, P2, 3.0 / 4.0); // get 1/16 of the [P3, P0] segment double dx = (P3.x - P0.x) / 16.0; double dy = (P3.y - P0.y) / 16.0; // calculate control point 1 Point c1 = getPointOnSegment(P0, P1, 3.0 / 8.0); // calculate control point 2 Point c2 = getPointOnSegment(PA, PB, 3.0 / 8.0); c2.x = c2.x - dx; c2.y = c2.y - dy; // calculate control point 3 Point c3 = getPointOnSegment(PB, PA, 3.0 / 8.0); c3.x = c3.x + dx; c3.y = c3.y + dy; // calculate control point 4 Point c4 = getPointOnSegment(P3, P2, 3.0 / 8.0); // calculate the 3 anchor points (as midpoints of the control segments) Point a1 = new Point(((c1.x + c2.x) / 2.0), ((c1.y + c2.y) / 2.0)); Point a2 = new Point(((PA.x + PB.x) / 2.0), ((PA.y + PB.y) / 2.0)); Point a3 = new Point(((c3.x + c4.x) / 2.0), ((c3.y + c4.y) / 2.0)); // draw the four quadratic sub-segments curved(c1.x, c1.y, a1.x, a1.y); curved(c2.x, c2.y, a2.x, a2.y); curved(c3.x, c3.y, a3.x, a3.y); curved(c4.x, c4.y, P3.x, P3.y); if (Trace.font_cubic) { Trace.trace("Cubic Curve\n"); Trace.trace("P0:\t" + P0.x + "\t" + P0.y); Trace.trace("c1:\t" + c1.x + "\t" + c1.y + "\t\tP1:\t" + P1.x + "\t" + P1.y); Trace.trace("a1:\t" + a1.x + "\t" + a1.y); Trace.trace("c2:\t" + c2.x + "\t" + c2.y); Trace.trace("a2:\t" + a2.x + "\t" + a2.y); Trace.trace("c3:\t" + c3.x + "\t" + c3.y); Trace.trace("a3:\t" + a3.x + "\t" + a3.y); Trace.trace("c4:\t" + c4.x + "\t" + c4.y + "\t\tP2:\t" + P2.x + "\t" + P2.y); Trace.trace("P3:\t" + P3.x + "\t" + P3.y); } } }