/* * Copyright (C) 2010-2016 JPEXS, All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3.0 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. */ package com.jpexs.decompiler.flash.types.shaperecords; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.SWFOutputStream; import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; import com.jpexs.decompiler.flash.helpers.FontHelper; import com.jpexs.decompiler.flash.tags.base.NeedsCharacters; import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.MATRIX; import com.jpexs.decompiler.flash.types.RECT; import com.jpexs.decompiler.flash.types.RGB; import com.jpexs.decompiler.flash.types.RGBA; import com.jpexs.decompiler.flash.types.SHAPE; import com.jpexs.helpers.ConcreteClasses; import com.jpexs.helpers.SerializableImage; import java.awt.Color; import java.awt.Font; import java.awt.Rectangle; import java.awt.Shape; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * * @author JPEXS */ @ConcreteClasses({CurvedEdgeRecord.class, StraightEdgeRecord.class, StyleChangeRecord.class, EndShapeRecord.class}) public abstract class SHAPERECORD implements Cloneable, NeedsCharacters, Serializable { public static final int MAX_CHARACTERS_IN_FONT_PREVIEW = 400; public static final boolean DRAW_BOUNDING_BOX = false; public abstract void calculateBits(); @Override public void getNeededCharacters(Set<Integer> needed) { } @Override public boolean replaceCharacter(int oldCharacterId, int newCharacterId) { return false; } @Override public boolean removeCharacter(int characterId) { return false; } public abstract int changeX(int x); public abstract int changeY(int y); public abstract void flip(); public static RECT getBounds(List<SHAPERECORD> records) { int x = 0; int y = 0; int max_x = 0; int max_y = 0; int min_x = Integer.MAX_VALUE; int min_y = Integer.MAX_VALUE; boolean started = false; for (SHAPERECORD r : records) { if (r instanceof CurvedEdgeRecord) { CurvedEdgeRecord curverEdge = (CurvedEdgeRecord) r; int x2 = x + curverEdge.controlDeltaX; int y2 = y + curverEdge.controlDeltaY; if (x2 > max_x) { max_x = x2; } if (y2 > max_y) { max_y = y2; } if (started) { if (y2 < min_y) { min_y = y2; } if (x2 < min_x) { min_x = x2; } } } x = r.changeX(x); y = r.changeY(y); if (x > max_x) { max_x = x; } if (y > max_y) { max_y = y; } if (r.isMove()) { started = true; } if (started) { if (y < min_y) { min_y = y; } if (x < min_x) { min_x = x; } } } return new RECT(min_x, max_x, min_y, max_y); } public static CurvedEdgeRecord straightToCurve(StraightEdgeRecord ser) { CurvedEdgeRecord ret = new CurvedEdgeRecord(); ret.controlDeltaX = ser.deltaX / 2; ret.controlDeltaY = ser.deltaY / 2; ret.anchorDeltaX = ser.deltaX - ret.controlDeltaX; ret.anchorDeltaY = ser.deltaY - ret.controlDeltaY; return ret; } public static void shapeListToImage(SWF swf, List<SHAPE> shapes, SerializableImage image, int frame, Color color, ColorTransform colorTransform) { if (shapes.isEmpty()) { return; } int prevWidth = image.getWidth(); int prevHeight = image.getHeight(); int maxw = 0; int maxh = 0; int minXMin = 0; int minYMin = 0; for (SHAPE s : shapes) { RECT r = SHAPERECORD.getBounds(s.shapeRecords); if (r.Xmax < r.Xmin || r.Ymax < r.Ymin) { continue; } if (r.getWidth() > maxw) { maxw = r.getWidth(); } if (r.getHeight() > maxh) { maxh = r.getHeight(); } if (r.Xmin < minXMin) { minXMin = r.Xmin; } if (r.Ymin < minYMin) { minYMin = r.Ymin; } } int shapeCount = Math.min(MAX_CHARACTERS_IN_FONT_PREVIEW, shapes.size()); int frameCount = (shapes.size() - 1) / MAX_CHARACTERS_IN_FONT_PREVIEW + 1; if (frameCount < 1) { frameCount = 1; } if (frame >= frameCount) { frame = frameCount - 1; } int cols = (int) Math.ceil(Math.sqrt(shapeCount)); int pos = frame * MAX_CHARACTERS_IN_FONT_PREVIEW; int w2 = (int) (prevWidth * SWF.unitDivisor / cols); int h2 = (int) (prevHeight * SWF.unitDivisor / cols); if (maxw == 0) { return; } int mh = maxh * w2 / maxw; int mw; if (mh > h2) { mw = maxw * h2 / maxh; mh = h2; } else { mw = w2; } float ratio = (float) mw / (float) maxw; loopy: for (int y = 0; y < cols; y++) { for (int x = 0; x < cols; x++) { if (pos >= shapes.size()) { break loopy; } // shapeNum: 1 SHAPE shape = shapes.get(pos); List<SHAPERECORD> records = shape.shapeRecords; RECT bounds = SHAPERECORD.getBounds(records); int w1 = bounds.getWidth(); int h1 = bounds.getHeight(); double w = ratio * w1; double h = ratio * h1; double px = x * w2 + w2 / 2 - w / 2 - minXMin * ratio; double py = y * h2 - minYMin * ratio; Matrix transformation = Matrix.getTranslateInstance(px, py); transformation.scale(ratio); BitmapExporter.export(swf, shape, color, image, transformation, transformation, colorTransform); // draw bounding boxes if (DRAW_BOUNDING_BOX) { RGB borderColor = new RGBA(Color.black); RGB fillColor = new RGBA(new Color(255, 255, 255, 0)); transformation = Matrix.getTranslateInstance(bounds.Xmin, bounds.Ymin).preConcatenate(transformation); TextTag.drawBorder(swf, image, borderColor, fillColor, bounds, new MATRIX(), transformation, colorTransform); } pos++; } } } public abstract boolean isMove(); public static List<SHAPE> systemFontCharactersToSHAPES(Font font, int fontSize, String characters) { List<SHAPE> ret = new ArrayList<>(); for (int i = 0; i < characters.length(); i++) { ret.add(systemFontCharacterToSHAPE(font, fontSize, characters.charAt(i))); } return ret; } public static SHAPE systemFontCharacterToSHAPE(Font font, int fontSize, char character) { return fontCharacterToSHAPE(font, fontSize, character); } public static SHAPE fontCharacterToSHAPE(final Font font, float fontSize, char character) { int multiplier = 1; if (fontSize > 1024) { multiplier = (int) (fontSize / 1024); fontSize = 1024; } List<SHAPERECORD> retList = new ArrayList<>(); Font f = font.deriveFont(fontSize); GlyphVector v = FontHelper.createGlyphVector(f, character); Shape shp = v.getOutline(); double[] points = new double[6]; int lastX = 0; int lastY = 0; int startX = 0; int startY = 0; for (PathIterator it = shp.getPathIterator(null); !it.isDone(); it.next()) { int type = it.currentSegment(points); switch (type) { case PathIterator.SEG_MOVETO: StyleChangeRecord scr = new StyleChangeRecord(); scr.stateMoveTo = true; scr.moveDeltaX = multiplier * (int) Math.round(points[0]); scr.moveDeltaY = multiplier * (int) Math.round(points[1]); scr.moveBits = SWFOutputStream.getNeededBitsS(scr.moveDeltaX, scr.moveDeltaY); retList.add(scr); lastX = (int) Math.round(points[0]); lastY = (int) Math.round(points[1]); startX = lastX; startY = lastY; break; case PathIterator.SEG_LINETO: StraightEdgeRecord ser = new StraightEdgeRecord(); ser.deltaX = multiplier * (((int) Math.round(points[0])) - lastX); ser.deltaY = multiplier * (((int) Math.round(points[1])) - lastY); ser.generalLineFlag = ser.deltaX != 0 && ser.deltaY != 0; if (ser.deltaX == 0) { ser.vertLineFlag = true; } ser.numBits = SWFOutputStream.getNeededBitsS(ser.deltaX, ser.deltaY) - 2; if (ser.numBits < 0) { ser.numBits = 0; } retList.add(ser); lastX = (int) Math.round(points[0]); lastY = (int) Math.round(points[1]); break; case PathIterator.SEG_CUBICTO: double[] cubicCoords = new double[]{ lastX, lastY, Math.round(points[0]), Math.round(points[1]), Math.round(points[2]), Math.round(points[3]), Math.round(points[4]), Math.round(points[5]) }; double[][] quadCoords = approximateCubic(cubicCoords); for (int i = 0; i < quadCoords.length; i++) { CurvedEdgeRecord cer = new CurvedEdgeRecord(); cer.controlDeltaX = multiplier * (((int) Math.round(quadCoords[i][0])) - lastX); cer.controlDeltaY = multiplier * (((int) Math.round(quadCoords[i][1])) - lastY); cer.anchorDeltaX = multiplier * (((int) Math.round(quadCoords[i][2])) - ((int) Math.round(quadCoords[i][0]))); cer.anchorDeltaY = multiplier * (((int) Math.round(quadCoords[i][3])) - ((int) Math.round(quadCoords[i][1]))); cer.numBits = SWFOutputStream.getNeededBitsS(cer.controlDeltaX, cer.controlDeltaY, cer.anchorDeltaX, cer.anchorDeltaY) - 2; if (cer.numBits < 0) { cer.numBits = 0; } lastX = (int) Math.round(quadCoords[i][2]); lastY = (int) Math.round(quadCoords[i][3]); retList.add(cer); } break; case PathIterator.SEG_QUADTO: CurvedEdgeRecord cer = new CurvedEdgeRecord(); cer.controlDeltaX = multiplier * (((int) Math.round(points[0])) - lastX); cer.controlDeltaY = multiplier * (((int) Math.round(points[1])) - lastY); cer.anchorDeltaX = multiplier * (((int) Math.round(points[2])) - (int) Math.round(points[0])); cer.anchorDeltaY = multiplier * (((int) Math.round(points[3])) - (int) Math.round(points[1])); cer.numBits = SWFOutputStream.getNeededBitsS(cer.controlDeltaX, cer.controlDeltaY, cer.anchorDeltaX, cer.anchorDeltaY) - 2; if (cer.numBits < 0) { cer.numBits = 0; } retList.add(cer); lastX = (int) Math.round(points[2]); lastY = (int) Math.round(points[3]); break; case PathIterator.SEG_CLOSE: //Closing line back to last SEG_MOVETO if ((startX == lastX) && (startY == lastY)) { break; } StraightEdgeRecord closeSer = new StraightEdgeRecord(); closeSer.generalLineFlag = true; closeSer.deltaX = multiplier * ((int) Math.round((startX - lastX))); closeSer.deltaY = multiplier * ((int) Math.round((startY - lastY))); closeSer.numBits = SWFOutputStream.getNeededBitsS(closeSer.deltaX, closeSer.deltaY) - 2; if (closeSer.numBits < 0) { closeSer.numBits = 0; } retList.add(closeSer); lastX = startX; lastY = startY; break; } } SHAPE shape = new SHAPE(); StyleChangeRecord init; if (!retList.isEmpty() && retList.get(0) instanceof StyleChangeRecord) { init = (StyleChangeRecord) retList.get(0); } else { init = new StyleChangeRecord(); retList.add(0, init); } retList.add(new EndShapeRecord()); init.stateFillStyle0 = true; init.fillStyle0 = 1; shape.shapeRecords = retList; shape.numFillBits = 1; shape.numLineBits = 0; return shape; } // Taken from org.apache.fop.afp.util public static double[][] approximateCubic(double[] cubicControlPointCoords) { if (cubicControlPointCoords.length < 8) { throw new IllegalArgumentException("Must have at least 8 coordinates"); } //extract point objects from source array Point2D p0 = new Point2D.Double(cubicControlPointCoords[0], cubicControlPointCoords[1]); Point2D p1 = new Point2D.Double(cubicControlPointCoords[2], cubicControlPointCoords[3]); Point2D p2 = new Point2D.Double(cubicControlPointCoords[4], cubicControlPointCoords[5]); Point2D p3 = new Point2D.Double(cubicControlPointCoords[6], cubicControlPointCoords[7]); //calculates the useful base points Point2D pa = getPointOnSegment(p0, p1, 3.0 / 4.0); Point2D pb = getPointOnSegment(p3, p2, 3.0 / 4.0); //get 1/16 of the [P3, P0] segment double dx = (p3.getX() - p0.getX()) / 16.0; double dy = (p3.getY() - p0.getY()) / 16.0; //calculates control point 1 Point2D pc1 = getPointOnSegment(p0, p1, 3.0 / 8.0); //calculates control point 2 Point2D pc2 = getPointOnSegment(pa, pb, 3.0 / 8.0); pc2 = movePoint(pc2, -dx, -dy); //calculates control point 3 Point2D pc3 = getPointOnSegment(pb, pa, 3.0 / 8.0); pc3 = movePoint(pc3, dx, dy); //calculates control point 4 Point2D pc4 = getPointOnSegment(p3, p2, 3.0 / 8.0); //calculates the 3 anchor points Point2D pa1 = getMidPoint(pc1, pc2); Point2D pa2 = getMidPoint(pa, pb); Point2D pa3 = getMidPoint(pc3, pc4); //return the points for the four quadratic curves return new double[][]{ {pc1.getX(), pc1.getY(), pa1.getX(), pa1.getY()}, {pc2.getX(), pc2.getY(), pa2.getX(), pa2.getY()}, {pc3.getX(), pc3.getY(), pa3.getX(), pa3.getY()}, {pc4.getX(), pc4.getY(), p3.getX(), p3.getY()}}; } private static Point2D.Double movePoint(Point2D point, double dx, double dy) { return new Point2D.Double(point.getX() + dx, point.getY() + dy); } private static Point2D getMidPoint(Point2D p0, Point2D p1) { return getPointOnSegment(p0, p1, 0.5); } private static Point2D getPointOnSegment(Point2D p0, Point2D p1, double ratio) { double x = p0.getX() + ((p1.getX() - p0.getX()) * ratio); double y = p0.getY() + ((p1.getY() - p0.getY()) * ratio); return new Point2D.Double(x, y); } public static SHAPE resizeSHAPE(SHAPE shp, double multiplier) { SHAPE ret = new SHAPE(); ret.numFillBits = shp.numFillBits; ret.numLineBits = shp.numLineBits; List<SHAPERECORD> recs = new ArrayList<>(); for (SHAPERECORD r : shp.shapeRecords) { SHAPERECORD c = r.clone(); if (c instanceof StyleChangeRecord) { StyleChangeRecord scr = (StyleChangeRecord) c; scr.moveDeltaX = (int) (multiplier * scr.moveDeltaX); scr.moveDeltaY = (int) (multiplier * scr.moveDeltaY); scr.calculateBits(); } if (c instanceof CurvedEdgeRecord) { CurvedEdgeRecord cer = (CurvedEdgeRecord) c; cer.controlDeltaX = (int) (multiplier * cer.controlDeltaX); cer.controlDeltaY = (int) (multiplier * cer.controlDeltaY); cer.anchorDeltaX = (int) (multiplier * cer.anchorDeltaX); cer.anchorDeltaY = (int) (multiplier * cer.anchorDeltaY); cer.calculateBits(); } if (c instanceof StraightEdgeRecord) { StraightEdgeRecord ser = (StraightEdgeRecord) c; ser.deltaX = (int) (multiplier * ser.deltaX); ser.deltaY = (int) (multiplier * ser.deltaY); ser.calculateBits(); } recs.add(c); } ret.shapeRecords = recs; return ret; } public static Shape moveShapeToStart(Shape s) { Rectangle bds = s.getBounds(); s = AffineTransform.getTranslateInstance(-bds.x, -bds.y).createTransformedShape(s); return s; } public static Shape twipToPixelShape(Shape s) { Rectangle bds = s.getBounds(); int dx = -bds.x - bds.width / 2; int dy = -bds.y - bds.height / 2; s = AffineTransform.getTranslateInstance(dx, dy).createTransformedShape(s); s = AffineTransform.getScaleInstance(1 / SWF.unitDivisor, 1 / SWF.unitDivisor).createTransformedShape(s); s = AffineTransform.getTranslateInstance(-dx / SWF.unitDivisor, -dy / SWF.unitDivisor).createTransformedShape(s); return s; } @Override public SHAPERECORD clone() { try { return (SHAPERECORD) super.clone(); } catch (CloneNotSupportedException ex) { throw new RuntimeException(); } } }