diff --git a/CHANGELOG.md b/CHANGELOG.md index c48da43f0..4bc83501a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. - [#1860] FLA export - EmptyStackException during exporting MorphShape - [#1782] FLA export - exporting from SWF files inside bundles (like binarysearch) - Expand correct tree on SWF load +- [#1679] FLA export - MorphShapes (shape tween) ## [16.0.3] - 2022-11-02 ### Fixed @@ -2459,6 +2460,7 @@ All notable changes to this project will be documented in this file. [alpha 7]: https://github.com/jindrapetrik/jpexs-decompiler/releases/tag/alpha7 [#1860]: https://www.free-decompiler.com/flash/issues/1860 [#1782]: https://www.free-decompiler.com/flash/issues/1782 +[#1679]: https://www.free-decompiler.com/flash/issues/1679 [#1817]: https://www.free-decompiler.com/flash/issues/1817 [#1816]: https://www.free-decompiler.com/flash/issues/1816 [#1859]: https://www.free-decompiler.com/flash/issues/1859 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java index 8ea363080..84bf72e52 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java @@ -230,8 +230,8 @@ public abstract class MorphShapeTag extends DrawableTag { } StyleChangeRecord scr = scr1.clone(); if (scr1.stateMoveTo || scr2.stateMoveTo) { - scr.moveDeltaX = startPosX + (endPosX - startPosX) * ratio / MAX_RATIO; - scr.moveDeltaY = startPosY + (endPosY - startPosY) * ratio / MAX_RATIO; + scr.moveDeltaX = startPosX + (int)Math.round((endPosX - startPosX) * ratio / (double)MAX_RATIO); + scr.moveDeltaY = startPosY + (int)Math.round((endPosY - startPosY) * ratio / (double)MAX_RATIO); scr.stateMoveTo = scr.moveDeltaX != posX || scr.moveDeltaY != posY; } finalRecords.add(scr); @@ -264,10 +264,10 @@ public abstract class MorphShapeTag extends DrawableTag { continue; } CurvedEdgeRecord cer = new CurvedEdgeRecord(); - cer.controlDeltaX = cer1.controlDeltaX + (cer2.controlDeltaX - cer1.controlDeltaX) * ratio / MAX_RATIO; - cer.controlDeltaY = cer1.controlDeltaY + (cer2.controlDeltaY - cer1.controlDeltaY) * ratio / MAX_RATIO; - cer.anchorDeltaX = cer1.anchorDeltaX + (cer2.anchorDeltaX - cer1.anchorDeltaX) * ratio / MAX_RATIO; - cer.anchorDeltaY = cer1.anchorDeltaY + (cer2.anchorDeltaY - cer1.anchorDeltaY) * ratio / MAX_RATIO; + cer.controlDeltaX = cer1.controlDeltaX + (int)Math.round((cer2.controlDeltaX - cer1.controlDeltaX) * ratio / (double)MAX_RATIO); + cer.controlDeltaY = cer1.controlDeltaY + (int)Math.round((cer2.controlDeltaY - cer1.controlDeltaY) * ratio / (double)MAX_RATIO); + cer.anchorDeltaX = cer1.anchorDeltaX + (int)Math.round((cer2.anchorDeltaX - cer1.anchorDeltaX) * ratio / (double)MAX_RATIO); + cer.anchorDeltaY = cer1.anchorDeltaY + (int)Math.round((cer2.anchorDeltaY - cer1.anchorDeltaY) * ratio / (double)MAX_RATIO); startPosX += cer1.controlDeltaX + cer1.anchorDeltaX; startPosY += cer1.controlDeltaY + cer1.anchorDeltaY; endPosX += cer2.controlDeltaX + cer2.anchorDeltaX; @@ -290,8 +290,8 @@ public abstract class MorphShapeTag extends DrawableTag { StraightEdgeRecord ser = new StraightEdgeRecord(); ser.generalLineFlag = true; ser.vertLineFlag = false; - ser.deltaX = ser1.deltaX + (ser2.deltaX - ser1.deltaX) * ratio / MAX_RATIO; - ser.deltaY = ser1.deltaY + (ser2.deltaY - ser1.deltaY) * ratio / MAX_RATIO; + ser.deltaX = ser1.deltaX + (int)Math.round((ser2.deltaX - ser1.deltaX) * ratio / (double)MAX_RATIO); + ser.deltaY = ser1.deltaY + (int)Math.round((ser2.deltaY - ser1.deltaY) * ratio / (double)MAX_RATIO); startPosX += ser1.deltaX; startPosY += ser1.deltaY; endPosX += ser2.deltaX; @@ -307,7 +307,7 @@ public abstract class MorphShapeTag extends DrawableTag { shape.shapeRecords = finalRecords; return shape; } - + @Override public int getUsedParameters() { return PARAMETER_RATIO; 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 d11b7ade5..c79f73f73 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 @@ -137,11 +137,13 @@ import com.jpexs.helpers.SerializableImage; import com.jpexs.helpers.utf8.Utf8Helper; import java.awt.Font; import java.awt.Point; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -156,6 +158,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; @@ -163,11 +167,17 @@ import javax.xml.stream.XMLStreamException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Source; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -246,7 +256,7 @@ public class XFLConverter { } } if (!hasMove) { - ret.append("!").append(startX).append(" ").append(startY); + ret.append("! ").append(startX).append(" ").append(startY); } for (SHAPERECORD rec : records) { convertShapeEdge(mat, rec, x, y, ret); @@ -810,47 +820,34 @@ public class XFLConverter { strokesNewStr.writeStartElement("strokes"); if (fillStyleCount > 0 || lineStyleCount > 0) { - if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { - - boolean empty = false; - if ((fillStyle0 <= 0) && (fillStyle1 <= 0) && (strokeStyle > 0) && morphshape) { - if (shapeNum == 4) { - if (strokeStyleOrig > 0) { - if (actualLinestyles != null && !((LINESTYLE2) actualLinestyles.lineStyles2[strokeStyleOrig]).hasFillFlag) { - RGBA color = (RGBA) actualLinestyles.lineStyles2[strokeStyleOrig].getColor(); - if (color.alpha == 0 && color.red == 0 && color.green == 0 && color.blue == 0) { - empty = true; - } - } - } - } + if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { + currentLayer.writeStartElement("Edge"); + if (fillStyle0 > -1) { + currentLayer.writeAttribute("fillStyle0", fillStyle0); } - if (!empty) { - currentLayer.writeStartElement("Edge"); - if (fillStyle0 > -1) { - currentLayer.writeAttribute("fillStyle0", fillStyle0); - } - if (fillStyle1 > -1) { - currentLayer.writeAttribute("fillStyle1", fillStyle1); - } - if (strokeStyle > -1) { - currentLayer.writeAttribute("strokeStyle", strokeStyle); - } - StringBuilder edgesSb = new StringBuilder(); - convertShapeEdges(startEdgeX, startEdgeY, mat, edges, edgesSb); - currentLayer.writeAttribute("edges", edgesSb.toString()); - currentLayer.writeEndElement(); - hasEdge = true; + if (fillStyle1 > -1) { + currentLayer.writeAttribute("fillStyle1", fillStyle1); } + if (strokeStyle > -1) { + currentLayer.writeAttribute("strokeStyle", strokeStyle); + } + StringBuilder edgesSb = new StringBuilder(); + convertShapeEdges(startEdgeX, startEdgeY, mat, edges, edgesSb); + currentLayer.writeAttribute("edges", edgesSb.toString()); + currentLayer.writeEndElement(); + hasEdge = true; } - } if (currentLayer.length() > 0) { currentLayer.writeEndElement(); // edges currentLayer.writeEndElement(); // DOMShape } if (currentLayer.length() > 0 && hasEdge) { //no empty layers - layers.add(currentLayer.toString()); + String s = currentLayer.toString(); + if (morphshape) { + s = removeOnlyStrokeEdgesBeforeSameFilled(s); + } + layers.add(s); } currentLayer.setLength(0); hasEdge = false; @@ -905,43 +902,28 @@ public class XFLConverter { styleChange = true; } if (scr.stateLineStyle) { - strokeStyle = scr.lineStyle;// == 0 ? 0 : lineStyleCount - lastLineStyleCount + scr.lineStyle; + strokeStyle = scr.lineStyle; strokeStyleOrig = scr.lineStyle - 1; styleChange = true; } if (!edges.isEmpty()) { - if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { - boolean empty = false; - if ((fillStyle0 <= 0) && (fillStyle1 <= 0) && (strokeStyle > 0) && morphshape) { - if (shapeNum == 4) { - if (strokeStyleOrig > 0) { - if (actualLinestyles != null && !((LINESTYLE2) actualLinestyles.lineStyles2[strokeStyleOrig]).hasFillFlag) { - RGBA color = (RGBA) actualLinestyles.lineStyles2[strokeStyleOrig].getColor(); - if (color.alpha == 0 && color.red == 0 && color.green == 0 && color.blue == 0) { - empty = true; - } - } - } - } + if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { + currentLayer.writeStartElement("Edge"); + if (lastFillStyle0 > -1) { + currentLayer.writeAttribute("fillStyle0", lastFillStyle0); } - if (!empty) { - currentLayer.writeStartElement("Edge"); - if (lastFillStyle0 > -1) { - currentLayer.writeAttribute("fillStyle0", lastFillStyle0); - } - if (lastFillStyle1 > -1) { - currentLayer.writeAttribute("fillStyle1", lastFillStyle1); - } - if (lastStrokeStyle > -1) { - currentLayer.writeAttribute("strokeStyle", lastStrokeStyle); - } - StringBuilder edgesSb = new StringBuilder(); - convertShapeEdges(startEdgeX, startEdgeY, mat, edges, edgesSb); - currentLayer.writeAttribute("edges", edgesSb.toString()); - currentLayer.writeEndElement(); - hasEdge = true; + if (lastFillStyle1 > -1) { + currentLayer.writeAttribute("fillStyle1", lastFillStyle1); } - + if (lastStrokeStyle > -1) { + currentLayer.writeAttribute("strokeStyle", lastStrokeStyle); + } + StringBuilder edgesSb = new StringBuilder(); + convertShapeEdges(startEdgeX, startEdgeY, mat, edges, edgesSb); + currentLayer.writeAttribute("edges", edgesSb.toString()); + currentLayer.writeEndElement(); + hasEdge = true; + startEdgeX = x; startEdgeY = y; } @@ -953,38 +935,22 @@ public class XFLConverter { y = edge.changeY(y); } if (!edges.isEmpty()) { - if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { - - boolean empty = false; - if ((fillStyle0 <= 0) && (fillStyle1 <= 0) && (strokeStyle > 0) && morphshape) { - if (shapeNum == 4) { - if (strokeStyleOrig > 0) { - if (actualLinestyles != null && !((LINESTYLE2) actualLinestyles.lineStyles2[strokeStyleOrig]).hasFillFlag) { - RGBA color = (RGBA) actualLinestyles.lineStyles2[strokeStyleOrig].getColor(); - if (color.alpha == 0 && color.red == 0 && color.green == 0 && color.blue == 0) { - empty = true; - } - } - } - } + if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { + currentLayer.writeStartElement("Edge"); + if (fillStyle0 > -1) { + currentLayer.writeAttribute("fillStyle0", fillStyle0); } - if (!empty) { - currentLayer.writeStartElement("Edge"); - if (fillStyle0 > -1) { - currentLayer.writeAttribute("fillStyle0", fillStyle0); - } - if (fillStyle1 > -1) { - currentLayer.writeAttribute("fillStyle1", fillStyle1); - } - if (strokeStyle > -1) { - currentLayer.writeAttribute("strokeStyle", strokeStyle); - } - StringBuilder edgesSb = new StringBuilder(); - convertShapeEdges(startEdgeX, startEdgeY, mat, edges, edgesSb); - currentLayer.writeAttribute("edges", edgesSb.toString()); - currentLayer.writeEndElement(); - hasEdge = true; + if (fillStyle1 > -1) { + currentLayer.writeAttribute("fillStyle1", fillStyle1); } + if (strokeStyle > -1) { + currentLayer.writeAttribute("strokeStyle", strokeStyle); + } + StringBuilder edgesSb = new StringBuilder(); + convertShapeEdges(startEdgeX, startEdgeY, mat, edges, edgesSb); + currentLayer.writeAttribute("edges", edgesSb.toString()); + currentLayer.writeEndElement(); + hasEdge = true; } } edges.clear(); @@ -993,11 +959,86 @@ public class XFLConverter { currentLayer.writeEndElement(); // DOMShape if (currentLayer.length() > 0 && hasEdge) { //no empty layers - layers.add(currentLayer.toString()); + String s = currentLayer.toString(); + if (morphshape) { + s = removeOnlyStrokeEdgesBeforeSameFilled(s); + } + layers.add(s); } } return layers; } + + /** + * A hack. This will remove a stroked path with no fill which has same stroke as subsequent path. + * This happens in the morphshape edges. This needs to be cleaned up before exporting to FLA. + * + * @param layer + * @return + */ + private static String removeOnlyStrokeEdgesBeforeSameFilled(String layer) { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + + dbf.setNamespaceAware(false); + dbf.setValidating(false); + DocumentBuilder db; + try { + db = dbf.newDocumentBuilder(); + String docString = "" + layer + ""; + Document doc = db.parse(new ByteArrayInputStream(docString.getBytes("UTF-8"))); + NodeList edgesParentList = doc.getElementsByTagName("edges"); + String prevStrokeOnly = null; + String prevEdgesStr = ""; + for (int j = 0; j < edgesParentList.getLength(); j++) { + Node edgesParent = edgesParentList.item(j); + NodeList edges = edgesParent.getChildNodes(); + Node prevNode = null; + for (int i = 0; i < edges.getLength(); i++) { + Node edge = edges.item(i); + if (edge.getNodeType() == Node.TEXT_NODE) { + continue; + } + NamedNodeMap attributes = edge.getAttributes(); + Node strokeStyleNode = attributes.getNamedItem("strokeStyle"); + Node fillStyle0Node = attributes.getNamedItem("fillStyle0"); + Node fillStyle1Node = attributes.getNamedItem("fillStyle1"); + Node edgesNode = attributes.getNamedItem("edges"); + String edgesStr = edgesNode.getNodeValue(); + + String strokeStyle = strokeStyleNode != null ? strokeStyleNode.getNodeValue() : null; + String fillStyle0 = fillStyle0Node != null ? fillStyle0Node.getNodeValue() : null; + String fillStyle1 = fillStyle1Node != null ? fillStyle1Node.getNodeValue() : null; + + if (prevStrokeOnly != null && + strokeStyle != null && + strokeStyle.equals(prevStrokeOnly) && + prevEdgesStr.equals(edgesStr)) { + Node edgeToRemove = prevNode; + edgeToRemove.getParentNode().removeChild(edgeToRemove); + } + + prevStrokeOnly = null; + if (strokeStyle != null && fillStyle0 == null && fillStyle1 == null) { + prevStrokeOnly = strokeStyle; + } + prevNode = edge; + prevEdgesStr = edgesStr; + } + } + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StringWriter writer = new StringWriter(); + transformer.transform(new DOMSource(doc), new StreamResult(writer)); + String output = writer.getBuffer().toString(); + output = output.trim(); + output = output.substring(3, output.length() - 4); + return output; + } catch (TransformerException | SAXException | IOException | ParserConfigurationException ex) { + Logger.getLogger(XFLConverter.class.getName()).log(Level.SEVERE, null, ex); + } + return layer; + } private static int getLayerCount(ReadOnlyTagList tags) { int maxDepth = 0; @@ -2269,8 +2310,8 @@ public class XFLConverter { int characterId = -1; int ratio = -1; boolean shapeTween = false; - boolean lastShapeTween = false; MorphShapeTag shapeTweener = null; + int lastTweenRatio = -1; //Add ShowFrameTag to the end when there is one last missing List timTags = timelineTags.toArrayList(); @@ -2360,7 +2401,8 @@ public class XFLConverter { if (shapeTween && character != null) { MorphShapeTag m = (MorphShapeTag) character; shapeTweener = m; - shapeTween = false; + shapeTween = false; + lastTweenRatio = ratio; } character = null; metadata = null; @@ -2384,7 +2426,18 @@ public class XFLConverter { if (frame + 1 <= endFrame) { lastIn = true; - if ((character instanceof ShapeTag) && (nonLibraryShapes.contains(characterId) || shapeTweener != null)) { + if (shapeTweener != null) { + MorphShapeTag m = shapeTweener; + XFLXmlWriter addLastWriter = new XFLXmlWriter(); + SHAPEWITHSTYLE endShape = m.getShapeAtRatio(lastTweenRatio); + convertShape(characters, matrix, m.getShapeNum() == 1 ? 3 : 4, endShape.shapeRecords, m.getFillStyles().getFillStylesAt(lastTweenRatio), m.getLineStyles().getLineStylesAt(m.getShapeNum(), lastTweenRatio), true, false, addLastWriter); + duration--; + convertFrame(true, null, null, frame - duration, duration, "", lastElements, files, writer2); + duration = 1; + lastElements = addLastWriter.toString(); + shapeTweener = null; + } + if ((character instanceof ShapeTag) && (nonLibraryShapes.contains(characterId))) { // || shapeTweener != null)) { ShapeTag shape = (ShapeTag) character; convertShape(characters, matrix, shape.getShapeNum(), shape.getShapes().shapeRecords, shape.getShapes().fillStyles, shape.getShapes().lineStyles, false, false, elementsWriter); shapeTween = false; @@ -2409,7 +2462,7 @@ public class XFLConverter { frame++; String elements = elementsWriter.toString(); if (!elements.equals(lastElements) && frame > 0) { - convertFrame(lastShapeTween, null, null, frame - duration, duration, "", lastElements, files, writer2); + convertFrame(false, null, null, frame - duration, duration, "", lastElements, files, writer2); duration = 1; } else if (frame == 0) { duration = 1; @@ -2417,19 +2470,16 @@ public class XFLConverter { duration++; } - lastShapeTween = shapeTween; lastElements = elements; if (frame > endFrame) { if (lastIn) { lastElements = ""; - lastShapeTween = false; lastIn = false; } } } else { if (lastIn) { lastElements = ""; - lastShapeTween = false; lastIn = false; } frame++; @@ -2443,7 +2493,7 @@ public class XFLConverter { } if (!lastElements.isEmpty()) { frame++; - convertFrame(lastShapeTween, null, null, (frame - duration < 0 ? 0 : frame - duration), duration, "", lastElements, files, writer2); + convertFrame(false, null, null, (frame - duration < 0 ? 0 : frame - duration), duration, "", lastElements, files, writer2); } afterStr = "" + afterStr;