diff --git a/CHANGELOG.md b/CHANGELOG.md index d30cbb32e..63bb755ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,8 @@ All notable changes to this project will be documented in this file. - Font export - not setting ascent and descent - [#2471] PDF export - ignore control characters - [#2471] SVG export with typeface - white-space:pre style +- [#1826], [#2416] FLA export - shapes - missing fills when fillStyle0/1 has incorrect orientation +- [#2532], [#1011], [#2165] FLA export - shapes - missing fills on path crossings, small shapes ### Changed - Icon of "Deobfuscation options" menu from pile of pills to medkit @@ -3830,7 +3832,7 @@ Major version of SWF to XML export changed to 2. ### Added - Initial public release -[Unreleased]: https://github.com/jindrapetrik/jpexs-decompiler/compare/version24.0.1...dev +,[Unreleased]: https://github.com/jindrapetrik/jpexs-decompiler/compare/version24.0.1...dev [24.0.1]: https://github.com/jindrapetrik/jpexs-decompiler/compare/version24.0.0...version24.0.1 [24.0.0]: https://github.com/jindrapetrik/jpexs-decompiler/compare/version23.0.1...version24.0.0 [23.0.1]: https://github.com/jindrapetrik/jpexs-decompiler/compare/version23.0.0...version23.0.1 @@ -4033,6 +4035,11 @@ Major version of SWF to XML export changed to 2. [#2517]: https://www.free-decompiler.com/flash/issues/2517 [#2522]: https://www.free-decompiler.com/flash/issues/2522 [#2525]: https://www.free-decompiler.com/flash/issues/2525 +[#1826]: https://www.free-decompiler.com/flash/issues/1826 +[#2416]: https://www.free-decompiler.com/flash/issues/2416 +[#2532]: https://www.free-decompiler.com/flash/issues/2532 +[#1011]: https://www.free-decompiler.com/flash/issues/1011 +[#2165]: https://www.free-decompiler.com/flash/issues/2165 [#2476]: https://www.free-decompiler.com/flash/issues/2476 [#2404]: https://www.free-decompiler.com/flash/issues/2404 [#1418]: https://www.free-decompiler.com/flash/issues/1418 @@ -4058,7 +4065,6 @@ Major version of SWF to XML export changed to 2. [#2469]: https://www.free-decompiler.com/flash/issues/2469 [#2475]: https://www.free-decompiler.com/flash/issues/2475 [#2427]: https://www.free-decompiler.com/flash/issues/2427 -[#1826]: https://www.free-decompiler.com/flash/issues/1826 [#2448]: https://www.free-decompiler.com/flash/issues/2448 [#2370]: https://www.free-decompiler.com/flash/issues/2370 [#2453]: https://www.free-decompiler.com/flash/issues/2453 @@ -4071,7 +4077,6 @@ Major version of SWF to XML export changed to 2. [#2418]: https://www.free-decompiler.com/flash/issues/2418 [#2397]: https://www.free-decompiler.com/flash/issues/2397 [#2425]: https://www.free-decompiler.com/flash/issues/2425 -[#2416]: https://www.free-decompiler.com/flash/issues/2416 [#2394]: https://www.free-decompiler.com/flash/issues/2394 [#2400]: https://www.free-decompiler.com/flash/issues/2400 [#2413]: https://www.free-decompiler.com/flash/issues/2413 @@ -4259,7 +4264,6 @@ Major version of SWF to XML export changed to 2. [#2031]: https://www.free-decompiler.com/flash/issues/2031 [#1866]: https://www.free-decompiler.com/flash/issues/1866 [#503]: https://www.free-decompiler.com/flash/issues/503 -[#1011]: https://www.free-decompiler.com/flash/issues/1011 [#1257]: https://www.free-decompiler.com/flash/issues/1257 [#1902]: https://www.free-decompiler.com/flash/issues/1902 [#1903]: https://www.free-decompiler.com/flash/issues/1903 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/Matrix.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/Matrix.java index 23795f1ce..fae9c0959 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/Matrix.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/Matrix.java @@ -172,7 +172,7 @@ public final class Matrix implements Cloneable { public java.awt.Point transform(java.awt.Point point) { Point p = transform(point.x, point.y); - return new java.awt.Point((int) p.x, (int) p.y); + return new java.awt.Point((int) Math.round(p.x), (int) Math.round(p.y)); } public Point2D transform(Point2D point) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/ShapeExporterBase.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/ShapeExporterBase.java index 419668778..7a10f8a05 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/ShapeExporterBase.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/ShapeExporterBase.java @@ -132,6 +132,12 @@ public abstract class ShapeExporterBase implements IShapeExporter { _lineStyles = cachedData.lineStyles; _fillPaths = cachedData.fillPaths; _linePaths = cachedData.linePaths; + + handleFillPaths(_fillPaths); + } + + protected void handleFillPaths(List> fillPaths) { + } /** diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java index dd45e8a16..31abb585c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java @@ -536,6 +536,18 @@ public class BezierEdge implements Serializable { } calcParams(); } + + public static final double ROUND_VALUE = 1; + + public void roundX() { + for (int i = 0; i < this.points.size(); i++) { + this.points.set(i, new Point2D.Double( + Math.round(this.points.get(i).getX() * ROUND_VALUE) / ROUND_VALUE, + Math.round(this.points.get(i).getY() * ROUND_VALUE) / ROUND_VALUE + )); + } + calcParams(); + } @Override public int hashCode() { @@ -731,4 +743,19 @@ public class BezierEdge implements Serializable { //rectIntersection(new Rectangle2D.Double(0,0,50,50), new Rectangle2D.Double(0,50,50,50), out); //System.out.println("out = "+out); } + + + public void shrinkToLine() { + if (points.size() == 3) { + double det = (points.get(1).getX() - points.get(0).getX()) + * (points.get(2).getY() - points.get(0).getY()) + - (points.get(1).getY() - points.get(0).getY()) + * (points.get(2).getX() - points.get(0).getX()); + if (det == 0) { + points.remove(1); + revPoints.remove(1); + calcParams(); + } + } + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/shapes/ShapeTransformer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/shapes/ShapeTransformer.java new file mode 100644 index 000000000..9512708fa --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/shapes/ShapeTransformer.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2010-2025 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.shapes; + +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +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.MORPHFILLSTYLE; +import com.jpexs.decompiler.flash.types.MORPHFILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.MORPHLINESTYLE2; +import com.jpexs.decompiler.flash.types.MORPHLINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; +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 java.awt.Point; +import java.util.ArrayList; +import java.util.List; + +/** + * Transforms shapes with matrix. + * @author JPEXS + */ +public class ShapeTransformer { + + /** + * Transform styles. + * @param matrix Matrix + * @param fillStyles Fill styles + * @param lineStyles Line styles + * @param shapeNum Shape type (DefineShape = 1, DefineShape2 = 2, etc.) + */ + public void transformStyles(Matrix matrix, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, int shapeNum) { + List fillStyleToTransform = new ArrayList<>(); + for (FILLSTYLE fs : fillStyles.fillStyles) { + fillStyleToTransform.add(fs); + } + + double strokeScale = Math.max(Math.abs(matrix.scaleX), Math.abs(matrix.scaleY)); + if (shapeNum >= 4) { + for (LINESTYLE2 ls : lineStyles.lineStyles2) { + if (ls.hasFillFlag) { + fillStyleToTransform.add(ls.fillType); + } + ls.width *= strokeScale; + } + } else { + for (LINESTYLE ls : lineStyles.lineStyles) { + ls.width *= strokeScale; + } + } + + for (FILLSTYLE fs : fillStyleToTransform) { + switch (fs.fillStyleType) { + case FILLSTYLE.CLIPPED_BITMAP: + case FILLSTYLE.NON_SMOOTHED_CLIPPED_BITMAP: + case FILLSTYLE.NON_SMOOTHED_REPEATING_BITMAP: + case FILLSTYLE.REPEATING_BITMAP: + fs.bitmapMatrix = new Matrix(fs.bitmapMatrix).preConcatenate(matrix).toMATRIX(); + break; + case FILLSTYLE.LINEAR_GRADIENT: + case FILLSTYLE.RADIAL_GRADIENT: + case FILLSTYLE.FOCAL_RADIAL_GRADIENT: + fs.gradientMatrix = new Matrix(fs.gradientMatrix).preConcatenate(matrix).toMATRIX(); + break; + } + } + } + + /** + * Transform morph styles. + * @param matrix Matrix + * @param fillStyles Fill styles + * @param lineStyles Line styles + * @param morphShapeNum Morphshape type (DefineMorphshape = 1, DefineMorphshape2 = 2) + * @param doStart Modify start styles + * @param doEnd Modify end styles + */ + public void transformMorphStyles(Matrix matrix, MORPHFILLSTYLEARRAY fillStyles, MORPHLINESTYLEARRAY lineStyles, int morphShapeNum, boolean doStart, boolean doEnd) { + List fillStyleToTransform = new ArrayList<>(); + for (MORPHFILLSTYLE fs : fillStyles.fillStyles) { + fillStyleToTransform.add(fs); + } + + if (morphShapeNum == 2) { + for (MORPHLINESTYLE2 ls : lineStyles.lineStyles2) { + if (ls.hasFillFlag) { + fillStyleToTransform.add(ls.fillType); + } + } + } + + for (MORPHFILLSTYLE fs : fillStyleToTransform) { + switch (fs.fillStyleType) { + case FILLSTYLE.CLIPPED_BITMAP: + case FILLSTYLE.NON_SMOOTHED_CLIPPED_BITMAP: + case FILLSTYLE.NON_SMOOTHED_REPEATING_BITMAP: + case FILLSTYLE.REPEATING_BITMAP: + if (doStart) { + fs.startBitmapMatrix = new Matrix(fs.startBitmapMatrix).preConcatenate(matrix).toMATRIX(); + } + if (doEnd) { + fs.endBitmapMatrix = new Matrix(fs.endBitmapMatrix).preConcatenate(matrix).toMATRIX(); + } + break; + case FILLSTYLE.LINEAR_GRADIENT: + case FILLSTYLE.RADIAL_GRADIENT: + case FILLSTYLE.FOCAL_RADIAL_GRADIENT: + if (doStart) { + fs.startGradientMatrix = new Matrix(fs.startGradientMatrix).preConcatenate(matrix).toMATRIX(); + } + if (doEnd) { + fs.endGradientMatrix = new Matrix(fs.endGradientMatrix).preConcatenate(matrix).toMATRIX(); + } + break; + } + } + } + + /** + * Transform SHAPE. + * @param matrix Matrix + * @param shape SHAPE + * @param shapeNum Shape type (DefineShape = 1, DefineShape2 = 2, etc.) + */ + public void transformSHAPE(Matrix matrix, SHAPE shape, int shapeNum) { + transformShapeRecords(matrix, shape.shapeRecords, shapeNum); + } + + /** + * Transform SHAPERECORDs. + * @param matrix Matrix + * @param shapeRecords Records + * @param shapeNum Shape type (DefineShape = 1, DefineShape2 = 2, etc.) + */ + public void transformShapeRecords(Matrix matrix, List shapeRecords, int shapeNum) { + int x = 0; + int y = 0; + StyleChangeRecord lastStyleChangeRecord = null; + boolean wasMoveTo = false; + for (SHAPERECORD rec : shapeRecords) { + if (rec instanceof StyleChangeRecord) { + StyleChangeRecord scr = (StyleChangeRecord) rec; + lastStyleChangeRecord = scr; + if (scr.stateNewStyles) { + transformStyles(matrix, scr.fillStyles, scr.lineStyles, shapeNum); + } + if (scr.stateMoveTo) { + Point nextPoint = new Point(scr.moveDeltaX, scr.moveDeltaY); + x = scr.changeX(x); + y = scr.changeY(y); + Point nextPoint2 = matrix.transform(nextPoint); + scr.moveDeltaX = nextPoint2.x; + scr.moveDeltaY = nextPoint2.y; + scr.calculateBits(); + wasMoveTo = true; + } + } + + if (((rec instanceof StraightEdgeRecord) || (rec instanceof CurvedEdgeRecord)) && !wasMoveTo) { + if (lastStyleChangeRecord != null) { + Point nextPoint2 = matrix.transform(new Point(x, y)); + if (nextPoint2.x != 0 || nextPoint2.y != 0) { + lastStyleChangeRecord.stateMoveTo = true; + lastStyleChangeRecord.moveDeltaX = nextPoint2.x; + lastStyleChangeRecord.moveDeltaY = nextPoint2.y; + lastStyleChangeRecord.calculateBits(); + wasMoveTo = true; + } + } + } + if (rec instanceof StraightEdgeRecord) { + StraightEdgeRecord ser = (StraightEdgeRecord) rec; + ser.generalLineFlag = true; + ser.vertLineFlag = false; + Point currentPoint = new Point(x, y); + Point nextPoint = new Point(x + ser.deltaX, y + ser.deltaY); + x = ser.changeX(x); + y = ser.changeY(y); + Point currentPoint2 = matrix.transform(currentPoint); + Point nextPoint2 = matrix.transform(nextPoint); + ser.deltaX = nextPoint2.x - currentPoint2.x; + ser.deltaY = nextPoint2.y - currentPoint2.y; + ser.simplify(); + } + if (rec instanceof CurvedEdgeRecord) { + CurvedEdgeRecord cer = (CurvedEdgeRecord) rec; + Point currentPoint = new Point(x, y); + Point controlPoint = new Point(x + cer.controlDeltaX, y + cer.controlDeltaY); + Point anchorPoint = new Point(x + cer.controlDeltaX + cer.anchorDeltaX, y + cer.controlDeltaY + cer.anchorDeltaY); + x = cer.changeX(x); + y = cer.changeY(y); + + Point currentPoint2 = matrix.transform(currentPoint); + Point controlPoint2 = matrix.transform(controlPoint); + Point anchorPoint2 = matrix.transform(anchorPoint); + + cer.controlDeltaX = controlPoint2.x - currentPoint2.x; + cer.controlDeltaY = controlPoint2.y - currentPoint2.y; + cer.anchorDeltaX = anchorPoint2.x - controlPoint2.x; + cer.anchorDeltaY = anchorPoint2.y - controlPoint2.y; + cer.calculateBits(); + } + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/shapes/package-info.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/shapes/package-info.java new file mode 100644 index 000000000..19437a542 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/shapes/package-info.java @@ -0,0 +1,4 @@ +/** + * Shape tools. + */ +package com.jpexs.decompiler.flash.shapes; 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 ab4ca38d1..025c9844d 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 @@ -66,6 +66,7 @@ import com.jpexs.decompiler.flash.helpers.GraphTextWriter; import com.jpexs.decompiler.flash.helpers.HighlightedTextWriter; import com.jpexs.decompiler.flash.helpers.NulWriter; import com.jpexs.decompiler.flash.helpers.StringBuilderTextWriter; +import com.jpexs.decompiler.flash.shapes.ShapeTransformer; import com.jpexs.decompiler.flash.tags.ABCContainerTag; import com.jpexs.decompiler.flash.tags.CSMSettingsTag; import com.jpexs.decompiler.flash.tags.DefineButton2Tag; @@ -143,7 +144,9 @@ import com.jpexs.decompiler.flash.types.filters.FILTER; import com.jpexs.decompiler.flash.types.filters.GLOWFILTER; import com.jpexs.decompiler.flash.types.filters.GRADIENTBEVELFILTER; import com.jpexs.decompiler.flash.types.filters.GRADIENTGLOWFILTER; +import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; 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.decompiler.flash.types.sound.MP3FRAME; import com.jpexs.decompiler.flash.types.sound.MP3SOUNDDATA; @@ -249,7 +252,9 @@ public class XFLConverter { */ private final boolean DEBUG_EXPORT_LAYER_DEPTHS = false; - private static final DecimalFormat EDGE_DECIMAL_FORMAT = new DecimalFormat("0.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + private static final DecimalFormat EDGE_DECIMAL_FORMAT = new DecimalFormat("0.##", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + + private static final double SMALL_DIVISOR = 20; static { EDGE_DECIMAL_FORMAT.setGroupingUsed(false); @@ -287,12 +292,28 @@ public class XFLConverter { } } + private static String numEdgeToString(double value) { + if (value == Math.floor(value)) { + long lval = (long) value; + return "" + lval; + } + long integerPart = (long) Math.floor(value); + double fractionalPart = value - integerPart; + int fractionalPart256 = (int) Math.floor(fractionalPart * 256); + String h = Long.toHexString(integerPart).toUpperCase(); + if (h.length() > 6) { + h = h.substring(h.length() - 6, h.length()); + } + return "#" + h + "." + String.format("%02X", fractionalPart256); + } + private static String formatEdgeDouble(double value) { if (value % 1 == 0) { return "" + (int) value; } - value = Math.round(value * 2.0) / 2.0; - return EDGE_DECIMAL_FORMAT.format(value); + //value = Math.round(value * 1000.0) / 1000.0; + //return EDGE_DECIMAL_FORMAT.format(value); + return numEdgeToString(value); } private static String convertShapeEdge(MATRIX mat, ShapeRecordAdvanced record, double x, double y) { @@ -624,8 +645,8 @@ public class XFLConverter { return false; } - private static void convertShape(Reference lastImportedId, Map characterNameMap, SWF swf, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape, boolean useLayers, XFLXmlWriter writer) throws XMLStreamException { - List layers = getShapeLayers(lastImportedId, characterNameMap, swf, mat, shapeNum, shapeRecords, fillStyles, lineStyles, morphshape); + private static void convertShape(Reference lastImportedId, Map characterNameMap, SWF swf, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape, boolean useLayers, XFLXmlWriter writer, int characterId, boolean small) throws XMLStreamException { + List layers = getShapeLayers(lastImportedId, characterNameMap, swf, mat, shapeNum, shapeRecords, fillStyles, lineStyles, morphshape, characterId, small); if (!useLayers) { for (int l = layers.size() - 1; l >= 0; l--) { writer.writeCharactersRaw(layers.get(l)); @@ -646,7 +667,35 @@ public class XFLConverter { } } - private static List getShapeLayers(Reference lastImportedId, Map characterNameMap, SWF swf, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape) throws XMLStreamException { + private static boolean isShapeSmall(ShapeTag shape) { + if (true) { + //return false; + } + int LIMIT = 1000; + + if (shape.shapeBounds.getWidth() < LIMIT && shape.shapeBounds.getHeight() < LIMIT) { + return true; + } + return false; + + /*List records = shape.shapes.shapeRecords; + + + int limit = 10; + for (SHAPERECORD rec : records) { + if ((rec instanceof StraightEdgeRecord) || (rec instanceof CurvedEdgeRecord)) { + int changeX = Math.abs(rec.changeX(0)); + int changeY = Math.abs(rec.changeY(0)); + int change = Math.max(changeX, changeY); + if (change != 0 && change < limit) { + return true; + } + } + } + return false;*/ + } + + private static List getShapeLayers(Reference lastImportedId, Map characterNameMap, SWF swf, MATRIX mat, int shapeNum, List shapeRecords, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, boolean morphshape, int characterId, boolean small) throws XMLStreamException { if (mat == null) { mat = new MATRIX(); } @@ -654,6 +703,18 @@ public class XFLConverter { List shapeRecordsAdvanced; ShapeFixer fixer = morphshape ? new MorphShapeFixer() : new ShapeFixer(); + Logger.getLogger(ShapeFixer.class.getName()).log(Level.FINE, "Fixing character {0}...", characterId); + + if (small) { + shapeRecords = Helper.deepCopy(shapeRecords); + ShapeTransformer shapeTransformer = new ShapeTransformer(); + Matrix scale = Matrix.getScaleInstance(SMALL_DIVISOR); + shapeTransformer.transformShapeRecords(scale, shapeRecords, shapeNum); + fillStyles = Helper.deepCopy(fillStyles); + lineStyles = Helper.deepCopy(lineStyles); + shapeTransformer.transformStyles(scale, fillStyles, lineStyles, shapeNum); + } + shapeRecordsAdvanced = fixer.fix(shapeRecords, shapeNum, fillStyles, lineStyles); List edges = new ArrayList<>(); @@ -1030,6 +1091,20 @@ public class XFLConverter { return ret; } + private static Set getSmallShapes(SWF swf) { + Set result = new LinkedHashSet<>(); + Map chars = swf.getCharacters(true); + for (int id : chars.keySet()) { + if (chars.get(id) instanceof ShapeTag) { + ShapeTag shape = (ShapeTag) chars.get(id); + if (isShapeSmall(shape)) { + result.add(shape); + } + } + } + return result; + } + private static Set getCharactersAndAllDependent(SWF swf) { Set ret = new LinkedIdentityHashSet<>(); @@ -1285,10 +1360,13 @@ public class XFLConverter { return (DEBUG_EXPORT_LAYER_DEPTHS ? "MaskedSymbol " : "Symbol ") + symbolId; } - private static void convertSymbolInstance(int frame, AccessibilityBag accessibility, Reference lastImportedId, Map characterNameMap, SWF swf, String name, MATRIX matrix, ColorTransform colorTransform, boolean cacheAsBitmap, int blendMode, List filters, boolean isVisible, RGBA backgroundColor, CLIPACTIONS clipActions, Amf3Value metadata, CharacterTag tag, FLAVersion flaVersion, XFLXmlWriter writer) throws XMLStreamException { + private static void convertSymbolInstance(int frame, AccessibilityBag accessibility, Reference lastImportedId, Map characterNameMap, SWF swf, String name, MATRIX matrix, ColorTransform colorTransform, boolean cacheAsBitmap, int blendMode, List filters, boolean isVisible, RGBA backgroundColor, CLIPACTIONS clipActions, Amf3Value metadata, CharacterTag tag, FLAVersion flaVersion, XFLXmlWriter writer, boolean small) throws XMLStreamException { if (matrix == null) { matrix = new MATRIX(); } + if (small) { + matrix = new Matrix(matrix).concatenate(Matrix.getScaleInstance(1 / SMALL_DIVISOR, 1 / SMALL_DIVISOR)).toMATRIX(); + } if (tag instanceof DefineButtonTag) { DefineButtonTag bt = (DefineButtonTag) tag; DefineButtonCxformTag bcx = (DefineButtonCxformTag) bt.getSwf().getCharacterIdTag(bt.buttonId, DefineButtonCxformTag.ID); @@ -1496,16 +1574,16 @@ public class XFLConverter { return date.getTime() / 1000; } - private void convertLibrary(Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, HashMap datfiles, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack) throws XMLStreamException { + private void convertLibrary(Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, HashMap datfiles, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Set smallShapes) throws XMLStreamException { statusStack.pushStatus("media"); convertMedia(lastItemIdNumber, charactersExportedInFirstFrame, lastImportedId, characterNameMap, characterImportLinkageURL, characters, swf, characterVariables, characterClasses, tags, files, datfiles, writer, statusStack); statusStack.popStatus(); statusStack.pushStatus("symbols"); - convertSymbols(lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, tags, files, flaVersion, writer, placeToMaskedSymbol, multiUsageMorphShapes, statusStack); + convertSymbols(lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, tags, files, flaVersion, writer, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, smallShapes); statusStack.popStatus(); } - private void convertSymbols(Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack) throws XMLStreamException { + private void convertSymbols(Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Set smallShapes) throws XMLStreamException { //boolean hasSymbol = false; Reference nextClipId = new Reference<>(-1); writer.writeStartElement("symbols"); @@ -1643,7 +1721,7 @@ public class XFLConverter { case 4: ok = rec.buttonStateHitTest; break; - } + } if (!ok) { break; } @@ -1666,18 +1744,18 @@ public class XFLConverter { } CharacterTag character = button.getSwf().getCharacter(rec.characterId); if (character != null) { - MATRIX matrix = rec.placeMatrix; + MATRIX matrix = rec.placeMatrix; XFLXmlWriter recCharWriter = new XFLXmlWriter(); if ((character instanceof ShapeTag) && (nonLibraryShapes.contains(character))) { ShapeTag shape = (ShapeTag) character; statusStack.pushStatus(character.toString()); - convertShape(lastImportedId, characterNameMap, character.getSwf(), matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, recCharWriter); + convertShape(lastImportedId, characterNameMap, character.getSwf(), matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, recCharWriter, rec.characterId, false /*nonlibrary*/); statusStack.popStatus(); } else if (character instanceof MorphShapeTag) { //can happen for HIT_TEST frame ShapeTag shape = ((MorphShapeTag) character).getStartShapeTag(); statusStack.pushStatus(character.toString()); - convertShape(lastImportedId, characterNameMap, character.getSwf(), matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, true, false, recCharWriter); + convertShape(lastImportedId, characterNameMap, character.getSwf(), matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, true, false, recCharWriter, rec.characterId, false); statusStack.popStatus(); } else if (character instanceof TextTag) { statusStack.pushStatus(character.toString()); @@ -1692,13 +1770,19 @@ public class XFLConverter { convertImageInstance(lastImportedId, characterNameMap, swf, null, matrix, (ImageTag) character, recCharWriter); statusStack.popStatus(); } else { - convertSymbolInstance(-1, new AccessibilityBag() /*???*/, lastImportedId, characterNameMap, swf, null, matrix, colorTransformAlpha, false, blendMode, filters, true, null, null, null, character.getSwf().getCharacter(rec.characterId), flaVersion, recCharWriter); + boolean small = false; + if (character instanceof ShapeTag) { + ShapeTag shape = (ShapeTag) character; + if (smallShapes.contains(shape)) { + small = true; + } + } + convertSymbolInstance(-1, new AccessibilityBag() /*???*/, lastImportedId, characterNameMap, swf, null, matrix, colorTransformAlpha, false, blendMode, filters, true, null, null, null, character.getSwf().getCharacter(rec.characterId), flaVersion, recCharWriter, small); } int emptyDuration = frame - lastFrame - 1; lastFrame = frame + duration - 1; - - + if (emptyDuration > 0) { symbolStr.writeStartElement("DOMFrame", new String[]{ "index", Integer.toString(frame - emptyDuration), @@ -1707,7 +1791,7 @@ public class XFLConverter { symbolStr.writeElementValue("elements", ""); symbolStr.writeEndElement(); } - + if (duration > 1) { symbolStr.writeStartElement("DOMFrame", new String[]{ "index", Integer.toString(frame), @@ -1716,12 +1800,12 @@ public class XFLConverter { } else { symbolStr.writeStartElement("DOMFrame", new String[]{ "index", Integer.toString(frame), - "keyMode", Integer.toString(KEY_MODE_NORMAL)}); + "keyMode", Integer.toString(KEY_MODE_NORMAL)}); } symbolStr.writeStartElement("elements"); symbolStr.writeCharactersRaw(recCharWriter.toString()); symbolStr.writeEndElement(); - symbolStr.writeEndElement(); + symbolStr.writeEndElement(); frame += duration - 1; } else { logger.log(Level.WARNING, "Character with id={0} was not found.", rec.characterId); @@ -1743,9 +1827,9 @@ public class XFLConverter { } final ScriptPack spriteScriptPack = characterScriptPacks.containsKey(sprite) ? characterScriptPacks.get(sprite) : null; - extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, sprite.getTags(), swf.getCharacterId(sprite), writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, sprite.getTags(), swf.getCharacterId(sprite), writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), sprite, swf.getCharacterId(sprite), characterVariables.get(sprite), nonLibraryShapes, tags, sprite.getTags(), getSymbolName(lastImportedId, characterNameMap, swf, symbol), flaVersion, files, symbolStr, spriteScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), sprite, swf.getCharacterId(sprite), characterVariables.get(sprite), nonLibraryShapes, tags, sprite.getTags(), getSymbolName(lastImportedId, characterNameMap, swf, symbol), flaVersion, files, symbolStr, spriteScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); } else if (symbol instanceof ShapeTag) { symbolStr.writeStartElement("timeline"); @@ -1755,7 +1839,7 @@ public class XFLConverter { symbolStr.writeStartElement("layers"); SHAPEWITHSTYLE shapeWithStyle = shape.getShapes(); if (shapeWithStyle != null) { - convertShape(lastImportedId, characterNameMap, symbol.getSwf(), null, shape.getShapeNum(), shapeWithStyle.shapeRecords, shapeWithStyle.fillStyles, shapeWithStyle.lineStyles, false, true, symbolStr); + convertShape(lastImportedId, characterNameMap, symbol.getSwf(), null, shape.getShapeNum(), shapeWithStyle.shapeRecords, shapeWithStyle.fillStyles, shapeWithStyle.lineStyles, false, true, symbolStr, symbol.getCharacterId(), smallShapes.contains(shape)); } symbolStr.writeEndElement(); // layers @@ -1786,11 +1870,11 @@ public class XFLConverter { } statusStack.pushStatus("extracting multilevel clips"); - extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, swf.getTags(), -1, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, swf.getTags(), -1, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); statusStack.pushStatus("converting multiusage morphshapes"); - extractMultiUsageMorphShapes(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, writer, swf, nonLibraryShapes, flaVersion, files, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + extractMultiUsageMorphShapes(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, writer, swf, nonLibraryShapes, flaVersion, files, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); /*if (hasSymbol) { @@ -2438,7 +2522,7 @@ public class XFLConverter { writer.writeEndElement(); } - private static void convertFrames(AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, List onlyFrames, int startFrame, int endFrame, String prevStr, String afterStr, List nonLibraryShapes, ReadOnlyTagList timelineTags, int depth, FLAVersion flaVersion, XFLXmlWriter writer, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters) throws XMLStreamException { + private static void convertFrames(AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, List onlyFrames, int startFrame, int endFrame, String prevStr, String afterStr, List nonLibraryShapes, ReadOnlyTagList timelineTags, int depth, FLAVersion flaVersion, XFLXmlWriter writer, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { Logger.getLogger(XFLConverter.class.getName()).log(Level.FINE, "Converting frames of {0}", symbolName); boolean lastIn = false; XFLXmlWriter writer2 = new XFLXmlWriter(); @@ -2503,7 +2587,7 @@ public class XFLConverter { shapeTweener = m; shapeTween = false; } - } + } if (newCharId == -1 && newCharCls == null) { newCharacter = character; } @@ -2627,12 +2711,12 @@ public class XFLConverter { if ((character instanceof MorphShapeTag) && (!multiUsageMorphShapes.contains(character.getCharacterId()))) { MorphShapeTag m2 = (MorphShapeTag) character; statusStack.pushStatus(m2.toString()); - convertShape(lastImportedId, characterNameMap, swf, matrix, m2.getShapeNum() == 1 ? 3 : 4, m2.getStartEdges().shapeRecords, m2.getFillStyles().getStartFillStyles(), m2.getLineStyles().getStartLineStyles(m2.getShapeNum()), true, false, addLastWriter); + convertShape(lastImportedId, characterNameMap, swf, matrix, m2.getShapeNum() == 1 ? 3 : 4, m2.getStartEdges().shapeRecords, m2.getFillStyles().getStartFillStyles(), m2.getLineStyles().getStartLineStyles(m2.getShapeNum()), true, false, addLastWriter, m2.getCharacterId(), false /*??*/); statusStack.popStatus(); shapeTween = true; } else { SHAPEWITHSTYLE endShape = m.getShapeAtRatio(65535); //lastTweenRatio); - convertShape(lastImportedId, characterNameMap, swf, matrix, m.getShapeNum() == 1 ? 3 : 4, endShape.shapeRecords, m.getFillStyles().getFillStylesAt(65535), m.getLineStyles().getLineStylesAt(m.getShapeNum(), 65535), true, false, addLastWriter); + convertShape(lastImportedId, characterNameMap, swf, matrix, m.getShapeNum() == 1 ? 3 : 4, endShape.shapeRecords, m.getFillStyles().getFillStylesAt(65535), m.getLineStyles().getLineStylesAt(m.getShapeNum(), 65535), true, false, addLastWriter, m.getCharacterId(), false /*??*/); } Integer ease = EasingDetector.getEaseFromShapeRatios(morphShapeRatios); @@ -2651,7 +2735,7 @@ public class XFLConverter { } if (character instanceof ShapeTag && standaloneShapeTweener != null) { - convertSymbolInstance(frame, accessibility, lastImportedId, characterNameMap, swf, instanceName, standaloneShapeTweenerMatrix, colorTransForm, cacheAsBitmap, blendMode, filters, isVisible, backGroundColor, clipActions, metadata, standaloneShapeTweener, flaVersion, elementsWriter); + convertSymbolInstance(frame, accessibility, lastImportedId, characterNameMap, swf, instanceName, standaloneShapeTweenerMatrix, colorTransForm, cacheAsBitmap, blendMode, filters, isVisible, backGroundColor, clipActions, metadata, standaloneShapeTweener, flaVersion, elementsWriter, false); standaloneShapeTweener = null; } else if ((character instanceof ShapeTag) && (nonLibraryShapes.contains(character))) { if (lastCharacter == character && Objects.equals(matrix, lastMatrix)) { @@ -2659,7 +2743,7 @@ public class XFLConverter { } else { ShapeTag shape = (ShapeTag) character; statusStack.pushStatus(character.toString()); - convertShape(lastImportedId, characterNameMap, swf, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, elementsWriter); + convertShape(lastImportedId, characterNameMap, swf, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, elementsWriter, shape.getCharacterId(), false /*nonlibrary*/); statusStack.popStatus(); } shapeTween = false; @@ -2671,14 +2755,14 @@ public class XFLConverter { shapeTweener = null; standaloneShapeTweener = m; standaloneShapeTweenerMatrix = matrix; - convertSymbolInstance(frame, accessibility, lastImportedId, characterNameMap, swf, instanceName, matrix, colorTransForm, cacheAsBitmap, blendMode, filters, isVisible, backGroundColor, clipActions, metadata, character, flaVersion, elementsWriter); + convertSymbolInstance(frame, accessibility, lastImportedId, characterNameMap, swf, instanceName, matrix, colorTransForm, cacheAsBitmap, blendMode, filters, isVisible, backGroundColor, clipActions, metadata, character, flaVersion, elementsWriter, false); } else { morphShapeRatios.add(ratio == -1 ? 0 : ratio); if (lastCharacter == m && Objects.equals(matrix, lastMatrix)) { elementsWriter.writeCharactersRaw(lastElements); } else { statusStack.pushStatus(m.toString()); - convertShape(lastImportedId, characterNameMap, swf, matrix, m.getShapeNum() == 1 ? 3 : 4, m.getStartEdges().shapeRecords, m.getFillStyles().getStartFillStyles(), m.getLineStyles().getStartLineStyles(m.getShapeNum()), true, false, elementsWriter); + convertShape(lastImportedId, characterNameMap, swf, matrix, m.getShapeNum() == 1 ? 3 : 4, m.getStartEdges().shapeRecords, m.getFillStyles().getStartFillStyles(), m.getLineStyles().getStartLineStyles(m.getShapeNum()), true, false, elementsWriter, m.getCharacterId(), false /*?*/); statusStack.popStatus(); } shapeTween = true; @@ -2694,7 +2778,14 @@ public class XFLConverter { } else if (character instanceof ImageTag) { convertImageInstance(lastImportedId, characterNameMap, swf, instanceName, matrix, (ImageTag) character, elementsWriter); } else if (character != null) { - convertSymbolInstance(frame, accessibility, lastImportedId, characterNameMap, swf, instanceName, matrix, colorTransForm, cacheAsBitmap, blendMode, filters, isVisible, backGroundColor, clipActions, metadata, character, flaVersion, elementsWriter); + boolean small = false; + if (character instanceof ShapeTag) { + ShapeTag shape = (ShapeTag) character; + if (smallShapes.contains(shape)) { + small = true; + } + } + convertSymbolInstance(frame, accessibility, lastImportedId, characterNameMap, swf, instanceName, matrix, colorTransForm, cacheAsBitmap, blendMode, filters, isVisible, backGroundColor, clipActions, metadata, character, flaVersion, elementsWriter, small); } } @@ -3882,11 +3973,12 @@ public class XFLConverter { List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, - Set characters + Set characters, + Set smallShapes ) throws XMLStreamException { XFLXmlWriter symbolStr = new XFLXmlWriter(); - extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, timelineTags, spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, timelineTags, spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); if (nextClipId.getVal() < 0) { nextClipId.setVal(swf.getNextCharacterId()); @@ -3904,7 +3996,7 @@ public class XFLConverter { "lastModified", Long.toString(getTimestamp(swf))}); symbolStr.writeAttribute("symbolType", "graphic"); - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, timelineTags, timelineTags, getMaskedSymbolName(objectId), flaVersion, files, symbolStr, null, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, timelineTags, timelineTags, getMaskedSymbolName(objectId), flaVersion, files, symbolStr, null, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); symbolStr.writeEndElement(); // DOMSymbolItem String symbolStr2 = prettyFormatXML(symbolStr.toString()); @@ -3992,7 +4084,8 @@ public class XFLConverter { List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, - Set characters + Set characters, + Set smallShapes ) throws XMLStreamException { for (int objectId : multiUsageMorphShapes) { @@ -4024,7 +4117,7 @@ public class XFLConverter { } } - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, swf.getTags(), new ReadOnlyTagList(timelineTags), getSymbolName(lastImportedId, characterNameMap, swf, swf.getCharacter(objectId)), flaVersion, files, symbolStr, null, new HashMap<>(), new ArrayList<>(), statusStack, characterImportLinkageURL, characters); + convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, swf.getTags(), new ReadOnlyTagList(timelineTags), getSymbolName(lastImportedId, characterNameMap, swf, swf.getCharacter(objectId)), flaVersion, files, symbolStr, null, new HashMap<>(), new ArrayList<>(), statusStack, characterImportLinkageURL, characters, smallShapes); symbolStr.writeEndElement(); // DOMSymbolItem String symbolStr2 = prettyFormatXML(symbolStr.toString()); @@ -4062,7 +4155,8 @@ public class XFLConverter { List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, - Set characters + Set characters, + Set smallShapes ) throws XMLStreamException { int f = 0; @@ -4275,7 +4369,7 @@ public class XFLConverter { //set timelined? delegatedTimeline.add(showFrame); } - addExtractedClip(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, new ReadOnlyTagList(delegatedTimeline), spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + addExtractedClip(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, new ReadOnlyTagList(delegatedTimeline), spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); placeToMaskedSymbol.put(secondPlace, new MultiLevelClip(secondPlace, nextClipId.getVal(), numFrames)); } } @@ -4297,7 +4391,7 @@ public class XFLConverter { } //Note: symbolId argument might be a virtual symbol like MaskedSymbol - private void convertTimelines(Map characterScriptPacks, Reference lastImportedId, Map characterNameMap, SWF swf, AbcIndexing abcIndex, CharacterTag sprite, int symbolId, String linkageIdentifier, List nonLibraryShapes, ReadOnlyTagList tags, ReadOnlyTagList timelineTags, String spriteName, FLAVersion flaVersion, HashMap files, XFLXmlWriter writer, ScriptPack scriptPack, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters) throws XMLStreamException { + private void convertTimelines(Map characterScriptPacks, Reference lastImportedId, Map characterNameMap, SWF swf, AbcIndexing abcIndex, CharacterTag sprite, int symbolId, String linkageIdentifier, List nonLibraryShapes, ReadOnlyTagList tags, ReadOnlyTagList timelineTags, String spriteName, FLAVersion flaVersion, HashMap files, XFLXmlWriter writer, ScriptPack scriptPack, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { ScriptPack characterScriptPack = sprite == null ? null : characterScriptPacks.containsKey(sprite) ? characterScriptPacks.get(sprite) : null; if (sprite == null && symbolId == -1) { @@ -4688,7 +4782,7 @@ public class XFLConverter { "color", randomOutlineColor(), "layerType", "mask", "locked", "true"}); - convertFrames(accessibility, symbolName, lastImportedId, characterNameMap, swf, depthToFramesList.get(po.getDepth()), clipFrame, lastFrame, "", "", nonLibraryShapes, sceneTimelineTags, po.getDepth(), flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + convertFrames(accessibility, symbolName, lastImportedId, characterNameMap, swf, depthToFramesList.get(po.getDepth()), clipFrame, lastFrame, "", "", nonLibraryShapes, sceneTimelineTags, po.getDepth(), flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); writer.writeEndElement(); int parentIndex = index; @@ -4710,7 +4804,7 @@ public class XFLConverter { handledClips.add(po2); for (int ndx = po.getClipDepth() - 1; ndx > po2.getClipDepth(); ndx--) { - boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(ndx), ndx, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(ndx), ndx, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); for (int i = clipFrame; i <= lastFrame; i++) { depthToFramesList.get(ndx).remove((Integer) i); } @@ -4789,7 +4883,7 @@ public class XFLConverter { } for (int nd = po.getClipDepth() - 1; nd > po.getDepth(); nd--) { - boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(nd), nd, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(nd), nd, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); for (int i = clipFrame; i <= lastFrame; i++) { depthToFramesList.get(nd).remove((Integer) i); } @@ -4826,7 +4920,7 @@ public class XFLConverter { } } - boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(d), d, 0, Integer.MAX_VALUE, -1, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(d), d, 0, Integer.MAX_VALUE, -1, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); if (nonEmpty) { index++; } @@ -4865,7 +4959,7 @@ public class XFLConverter { writer.writeEndElement(); //DOMLayer } - private boolean writeLayer(AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, int index, List onlyFrames, int d, int startFrame, int endFrame, int parentLayer, XFLXmlWriter writer, List nonLibraryShapes, ReadOnlyTagList timelineTags, FLAVersion flaVersion, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters) throws XMLStreamException { + private boolean writeLayer(AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, int index, List onlyFrames, int d, int startFrame, int endFrame, int parentLayer, XFLXmlWriter writer, List nonLibraryShapes, ReadOnlyTagList timelineTags, FLAVersion flaVersion, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { XFLXmlWriter layerPrev = new XFLXmlWriter(); statusStack.pushStatus("layer " + (index + 1)); //System.err.println("- writing layer " + (index + 1) + (startFrame == 0 && endFrame == Integer.MAX_VALUE ? ", all frames": ", frame " + startFrame + " to " + endFrame)); @@ -4884,7 +4978,7 @@ public class XFLConverter { layerPrev.writeCharacters(""); // todo honfika: hack to close start tag String layerAfter = ""; int prevLength = writer.length(); - convertFrames(accessibility, symbolName, lastImportedId, characterNameMap, swf, onlyFrames, startFrame, endFrame, layerPrev.toString(), layerAfter, nonLibraryShapes, timelineTags, d, flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + convertFrames(accessibility, symbolName, lastImportedId, characterNameMap, swf, onlyFrames, startFrame, endFrame, layerPrev.toString(), layerAfter, nonLibraryShapes, timelineTags, d, flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); return writer.length() != prevLength; } @@ -5549,11 +5643,13 @@ public class XFLConverter { } convertFonts(lastItemIdNumber, lastImportedId, characterNameMap, swf, characters, domDocument, statusStack, characterVariables, characterClasses, charactersExportedInFirstFrame, characterImportLinkageURL); - convertLibrary(lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, swf.getTags(), files, datfiles, flaVersion, domDocument, placeToMaskedSymbol, multiUsageMorphShapes, statusStack); + Set smallShapes = getSmallShapes(swf); + + convertLibrary(lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, swf.getTags(), files, datfiles, flaVersion, domDocument, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, smallShapes); //domDocument.writeStartElement("timelines"); statusStack.pushStatus("main timeline"); - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, -1, null, nonLibraryShapes, swf.getTags(), swf.getTags(), null, flaVersion, files, domDocument, documentScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters); + convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, -1, null, nonLibraryShapes, swf.getTags(), swf.getTags(), null, flaVersion, files, domDocument, documentScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); //domDocument.writeEndElement(); @@ -6062,10 +6158,9 @@ public class XFLConverter { } - private static void convertAdjustColorFilter(COLORMATRIXFILTER filter, XFLXmlWriter writer) throws XMLStreamException { ColorMatrixConvertor colorMatrixConvertor = new ColorMatrixConvertor(filter.matrix); - + writer.writeEmptyElement("AdjustColorFilter", new String[]{ "brightness", Integer.toString(colorMatrixConvertor.getBrightness()), "contrast", Integer.toString(colorMatrixConvertor.getContrast()), diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathOrientation.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathOrientation.java new file mode 100644 index 000000000..3df4df0ce --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathOrientation.java @@ -0,0 +1,150 @@ +package com.jpexs.decompiler.flash.xfl.shapefixer; + +import com.jpexs.helpers.Reference; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.FlatteningPathIterator; +import java.awt.geom.PathIterator; +import java.util.ArrayList; +import java.util.List; + +/** + * Path orientation detector. + * @author JPEXS + */ +public final class PathOrientation { + + public enum Orientation { + CLOCKWISE, COUNTER_CLOCKWISE, DEGENERATE, OPEN_CONTOUR + } + + /** + * Result per closed subpath in drawing order. Open contours are reported + * once as OPEN_CONTOUR. + */ + public static void orientations(Shape shape, List result, List areas) { + // Flatten curves to line segments for robust area computation. + // flatness ~0.5px is usually fine; limit prevents infinite subdivision on pathological curves. + final double flatness = 0.5; + final int limit = 10; + + PathIterator it = shape.getPathIterator((AffineTransform) null); + FlatteningPathIterator fpi = new FlatteningPathIterator(it, flatness, limit); + + double[] coords = new double[6]; + List current = new ArrayList<>(); + + double startX = 0; + double startY = 0; + double lastX = 0; + double lastY = 0; + boolean hasOpen = false; + + while (!fpi.isDone()) { + int seg = fpi.currentSegment(coords); + switch (seg) { + case PathIterator.SEG_MOVETO: + // Start a new subpath + if (!current.isEmpty()) { + // Previous subpath ended without SEG_CLOSE + result.add(Orientation.OPEN_CONTOUR); + areas.add(0.0); + hasOpen = true; + current.clear(); + } + startX = lastX = coords[0]; + startY = lastY = coords[1]; + current.add(new double[]{lastX, lastY}); + break; + + case PathIterator.SEG_LINETO: + lastX = coords[0]; + lastY = coords[1]; + current.add(new double[]{lastX, lastY}); + break; + + case PathIterator.SEG_CLOSE: + // Close current subpath by linking back to start point + if (!current.isEmpty()) { + Reference orientationRef = new Reference<>(null); + Reference areaRef = new Reference<>(0.0); + orientationOfClosedRing(current, areaRef, orientationRef); + result.add(orientationRef.getVal()); + areas.add(areaRef.getVal()); + current.clear(); + } else { + // SEG_CLOSE without points – ignore + } + // Reset last point to start of next potential subpath + lastX = startX; + lastY = startY; + break; + + default: + // Should not happen because we flattened, but keep for completeness + throw new IllegalStateException("Unexpected segment type: " + seg); + } + fpi.next(); + } + + // If path ended without SEG_CLOSE for the last subpath + if (!current.isEmpty()) { + result.add(Orientation.OPEN_CONTOUR); + areas.add(0.0); + hasOpen = true; + } + + + } + + /** + * Compute orientation of a closed ring using the shoelace formula over its + * vertices. + */ + private static void orientationOfClosedRing(List pts, Reference areaRef, Reference resultRef) { + // Ensure first != last; algorithm handles implicit closing edge (last->first) + if (pts.size() < 3) { + resultRef.setVal(Orientation.DEGENERATE); + areaRef.setVal(0.0); + return; + } + double area2 = 0.0; // 2 * signed area + for (int i = 0, n = pts.size(); i < n; i++) { + double[] a = pts.get(i); + double[] b = pts.get((i + 1) % n); + area2 += (a[0] * b[1]) - (b[0] * a[1]); + } + // Tolerance to treat near-zero areas as degenerate (units are in user space) + double eps = 1e-9; + if (Math.abs(area2) <= eps) { + resultRef.setVal(Orientation.DEGENERATE); + areaRef.setVal(0.0); + return; + } + + resultRef.setVal(area2 > 0 ? Orientation.CLOCKWISE : Orientation.COUNTER_CLOCKWISE); + areaRef.setVal(Math.abs(area2/2)); + } + + public static void orientationSingleClosed(Shape shape, Reference orientationRef, Reference areaRef) { + List result = new ArrayList<>(); + List areas = new ArrayList<>(); + orientations(shape, result, areas); + if (result.isEmpty()) { + orientationRef.setVal(Orientation.DEGENERATE); + areaRef.setVal(0.0); + return; + } + // Prefer the first closed orientation encountered + for (int i = 0; i < result.size(); i++) { + Orientation o = result.get(i); + if (o != Orientation.OPEN_CONTOUR) { + orientationRef.setVal(o); + areaRef.setVal(areas.get(i)); + return; + } + } + orientationRef.setVal(Orientation.OPEN_CONTOUR); + areaRef.setVal(0.0); + } +} 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 2478196cd..cd1354c74 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 @@ -111,7 +111,7 @@ public class ShapeFixer { //int oldpct = 0; loopi1: for (int i1 = 0; i1 < shapes.size(); i1++) { - int layer = layers.get(i1); + int layer = layers.get(i1); /*if (i1 % 10 == 0) { int pct = i1 * 100 / shapes.size(); if (oldpct != pct) { @@ -132,20 +132,19 @@ public class ShapeFixer { if (layers.get(i2) != layer) { continue; } - - + //its with fills vs stroke only, we can ignore these, I hope - if (fillStyles0.get(i1) == 0 + if (fillStyles0.get(i1) == 0 && fillStyles1.get(i1) == 0 && (fillStyles0.get(i2) != 0 || fillStyles1.get(i2) != 0)) { continue; } - if (fillStyles0.get(i2) == 0 + if (fillStyles0.get(i2) == 0 && fillStyles1.get(i2) == 0 && (fillStyles0.get(i1) != 0 || fillStyles1.get(i1) != 0)) { continue; } - + loopj2: for (int j2 = 0; j2 < shapes.get(i2).size(); j2++) { BezierEdge be2 = shapes.get(i2).get(j2); @@ -214,13 +213,49 @@ public class ShapeFixer { continue; } - if (t1Ref.size() == 2) { + if (t1Ref.size() > 1) { + double eps = 1 / BezierEdge.ROUND_VALUE; + Point2D last = intPoints.get(0); + for (int i = 1; i < intPoints.size(); i++) { + Point2D current = intPoints.get(i); + if (current.distance(last) < eps) { + intPoints.remove(i); + t1Ref.remove(i); + t2Ref.remove(i); + i--; + continue; + } + last = current; + } + } + + /*if (t1Ref.size() == 2) { + if (intPoints.get(0).distance(intPoints.get(1)) < 1/256f) { + t1Ref.remove(1); + t2Ref.remove(1); + intPoints.remove(1); + } + } + if (t1Ref.size() == 3) { + + }*/ + if (t1Ref.size() == 1) { if ((t1Ref.get(0) == 0 || t1Ref.get(0) == 1) && (t2Ref.get(0) == 0 || t2Ref.get(0) == 1)) { continue; } - } - + } + + //sharing start end end point + if (t1Ref.size() == 2) { + if ((t1Ref.get(0) == 0 || t1Ref.get(0) == 1) + && (t1Ref.get(1) == 0 || t1Ref.get(1) == 1) + && (t2Ref.get(0) == 0 || t2Ref.get(0) == 1) + && (t2Ref.get(1) == 0 || t2Ref.get(1) == 1)) { + continue; + } + } + if (DEBUG_PRINT) { System.err.println("intersects " + be1.toSvg() + " " + be2.toSvg()); System.err.println(" fillstyle0: " + fillStyles0.get(i1) + " , " + fillStyles0.get(i2)); @@ -265,10 +300,15 @@ public class ShapeFixer { be2L.setEndPoint(intP); be2R.setBeginPoint(intP); - be1L.roundHalf(); - be1R.roundHalf(); - be2L.roundHalf(); - be2R.roundHalf(); + /*be1L.roundX(); + be1R.roundX(); + be2L.roundX(); + be2R.roundX();*/ + be1L.roundX(); + be1R.roundX(); + be2L.roundX(); + be2R.roundX(); + if (i1 == i2) { if (j1 < j2) { shapes.get(i1).remove(j2); @@ -333,6 +373,13 @@ public class ShapeFixer { } } } + + for (int i1 = 0; i1 < shapes.size(); i1++) { + for (int j1 = 0; j1 < shapes.get(i1).size(); j1++) { + BezierEdge be1 = shapes.get(i1).get(j1); + be1.shrinkToLine(); + } + } } private void splitToLayers( @@ -493,6 +540,8 @@ public class ShapeFixer { beforeHandle(shapeNum, shapes, fillStyles0, fillStyles1, lineStyles, layers, baseFillStyles, baseLineStyles, fillStyleLayers, lineStyleLayers); if (Configuration.flaExportFixShapes.get()) { + SwitchedFillSidesFixer switchedFillSidesFixer = new SwitchedFillSidesFixer(); + switchedFillSidesFixer.fixSwitchedFills(shapeNum, records, baseFillStyles, baseLineStyles, shapes, fillStyles0, fillStyles1, layers); detectOverlappingEdges(shapes, fillStyles0, fillStyles1, lineStyles, layers); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java new file mode 100644 index 000000000..8beb825d7 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2010-2025 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.SWF; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.shape.CurvedEdge; +import com.jpexs.decompiler.flash.exporters.shape.IEdge; +import com.jpexs.decompiler.flash.exporters.shape.ShapeExporterBase; +import com.jpexs.decompiler.flash.math.BezierEdge; +import com.jpexs.decompiler.flash.tags.base.ShapeTag; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.GRADRECORD; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.RGB; +import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; +import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; +import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import com.jpexs.helpers.Reference; +import java.awt.geom.Area; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Switched fill sides fixer. This will fix orientation of fillstyle0 and + * fillstyle1 to be on left and right side of the vector. + * + * @author JPEXS + */ +public class SwitchedFillSidesFixer { + + private static double polygonArea(List loop) { + double area = 0; + for (IEdge e : loop) { + assert (e != null); + if (e instanceof CurvedEdge) { + CurvedEdge ce = (CurvedEdge) e; + area += (e.getFromX() * ce.getControlY() - ce.getControlX() * e.getFromY()); + area += (ce.getControlX() * ce.getToY() - ce.getToX() * ce.getControlY()); + continue; + } + area += (e.getFromX() * e.getToY() - e.getToX() * e.getFromY()); + } + return area / 2.0; + } + + private BezierEdge iedgeToBezier(IEdge ie) { + assert (ie != null); + if (ie instanceof CurvedEdge) { + CurvedEdge ce = (CurvedEdge) ie; + return new BezierEdge(ce.getFromX(), ce.getFromY(), + ce.getControlX(), ce.getControlY(), + ce.getToX(), ce.getToY() + ); + } else { + return new BezierEdge(ie.getFromX(), ie.getFromY(), ie.getToX(), ie.getToY()); + } + } + + private static class Polygon { + + List list; + List children = new ArrayList<>(); + boolean ccw = false; + GeneralPath path; + int fillStyle; + boolean filled = true; + Polygon parent = null; + Area areaObj; + double area; + Rectangle2D bbox; + + public Polygon(List list, int fillStyle) { + this.list = list; + /*double polyArea = polygonArea(list); + if (polyArea < 0) { + ccw = true; + }*/ + path = toPath(); + //this.ccw = PathOrientation.orientationSingleClosed(path) == PathOrientation.Orientation.COUNTER_CLOCKWISE; + Reference orientationRef = new Reference<>(null); + Reference areaRef = new Reference<>(0.0); + PathOrientation.orientationSingleClosed(path, orientationRef, areaRef); + this.ccw = orientationRef.getVal() == PathOrientation.Orientation.COUNTER_CLOCKWISE; + this.area = areaRef.getVal(); + this.areaObj = new Area(path); + this.bbox = this.areaObj.getBounds2D(); + this.fillStyle = fillStyle; + } + + private GeneralPath toPath() { + GeneralPath gp = new GeneralPath(); + int lastX = Integer.MAX_VALUE; + int lastY = Integer.MAX_VALUE; + for (IEdge e : list) { + if (lastX == Integer.MAX_VALUE || lastX != e.getFromX() || lastY != e.getFromY()) { + gp.moveTo(e.getFromX(), e.getFromY()); + } + if (e instanceof CurvedEdge) { + CurvedEdge ce = (CurvedEdge) e; + gp.quadTo(ce.getControlX(), ce.getControlY(), ce.getToX(), ce.getToY()); + } else { + gp.lineTo(e.getToX(), e.getToY()); + } + lastX = e.getToX(); + lastY = e.getToY(); + } + if (lastX == list.get(0).getFromX() && lastY == list.get(0).getFromY()) { + gp.closePath(); + } + return gp; + } + + public boolean contains(Polygon other) { + if (other.areaObj.isEmpty()) { + return false; + } + Area diff = new Area(other.areaObj); + diff.subtract(areaObj); + return diff.isEmpty(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (IEdge e : list) { + sb.append("M ").append(e.getFromX()).append(" ").append(e.getFromY()).append(" "); + if (e instanceof CurvedEdge) { + CurvedEdge ce = (CurvedEdge) e; + sb.append("Q ").append(ce.getControlX()).append(" ").append(ce.getControlY()).append(" "); + } else { + sb.append("L "); + } + sb.append(e.getToX()).append(" ").append(e.getToY()).append(" "); + } + return sb.toString().trim(); + } + + } + + static class GridIndex { + + // Simple uniform grid over bbox domain + private final double cellSize; + private final Map> cells = new HashMap<>(); + private final double minX, minY; + + GridIndex(Collection polys, double cellSize) { + this.cellSize = cellSize; + // Compute global origin (minX/minY) to keep keys small + double minx = Double.POSITIVE_INFINITY, miny = Double.POSITIVE_INFINITY; + for (Polygon w : polys) { + Rectangle2D b = w.bbox; + if (b.getMinX() < minx) { + minx = b.getMinX(); + } + if (b.getMinY() < miny) { + miny = b.getMinY(); + } + } + this.minX = minx; + this.minY = miny; + + // Insert + for (Polygon w : polys) { + forEachCell(w.bbox, (gx, gy) -> { + cells.computeIfAbsent(key(gx, gy), k -> new ArrayList<>()).add(w); + }); + } + } + + private long key(int gx, int gy) { + // Pack two 32-bit ints into one long + return ((long) gx << 32) ^ (gy & 0xffffffffL); + } + + private int gx(double x) { + return (int) Math.floor((x - minX) / cellSize); + } + + private int gy(double y) { + return (int) Math.floor((y - minY) / cellSize); + } + + private void forEachCell(Rectangle2D r, CellConsumer cc) { + int x0 = gx(r.getMinX()); + int x1 = gx(r.getMaxX()); + int y0 = gy(r.getMinY()); + int y1 = gy(r.getMaxY()); + for (int x = x0; x <= x1; x++) { + for (int y = y0; y <= y1; y++) { + cc.accept(x, y); + } + } + } + + List query(Rectangle2D r) { + // Collect candidates from overlapping cells (deduplicated) + HashSet set = new HashSet<>(); + forEachCell(r, (gx, gy) -> { + List bucket = cells.get(key(gx, gy)); + if (bucket != null) { + set.addAll(bucket); + } + }); + return new ArrayList<>(set); + } + } + + interface CellConsumer { + + void accept(int gx, int gy); + } + + public static void buildContainment(List polygons) { + + Map> byStyle = polygons.stream() + .collect(java.util.stream.Collectors.groupingBy(w -> w.fillStyle)); + + for (Map.Entry> e : byStyle.entrySet()) { + List group = e.getValue(); + + double avgW = group.stream().mapToDouble(w -> w.bbox.getWidth()).average().orElse(1.0); + double avgH = group.stream().mapToDouble(w -> w.bbox.getHeight()).average().orElse(1.0); + double cellSize = Math.max(1.0, Math.max(avgW, avgH)); + + GridIndex index = new GridIndex(group, cellSize); + + group.sort((a, b) -> Double.compare(b.area, a.area)); + + for (int i = group.size() - 1; i >= 0; i--) { + Polygon inner = group.get(i); + List candidates = index.query(inner.bbox); + + Polygon bestParent = null; + double bestArea = Double.POSITIVE_INFINITY; + + for (Polygon outer : candidates) { + if (outer == inner) { + continue; + } + if (outer.area <= inner.area) { + continue; // only larger can contain + } + if (!outer.bbox.contains(inner.bbox)) { + continue; // cheap reject + } + if (outer.contains(inner)) { + if (outer.area < bestArea) { + bestArea = outer.area; + bestParent = outer; + } + } + } + if (bestParent != null) { + bestParent.children.add(inner); + } + } + } + } + + private void fixSidesInLayer( + List> fillList, + List> shapes, + List fillStyles0, + List fillStyles1, + int layer, + int startIndex, int endIndex, + Map globalToLocalFillStyleMap + ) { + + layer++; + + if (layer >= fillList.size()) { + Logger.getLogger(SwitchedFillSidesFixer.class.getName()).warning("FillResolver - Layer value larger than fill list size."); + return; + } + int fillStyleIdx = Integer.MAX_VALUE; + List currentList = new ArrayList<>(); + List> allLists = new ArrayList<>(); + List listFills = new ArrayList<>(); + int lastToX = Integer.MAX_VALUE; + int lastToY = Integer.MAX_VALUE; + int lastMoveToX = Integer.MAX_VALUE; + int lastMoveToY = Integer.MAX_VALUE; + for (int i = 0; i < fillList.get(layer).size(); i++) { + IEdge e = fillList.get(layer).get(i); + if (fillStyleIdx != e.getFillStyleIdx() + || (e.getFromX() != lastToX) || (e.getFromY() != lastToY) + || (e.getFromX() == lastMoveToX && e.getFromY() == lastMoveToY)) { + if (fillStyleIdx != Integer.MAX_VALUE) { + allLists.add(currentList); + listFills.add(fillStyleIdx); + currentList = new ArrayList<>(); + } + fillStyleIdx = e.getFillStyleIdx(); + lastMoveToX = e.getFromX(); + lastMoveToY = e.getFromY(); + } + currentList.add(e); + lastToX = e.getToX(); + lastToY = e.getToY(); + } + if (!currentList.isEmpty()) { + allLists.add(currentList); + listFills.add(fillStyleIdx); + } + + List polygons = new ArrayList<>(); + for (int i = 0; i < allLists.size(); i++) { + List list = allLists.get(i); + polygons.add(new Polygon(list, listFills.get(i))); + } + + /*for (Polygon outer : polygons) { + for (Polygon inner : polygons) { + if (outer != inner && inner.fillStyle == outer.fillStyle) { + boolean cont = outer.contains(inner); + + if (cont) { + if (inner.children.contains(outer)) { + inner.children.remove(outer); + } + outer.children.add(inner); + } + } + } + } + + loopmod: + while (true) { + for (Polygon poly : polygons) { + for (int c = 0; c < poly.children.size(); c++) { + for (int c2 = 0; c2 < poly.children.size(); c2++) { + if (poly.children.get(c).children.contains(poly.children.get(c2))) { + poly.children.remove(c2); + continue loopmod; + } + } + } + } + break; + }*/ + buildContainment(polygons); + + for (Polygon poly : polygons) { + for (Polygon child : poly.children) { + child.parent = poly; + } + } + + for (Polygon poly : polygons) { + int depth = 0; + + Polygon parent = poly.parent; + while (parent != null) { + parent = parent.parent; + depth++; + } + + poly.filled = depth % 2 == 0; + } + + Map> beToFillStyle0List = new LinkedHashMap<>(); + Map> beToFillStyle1List = new LinkedHashMap<>(); + + Map beToFillStyle0 = new LinkedHashMap<>(); + Map beToFillStyle1 = new LinkedHashMap<>(); + + for (int i = 0; i < polygons.size(); i++) { + Polygon polygon = polygons.get(i); + List list = polygon.list; + fillStyleIdx = listFills.get(i); + boolean clockwise = !polygon.ccw; + + for (IEdge e : list) { + BezierEdge be = iedgeToBezier(e); + BezierEdge beRev = be.reverse(); + int localFs = globalToLocalFillStyleMap.get(fillStyleIdx); + + BezierEdge search = new BezierEdge(180.0, -3040.0, 480.0, -3400.0); + + boolean print = false; + + if (be.equals(search)) { + System.err.println("xxx"); + System.err.println("" + polygon); + print = true; + } + if (be.equals(search.reverse())) { + System.err.println("yyy"); + print = true; + } + + if (print) { + System.err.println("localFS: " + localFs); + System.err.println("filled: " + polygon.filled); + System.err.println("clockwise: " + clockwise); + } + + if (polygon.filled == clockwise) { + if (!beToFillStyle1List.containsKey(be)) { + beToFillStyle1List.put(be, new ArrayList<>()); + } + if (!beToFillStyle0List.containsKey(beRev)) { + beToFillStyle0List.put(beRev, new ArrayList<>()); + } + beToFillStyle1List.get(be).add(localFs); + beToFillStyle0List.get(beRev).add(localFs); + + if (print) { + System.err.println("setting FS1 and rev FS0"); + } + + } else { + if (!beToFillStyle0List.containsKey(be)) { + beToFillStyle0List.put(be, new ArrayList<>()); + } + if (!beToFillStyle1List.containsKey(beRev)) { + beToFillStyle1List.put(beRev, new ArrayList<>()); + } + + beToFillStyle0List.get(be).add(localFs); + beToFillStyle1List.get(beRev).add(localFs); + + if (print) { + System.err.println("setting FS0 and rev FS1"); + } + + } + + if (print) { + System.err.println(""); + } + + } + + } + + for (BezierEdge be : beToFillStyle0List.keySet()) { + /*for (int i = beToFillStyle0List.get(be).size() - 1; i >= 0; i--) { + Integer fs = beToFillStyle0List.get(be).get(i); + if (beToFillStyle1List.containsKey(be) && beToFillStyle1List.get(be).contains(fs)) { + beToFillStyle0List.get(be).remove(fs); + beToFillStyle1List.get(be).remove(fs); + } + }*/ + int fs = -1; + if (beToFillStyle0List.get(be).size() == 1) { + fs = beToFillStyle0List.get(be).get(0); + } + if (!beToFillStyle0.containsKey(be) || beToFillStyle0.get(be) > 0 || fs == -1) { + beToFillStyle0.put(be, fs); + } + if (!beToFillStyle1.containsKey(be.reverse()) || beToFillStyle1.get(be.reverse()) > 0 || fs == -1) { + beToFillStyle1.put(be.reverse(), fs); + } + } + + for (BezierEdge be : beToFillStyle1List.keySet()) { + int fs = -1; + if (beToFillStyle1List.get(be).size() == 1) { + fs = beToFillStyle1List.get(be).get(0); + } + if (!beToFillStyle1.containsKey(be) || beToFillStyle1.get(be) > 0 || fs == -1) { + beToFillStyle1.put(be, fs); + } + if (!beToFillStyle0.containsKey(be.reverse()) || beToFillStyle0.get(be.reverse()) > 0 || fs == -1) { + beToFillStyle0.put(be.reverse(), fs); + } + } + + for (int i = startIndex; i < endIndex; i++) { + List shape = shapes.get(i); + for (int j = 0; j < shape.size(); j++) { + BezierEdge be = shape.get(j); + if (be.isEmpty()) { + continue; + } + + Integer fs0before = fillStyles0.get(i); + Integer fs1before = fillStyles1.get(i); + + if (fs0before == 0 && fs1before == 0) { //only strokes + break; + } + + Integer fs0after = beToFillStyle0.get(be); + Integer fs1after = beToFillStyle1.get(be); + + if (fs0after == null) { + fs0after = 0; + } + if (fs1after == null) { + fs1after = 0; + } + + if (fs0after == -1 || fs1after == -1) { + break; + } + + if (fs0after == 0 && Objects.equals(fs1after, fs1before)) { + fs0after = fs0before; + } else if (fs1after == 0 && Objects.equals(fs0after, fs0before)) { + fs1after = fs1before; + } + + fillStyles0.set(i, fs0after); + fillStyles1.set(i, fs1after); + + if (!Objects.equals(fs0before, fs0after) || !Objects.equals(fs1before, fs1after)) { + Logger.getLogger(SwitchedFillSidesFixer.class.getName()).log(Level.FINE, "Changed edge {0} - old: {1}, {2} new: {3}, {4}", new Object[]{be, fs0before, fs1before, fs0after, fs1after}); + } + break; + } + } + } + + public void fixSwitchedFills( + int shapeNum, + List records, + FILLSTYLEARRAY fillStyles, + LINESTYLEARRAY lineStyles, + List> shapes, + List fillStyles0, + List fillStyles1, + List layers + ) { + + SHAPEWITHSTYLE shp = new SHAPEWITHSTYLE(); + shp.shapeRecords = records; + shp.fillStyles = fillStyles; + shp.lineStyles = lineStyles; + + List> fillList = new ArrayList<>(); + + SWF swf = new SWF(); + new ShapeExporterBase(ShapeTag.WIND_EVEN_ODD, shapeNum, swf, shp, null) { + @Override + protected void handleFillPaths(List> fillPaths) { + fillList.addAll(fillPaths); + } + + @Override + public void beginShape() { + } + + @Override + public void endShape() { + } + + @Override + public void beginFills() { + } + + @Override + public void endFills() { + } + + @Override + public void beginLines() { + } + + @Override + public void endLines(boolean close) { + } + + @Override + public void beginFill(RGB color) { + } + + @Override + public void beginGradientFill(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + } + + @Override + public void beginBitmapFill(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { + } + + @Override + public void endFill() { + } + + @Override + public void lineStyle(double thickness, RGB color, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit, boolean noClose) { + } + + @Override + public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + } + + @Override + public void lineBitmapStyle(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { + } + + @Override + public void moveTo(double x, double y) { + } + + @Override + public void lineTo(double x, double y) { + } + + @Override + public void curveTo(double controlX, double controlY, double anchorX, double anchorY) { + } + }; + + Map globalToLocalFillStyleMap = new LinkedHashMap<>(); + int lastFs = 0; + globalToLocalFillStyleMap.put(0, 0); + for (int i = 0; i < fillStyles.fillStyles.length; i++) { + lastFs++; + globalToLocalFillStyleMap.put(lastFs, lastFs); + } + for (SHAPERECORD rec : records) { + if (rec instanceof StyleChangeRecord) { + StyleChangeRecord scr = (StyleChangeRecord) rec; + if (scr.stateNewStyles) { + for (int i = 0; i < scr.fillStyles.fillStyles.length; i++) { + lastFs++; + globalToLocalFillStyleMap.put(lastFs, i + 1); + } + } + } + } + + int from = 0; + for (int i = 1; i < layers.size(); i++) { + if (!layers.get(i).equals(layers.get(i - 1))) { + fixSidesInLayer(fillList, shapes, fillStyles0, fillStyles1, layers.get(i - 1), from, i, globalToLocalFillStyleMap); + from = i; + } + } + if (!layers.isEmpty()) { + fixSidesInLayer(fillList, shapes, fillStyles0, fillStyles1, layers.get(layers.size() - 1), from, layers.size(), globalToLocalFillStyleMap); + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixerFloat.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixerFloat.java new file mode 100644 index 000000000..4e856e2c9 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixerFloat.java @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2010-2025 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 java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Switched fill sides fixer. Float version. This will fix orientation of + * fillstyle0 and fillstyle1 to be on left and right side of the vector. + * + * WIP: incomplete, use non-float version instead + * + * @author JPEXS + */ +public class SwitchedFillSidesFixerFloat { + + class Edge { + + int fromId; + int controlId = -1; + int toId; + + int fillStyleIdx; + + public Edge(int fromId, int controlId, int toId, int fillStyleIdx) { + this.fromId = fromId; + this.controlId = controlId; + this.toId = toId; + this.fillStyleIdx = fillStyleIdx; + } + + public Edge(int fromId, int toId, int fillStyleIdx) { + this.fromId = fromId; + this.toId = toId; + this.fillStyleIdx = fillStyleIdx; + } + + public Edge reverseWithNewFillStyle(int newFillStyleIdx) { + return new Edge(toId, controlId, fromId, newFillStyleIdx); + } + + public Edge reverse() { + return new Edge(toId, controlId, fromId, fillStyleIdx); + } + + public Edge sameWithNewFillStyle(int newFillStyleIdx) { + return new Edge(fromId, controlId, toId, newFillStyleIdx); + } + + public BezierEdge toBezierEdge(List idToPoint) { + Point2D from = idToPoint.get(fromId); + Point2D to = idToPoint.get(toId); + if (controlId != -1) { + Point2D control = idToPoint.get(controlId); + return new BezierEdge(Arrays.asList(from, control, to)); + } + return new BezierEdge(Arrays.asList(from, to)); + } + } + + boolean USE_REVERSE_LOOKUP = true; + + private Map> createEdgeMap( + List> shapes, + List fillStyles0, + List fillStyles1, + List layers, + int from, + int to, + List idToPoint, + Map pointToId + ) { + + Map> currentFillEdgeMap = new HashMap<>(); + + for (int i = from; i < to; i++) { + List subPath = new ArrayList<>(); + for (BezierEdge be : shapes.get(i)) { + int fromId = pointToId.get(be.getBeginPoint()); + int toId = pointToId.get(be.getEndPoint()); + int controlId = -1; + if (be.points.size() == 3) { + controlId = pointToId.get(be.points.get(1)); + } + subPath.add(new Edge(fromId, controlId, toId, fillStyles1.get(i))); + } + processSubPath(subPath, fillStyles0.get(i), fillStyles1.get(i), currentFillEdgeMap); + } + + cleanEdgeMap(currentFillEdgeMap); + + return currentFillEdgeMap; + } + + private void processSubPath(List subPath, int fillStyleIdx0, int fillStyleIdx1, + Map> currentFillEdgeMap) { + List path; + if (fillStyleIdx0 != 0) { + path = currentFillEdgeMap.get(fillStyleIdx0); + if (path == null) { + path = new ArrayList<>(); + currentFillEdgeMap.put(fillStyleIdx0, path); + } + for (int j = subPath.size() - 1; j >= 0; j--) { + Edge rev = subPath.get(j).reverseWithNewFillStyle(fillStyleIdx0); + path.add(rev); + } + + } + if (fillStyleIdx1 != 0) { + path = currentFillEdgeMap.get(fillStyleIdx1); + if (path == null) { + path = new ArrayList<>(); + currentFillEdgeMap.put(fillStyleIdx1, path); + } + appendEdges(path, subPath); + } + } + + private List createPathFromEdgeMap(Map> edgeMap) { + List newPath = new ArrayList<>(); + List styleIdxArray = new ArrayList<>(); + for (Integer styleIdx : edgeMap.keySet()) { + styleIdxArray.add(styleIdx); + } + Collections.sort(styleIdxArray); + for (int i = 0; i < styleIdxArray.size(); i++) { + appendEdges(newPath, edgeMap.get(styleIdxArray.get(i))); + } + return newPath; + } + + private void appendEdges(List v1, List v2) { + for (int i = 0; i < v2.size(); i++) { + v1.add(v2.get(i)); + } + } + + private void cleanEdgeMap(Map> edgeMap) { + for (Integer styleIdx : edgeMap.keySet()) { + List subPath = edgeMap.get(styleIdx); + if (subPath != null && !subPath.isEmpty()) { + int idx; + Edge prevEdge = null; + List tmpPath = new ArrayList<>(); + Map> coordMap = createCoordMap(subPath); + Map> reverseCoordMap = createReverseCoordMap(subPath); + while (!subPath.isEmpty()) { + idx = 0; + while (idx < subPath.size()) { + if (prevEdge != null) { + Edge subPathEdge = subPath.get(idx); + if (prevEdge.toId != subPathEdge.fromId) { + Edge edge = findNextEdgeInCoordMap(coordMap, prevEdge); + if (edge != null) { + idx = subPath.indexOf(edge); + } else { + Edge revEdge = findNextEdgeInCoordMap(reverseCoordMap, prevEdge); + + if (revEdge != null) { + if (USE_REVERSE_LOOKUP) { + idx = subPath.indexOf(revEdge); + Edge r = revEdge.reverseWithNewFillStyle(revEdge.fillStyleIdx); + updateEdgeInCoordMap(coordMap, revEdge, r); + updateEdgeInReverseCoordMap(reverseCoordMap, revEdge, r); + subPath.set(idx, r); + } else { + idx = 0; + prevEdge = null; + } + } else { + idx = 0; + prevEdge = null; + } + } + continue; + } + } + + Edge edge = subPath.remove(idx); + tmpPath.add(edge); + removeEdgeFromCoordMap(coordMap, edge); + removeEdgeFromReverseCoordMap(reverseCoordMap, edge); + prevEdge = edge; + } + } + edgeMap.put(styleIdx, tmpPath); + } + } + } + + private Map> createCoordMap(List path) { + Map> coordMap = new HashMap<>(); + for (int i = 0; i < path.size(); i++) { + Edge edge = path.get(i); + List coordMapArray = coordMap.get(edge.fromId); + if (coordMapArray == null) { + List list = new ArrayList<>(); + list.add(path.get(i)); + coordMap.put(edge.fromId, list); + } else { + coordMapArray.add(path.get(i)); + } + } + return coordMap; + } + + private Map> createReverseCoordMap(List path) { + Map> coordMap = new HashMap<>(); + for (int i = 0; i < path.size(); i++) { + Edge edge = path.get(i); + List coordMapArray = coordMap.get(edge.toId); + if (coordMapArray == null) { + List list = new ArrayList<>(); + list.add(path.get(i)); + coordMap.put(edge.toId, list); + } else { + coordMapArray.add(path.get(i)); + } + } + return coordMap; + } + + private void removeEdgeFromCoordMap(Map> coordMap, Edge edge) { + List coordMapArray = coordMap.get(edge.fromId); + if (coordMapArray != null) { + if (coordMapArray.size() == 1) { + coordMap.remove(edge.fromId); + } else { + int i = coordMapArray.indexOf(edge); + if (i > -1) { + coordMapArray.remove(i); + } + } + } + } + + private void removeEdgeFromReverseCoordMap(Map> coordMap, Edge edge) { + List coordMapArray = coordMap.get(edge.toId); + if (coordMapArray != null) { + if (coordMapArray.size() == 1) { + coordMap.remove(edge.toId); + } else { + int i = coordMapArray.indexOf(edge); + if (i > -1) { + coordMapArray.remove(i); + } + } + } + } + + private Edge findNextEdgeInCoordMap(Map> coordMap, Edge edge) { + List coordMapArray = coordMap.get(edge.toId); + if (coordMapArray != null && !coordMapArray.isEmpty()) { + return coordMapArray.get(0); + } + return null; + } + + private Edge updateEdgeInCoordMap(Map> coordMap, Edge edge, Edge newEdge) { + coordMap.get(edge.fromId).remove(edge); + + if (!coordMap.containsKey(newEdge.fromId)) { + coordMap.put(newEdge.fromId, new ArrayList<>()); + } + coordMap.get(newEdge.fromId).add(newEdge); + return null; + } + + private Edge updateEdgeInReverseCoordMap(Map> coordMap, Edge edge, Edge newEdge) { + + coordMap.get(edge.toId).remove(edge); + + if (!coordMap.containsKey(newEdge.toId)) { + coordMap.put(newEdge.toId, new ArrayList<>()); + } + coordMap.get(newEdge.toId).add(newEdge); + return null; + } + + private void fixSidesInLayer(List> shapes, + List fillStyles0, + List fillStyles1, + List layers, + int from, + int to) { + Set allPoints = new LinkedHashSet<>(); + + for (int i = from; i < to; i++) { + for (BezierEdge be : shapes.get(i)) { + for (Point2D p : be.points) { + allPoints.add(p); + } + } + } + List idToPoint = new ArrayList<>(allPoints); + Map pointToId = new HashMap<>(); + for (int i = 0; i < idToPoint.size(); i++) { + pointToId.put(idToPoint.get(i), i); + } + Map> currentFillEdgeMap = createEdgeMap(shapes, fillStyles0, fillStyles1, layers, from, to, idToPoint, pointToId); + + List edges = createPathFromEdgeMap(currentFillEdgeMap); + + //------------------------------------- + int fillStyleIdx = Integer.MAX_VALUE; + List currentList = new ArrayList<>(); + List> allLists = new ArrayList<>(); + List listFills = new ArrayList<>(); + int lastTo = -1; + for (int i = 0; i < edges.size(); i++) { + Edge e = edges.get(i); + if (fillStyleIdx != e.fillStyleIdx) { //|| e.fromId != lastTo) { + if (fillStyleIdx != Integer.MAX_VALUE) { + allLists.add(currentList); + listFills.add(fillStyleIdx); + currentList = new ArrayList<>(); + } + fillStyleIdx = e.fillStyleIdx; + } + currentList.add(e); + lastTo = e.toId; + } + if (!currentList.isEmpty()) { + allLists.add(currentList); + listFills.add(fillStyleIdx); + } + + Map beToFillStyle0 = new LinkedHashMap<>(); + Map beToFillStyle1 = new LinkedHashMap<>(); + + for (int i = 0; i < allLists.size(); i++) { + List list = allLists.get(i); + fillStyleIdx = listFills.get(i); + + double poly = 0; + for (Edge e : list) { + Point2D fromP = idToPoint.get(e.fromId); + Point2D toP; + if (e.controlId != -1) { + toP = idToPoint.get(e.controlId); + poly += fromP.getX() * toP.getY() - toP.getX() * fromP.getY(); + fromP = toP; + } + toP = idToPoint.get(e.toId); + poly += fromP.getX() * toP.getY() - toP.getX() * fromP.getY(); + } + + boolean clockwise = poly > 0; + for (Edge e : list) { + BezierEdge be = e.toBezierEdge(idToPoint); + BezierEdge beRev = be.reverse(); + + /*if (be.getBeginPoint().equals(new Point2D.Double(12580.0,4280.0)) + && be.getEndPoint().equals(new Point2D.Double(12680.0,4240.0))) { + System.err.println("xxx: " + be); + System.err.println("FS: " + fillStyleIdx); + System.err.println("ClockWise: " + clockwise); + } + + if (be.getBeginPoint().equals(new Point2D.Double(12680.0,4240.0)) + && be.getEndPoint().equals(new Point2D.Double(12580.0,4280.0))) { + System.err.println("xxx2: " + be); + System.err.println("FS: " + fillStyleIdx); + System.err.println("ClockWise: " + clockwise); + }*/ + if (be.getBeginPoint().equals(new Point2D.Double(12500.0, 3580.0)) + && be.points.get(1).equals(new Point2D.Double(12520.0, 3600.0)) + && be.getEndPoint().equals(new Point2D.Double(12560.0, 3580.0))) { + System.err.println("xxx: " + be); + System.err.println("FS: " + fillStyleIdx); + System.err.println("ClockWise: " + clockwise); + } + + if (be.getBeginPoint().equals(new Point2D.Double(12560.0, 3580.0)) + && be.points.get(1).equals(new Point2D.Double(12520.0, 3600.0)) + && be.getEndPoint().equals(new Point2D.Double(12500.0, 3580.0))) { + System.err.println("xxx2: " + be); + System.err.println("FS: " + fillStyleIdx); + System.err.println("ClockWise: " + clockwise); + } + + if (clockwise) { + beToFillStyle1.put(be, fillStyleIdx); + beToFillStyle0.put(beRev, fillStyleIdx); + } else { + beToFillStyle0.put(be, fillStyleIdx); + beToFillStyle1.put(beRev, fillStyleIdx); + } + } + + } + + for (int i = from; i < to; i++) { + List shape = shapes.get(i); + for (int j = 0; j < shape.size(); j++) { + BezierEdge be = shape.get(j); + Integer fs0before = fillStyles0.get(i); + Integer fs1before = fillStyles1.get(i); + + if (fs0before == 0 && fs1before == 0) { //only strokes + break; + } + + if (be.getBeginPoint().equals(new Point2D.Double(12580.0, 4280.0)) + && be.getEndPoint().equals(new Point2D.Double(12680.0, 4240.0))) { + System.err.println("yyy"); + } + + Integer fs0after = beToFillStyle0.get(be); + Integer fs1after = beToFillStyle1.get(be); + + if (fs0after == null) { + fs0after = 0; + } + if (fs1after == null) { + fs1after = 0; + } + + fillStyles0.set(i, fs0after); + fillStyles1.set(i, fs1after); + + if (!Objects.equals(fs0before, fs0after) || !Objects.equals(fs1before, fs1after)) { + Logger.getLogger(SwitchedFillSidesFixerFloat.class.getName()).log(Level.FINE, "Changed edge {0} - old: {1}, {2} new: {3}, {4}", new Object[]{be, fs0before, fs1before, fs0after, fs1after}); + } + break; + } + } + } + + public void fixSwitchedFills( + List> shapes, + List fillStyles0, + List fillStyles1, + List layers + ) { + + int from = 0; + for (int i = 1; i < layers.size(); i++) { + if (!layers.get(i).equals(layers.get(i - 1))) { + fixSidesInLayer(shapes, fillStyles0, fillStyles1, layers, from, i); + from = i; + } + } + if (!layers.isEmpty()) { + fixSidesInLayer(shapes, fillStyles0, fillStyles1, layers, from, layers.size()); + } + } +} diff --git a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java index 58d8dda47..47d0324c5 100644 --- a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java @@ -37,6 +37,7 @@ import com.jpexs.decompiler.flash.importers.amf.AmfParseException; import com.jpexs.decompiler.flash.importers.amf.amf0.Amf0Importer; import com.jpexs.decompiler.flash.importers.amf.amf3.Amf3Importer; import com.jpexs.decompiler.flash.math.BezierUtils; +import com.jpexs.decompiler.flash.shapes.ShapeTransformer; import com.jpexs.decompiler.flash.sol.SolFile; import com.jpexs.decompiler.flash.tags.DefineMorphShape2Tag; import com.jpexs.decompiler.flash.tags.DefineShape4Tag; @@ -2299,147 +2300,7 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel mainPanel.clearEditingStatus(); } - private void transformStyles(Matrix matrix, FILLSTYLEARRAY fillStyles, LINESTYLEARRAY lineStyles, int shapeNum) { - List fillStyleToTransform = new ArrayList<>(); - for (FILLSTYLE fs : fillStyles.fillStyles) { - fillStyleToTransform.add(fs); - } - if (shapeNum >= 4) { - for (LINESTYLE2 ls : lineStyles.lineStyles2) { - if (ls.hasFillFlag) { - fillStyleToTransform.add(ls.fillType); - } - } - } - - for (FILLSTYLE fs : fillStyleToTransform) { - switch (fs.fillStyleType) { - case FILLSTYLE.CLIPPED_BITMAP: - case FILLSTYLE.NON_SMOOTHED_CLIPPED_BITMAP: - case FILLSTYLE.NON_SMOOTHED_REPEATING_BITMAP: - case FILLSTYLE.REPEATING_BITMAP: - fs.bitmapMatrix = new Matrix(fs.bitmapMatrix).preConcatenate(matrix).toMATRIX(); - break; - case FILLSTYLE.LINEAR_GRADIENT: - case FILLSTYLE.RADIAL_GRADIENT: - case FILLSTYLE.FOCAL_RADIAL_GRADIENT: - fs.gradientMatrix = new Matrix(fs.gradientMatrix).preConcatenate(matrix).toMATRIX(); - break; - } - } - } - - private void transformMorphStyles(Matrix matrix, MORPHFILLSTYLEARRAY fillStyles, MORPHLINESTYLEARRAY lineStyles, int morphShapeNum, boolean doStart, boolean doEnd) { - List fillStyleToTransform = new ArrayList<>(); - for (MORPHFILLSTYLE fs : fillStyles.fillStyles) { - fillStyleToTransform.add(fs); - } - - if (morphShapeNum == 2) { - for (MORPHLINESTYLE2 ls : lineStyles.lineStyles2) { - if (ls.hasFillFlag) { - fillStyleToTransform.add(ls.fillType); - } - } - } - - for (MORPHFILLSTYLE fs : fillStyleToTransform) { - switch (fs.fillStyleType) { - case FILLSTYLE.CLIPPED_BITMAP: - case FILLSTYLE.NON_SMOOTHED_CLIPPED_BITMAP: - case FILLSTYLE.NON_SMOOTHED_REPEATING_BITMAP: - case FILLSTYLE.REPEATING_BITMAP: - if (doStart) { - fs.startBitmapMatrix = new Matrix(fs.startBitmapMatrix).preConcatenate(matrix).toMATRIX(); - } - if (doEnd) { - fs.endBitmapMatrix = new Matrix(fs.endBitmapMatrix).preConcatenate(matrix).toMATRIX(); - } - break; - case FILLSTYLE.LINEAR_GRADIENT: - case FILLSTYLE.RADIAL_GRADIENT: - case FILLSTYLE.FOCAL_RADIAL_GRADIENT: - if (doStart) { - fs.startGradientMatrix = new Matrix(fs.startGradientMatrix).preConcatenate(matrix).toMATRIX(); - } - if (doEnd) { - fs.endGradientMatrix = new Matrix(fs.endGradientMatrix).preConcatenate(matrix).toMATRIX(); - } - break; - } - } - } - - private void transformSHAPE(Matrix matrix, SHAPE shape, int shapeNum) { - int x = 0; - int y = 0; - StyleChangeRecord lastStyleChangeRecord = null; - boolean wasMoveTo = false; - for (SHAPERECORD rec : shape.shapeRecords) { - if (rec instanceof StyleChangeRecord) { - StyleChangeRecord scr = (StyleChangeRecord) rec; - lastStyleChangeRecord = scr; - if (scr.stateNewStyles) { - transformStyles(matrix, scr.fillStyles, scr.lineStyles, shapeNum); - } - if (scr.stateMoveTo) { - Point nextPoint = new Point(scr.moveDeltaX, scr.moveDeltaY); - x = scr.changeX(x); - y = scr.changeY(y); - Point nextPoint2 = matrix.transform(nextPoint); - scr.moveDeltaX = nextPoint2.x; - scr.moveDeltaY = nextPoint2.y; - scr.calculateBits(); - wasMoveTo = true; - } - } - - if (((rec instanceof StraightEdgeRecord) || (rec instanceof CurvedEdgeRecord)) && !wasMoveTo) { - if (lastStyleChangeRecord != null) { - Point nextPoint2 = matrix.transform(new Point(x, y)); - if (nextPoint2.x != 0 || nextPoint2.y != 0) { - lastStyleChangeRecord.stateMoveTo = true; - lastStyleChangeRecord.moveDeltaX = nextPoint2.x; - lastStyleChangeRecord.moveDeltaY = nextPoint2.y; - lastStyleChangeRecord.calculateBits(); - wasMoveTo = true; - } - } - } - if (rec instanceof StraightEdgeRecord) { - StraightEdgeRecord ser = (StraightEdgeRecord) rec; - ser.generalLineFlag = true; - ser.vertLineFlag = false; - Point currentPoint = new Point(x, y); - Point nextPoint = new Point(x + ser.deltaX, y + ser.deltaY); - x = ser.changeX(x); - y = ser.changeY(y); - Point currentPoint2 = matrix.transform(currentPoint); - Point nextPoint2 = matrix.transform(nextPoint); - ser.deltaX = nextPoint2.x - currentPoint2.x; - ser.deltaY = nextPoint2.y - currentPoint2.y; - ser.simplify(); - } - if (rec instanceof CurvedEdgeRecord) { - CurvedEdgeRecord cer = (CurvedEdgeRecord) rec; - Point currentPoint = new Point(x, y); - Point controlPoint = new Point(x + cer.controlDeltaX, y + cer.controlDeltaY); - Point anchorPoint = new Point(x + cer.controlDeltaX + cer.anchorDeltaX, y + cer.controlDeltaY + cer.anchorDeltaY); - x = cer.changeX(x); - y = cer.changeY(y); - - Point currentPoint2 = matrix.transform(currentPoint); - Point controlPoint2 = matrix.transform(controlPoint); - Point anchorPoint2 = matrix.transform(anchorPoint); - - cer.controlDeltaX = controlPoint2.x - currentPoint2.x; - cer.controlDeltaY = controlPoint2.y - currentPoint2.y; - cer.anchorDeltaX = anchorPoint2.x - controlPoint2.x; - cer.anchorDeltaY = anchorPoint2.y - controlPoint2.y; - cer.calculateBits(); - } - } - } + private RECT transformRECT(Matrix matrix, RECT rect) { ExportRectangle shapeRect = matrix.transform(new ExportRectangle(rect)); @@ -2494,14 +2355,16 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel } } + ShapeTransformer shapeTransformer = new ShapeTransformer(); + oldShapeRecords = Helper.deepCopy(shape.shapes.shapeRecords); - transformSHAPE(matrix, shape.shapes, shape.getShapeNum()); + shapeTransformer.transformSHAPE(matrix, shape.shapes, shape.getShapeNum()); if (checkShapeLarge(shape.shapes.shapeRecords)) { shape.shapes.shapeRecords = oldShapeRecords; return; } oldShapeRecords = null; - transformStyles(matrix, shape.shapes.fillStyles, shape.shapes.lineStyles, shape.getShapeNum()); + shapeTransformer.transformStyles(matrix, shape.shapes.fillStyles, shape.shapes.lineStyles, shape.getShapeNum()); shape.shapeBounds = newShapeBounds; if (shape instanceof DefineShape4Tag) { @@ -2526,8 +2389,10 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel newEdgeBounds = transformRECT(matrix, morphShape2.startEdgeBounds); } + ShapeTransformer shapeTransformer = new ShapeTransformer(); + oldShapeRecords = Helper.deepCopy(morphShape.startEdges.shapeRecords); - transformSHAPE(matrix, morphShape.startEdges, morphShape.getShapeNum() == 1 ? 3 : 4); + shapeTransformer.transformSHAPE(matrix, morphShape.startEdges, morphShape.getShapeNum() == 1 ? 3 : 4); if (checkShapeLarge(morphShape.startEdges.shapeRecords)) { morphShape.startEdges.shapeRecords = oldShapeRecords; return; @@ -2538,7 +2403,7 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel DefineMorphShape2Tag morphShape2 = (DefineMorphShape2Tag) morphShape; morphShape2.startEdgeBounds = newEdgeBounds; } - transformMorphStyles(matrix, morphShape.morphFillStyles, morphShape.morphLineStyles, morphShape.getShapeNum(), true, false); + shapeTransformer.transformMorphStyles(matrix, morphShape.morphFillStyles, morphShape.morphLineStyles, morphShape.getShapeNum(), true, false); } if (morphDisplayMode == MORPH_END) { @@ -2551,9 +2416,11 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel DefineMorphShape2Tag morphShape2 = (DefineMorphShape2Tag) morphShape; newEdgeBounds = transformRECT(matrix, morphShape2.endEdgeBounds); } + + ShapeTransformer shapeTransformer = new ShapeTransformer(); oldShapeRecords = Helper.deepCopy(morphShape.endEdges.shapeRecords); - transformSHAPE(matrix, morphShape.endEdges, morphShape.getShapeNum() == 1 ? 3 : 4); + shapeTransformer.transformSHAPE(matrix, morphShape.endEdges, morphShape.getShapeNum() == 1 ? 3 : 4); if (checkShapeLarge(morphShape.endEdges.shapeRecords)) { morphShape.endEdges.shapeRecords = oldShapeRecords; return; @@ -2564,7 +2431,7 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel DefineMorphShape2Tag morphShape2 = (DefineMorphShape2Tag) morphShape; morphShape2.endEdgeBounds = newEdgeBounds; } - transformMorphStyles(matrix, morphShape.morphFillStyles, morphShape.morphLineStyles, morphShape.getShapeNum(), false, true); + shapeTransformer.transformMorphStyles(matrix, morphShape.morphFillStyles, morphShape.morphLineStyles, morphShape.getShapeNum(), false, true); } morphShape.getSwf().clearShapeCache(); }