From 9bfb8b117d283417739cc5c0a829e454fdc6f093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Sun, 12 Nov 2023 23:37:16 +0100 Subject: [PATCH] FLA export - fixers refactoring --- .../decompiler/flash/xfl/XFLConverter.java | 34 +- .../flash/xfl/shapefixer/MorphShapeFixer.java | 528 ++++++++++++++++++ .../flash/xfl/shapefixer/ShapeFixer.java | 446 +-------------- 3 files changed, 557 insertions(+), 451 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/MorphShapeFixer.java diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java index 461e909e0..7c875d051 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java @@ -141,6 +141,7 @@ import com.jpexs.decompiler.flash.types.sound.MP3FRAME; import com.jpexs.decompiler.flash.types.sound.MP3SOUNDDATA; import com.jpexs.decompiler.flash.types.sound.SoundFormat; import com.jpexs.decompiler.flash.xfl.shapefixer.CurvedEdgeRecordAdvanced; +import com.jpexs.decompiler.flash.xfl.shapefixer.MorphShapeFixer; import com.jpexs.decompiler.flash.xfl.shapefixer.ShapeFixer; import com.jpexs.decompiler.flash.xfl.shapefixer.ShapeRecordAdvanced; import com.jpexs.decompiler.flash.xfl.shapefixer.StraightEdgeRecordAdvanced; @@ -612,8 +613,8 @@ public class XFLConverter { return false; } - private static void convertShape(SWF swf, HashMap characters, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape, boolean useLayers, XFLXmlWriter writer, boolean useFixer) throws XMLStreamException { - List layers = getShapeLayers(swf, characters, mat, shapeNum, shapeRecords, fillStyles, lineStyles, morphshape, useFixer); + private static void convertShape(SWF swf, HashMap characters, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape, boolean useLayers, XFLXmlWriter writer) throws XMLStreamException { + List layers = getShapeLayers(swf, characters, mat, shapeNum, shapeRecords, fillStyles, lineStyles, morphshape); if (!useLayers) { for (int l = layers.size() - 1; l >= 0; l--) { writer.writeCharactersRaw(layers.get(l)); @@ -805,25 +806,16 @@ public class XFLConverter { return ret; } - private static List getShapeLayers(SWF swf, HashMap characters, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape, boolean useFixer) throws XMLStreamException { + private static List getShapeLayers(SWF swf, HashMap characters, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape) throws XMLStreamException { if (mat == null) { mat = new MATRIX(); } List shapeRecordsAdvanced; - useFixer = true; - if (useFixer) { - ShapeFixer fixer = new ShapeFixer(); - shapeRecordsAdvanced = fixer.fix(shapeRecords, morphshape, shapeNum, fillStyles, lineStyles); - } else { - shapeRecordsAdvanced = new ArrayList<>(); - for (SHAPERECORD rec : shapeRecords) { - ShapeRecordAdvanced arec = ShapeRecordAdvanced.createFromSHAPERECORD(rec); - if (arec != null) { - shapeRecordsAdvanced.add(arec); - } - } - } + + ShapeFixer fixer = morphshape ? new MorphShapeFixer() : new ShapeFixer(); + shapeRecordsAdvanced = fixer.fix(shapeRecords, shapeNum, fillStyles, lineStyles); + List edges = new ArrayList<>(); int lineStyleCount = 0; @@ -1850,7 +1842,7 @@ public class XFLConverter { int characterId = character.getCharacterId(); if ((character instanceof ShapeTag) && (nonLibraryShapes.contains(characterId))) { ShapeTag shape = (ShapeTag) character; - convertShape(swf, characters, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, recCharWriter, true); + convertShape(swf, characters, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, recCharWriter); } else if (character instanceof TextTag) { convertText(null, (TextTag) character, matrix, filters, null, recCharWriter); } else if (character instanceof DefineVideoStreamTag) { @@ -1912,7 +1904,7 @@ public class XFLConverter { symbolStr.writeStartElement("layers"); SHAPEWITHSTYLE shapeWithStyle = shape.getShapes(); if (shapeWithStyle != null) { - convertShape(swf, characters, null, shape.getShapeNum(), shapeWithStyle.shapeRecords, shapeWithStyle.fillStyles, shapeWithStyle.lineStyles, false, true, symbolStr, true); + convertShape(swf, characters, null, shape.getShapeNum(), shapeWithStyle.shapeRecords, shapeWithStyle.fillStyles, shapeWithStyle.lineStyles, false, true, symbolStr); } symbolStr.writeEndElement(); // layers @@ -2672,7 +2664,7 @@ public class XFLConverter { MorphShapeTag m = shapeTweener; XFLXmlWriter addLastWriter = new XFLXmlWriter(); SHAPEWITHSTYLE endShape = m.getShapeAtRatio(65535); //lastTweenRatio); - convertShape(swf, characters, matrix, m.getShapeNum() == 1 ? 3 : 4, endShape.shapeRecords, m.getFillStyles().getFillStylesAt(lastTweenRatio), m.getLineStyles().getLineStylesAt(m.getShapeNum(), lastTweenRatio), true, false, addLastWriter, false); + convertShape(swf, characters, matrix, m.getShapeNum() == 1 ? 3 : 4, endShape.shapeRecords, m.getFillStyles().getFillStylesAt(lastTweenRatio), m.getLineStyles().getLineStylesAt(m.getShapeNum(), lastTweenRatio), true, false, addLastWriter); //duration--; convertFrame(true, null, null, frame - duration, duration, "", lastElements, files, writer2); duration = 1; @@ -2688,7 +2680,7 @@ public class XFLConverter { standaloneShapeTweener = null; } else if ((character instanceof ShapeTag) && (nonLibraryShapes.contains(characterId))) { // || shapeTweener != null)) { ShapeTag shape = (ShapeTag) character; - convertShape(swf, characters, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, elementsWriter, true); + convertShape(swf, characters, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, elementsWriter); shapeTween = false; shapeTweener = null; @@ -2706,7 +2698,7 @@ public class XFLConverter { elementsWriter.writeCharactersRaw(lastElements); } else { statusStack.pushStatus(m.toString()); - convertShape(swf, characters, matrix, m.getShapeNum() == 1 ? 3 : 4, m.getStartEdges().shapeRecords, m.getFillStyles().getStartFillStyles(), m.getLineStyles().getStartLineStyles(m.getShapeNum()), true, false, elementsWriter, false); + convertShape(swf, characters, matrix, m.getShapeNum() == 1 ? 3 : 4, m.getStartEdges().shapeRecords, m.getFillStyles().getStartFillStyles(), m.getLineStyles().getStartLineStyles(m.getShapeNum()), true, false, elementsWriter); statusStack.popStatus(); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/MorphShapeFixer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/MorphShapeFixer.java new file mode 100644 index 000000000..5f26047c3 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/MorphShapeFixer.java @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2010-2023 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.xfl.shapefixer; + +import com.jpexs.decompiler.flash.math.BezierEdge; +import com.jpexs.decompiler.flash.math.Distances; +import com.jpexs.decompiler.flash.types.FILLSTYLE; +import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.LINESTYLE; +import com.jpexs.decompiler.flash.types.LINESTYLE2; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; +import java.awt.Rectangle; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * Morphshape fixer. Works as ShapeFixer but also removes duplicated paths, + * calculates holes. + * + * @author JPEXS + */ +public class MorphShapeFixer extends ShapeFixer { + + /** + * Hook from base ShapeFixer + * + * @param shapeNum + * @param shapes + * @param fillStyles0 + * @param fillStyles1 + * @param lineStyles + * @param layers + * @param baseFillStyles + * @param baseLineStyles + * @param fillStyleLayers + * @param lineStyleLayers + */ + @Override + protected void beforeHandle(int shapeNum, List> shapes, List fillStyles0, List fillStyles1, List lineStyles, List layers, FILLSTYLEARRAY baseFillStyles, LINESTYLEARRAY baseLineStyles, List fillStyleLayers, List lineStyleLayers) { + removeEmptyEdges(shapes); + mergeSimilar(shapeNum, shapes, fillStyles0, lineStyles, layers, baseFillStyles, baseLineStyles, fillStyleLayers, lineStyleLayers); + mergeWithSamePrefix(shapes, fillStyles0, fillStyles1, lineStyles); + clearDuplicatePathsNextToEachOther(shapes, fillStyles0, lineStyles); + fixHolesAndAntiClockwise(shapes, fillStyles0, fillStyles1, lineStyles, layers); + } + + private boolean isEmptyPath(List path) { + for (BezierEdge be : path) { + if (!be.isEmpty()) { + return false; + } + } + return true; + } + + private void fixHolesAndAntiClockwise( + List> shapes, + List fillStyles0, + List fillStyles1, + List lineStyles, + List layers + ) { + List> closedShapes = new ArrayList<>(); + List closedFillStyle = new ArrayList<>(); + List closedShapesI = new ArrayList<>(); + List closedShapesJ = new ArrayList<>(); + + Set closedHolesI = new LinkedHashSet<>(); + + for (int i = 0; i < shapes.size(); i++) { + Point2D lastMoveTo = null; + BezierEdge lastEdge = null; + int moveToIndex = 0; + List batch = new ArrayList<>(); + for (int j = 0; j < shapes.get(i).size(); j++) { + BezierEdge be = shapes.get(i).get(j); + batch.add(be); + if (lastMoveTo != null && be.getEndPoint().equals(lastMoveTo)) { + closedFillStyle.add(fillStyles0.get(i)); + closedShapes.add(batch); + closedShapesI.add(i); + closedShapesJ.add(moveToIndex); + moveToIndex = j + 1; + lastMoveTo = be.getEndPoint(); + batch = new ArrayList<>(); + } + if (lastEdge != null) { + if (!lastEdge.getEndPoint().equals(be.getBeginPoint())) { + lastMoveTo = be.getBeginPoint(); + moveToIndex = j; + } + } else { + lastMoveTo = be.getBeginPoint(); + } + lastEdge = be; + } + } + + //reversing anti-clockwise + for (int i = 0; i < closedShapes.size(); i++) { + List list = closedShapes.get(i); + if (list.isEmpty()) { + continue; + } + List points = new ArrayList<>(); + points.add(list.get(0).getBeginPoint()); + for (BezierEdge be : list) { + if (be.points.size() == 3) { + points.add(be.points.get(1)); + } + points.add(be.getEndPoint()); + } + double sum = 0; + for (int j = 0; j < points.size(); j++) { + Point2D p1 = points.get(j); + Point2D p2 = points.get((j + 1) % points.size()); + sum += (p1.getX() * p2.getY() - p2.getX() * p1.getY()); + } + + if (sum < 0) { //anti clockwise + //reverse the list + List rev = new ArrayList<>(); + for (int j = 0; j < list.size(); j++) { + rev.add(list.get(list.size() - 1 - j).reverse()); + } + + int shapeI = closedShapesI.get(i); + int shapeJ = closedShapesJ.get(i); + for (int j = 0; j < list.size(); j++) { + shapes.get(shapeI).set(shapeJ + j, rev.get(j)); + } + } + } + + Map> fillStyleToClosed = new LinkedHashMap<>(); + for (int i = 0; i < closedShapes.size(); i++) { + int fs = closedFillStyle.get(i); + if (fs != 0) { + if (!fillStyleToClosed.containsKey(fs)) { + fillStyleToClosed.put(fs, new ArrayList<>()); + } + fillStyleToClosed.get(fs).add(i); + } + } + for (int fs : fillStyleToClosed.keySet()) { + GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + Map closedPaths = new LinkedHashMap<>(); + for (int i : fillStyleToClosed.get(fs)) { + List closed = closedShapes.get(i); + if (closed.isEmpty()) { + continue; + } + GeneralPath closedPath = new GeneralPath(GeneralPath.WIND_EVEN_ODD); + + closedPath.moveTo(closed.get(0).getBeginPoint().getX(), closed.get(0).getBeginPoint().getY()); + path.moveTo(closed.get(0).getBeginPoint().getX(), closed.get(0).getBeginPoint().getY()); + boolean isEmpty = true; + for (BezierEdge be : closed) { + if (be.isEmpty()) { + continue; + } + isEmpty = false; + if (be.points.size() == 3) { + closedPath.quadTo(be.points.get(1).getX(), be.points.get(1).getY(), be.getEndPoint().getX(), be.getEndPoint().getY()); + path.quadTo(be.points.get(1).getX(), be.points.get(1).getY(), be.getEndPoint().getX(), be.getEndPoint().getY()); + } else { + closedPath.lineTo(be.getEndPoint().getX(), be.getEndPoint().getY()); + path.lineTo(be.getEndPoint().getX(), be.getEndPoint().getY()); + } + } + closedPath.closePath(); + path.closePath(); + if (!isEmpty) { + closedPaths.put(i, closedPath); + } + } + + for (int i : closedPaths.keySet()) { + GeneralPath region = closedPaths.get(i); + Rectangle r = region.getBounds(); + double px; + double py; + do { + px = r.getX() + r.getWidth() * Math.random(); + py = r.getY() + r.getHeight() * Math.random(); + } while (!region.contains(px, py)); + + if (!path.contains(px, py)) { + closedHolesI.add(i); + } + } + } + + for (int i = closedShapes.size() - 1; i >= 0; i--) { + if (closedHolesI.contains(i)) { + int to = closedShapesJ.get(i) + closedShapes.get(i).size() - 1; + int from = closedShapesJ.get(i); + List list = shapes.get(closedShapesI.get(i)); + + //System.err.println("removing hole["+closedShapesI.get(i)+"]["+from+" to "+to+"]"); + for (int j = to; j >= from; j--) { + list.remove(j); + } + + int shapeI = closedShapesI.get(i); + + //add this path as new with fillstyle1 instead of fillstyle0 + shapes.add(shapeI + 1, closedShapes.get(i)); + closedShapes.set(i, new ArrayList<>()); + fillStyles0.add(shapeI + 1, 0); + fillStyles1.add(shapeI + 1, fillStyles0.get(shapeI)); + lineStyles.add(shapeI + 1, lineStyles.get(shapeI)); + layers.add(shapeI + 1, layers.get(shapeI)); + } + } + } + + private void clearDuplicatePathsNextToEachOther( + List> shapes, + List fillStyles0, + List lineStyles + ) { + List prevList = null; + int prevI = -1; + for (int i = 0; i < shapes.size(); i++) { + List list = shapes.get(i); + if (list.isEmpty()) { + continue; + } + if (prevList != null) { + if (fillStyles0.get(i) == fillStyles0.get(prevI) + && lineStyles.get(i) == lineStyles.get(prevI)) { + if (list.equals(prevList)) { + prevList.clear(); + } + } + } + prevI = i; + prevList = list; + } + } + + /** + * This will remove a stroked path with no fill which has same stroke as + * subsequent path (or is its prefix). This happens in the morphshape edges. + * This needs to be cleaned up before exporting to FLA. + */ + private void mergeWithSamePrefix( + List> shapes, + List fillStyles0, + List fillStyles1, + List lineStyles + ) { + List prevList = null; + int prevI = -1; + for (int i = 0; i < shapes.size(); i++) { + List list = shapes.get(i); + if (list.isEmpty()) { + continue; + } + if (prevList != null) { + + int prevFillStyle0 = fillStyles0.get(i - 1); + int prevFillStyle1 = fillStyles1.get(i - 1); + int prevLineStyle = lineStyles.get(i - 1); + int lineStyle = lineStyles.get(i); + int fillStyle0 = fillStyles0.get(i); + int fillStyle1 = fillStyles1.get(i); + + if (fillStyle0 == 0 && fillStyle1 == 0 && lineStyle != 0 && lineStyle == prevLineStyle) { + if (prevList.size() >= list.size()) { + boolean isPrefix = true; + for (int j = 0; j < list.size(); j++) { + if (!prevList.get(j).equals(list.get(j))) { + isPrefix = false; + break; + } + } + if (isPrefix) { + shapes.get(i).clear(); + continue; + } + } + } else if (prevFillStyle0 == 0 && prevFillStyle1 == 0 && prevLineStyle != 0 && lineStyle == prevLineStyle) { + //list startswitch prevList + if (list.size() >= prevList.size()) { + boolean isPrefix = true; + for (int j = 0; j < prevList.size(); j++) { + if (!prevList.get(j).equals(list.get(j))) { + isPrefix = false; + break; + } + } + if (isPrefix) { + shapes.get(prevI).clear(); + } + } + } + } + prevI = i; + prevList = list; + } + } + + /** + * Merges similar paths. This happens in morphshapes when one shape is + * transformed into multiple shapes. + * + * @param shapeNum + * @param shapes + * @param fillStyles0 + * @param lineStyles + * @param layers + * @param baseFillStyles + * @param baseLineStyles + * @param fillStyleLayers + * @param lineStyleLayers + */ + private void mergeSimilar( + int shapeNum, + List> shapes, + List fillStyles0, + List lineStyles, + List layers, + FILLSTYLEARRAY baseFillStyles, + LINESTYLEARRAY baseLineStyles, + List fillStyleLayers, + List lineStyleLayers + ) { + List> closedShapes = new ArrayList<>(); + List closedFillStyle = new ArrayList<>(); + List closedLineStyle = new ArrayList<>(); + List closedLayers = new ArrayList<>(); + List closedShapesI = new ArrayList<>(); + List closedShapesJ = new ArrayList<>(); + + Map replacements = new LinkedHashMap<>(); + + for (int i = 0; i < shapes.size(); i++) { + Point2D lastMoveTo = null; + BezierEdge lastEdge = null; + int moveToIndex = 0; + List batch = new ArrayList<>(); + for (int j = 0; j < shapes.get(i).size(); j++) { + BezierEdge be = shapes.get(i).get(j); + batch.add(be); + if (lastMoveTo != null && be.getEndPoint().equals(lastMoveTo)) { + closedFillStyle.add(fillStyles0.get(i)); + closedLineStyle.add(lineStyles.get(i)); + closedShapes.add(batch); + closedLayers.add(layers.get(i)); + closedShapesI.add(i); + closedShapesJ.add(moveToIndex); + moveToIndex = j + 1; + lastMoveTo = be.getEndPoint(); + batch = new ArrayList<>(); + } + if (lastEdge != null) { + if (!lastEdge.getEndPoint().equals(be.getBeginPoint())) { + lastMoveTo = be.getBeginPoint(); + moveToIndex = j; + } + } else { + lastMoveTo = be.getBeginPoint(); + } + lastEdge = be; + } + } + + Set removedShapes = new HashSet<>(); + for (int i1 = 0; i1 < closedShapes.size(); i1++) { + if (replacements.containsKey(i1)) { + continue; + } + for (int i2 = 0; i2 < closedShapes.size(); i2++) { + if (i1 == i2) { + continue; + } + if (replacements.containsKey(i2)) { + continue; + } + if (closedLayers.get(i1) != closedLayers.get(i2)) { + continue; + } + if (closedFillStyle.get(i1) > 0 && closedFillStyle.get(i2) > 0) { + if (closedFillStyle.get(i1) != closedFillStyle.get(i2)) { + FILLSTYLEARRAY fa = closedLayers.get(i1) == -1 ? baseFillStyles : fillStyleLayers.get(closedLayers.get(i1)); + FILLSTYLE fs1 = fa.fillStyles[closedFillStyle.get(i1) - 1]; + FILLSTYLE fs2 = fa.fillStyles[closedFillStyle.get(i2) - 1]; + if (!fs1.equals(fs2)) { + continue; + } + } + } + if (closedLineStyle.get(i1) > 0 && closedLineStyle.get(i2) > 0) { + if (closedLineStyle.get(i1) != closedLineStyle.get(i2)) { + LINESTYLEARRAY lsa = closedLayers.get(i1) == -1 ? baseLineStyles : lineStyleLayers.get(closedLayers.get(i1)); + if (shapeNum <= 3) { + LINESTYLE ls1 = lsa.lineStyles[closedLineStyle.get(i1) - 1]; + LINESTYLE ls2 = lsa.lineStyles[closedLineStyle.get(i2) - 1]; + if (!ls1.equals(ls2)) { + continue; + } + } else { + LINESTYLE2 ls1 = lsa.lineStyles2[closedLineStyle.get(i1) - 1]; + LINESTYLE2 ls2 = lsa.lineStyles2[closedLineStyle.get(i2) - 1]; + if (!ls1.equals(ls2)) { + continue; + } + } + } + } + + if (closedShapes.get(i1).size() <= 1 || closedShapes.get(i2).size() <= 1) { + continue; + } + + if (closedLineStyle.get(i1) > 0 && closedLineStyle.get(i2) == 0) { + continue; + } + + if (isEmptyPath(closedShapes.get(i1)) || isEmptyPath(closedShapes.get(i2))) { + continue; + } + + double dist = Distances.getBatchDistance(closedShapes.get(i1), closedShapes.get(i2)); + + if (dist <= 10) { //magic + /*System.err.println("dist = " + dist); + System.err.println("removed"); + System.err.println("removed shape["+closedShapesI.get(i2)+"]["+closedShapesJ.get(i2)+"], fs "+ closedFillStyle.get(i2) + ", ls " + closedLineStyle.get(i2)); + System.err.println("left shape["+closedShapesI.get(i1)+"]["+closedShapesJ.get(i1)+"], fs "+ closedFillStyle.get(i1)+ ", ls " + closedLineStyle.get(i1)); + */ + replacements.put(i2, i1); + removedShapes.add(i2); + } + } + } + + for (int i = 0; i < closedShapes.size(); i++) { + int repI = replacements.containsKey(i) ? replacements.get(i) : i; + List listI = closedShapes.get(repI); + + for (int j = i + 1; j < closedShapes.size(); j++) { + /*if (i == j) { + break; + }*/ + if (removedShapes.contains(j) && !replacements.containsKey(j)) { + continue; + } + int repJ = replacements.containsKey(j) ? replacements.get(j) : j; + List listJ = closedShapes.get(repJ); + + if (closedFillStyle.get(i) != closedFillStyle.get(j) + || closedLineStyle.get(i) != closedLineStyle.get(j)) { + continue; + } + + if (listI.equals(listJ)) { + replacements.remove(j); + removedShapes.add(j); + } + } + } + + for (int i = closedShapes.size() - 1; i >= 0; i--) { + int to = closedShapesJ.get(i) + closedShapes.get(i).size() - 1; + int from = closedShapesJ.get(i); + List list = shapes.get(closedShapesI.get(i)); + + if (removedShapes.contains(i)) { + //System.err.println("removing shape["+closedShapesI.get(i)+"]["+from+" to "+to+"]"); + for (int j = to; j >= from; j--) { + list.remove(j); + } + } + if (replacements.containsKey(i)) { + list.addAll(from, closedShapes.get(replacements.get(i))); + } + + if (!removedShapes.contains(i)) { + for (int j = to; j >= from; j--) { + if (list.get(j).isEmpty()) { + list.remove(j); + } + } + } + } + } + + /** + * + * @param shapes + */ + private void removeEmptyEdges(List> shapes) { + for (int i = 0; i < shapes.size(); i++) { + List list = shapes.get(i); + for (int j = 0; j < list.size(); j++) { + if (list.get(j).isEmpty()) { + list.remove(j); + j--; + } + } + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java index a79fa4aec..0387a0eac 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java @@ -17,11 +17,7 @@ package com.jpexs.decompiler.flash.xfl.shapefixer; import com.jpexs.decompiler.flash.math.BezierEdge; -import com.jpexs.decompiler.flash.math.Distances; -import com.jpexs.decompiler.flash.types.FILLSTYLE; import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; -import com.jpexs.decompiler.flash.types.LINESTYLE; -import com.jpexs.decompiler.flash.types.LINESTYLE2; import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; @@ -29,21 +25,14 @@ import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; import com.jpexs.helpers.Reference; -import java.awt.Rectangle; -import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Shape fixer. This will walk a shape and split crossed edges so FLA editor can - * properly load it. It also fixes morphshape - removes duplicated paths, calculate holes. + * properly load it. * * @author JPEXS */ @@ -92,19 +81,23 @@ public class ShapeFixer { } } - - private boolean isEmptyBatch(List batch) { - for (BezierEdge be : batch) { - if (!be.isEmpty()) { - return false; - } - } - return true; + + protected void beforeHandle( + int shapeNum, + List> shapes, + List fillStyles0, + List fillStyles1, + List lineStyles, + List layers, + FILLSTYLEARRAY baseFillStyles, + LINESTYLEARRAY baseLineStyles, + List fillStyleLayers, + List lineStyleLayers + ) { } public List fix( List records, - boolean morphshape, int shapeNum, FILLSTYLEARRAY baseFillStyles, LINESTYLEARRAY baseLineStyles @@ -187,415 +180,8 @@ public class ShapeFixer { x = rec.changeX(x); y = rec.changeY(y); } - - if (morphshape) { - - //Remove empty edges - for (int i = 0; i < shapes.size(); i++) { - List list = shapes.get(i); - for (int j = 0; j < list.size(); j++) { - if (list.get(j).isEmpty()) { - list.remove(j); - j--; - } - } - } - - List> closedShapes = new ArrayList<>(); - List closedFillStyle = new ArrayList<>(); - List closedLineStyle = new ArrayList<>(); - List closedLayers = new ArrayList<>(); - List closedShapesI = new ArrayList<>(); - List closedShapesJ = new ArrayList<>(); - - Map replacements = new LinkedHashMap<>(); - - for (int i = 0; i < shapes.size(); i++) { - Point2D lastMoveTo = null; - BezierEdge lastEdge = null; - int moveToIndex = 0; - List batch = new ArrayList<>(); - for (int j = 0; j < shapes.get(i).size(); j++) { - BezierEdge be = shapes.get(i).get(j); - batch.add(be); - if (lastMoveTo != null && be.getEndPoint().equals(lastMoveTo)) { - closedFillStyle.add(fillStyles0.get(i)); - closedLineStyle.add(lineStyles.get(i)); - closedShapes.add(batch); - closedLayers.add(layers.get(i)); - closedShapesI.add(i); - closedShapesJ.add(moveToIndex); - moveToIndex = j + 1; - lastMoveTo = be.getEndPoint(); - batch = new ArrayList<>(); - } - if (lastEdge != null) { - if (!lastEdge.getEndPoint().equals(be.getBeginPoint())) { - lastMoveTo = be.getBeginPoint(); - moveToIndex = j; - } - } else { - lastMoveTo = be.getBeginPoint(); - } - lastEdge = be; - } - } - - Set removedShapes = new HashSet<>(); - for (int i1 = 0; i1 < closedShapes.size(); i1++) { - if (replacements.containsKey(i1)) { - continue; - } - for (int i2 = 0; i2 < closedShapes.size(); i2++) { - if (i1 == i2) { - continue; - } - if (replacements.containsKey(i2)) { - continue; - } - if (closedLayers.get(i1) != closedLayers.get(i2)) { - continue; - } - if (closedFillStyle.get(i1) > 0 && closedFillStyle.get(i2) > 0) { - if (closedFillStyle.get(i1) != closedFillStyle.get(i2)) { - FILLSTYLEARRAY fa = closedLayers.get(i1) == -1 ? baseFillStyles : fillStyleLayers.get(closedLayers.get(i1)); - FILLSTYLE fs1 = fa.fillStyles[closedFillStyle.get(i1) - 1]; - FILLSTYLE fs2 = fa.fillStyles[closedFillStyle.get(i2) - 1]; - if (!fs1.equals(fs2)) { - continue; - } - } - } - if (closedLineStyle.get(i1) > 0 && closedLineStyle.get(i2) > 0) { - if (closedLineStyle.get(i1) != closedLineStyle.get(i2)) { - LINESTYLEARRAY lsa = closedLayers.get(i1) == -1 ? baseLineStyles : lineStyleLayers.get(closedLayers.get(i1)); - if (shapeNum <= 3) { - LINESTYLE ls1 = lsa.lineStyles[closedLineStyle.get(i1) - 1]; - LINESTYLE ls2 = lsa.lineStyles[closedLineStyle.get(i2) - 1]; - if (!ls1.equals(ls2)) { - continue; - } - } else { - LINESTYLE2 ls1 = lsa.lineStyles2[closedLineStyle.get(i1) - 1]; - LINESTYLE2 ls2 = lsa.lineStyles2[closedLineStyle.get(i2) - 1]; - if (!ls1.equals(ls2)) { - continue; - } - } - } - } - - if (closedShapes.get(i1).size() <= 1 || closedShapes.get(i2).size() <= 1) { - continue; - } - - if (closedLineStyle.get(i1) > 0 && closedLineStyle.get(i2) == 0) { - continue; - } - - if (isEmptyBatch(closedShapes.get(i1)) || isEmptyBatch(closedShapes.get(i2))) { - continue; - } - - double dist = Distances.getBatchDistance(closedShapes.get(i1), closedShapes.get(i2)); - - if (dist <= 10) { //magic - /*System.err.println("dist = " + dist); - System.err.println("removed"); - System.err.println("removed shape["+closedShapesI.get(i2)+"]["+closedShapesJ.get(i2)+"], fs "+ closedFillStyle.get(i2) + ", ls " + closedLineStyle.get(i2)); - System.err.println("left shape["+closedShapesI.get(i1)+"]["+closedShapesJ.get(i1)+"], fs "+ closedFillStyle.get(i1)+ ", ls " + closedLineStyle.get(i1)); - */ - replacements.put(i2, i1); - removedShapes.add(i2); - } - } - } - - for (int i = 0; i < closedShapes.size(); i++) { - int repI = replacements.containsKey(i) ? replacements.get(i) : i; - List listI = closedShapes.get(repI); - - for (int j = i + 1; j < closedShapes.size(); j++) { - /*if (i == j) { - break; - }*/ - if (removedShapes.contains(j) && !replacements.containsKey(j)) { - continue; - } - int repJ = replacements.containsKey(j) ? replacements.get(j) : j; - List listJ = closedShapes.get(repJ); - - if (closedFillStyle.get(i) != closedFillStyle.get(j) - || closedLineStyle.get(i) != closedLineStyle.get(j)) { - continue; - } - - if (listI.equals(listJ)) { - replacements.remove(j); - removedShapes.add(j); - } - } - } - - for (int i = closedShapes.size() - 1; i >= 0; i--) { - int to = closedShapesJ.get(i) + closedShapes.get(i).size() - 1; - int from = closedShapesJ.get(i); - List list = shapes.get(closedShapesI.get(i)); - - if (removedShapes.contains(i)) { - //System.err.println("removing shape["+closedShapesI.get(i)+"]["+from+" to "+to+"]"); - for (int j = to; j >= from; j--) { - list.remove(j); - } - } - if (replacements.containsKey(i)) { - list.addAll(from, closedShapes.get(replacements.get(i))); - } - - if (!removedShapes.contains(i)) { - for (int j = to; j >= from; j--) { - if (list.get(j).isEmpty()) { - list.remove(j); - } - } - } - } - - /* - * This will remove a stroked path with no fill which has same - * stroke as subsequent path (or is its prefix). This happens in the - * morphshape edges. This needs to be cleaned up before exporting to FLA. - */ - List prevList = null; - int prevI = -1; - for (int i = 0; i < shapes.size(); i++) { - List list = shapes.get(i); - if (list.isEmpty()) { - continue; - } - if (prevList != null) { - - int prevFillStyle0 = fillStyles0.get(i - 1); - int prevFillStyle1 = fillStyles1.get(i - 1); - int prevLineStyle = lineStyles.get(i - 1); - lineStyle = lineStyles.get(i); - fillStyle0 = fillStyles0.get(i); - fillStyle1 = fillStyles1.get(i); - - if (fillStyle0 == 0 && fillStyle1 == 0 && lineStyle != 0 && lineStyle == prevLineStyle) { - if (prevList.size() >= list.size()) { - boolean isPrefix = true; - for (int j = 0; j < list.size(); j++) { - if (!prevList.get(j).equals(list.get(j))) { - isPrefix = false; - break; - } - } - if (isPrefix) { - shapes.get(i).clear(); - continue; - } - } - } else if (prevFillStyle0 == 0 && prevFillStyle1 == 0 && prevLineStyle != 0 && lineStyle == prevLineStyle) { - //list startswitch prevList - if (list.size() >= prevList.size()) { - boolean isPrefix = true; - for (int j = 0; j < prevList.size(); j++) { - if (!prevList.get(j).equals(list.get(j))) { - isPrefix = false; - break; - } - } - if (isPrefix) { - shapes.get(prevI).clear(); - } - } - } - } - prevI = i; - prevList = list; - } - - //Clear obvious duplicates - //System.err.println("Clear obvious duplicates..."); - prevList = null; - prevI = -1; - //if (false) - for (int i = 0; i < shapes.size(); i++) { - List list = shapes.get(i); - if (list.isEmpty()) { - continue; - } - if (prevList != null) { - if (fillStyles0.get(i) == fillStyles0.get(prevI) - && lineStyles.get(i) == lineStyles.get(prevI)) { - if (list.equals(prevList)) { - System.err.println("clearing " + i); - prevList.clear(); - } - } - } - prevI = i; - prevList = list; - } - - //----------------------------FIND "holes" = apply wind even odd ------------- - closedShapes = new ArrayList<>(); - closedFillStyle = new ArrayList<>(); - closedLineStyle = new ArrayList<>(); - closedLayers = new ArrayList<>(); - closedShapesI = new ArrayList<>(); - closedShapesJ = new ArrayList<>(); - Set closedHolesI = new LinkedHashSet<>(); - - for (int i = 0; i < shapes.size(); i++) { - Point2D lastMoveTo = null; - BezierEdge lastEdge = null; - int moveToIndex = 0; - List batch = new ArrayList<>(); - for (int j = 0; j < shapes.get(i).size(); j++) { - BezierEdge be = shapes.get(i).get(j); - batch.add(be); - if (lastMoveTo != null && be.getEndPoint().equals(lastMoveTo)) { - closedFillStyle.add(fillStyles0.get(i)); - closedLineStyle.add(lineStyles.get(i)); - closedShapes.add(batch); - closedLayers.add(layers.get(i)); - closedShapesI.add(i); - closedShapesJ.add(moveToIndex); - moveToIndex = j + 1; - lastMoveTo = be.getEndPoint(); - batch = new ArrayList<>(); - } - if (lastEdge != null) { - if (!lastEdge.getEndPoint().equals(be.getBeginPoint())) { - lastMoveTo = be.getBeginPoint(); - moveToIndex = j; - } - } else { - lastMoveTo = be.getBeginPoint(); - } - lastEdge = be; - } - } - - //reversing anti-clockwise - for (int i = 0; i < closedShapes.size(); i++) { - List list = closedShapes.get(i); - if (list.isEmpty()) { - continue; - } - List points = new ArrayList<>(); - points.add(list.get(0).getBeginPoint()); - for (BezierEdge be : list) { - if (be.points.size() == 3) { - points.add(be.points.get(1)); - } - points.add(be.getEndPoint()); - } - double sum = 0; - for (int j = 0; j < points.size(); j++) { - Point2D p1 = points.get(j); - Point2D p2 = points.get((j + 1) % points.size()); - sum += (p1.getX() * p2.getY() - p2.getX() * p1.getY()); - } - - if (sum < 0) { //anti clockwise - //reverse the list - List rev = new ArrayList<>(); - for (int j = 0; j < list.size(); j++) { - rev.add(list.get(list.size() - 1 - j).reverse()); - } - - int shapeI = closedShapesI.get(i); - int shapeJ = closedShapesJ.get(i); - for (int j = 0; j < list.size(); j++) { - shapes.get(shapeI).set(shapeJ + j, rev.get(j)); - } - } - } - - Map> fillStyleToClosed = new LinkedHashMap<>(); - for (int i = 0; i < closedShapes.size(); i++) { - int fs = closedFillStyle.get(i); - if (fs != 0) { - if (!fillStyleToClosed.containsKey(fs)) { - fillStyleToClosed.put(fs, new ArrayList<>()); - } - fillStyleToClosed.get(fs).add(i); - } - } - for (int fs : fillStyleToClosed.keySet()) { - GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); - Map closedPaths = new LinkedHashMap<>(); - for (int i : fillStyleToClosed.get(fs)) { - List closed = closedShapes.get(i); - if (closed.isEmpty()) { - continue; - } - GeneralPath closedPath = new GeneralPath(GeneralPath.WIND_EVEN_ODD); - - closedPath.moveTo(closed.get(0).getBeginPoint().getX(), closed.get(0).getBeginPoint().getY()); - path.moveTo(closed.get(0).getBeginPoint().getX(), closed.get(0).getBeginPoint().getY()); - boolean isEmpty = true; - for (BezierEdge be : closed) { - if (be.isEmpty()) { - continue; - } - isEmpty = false; - if (be.points.size() == 3) { - closedPath.quadTo(be.points.get(1).getX(), be.points.get(1).getY(), be.getEndPoint().getX(), be.getEndPoint().getY()); - path.quadTo(be.points.get(1).getX(), be.points.get(1).getY(), be.getEndPoint().getX(), be.getEndPoint().getY()); - } else { - closedPath.lineTo(be.getEndPoint().getX(), be.getEndPoint().getY()); - path.lineTo(be.getEndPoint().getX(), be.getEndPoint().getY()); - } - } - closedPath.closePath(); - path.closePath(); - if (!isEmpty) { - closedPaths.put(i, closedPath); - } - } - - for (int i : closedPaths.keySet()) { - GeneralPath region = closedPaths.get(i); - Rectangle r = region.getBounds(); - double px; - double py; - do { - px = r.getX() + r.getWidth() * Math.random(); - py = r.getY() + r.getHeight() * Math.random(); - } while (!region.contains(px, py)); - - if (!path.contains(px, py)) { - closedHolesI.add(i); - } - } - } - - for (int i = closedShapes.size() - 1; i >= 0; i--) { - if (closedHolesI.contains(i)) { - int to = closedShapesJ.get(i) + closedShapes.get(i).size() - 1; - int from = closedShapesJ.get(i); - List list = shapes.get(closedShapesI.get(i)); - - //System.err.println("removing hole["+closedShapesI.get(i)+"]["+from+" to "+to+"]"); - for (int j = to; j >= from; j--) { - list.remove(j); - } - - int shapeI = closedShapesI.get(i); - shapes.add(shapeI + 1, closedShapes.get(i)); - closedShapes.set(i, new ArrayList<>()); - fillStyles0.add(shapeI + 1, 0); - fillStyles1.add(shapeI + 1, fillStyles0.get(shapeI)); - lineStyles.add(shapeI + 1, lineStyles.get(shapeI)); - layers.add(shapeI + 1, layers.get(shapeI)); - } - } - } - + + beforeHandle(shapeNum, shapes, fillStyles0, fillStyles1, lineStyles, layers, baseFillStyles, baseLineStyles, fillStyleLayers, lineStyleLayers); //------------------- detecting overlapping edges -------------------- List splittedPairs = new ArrayList<>();