From b7ec1ac83079f12fa2fa9086622bd232de337378 Mon Sep 17 00:00:00 2001 From: "honfika@gmail.com" Date: Thu, 22 Jun 2017 13:15:43 +0200 Subject: [PATCH] #1401 SVG export: duplicate pattern IDs fixed --- CHANGELOG.md | 1 + .../morphshape/SVGMorphShapeExporter.java | 923 ++++----- .../exporters/shape/SVGShapeExporter.java | 526 ++--- .../decompiler/flash/tags/base/ImageTag.java | 562 ++--- .../flash/tags/base/MorphShapeTag.java | 654 +++--- .../decompiler/flash/tags/base/ShapeTag.java | 426 ++-- .../decompiler/flash/tags/base/TextTag.java | 1560 +++++++------- .../decompiler/flash/gui/DebuggerHandler.java | 1804 ++++++++--------- 8 files changed, 3231 insertions(+), 3225 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04351b801..e34f12ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - [#1348] Cannot properly export frame with cyrillic symbols to SVG - [#1354] Various FLA export problem fixes - [#1367] Raw edit conditional type fix. +- [#1401] SVG export: duplicate pattern IDs fixed ## [10.0.0] - 2016-12-24 ### Added diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java index 13d886e37..72680fc31 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java @@ -1,460 +1,463 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.exporters.morphshape; - -import com.jpexs.decompiler.flash.SWF; -import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; -import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; -import com.jpexs.decompiler.flash.tags.base.ImageTag; -import com.jpexs.decompiler.flash.tags.enums.ImageFormat; -import com.jpexs.decompiler.flash.types.ColorTransform; -import com.jpexs.decompiler.flash.types.FILLSTYLE; -import com.jpexs.decompiler.flash.types.GRADIENT; -import com.jpexs.decompiler.flash.types.GRADRECORD; -import com.jpexs.decompiler.flash.types.LINESTYLE2; -import com.jpexs.decompiler.flash.types.RGB; -import com.jpexs.decompiler.flash.types.RGBA; -import com.jpexs.decompiler.flash.types.SHAPE; -import com.jpexs.helpers.Helper; -import com.jpexs.helpers.SerializableImage; -import java.awt.Color; -import org.w3c.dom.Element; - -/** - * - * @author JPEXS, Claus Wahlers - */ -public class SVGMorphShapeExporter extends DefaultSVGMorphShapeExporter { - - protected Element path; - - protected int lastPatternId; - - private final Color defaultColor; - - private final SWF swf; - - private final SVGExporter exporter; - - public SVGMorphShapeExporter(SWF swf, SHAPE shape, SHAPE endShape, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom) { - super(shape, endShape, colorTransform, zoom); - this.swf = swf; - this.defaultColor = defaultColor; - this.exporter = exporter; - } - - @Override - public void beginFill(RGB color, RGB colorEnd) { - if (color == null) { - color = new RGB(defaultColor == null ? Color.black : defaultColor); - } - if (colorEnd == null) { - colorEnd = new RGB(defaultColor == null ? Color.black : defaultColor); - } - finalizePath(); - path.setAttribute("stroke", "none"); - path.setAttribute("fill", color.toHexRGB()); - path.setAttribute("fill-rule", "evenodd"); - path.appendChild(createAnimateElement("fill", color.toHexRGB(), colorEnd.toHexRGB())); - if (color instanceof RGBA) { - RGBA colorA = (RGBA) color; - if (colorA.alpha != 255) { - path.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); - } - RGBA colorAEnd = (RGBA) colorEnd; - path.appendChild(createAnimateElement("fill-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat())); - } - } - - @Override - public void beginGradientFill(int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio, float focalPointRatioEnd) { - finalizePath(); - Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) - ? exporter.createElement("linearGradient") - : exporter.createElement("radialGradient"); - populateGradientElement(gradient, type, gradientRecords, gradientRecordsEnd, matrix, matrixEnd, spreadMethod, interpolationMethod, focalPointRatio); - int id = exporter.gradients.indexOf(gradient); - if (id < 0) { - // todo: filter same gradients - id = exporter.gradients.size(); - exporter.gradients.add(gradient); - } - gradient.setAttribute("id", "gradient" + id); - path.setAttribute("stroke", "none"); - path.setAttribute("fill", "url(#gradient" + id + ")"); - path.setAttribute("fill-rule", "evenodd"); - exporter.addToDefs(gradient); - } - - @Override - public void beginBitmapFill(int bitmapId, Matrix matrix, Matrix matrixEnd, boolean repeat, boolean smooth, ColorTransform colorTransform) { - finalizePath(); - ImageTag image = swf.getImage(bitmapId); - if (image != null) { - SerializableImage img = image.getImageCached(); - if (img != null) { - if (colorTransform != null) { - colorTransform.apply(img); - } - - int width = img.getWidth(); - int height = img.getHeight(); - lastPatternId++; - String patternId = "PatternID_" + lastPatternId; - ImageFormat format = image.getImageFormat(); - byte[] imageData = Helper.readStream(image.getImageData()); - String base64ImgData = Helper.byteArrayToBase64String(imageData); - path.setAttribute("style", "fill:url(#" + patternId + ")"); - Element pattern = exporter.createElement("pattern"); - pattern.setAttribute("id", patternId); - pattern.setAttribute("patternUnits", "userSpaceOnUse"); - pattern.setAttribute("overflow", "visible"); - pattern.setAttribute("width", "" + width); - pattern.setAttribute("height", "" + height); - pattern.setAttribute("viewBox", "0 0 " + width + " " + height); - if (matrix != null) { - matrix = matrix.clone(); - matrix.rotateSkew0 *= zoom / SWF.unitDivisor; - matrix.rotateSkew1 *= zoom / SWF.unitDivisor; - matrix.scaleX *= zoom / SWF.unitDivisor; - matrix.scaleY *= zoom / SWF.unitDivisor; - matrixEnd = matrixEnd.clone(); - matrixEnd.rotateSkew0 *= zoom / SWF.unitDivisor; - matrixEnd.rotateSkew1 *= zoom / SWF.unitDivisor; - matrixEnd.scaleX *= zoom / SWF.unitDivisor; - matrixEnd.scaleY *= zoom / SWF.unitDivisor; - addMatrixAnimation(pattern, "patternTransform", matrix, matrixEnd); - } - Element imageElement = exporter.createElement("image"); - imageElement.setAttribute("width", "" + width); - imageElement.setAttribute("height", "" + height); - imageElement.setAttribute("xlink:href", "data:image/" + format + ";base64," + base64ImgData); - pattern.appendChild(imageElement); - exporter.addToGroup(pattern); - } - } - } - - @Override - public void lineStyle(double thickness, double thicknessEnd, RGB color, RGB colorEnd, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit) { - finalizePath(); - thickness *= zoom / SWF.unitDivisor; - thicknessEnd *= zoom / SWF.unitDivisor; - path.setAttribute("fill", "none"); - if (color != null) { - path.setAttribute("stroke", color.toHexRGB()); - path.appendChild(createAnimateElement("stroke", color.toHexRGB(), colorEnd.toHexRGB())); - } - path.setAttribute("stroke-width", Double.toString(thickness == 0 ? 1 : thickness)); - path.appendChild(createAnimateElement("stroke-width", thickness, thicknessEnd)); - - if (color instanceof RGBA) { - RGBA colorA = (RGBA) color; - if (colorA.alpha != 255) { - path.setAttribute("stroke-opacity", Float.toString(colorA.getAlphaFloat())); - } - RGBA colorAEnd = (RGBA) colorEnd; - path.appendChild(createAnimateElement("fill-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat())); - } - switch (startCaps) { - case LINESTYLE2.NO_CAP: - path.setAttribute("stroke-linecap", "butt"); - break; - case LINESTYLE2.SQUARE_CAP: - path.setAttribute("stroke-linecap", "square"); - break; - default: - path.setAttribute("stroke-linecap", "round"); - break; - } - switch (joints) { - case LINESTYLE2.BEVEL_JOIN: - path.setAttribute("stroke-linejoin", "bevel"); - break; - case LINESTYLE2.ROUND_JOIN: - path.setAttribute("stroke-linejoin", "round"); - break; - default: - path.setAttribute("stroke-linejoin", "miter"); - if (miterLimit >= 1 && miterLimit != 4f) { - path.setAttribute("stroke-miterlimit", Double.toString(miterLimit)); - } - break; - } - } - - @Override - public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio, float focalPointRatioEnd) { - path.removeAttribute("stroke-opacity"); - Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) - ? exporter.createElement("linearGradient") - : exporter.createElement("radialGradient"); - populateGradientElement(gradient, type, gradientRecords, gradientRecordsEnd, matrix, matrixEnd, spreadMethod, interpolationMethod, focalPointRatio); - int id = exporter.gradients.indexOf(gradient); - if (id < 0) { - // todo: filter same gradients - id = exporter.gradients.size(); - exporter.gradients.add(gradient); - } - gradient.setAttribute("id", "gradient" + id); - path.setAttribute("stroke", "url(#gradient" + id + ")"); - path.setAttribute("fill", "none"); - exporter.addToDefs(gradient); - } - - private Element createAnimateElement(String attributeName, Object startValue, Object endValue) { - Element animate = exporter.createElement("animate"); - animate.setAttribute("dur", "2s"); // todo - animate.setAttribute("repeatCount", "indefinite"); - animate.setAttribute("attributeName", attributeName); - animate.setAttribute("values", startValue + ";" + endValue); - return animate; - } - - @Override - protected void finalizePath() { - if (path != null && pathData != null && pathData.length() > 0) { - path.setAttribute("d", pathData.toString().trim()); - path.appendChild(createAnimateElement("d", pathData.toString().trim(), pathDataEnd.toString().trim())); - exporter.addToGroup(path); - } - path = exporter.createElement("path"); - super.finalizePath(); - } - - private void addMatrixAnimation(Element element, String attribute, Matrix matrix, Matrix matrixEnd) { - final int animationLength = 2; // todo - final String animationLengthStr = animationLength + "s"; - - element.setAttribute(attribute, matrix.getSvgTransformationString(SWF.unitDivisor / zoom, 1)); - - // QR decomposition - double translateX = roundPixels400(matrix.translateX * zoom / SWF.unitDivisor); - double translateY = roundPixels400(matrix.translateY * zoom / SWF.unitDivisor); - double a = matrix.scaleX; - double b = matrix.rotateSkew0; - double c = matrix.rotateSkew1; - double d = matrix.scaleY; - double r = Math.sqrt(a * a + b * b); - double det = a * d - b * c; - double rotate = roundPixels400(Math.signum(b) * Math.acos(a / r) * 180 / Math.PI); - double scaleX = roundPixels400(r); - double scaleY = roundPixels400(det / r); - double skewX = roundPixels400(Math.atan((a * c + b * d) / (r * r)) * 180 / Math.PI); - - double translateXEnd = roundPixels400(matrixEnd.translateX * zoom / SWF.unitDivisor); - double translateYEnd = roundPixels400(matrixEnd.translateY * zoom / SWF.unitDivisor); - a = matrixEnd.scaleX; - b = matrixEnd.rotateSkew0; - c = matrixEnd.rotateSkew1; - d = matrixEnd.scaleY; - double rEnd = Math.sqrt(a * a + b * b); - double detEnd = a * d - b * c; - double rotateEnd = roundPixels400(Math.signum(b) * Math.acos(a / rEnd) * 180 / Math.PI); - double scaleXEnd = roundPixels400(rEnd); - double scaleYEnd = roundPixels400(detEnd / rEnd); - double skewXEnd = roundPixels400(Math.atan((a * c + b * d) / (rEnd * rEnd)) * 180 / Math.PI); - - Element animateClear = exporter.createElement("animateTransform"); - animateClear.setAttribute("dur", animationLengthStr); - animateClear.setAttribute("repeatCount", "indefinite"); - animateClear.setAttribute("attributeName", attribute); - animateClear.setAttribute("type", "scale"); - animateClear.setAttribute("additive", "replace"); - animateClear.setAttribute("from", "1"); - animateClear.setAttribute("to", "1"); - - Element animateTranslate = exporter.createElement("animateTransform"); - animateTranslate.setAttribute("dur", animationLengthStr); - animateTranslate.setAttribute("repeatCount", "indefinite"); - animateTranslate.setAttribute("attributeName", attribute); - animateTranslate.setAttribute("type", "translate"); - animateTranslate.setAttribute("additive", "sum"); - animateTranslate.setAttribute("from", translateX + " " + translateY); - animateTranslate.setAttribute("to", translateXEnd + " " + translateYEnd); - - Element animateRotate = exporter.createElement("animateTransform"); - animateRotate.setAttribute("dur", animationLengthStr); - animateRotate.setAttribute("repeatCount", "indefinite"); - animateRotate.setAttribute("attributeName", attribute); - animateRotate.setAttribute("type", "rotate"); - animateRotate.setAttribute("additive", "sum"); - animateRotate.setAttribute("from", Double.toString(rotate)); - animateRotate.setAttribute("to", Double.toString(rotateEnd)); - - Element animateScale = exporter.createElement("animateTransform"); - animateScale.setAttribute("dur", animationLengthStr); - animateScale.setAttribute("repeatCount", "indefinite"); - animateScale.setAttribute("attributeName", attribute); - animateScale.setAttribute("type", "scale"); - animateScale.setAttribute("additive", "sum"); - animateScale.setAttribute("from", scaleX + " " + scaleY); - animateScale.setAttribute("to", scaleXEnd + " " + scaleYEnd); - - Element animateSkewX = exporter.createElement("animateTransform"); - animateSkewX.setAttribute("dur", animationLengthStr); - animateSkewX.setAttribute("repeatCount", "indefinite"); - animateSkewX.setAttribute("attributeName", attribute); - animateSkewX.setAttribute("type", "skewX"); - animateSkewX.setAttribute("additive", "sum"); - animateSkewX.setAttribute("from", Double.toString(skewX)); - animateSkewX.setAttribute("to", Double.toString(skewXEnd)); - - element.appendChild(animateClear); - element.appendChild(animateTranslate); - element.appendChild(animateRotate); - element.appendChild(animateScale); - element.appendChild(animateSkewX); - - /* - // LDU decomposition - - double translateX = roundPixels400(matrix.translateX * zoom / SWF.unitDivisor); - double translateY = roundPixels400(matrix.translateY * zoom / SWF.unitDivisor); - double a = matrix.scaleX; - double b = matrix.rotateSkew0; - double c = matrix.rotateSkew1; - double d = matrix.scaleY; - double det = a * d - b * c; - double skewY = roundPixels400(Math.atan2(b, a) * 180 / Math.PI); - double scaleX = roundPixels400(a); - double scaleY = roundPixels400(det / a); - double skewX = roundPixels400(Math.atan2(c, a) * 180 / Math.PI); - - double translateXEnd = roundPixels400(matrixEnd.translateX * zoom / SWF.unitDivisor); - double translateYEnd = roundPixels400(matrixEnd.translateY * zoom / SWF.unitDivisor); - a = matrixEnd.scaleX; - b = matrixEnd.rotateSkew0; - c = matrixEnd.rotateSkew1; - d = matrixEnd.scaleY; - double detEnd = a * d - b * c; - double skewYEnd = roundPixels400(Math.atan2(b, a) * 180 / Math.PI); - double scaleXEnd = roundPixels400(a); - double scaleYEnd = roundPixels400(detEnd / a); - double skewXEnd = roundPixels400(Math.atan2(c, a) * 180 / Math.PI); - - Element animateClear = exporter.createElement("animateTransform"); - animateClear.setAttribute("dur", animationLengthStr); - animateClear.setAttribute("repeatCount", "indefinite"); - animateClear.setAttribute("attributeName", attribute); - animateClear.setAttribute("type", "scale"); - animateClear.setAttribute("additive", "replace"); - animateClear.setAttribute("from", "1"); - animateClear.setAttribute("to", "1"); - - Element animateTranslate = exporter.createElement("animateTransform"); - animateTranslate.setAttribute("dur", animationLengthStr); - animateTranslate.setAttribute("repeatCount", "indefinite"); - animateTranslate.setAttribute("attributeName", attribute); - animateTranslate.setAttribute("type", "translate"); - animateTranslate.setAttribute("additive", "sum"); - animateTranslate.setAttribute("from", translateX + " " + translateY); - animateTranslate.setAttribute("to", translateXEnd + " " + translateYEnd); - - Element animateSkewY = exporter.createElement("animateTransform"); - animateSkewY.setAttribute("dur", animationLengthStr); - animateSkewY.setAttribute("repeatCount", "indefinite"); - animateSkewY.setAttribute("attributeName", attribute); - animateSkewY.setAttribute("type", "skewY"); - animateSkewY.setAttribute("additive", "sum"); - animateSkewY.setAttribute("from", Double.toString(skewY)); - animateSkewY.setAttribute("to", Double.toString(skewYEnd)); - - Element animateScale = exporter.createElement("animateTransform"); - animateScale.setAttribute("dur", animationLengthStr); - animateScale.setAttribute("repeatCount", "indefinite"); - animateScale.setAttribute("attributeName", attribute); - animateScale.setAttribute("type", "scale"); - animateScale.setAttribute("additive", "sum"); - animateScale.setAttribute("from", scaleX + " " + scaleY); - animateScale.setAttribute("to", scaleXEnd + " " + scaleYEnd); - - Element animateSkewX = exporter.createElement("animateTransform"); - animateSkewX.setAttribute("dur", animationLengthStr); - animateSkewX.setAttribute("repeatCount", "indefinite"); - animateSkewX.setAttribute("attributeName", attribute); - animateSkewX.setAttribute("type", "skewX"); - animateSkewX.setAttribute("additive", "sum"); - animateSkewX.setAttribute("from", Double.toString(skewX)); - animateSkewX.setAttribute("to", Double.toString(skewXEnd)); - - element.appendChild(animateClear); - element.appendChild(animateTranslate); - element.appendChild(animateSkewY); - element.appendChild(animateScale); - element.appendChild(animateSkewX);*/ - } - - protected void populateGradientElement(Element gradient, int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio) { - gradient.setAttribute("gradientUnits", "userSpaceOnUse"); - if (type == FILLSTYLE.LINEAR_GRADIENT) { - gradient.setAttribute("x1", "-819.2"); - gradient.setAttribute("x2", "819.2"); - } else { - gradient.setAttribute("r", "819.2"); - gradient.setAttribute("cx", "0"); - gradient.setAttribute("cy", "0"); - if (focalPointRatio != 0) { - gradient.setAttribute("fx", Double.toString(819.2 * focalPointRatio)); - gradient.setAttribute("fy", "0"); - } - } - switch (spreadMethod) { - case GRADIENT.SPREAD_PAD_MODE: - gradient.setAttribute("spreadMethod", "pad"); - break; - case GRADIENT.SPREAD_REFLECT_MODE: - gradient.setAttribute("spreadMethod", "reflect"); - break; - case GRADIENT.SPREAD_REPEAT_MODE: - gradient.setAttribute("spreadMethod", "repeat"); - break; - } - if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) { - gradient.setAttribute("color-interpolation", "linearRGB"); - } - if (matrix != null) { - addMatrixAnimation(gradient, "gradientTransform", matrix, matrixEnd); - } - for (int i = 0; i < gradientRecords.length; i++) { - GRADRECORD record = gradientRecords[i]; - GRADRECORD recordEnd = gradientRecordsEnd[i]; - Element gradientEntry = exporter.createElement("stop"); - gradientEntry.setAttribute("offset", Double.toString(record.ratio / 255.0)); - gradientEntry.appendChild(createAnimateElement("offset", record.ratio / 255.0, recordEnd.ratio / 255.0)); - RGB color = record.color; - RGB colorEnd = recordEnd.color; - //if(colors.get(i) != 0) { - gradientEntry.setAttribute("stop-color", color.toHexRGB()); - gradientEntry.appendChild(createAnimateElement("stop-color", color.toHexRGB(), colorEnd.toHexRGB())); - //} - if (color instanceof RGBA) { - RGBA colorA = (RGBA) color; - if (colorA.alpha != 255) { - gradientEntry.setAttribute("stop-opacity", Float.toString(colorA.getAlphaFloat())); - } - RGBA colorAEnd = (RGBA) colorEnd; - gradientEntry.appendChild(createAnimateElement("stop-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat())); - } - gradient.appendChild(gradientEntry); - } - } - - protected double roundPixels400(double pixels) { - return Math.round(pixels * 10000) / 10000.0; - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.exporters.morphshape; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; +import com.jpexs.decompiler.flash.tags.base.ImageTag; +import com.jpexs.decompiler.flash.tags.enums.ImageFormat; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.FILLSTYLE; +import com.jpexs.decompiler.flash.types.GRADIENT; +import com.jpexs.decompiler.flash.types.GRADRECORD; +import com.jpexs.decompiler.flash.types.LINESTYLE2; +import com.jpexs.decompiler.flash.types.RGB; +import com.jpexs.decompiler.flash.types.RGBA; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.helpers.Helper; +import com.jpexs.helpers.SerializableImage; +import java.awt.Color; +import org.w3c.dom.Element; + +/** + * + * @author JPEXS, Claus Wahlers + */ +public class SVGMorphShapeExporter extends DefaultSVGMorphShapeExporter { + + protected Element path; + + protected int id; + + protected int lastPatternId; + + private final Color defaultColor; + + private final SWF swf; + + private final SVGExporter exporter; + + public SVGMorphShapeExporter(SWF swf, SHAPE shape, SHAPE endShape, int id, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom) { + super(shape, endShape, colorTransform, zoom); + this.swf = swf; + this.id = id; + this.defaultColor = defaultColor; + this.exporter = exporter; + } + + @Override + public void beginFill(RGB color, RGB colorEnd) { + if (color == null) { + color = new RGB(defaultColor == null ? Color.black : defaultColor); + } + if (colorEnd == null) { + colorEnd = new RGB(defaultColor == null ? Color.black : defaultColor); + } + finalizePath(); + path.setAttribute("stroke", "none"); + path.setAttribute("fill", color.toHexRGB()); + path.setAttribute("fill-rule", "evenodd"); + path.appendChild(createAnimateElement("fill", color.toHexRGB(), colorEnd.toHexRGB())); + if (color instanceof RGBA) { + RGBA colorA = (RGBA) color; + if (colorA.alpha != 255) { + path.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); + } + RGBA colorAEnd = (RGBA) colorEnd; + path.appendChild(createAnimateElement("fill-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat())); + } + } + + @Override + public void beginGradientFill(int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio, float focalPointRatioEnd) { + finalizePath(); + Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) + ? exporter.createElement("linearGradient") + : exporter.createElement("radialGradient"); + populateGradientElement(gradient, type, gradientRecords, gradientRecordsEnd, matrix, matrixEnd, spreadMethod, interpolationMethod, focalPointRatio); + int id = exporter.gradients.indexOf(gradient); + if (id < 0) { + // todo: filter same gradients + id = exporter.gradients.size(); + exporter.gradients.add(gradient); + } + gradient.setAttribute("id", "gradient" + id); + path.setAttribute("stroke", "none"); + path.setAttribute("fill", "url(#gradient" + id + ")"); + path.setAttribute("fill-rule", "evenodd"); + exporter.addToDefs(gradient); + } + + @Override + public void beginBitmapFill(int bitmapId, Matrix matrix, Matrix matrixEnd, boolean repeat, boolean smooth, ColorTransform colorTransform) { + finalizePath(); + ImageTag image = swf.getImage(bitmapId); + if (image != null) { + SerializableImage img = image.getImageCached(); + if (img != null) { + if (colorTransform != null) { + colorTransform.apply(img); + } + + int width = img.getWidth(); + int height = img.getHeight(); + lastPatternId++; + String patternId = "PatternID_" + id + "_" + lastPatternId; + ImageFormat format = image.getImageFormat(); + byte[] imageData = Helper.readStream(image.getImageData()); + String base64ImgData = Helper.byteArrayToBase64String(imageData); + path.setAttribute("style", "fill:url(#" + patternId + ")"); + Element pattern = exporter.createElement("pattern"); + pattern.setAttribute("id", patternId); + pattern.setAttribute("patternUnits", "userSpaceOnUse"); + pattern.setAttribute("overflow", "visible"); + pattern.setAttribute("width", "" + width); + pattern.setAttribute("height", "" + height); + pattern.setAttribute("viewBox", "0 0 " + width + " " + height); + if (matrix != null) { + matrix = matrix.clone(); + matrix.rotateSkew0 *= zoom / SWF.unitDivisor; + matrix.rotateSkew1 *= zoom / SWF.unitDivisor; + matrix.scaleX *= zoom / SWF.unitDivisor; + matrix.scaleY *= zoom / SWF.unitDivisor; + matrixEnd = matrixEnd.clone(); + matrixEnd.rotateSkew0 *= zoom / SWF.unitDivisor; + matrixEnd.rotateSkew1 *= zoom / SWF.unitDivisor; + matrixEnd.scaleX *= zoom / SWF.unitDivisor; + matrixEnd.scaleY *= zoom / SWF.unitDivisor; + addMatrixAnimation(pattern, "patternTransform", matrix, matrixEnd); + } + Element imageElement = exporter.createElement("image"); + imageElement.setAttribute("width", "" + width); + imageElement.setAttribute("height", "" + height); + imageElement.setAttribute("xlink:href", "data:image/" + format + ";base64," + base64ImgData); + pattern.appendChild(imageElement); + exporter.addToGroup(pattern); + } + } + } + + @Override + public void lineStyle(double thickness, double thicknessEnd, RGB color, RGB colorEnd, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit) { + finalizePath(); + thickness *= zoom / SWF.unitDivisor; + thicknessEnd *= zoom / SWF.unitDivisor; + path.setAttribute("fill", "none"); + if (color != null) { + path.setAttribute("stroke", color.toHexRGB()); + path.appendChild(createAnimateElement("stroke", color.toHexRGB(), colorEnd.toHexRGB())); + } + path.setAttribute("stroke-width", Double.toString(thickness == 0 ? 1 : thickness)); + path.appendChild(createAnimateElement("stroke-width", thickness, thicknessEnd)); + + if (color instanceof RGBA) { + RGBA colorA = (RGBA) color; + if (colorA.alpha != 255) { + path.setAttribute("stroke-opacity", Float.toString(colorA.getAlphaFloat())); + } + RGBA colorAEnd = (RGBA) colorEnd; + path.appendChild(createAnimateElement("fill-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat())); + } + switch (startCaps) { + case LINESTYLE2.NO_CAP: + path.setAttribute("stroke-linecap", "butt"); + break; + case LINESTYLE2.SQUARE_CAP: + path.setAttribute("stroke-linecap", "square"); + break; + default: + path.setAttribute("stroke-linecap", "round"); + break; + } + switch (joints) { + case LINESTYLE2.BEVEL_JOIN: + path.setAttribute("stroke-linejoin", "bevel"); + break; + case LINESTYLE2.ROUND_JOIN: + path.setAttribute("stroke-linejoin", "round"); + break; + default: + path.setAttribute("stroke-linejoin", "miter"); + if (miterLimit >= 1 && miterLimit != 4f) { + path.setAttribute("stroke-miterlimit", Double.toString(miterLimit)); + } + break; + } + } + + @Override + public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio, float focalPointRatioEnd) { + path.removeAttribute("stroke-opacity"); + Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) + ? exporter.createElement("linearGradient") + : exporter.createElement("radialGradient"); + populateGradientElement(gradient, type, gradientRecords, gradientRecordsEnd, matrix, matrixEnd, spreadMethod, interpolationMethod, focalPointRatio); + int id = exporter.gradients.indexOf(gradient); + if (id < 0) { + // todo: filter same gradients + id = exporter.gradients.size(); + exporter.gradients.add(gradient); + } + gradient.setAttribute("id", "gradient" + id); + path.setAttribute("stroke", "url(#gradient" + id + ")"); + path.setAttribute("fill", "none"); + exporter.addToDefs(gradient); + } + + private Element createAnimateElement(String attributeName, Object startValue, Object endValue) { + Element animate = exporter.createElement("animate"); + animate.setAttribute("dur", "2s"); // todo + animate.setAttribute("repeatCount", "indefinite"); + animate.setAttribute("attributeName", attributeName); + animate.setAttribute("values", startValue + ";" + endValue); + return animate; + } + + @Override + protected void finalizePath() { + if (path != null && pathData != null && pathData.length() > 0) { + path.setAttribute("d", pathData.toString().trim()); + path.appendChild(createAnimateElement("d", pathData.toString().trim(), pathDataEnd.toString().trim())); + exporter.addToGroup(path); + } + path = exporter.createElement("path"); + super.finalizePath(); + } + + private void addMatrixAnimation(Element element, String attribute, Matrix matrix, Matrix matrixEnd) { + final int animationLength = 2; // todo + final String animationLengthStr = animationLength + "s"; + + element.setAttribute(attribute, matrix.getSvgTransformationString(SWF.unitDivisor / zoom, 1)); + + // QR decomposition + double translateX = roundPixels400(matrix.translateX * zoom / SWF.unitDivisor); + double translateY = roundPixels400(matrix.translateY * zoom / SWF.unitDivisor); + double a = matrix.scaleX; + double b = matrix.rotateSkew0; + double c = matrix.rotateSkew1; + double d = matrix.scaleY; + double r = Math.sqrt(a * a + b * b); + double det = a * d - b * c; + double rotate = roundPixels400(Math.signum(b) * Math.acos(a / r) * 180 / Math.PI); + double scaleX = roundPixels400(r); + double scaleY = roundPixels400(det / r); + double skewX = roundPixels400(Math.atan((a * c + b * d) / (r * r)) * 180 / Math.PI); + + double translateXEnd = roundPixels400(matrixEnd.translateX * zoom / SWF.unitDivisor); + double translateYEnd = roundPixels400(matrixEnd.translateY * zoom / SWF.unitDivisor); + a = matrixEnd.scaleX; + b = matrixEnd.rotateSkew0; + c = matrixEnd.rotateSkew1; + d = matrixEnd.scaleY; + double rEnd = Math.sqrt(a * a + b * b); + double detEnd = a * d - b * c; + double rotateEnd = roundPixels400(Math.signum(b) * Math.acos(a / rEnd) * 180 / Math.PI); + double scaleXEnd = roundPixels400(rEnd); + double scaleYEnd = roundPixels400(detEnd / rEnd); + double skewXEnd = roundPixels400(Math.atan((a * c + b * d) / (rEnd * rEnd)) * 180 / Math.PI); + + Element animateClear = exporter.createElement("animateTransform"); + animateClear.setAttribute("dur", animationLengthStr); + animateClear.setAttribute("repeatCount", "indefinite"); + animateClear.setAttribute("attributeName", attribute); + animateClear.setAttribute("type", "scale"); + animateClear.setAttribute("additive", "replace"); + animateClear.setAttribute("from", "1"); + animateClear.setAttribute("to", "1"); + + Element animateTranslate = exporter.createElement("animateTransform"); + animateTranslate.setAttribute("dur", animationLengthStr); + animateTranslate.setAttribute("repeatCount", "indefinite"); + animateTranslate.setAttribute("attributeName", attribute); + animateTranslate.setAttribute("type", "translate"); + animateTranslate.setAttribute("additive", "sum"); + animateTranslate.setAttribute("from", translateX + " " + translateY); + animateTranslate.setAttribute("to", translateXEnd + " " + translateYEnd); + + Element animateRotate = exporter.createElement("animateTransform"); + animateRotate.setAttribute("dur", animationLengthStr); + animateRotate.setAttribute("repeatCount", "indefinite"); + animateRotate.setAttribute("attributeName", attribute); + animateRotate.setAttribute("type", "rotate"); + animateRotate.setAttribute("additive", "sum"); + animateRotate.setAttribute("from", Double.toString(rotate)); + animateRotate.setAttribute("to", Double.toString(rotateEnd)); + + Element animateScale = exporter.createElement("animateTransform"); + animateScale.setAttribute("dur", animationLengthStr); + animateScale.setAttribute("repeatCount", "indefinite"); + animateScale.setAttribute("attributeName", attribute); + animateScale.setAttribute("type", "scale"); + animateScale.setAttribute("additive", "sum"); + animateScale.setAttribute("from", scaleX + " " + scaleY); + animateScale.setAttribute("to", scaleXEnd + " " + scaleYEnd); + + Element animateSkewX = exporter.createElement("animateTransform"); + animateSkewX.setAttribute("dur", animationLengthStr); + animateSkewX.setAttribute("repeatCount", "indefinite"); + animateSkewX.setAttribute("attributeName", attribute); + animateSkewX.setAttribute("type", "skewX"); + animateSkewX.setAttribute("additive", "sum"); + animateSkewX.setAttribute("from", Double.toString(skewX)); + animateSkewX.setAttribute("to", Double.toString(skewXEnd)); + + element.appendChild(animateClear); + element.appendChild(animateTranslate); + element.appendChild(animateRotate); + element.appendChild(animateScale); + element.appendChild(animateSkewX); + + /* + // LDU decomposition + + double translateX = roundPixels400(matrix.translateX * zoom / SWF.unitDivisor); + double translateY = roundPixels400(matrix.translateY * zoom / SWF.unitDivisor); + double a = matrix.scaleX; + double b = matrix.rotateSkew0; + double c = matrix.rotateSkew1; + double d = matrix.scaleY; + double det = a * d - b * c; + double skewY = roundPixels400(Math.atan2(b, a) * 180 / Math.PI); + double scaleX = roundPixels400(a); + double scaleY = roundPixels400(det / a); + double skewX = roundPixels400(Math.atan2(c, a) * 180 / Math.PI); + + double translateXEnd = roundPixels400(matrixEnd.translateX * zoom / SWF.unitDivisor); + double translateYEnd = roundPixels400(matrixEnd.translateY * zoom / SWF.unitDivisor); + a = matrixEnd.scaleX; + b = matrixEnd.rotateSkew0; + c = matrixEnd.rotateSkew1; + d = matrixEnd.scaleY; + double detEnd = a * d - b * c; + double skewYEnd = roundPixels400(Math.atan2(b, a) * 180 / Math.PI); + double scaleXEnd = roundPixels400(a); + double scaleYEnd = roundPixels400(detEnd / a); + double skewXEnd = roundPixels400(Math.atan2(c, a) * 180 / Math.PI); + + Element animateClear = exporter.createElement("animateTransform"); + animateClear.setAttribute("dur", animationLengthStr); + animateClear.setAttribute("repeatCount", "indefinite"); + animateClear.setAttribute("attributeName", attribute); + animateClear.setAttribute("type", "scale"); + animateClear.setAttribute("additive", "replace"); + animateClear.setAttribute("from", "1"); + animateClear.setAttribute("to", "1"); + + Element animateTranslate = exporter.createElement("animateTransform"); + animateTranslate.setAttribute("dur", animationLengthStr); + animateTranslate.setAttribute("repeatCount", "indefinite"); + animateTranslate.setAttribute("attributeName", attribute); + animateTranslate.setAttribute("type", "translate"); + animateTranslate.setAttribute("additive", "sum"); + animateTranslate.setAttribute("from", translateX + " " + translateY); + animateTranslate.setAttribute("to", translateXEnd + " " + translateYEnd); + + Element animateSkewY = exporter.createElement("animateTransform"); + animateSkewY.setAttribute("dur", animationLengthStr); + animateSkewY.setAttribute("repeatCount", "indefinite"); + animateSkewY.setAttribute("attributeName", attribute); + animateSkewY.setAttribute("type", "skewY"); + animateSkewY.setAttribute("additive", "sum"); + animateSkewY.setAttribute("from", Double.toString(skewY)); + animateSkewY.setAttribute("to", Double.toString(skewYEnd)); + + Element animateScale = exporter.createElement("animateTransform"); + animateScale.setAttribute("dur", animationLengthStr); + animateScale.setAttribute("repeatCount", "indefinite"); + animateScale.setAttribute("attributeName", attribute); + animateScale.setAttribute("type", "scale"); + animateScale.setAttribute("additive", "sum"); + animateScale.setAttribute("from", scaleX + " " + scaleY); + animateScale.setAttribute("to", scaleXEnd + " " + scaleYEnd); + + Element animateSkewX = exporter.createElement("animateTransform"); + animateSkewX.setAttribute("dur", animationLengthStr); + animateSkewX.setAttribute("repeatCount", "indefinite"); + animateSkewX.setAttribute("attributeName", attribute); + animateSkewX.setAttribute("type", "skewX"); + animateSkewX.setAttribute("additive", "sum"); + animateSkewX.setAttribute("from", Double.toString(skewX)); + animateSkewX.setAttribute("to", Double.toString(skewXEnd)); + + element.appendChild(animateClear); + element.appendChild(animateTranslate); + element.appendChild(animateSkewY); + element.appendChild(animateScale); + element.appendChild(animateSkewX);*/ + } + + protected void populateGradientElement(Element gradient, int type, GRADRECORD[] gradientRecords, GRADRECORD[] gradientRecordsEnd, Matrix matrix, Matrix matrixEnd, int spreadMethod, int interpolationMethod, float focalPointRatio) { + gradient.setAttribute("gradientUnits", "userSpaceOnUse"); + if (type == FILLSTYLE.LINEAR_GRADIENT) { + gradient.setAttribute("x1", "-819.2"); + gradient.setAttribute("x2", "819.2"); + } else { + gradient.setAttribute("r", "819.2"); + gradient.setAttribute("cx", "0"); + gradient.setAttribute("cy", "0"); + if (focalPointRatio != 0) { + gradient.setAttribute("fx", Double.toString(819.2 * focalPointRatio)); + gradient.setAttribute("fy", "0"); + } + } + switch (spreadMethod) { + case GRADIENT.SPREAD_PAD_MODE: + gradient.setAttribute("spreadMethod", "pad"); + break; + case GRADIENT.SPREAD_REFLECT_MODE: + gradient.setAttribute("spreadMethod", "reflect"); + break; + case GRADIENT.SPREAD_REPEAT_MODE: + gradient.setAttribute("spreadMethod", "repeat"); + break; + } + if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) { + gradient.setAttribute("color-interpolation", "linearRGB"); + } + if (matrix != null) { + addMatrixAnimation(gradient, "gradientTransform", matrix, matrixEnd); + } + for (int i = 0; i < gradientRecords.length; i++) { + GRADRECORD record = gradientRecords[i]; + GRADRECORD recordEnd = gradientRecordsEnd[i]; + Element gradientEntry = exporter.createElement("stop"); + gradientEntry.setAttribute("offset", Double.toString(record.ratio / 255.0)); + gradientEntry.appendChild(createAnimateElement("offset", record.ratio / 255.0, recordEnd.ratio / 255.0)); + RGB color = record.color; + RGB colorEnd = recordEnd.color; + //if(colors.get(i) != 0) { + gradientEntry.setAttribute("stop-color", color.toHexRGB()); + gradientEntry.appendChild(createAnimateElement("stop-color", color.toHexRGB(), colorEnd.toHexRGB())); + //} + if (color instanceof RGBA) { + RGBA colorA = (RGBA) color; + if (colorA.alpha != 255) { + gradientEntry.setAttribute("stop-opacity", Float.toString(colorA.getAlphaFloat())); + } + RGBA colorAEnd = (RGBA) colorEnd; + gradientEntry.appendChild(createAnimateElement("stop-opacity", colorA.getAlphaFloat(), colorAEnd.getAlphaFloat())); + } + gradient.appendChild(gradientEntry); + } + } + + protected double roundPixels400(double pixels) { + return Math.round(pixels * 10000) / 10000.0; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/SVGShapeExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/SVGShapeExporter.java index 7d8539dcb..252012bab 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/SVGShapeExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/shape/SVGShapeExporter.java @@ -1,262 +1,264 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.exporters.shape; - -import com.jpexs.decompiler.flash.SWF; -import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; -import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; -import com.jpexs.decompiler.flash.tags.base.ImageTag; -import com.jpexs.decompiler.flash.tags.enums.ImageFormat; -import com.jpexs.decompiler.flash.types.ColorTransform; -import com.jpexs.decompiler.flash.types.FILLSTYLE; -import com.jpexs.decompiler.flash.types.GRADIENT; -import com.jpexs.decompiler.flash.types.GRADRECORD; -import com.jpexs.decompiler.flash.types.LINESTYLE2; -import com.jpexs.decompiler.flash.types.RGB; -import com.jpexs.decompiler.flash.types.RGBA; -import com.jpexs.decompiler.flash.types.SHAPE; -import com.jpexs.helpers.Helper; -import com.jpexs.helpers.SerializableImage; -import java.awt.Color; -import org.w3c.dom.Element; - -/** - * - * @author JPEXS, Claus Wahlers - */ -public class SVGShapeExporter extends DefaultSVGShapeExporter { - - protected Element path; - - protected int lastPatternId; - - private final Color defaultColor; - - private final SWF swf; - - private final SVGExporter exporter; - - public SVGShapeExporter(SWF swf, SHAPE shape, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom) { - super(swf, shape, colorTransform, zoom); - this.swf = swf; - this.defaultColor = defaultColor; - this.exporter = exporter; - } - - @Override - public void beginFill(RGB color) { - if (color == null && defaultColor != null) { - color = new RGB(defaultColor); - } - finalizePath(); - path.setAttribute("stroke", "none"); - if (color != null) { - path.setAttribute("fill", color.toHexRGB()); - } - path.setAttribute("fill-rule", "evenodd"); - if (color instanceof RGBA) { - RGBA colorA = (RGBA) color; - if (colorA.alpha != 255) { - path.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); - } - } - } - - @Override - public void beginGradientFill(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { - finalizePath(); - Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) - ? exporter.createElement("linearGradient") - : exporter.createElement("radialGradient"); - populateGradientElement(gradient, type, gradientRecords, matrix, spreadMethod, interpolationMethod, focalPointRatio); - int id = exporter.gradients.indexOf(gradient); - if (id < 0) { - // todo: filter same gradients - id = exporter.gradients.size(); - exporter.gradients.add(gradient); - } - String gradientId = "gradient" + id; - gradient.setAttribute("id", gradientId); - path.setAttribute("stroke", "none"); - path.setAttribute("fill", "url(#" + gradientId + ")"); - path.setAttribute("fill-rule", "evenodd"); - exporter.addToDefs(gradient); - } - - @Override - public void beginBitmapFill(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { - finalizePath(); - ImageTag image = swf.getImage(bitmapId); - if (image != null) { - SerializableImage img = image.getImageCached(); - if (img != null) { - if (colorTransform != null) { - colorTransform.apply(img); - } - - int width = img.getWidth(); - int height = img.getHeight(); - lastPatternId++; - String patternId = "PatternID_"; - patternId += lastPatternId; - ImageFormat format = image.getImageFormat(); - byte[] imageData = Helper.readStream(image.getImageData()); - String base64ImgData = Helper.byteArrayToBase64String(imageData); - path.setAttribute("style", "fill:url(#" + patternId + ")"); - Element pattern = exporter.createElement("pattern"); - pattern.setAttribute("id", patternId); - pattern.setAttribute("patternUnits", "userSpaceOnUse"); - pattern.setAttribute("overflow", "visible"); - pattern.setAttribute("width", "" + width); - pattern.setAttribute("height", "" + height); - pattern.setAttribute("viewBox", "0 0 " + width + " " + height); - if (matrix != null) { - pattern.setAttribute("patternTransform", matrix.getSvgTransformationString(SWF.unitDivisor / zoom, SWF.unitDivisor / zoom)); - } - Element imageElement = exporter.createElement("image"); - imageElement.setAttribute("width", "" + width); - imageElement.setAttribute("height", "" + height); - imageElement.setAttribute("xlink:href", "data:image/" + format + ";base64," + base64ImgData); - pattern.appendChild(imageElement); - exporter.addToGroup(pattern); - return; - } - } - - path.setAttribute("fill", "#ff0000"); - } - - @Override - public void lineStyle(double thickness, RGB color, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit) { - finalizePath(); - thickness *= zoom / SWF.unitDivisor; - path.setAttribute("fill", "none"); - if (color != null) { - path.setAttribute("stroke", color.toHexRGB()); - } - path.setAttribute("stroke-width", Double.toString(thickness == 0 ? 1 : thickness)); - if (color instanceof RGBA) { - RGBA colorA = (RGBA) color; - if (colorA.alpha != 255) { - path.setAttribute("stroke-opacity", Float.toString(colorA.getAlphaFloat())); - } - } - switch (startCaps) { - case LINESTYLE2.NO_CAP: - path.setAttribute("stroke-linecap", "butt"); - break; - case LINESTYLE2.SQUARE_CAP: - path.setAttribute("stroke-linecap", "square"); - break; - default: - path.setAttribute("stroke-linecap", "round"); - break; - } - switch (joints) { - case LINESTYLE2.BEVEL_JOIN: - path.setAttribute("stroke-linejoin", "bevel"); - break; - case LINESTYLE2.ROUND_JOIN: - path.setAttribute("stroke-linejoin", "round"); - break; - default: - path.setAttribute("stroke-linejoin", "miter"); - if (miterLimit >= 1 && miterLimit != 4f) { - path.setAttribute("stroke-miterlimit", Double.toString(miterLimit)); - } - break; - } - } - - @Override - public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { - path.removeAttribute("stroke-opacity"); - Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) - ? exporter.createElement("linearGradient") - : exporter.createElement("radialGradient"); - populateGradientElement(gradient, type, gradientRecords, matrix, spreadMethod, interpolationMethod, focalPointRatio); - int id = exporter.gradients.indexOf(gradient); - if (id < 0) { - // todo: filter same gradients - id = exporter.gradients.size(); - exporter.gradients.add(gradient); - } - gradient.setAttribute("id", "gradient" + id); - path.setAttribute("stroke", "url(#gradient" + id + ")"); - path.setAttribute("fill", "none"); - exporter.addToDefs(gradient); - } - - @Override - protected void finalizePath() { - if (path != null && pathData != null && pathData.length() > 0) { - path.setAttribute("d", pathData.toString().trim()); - exporter.addToGroup(path); - } - path = exporter.createElement("path"); - super.finalizePath(); - } - - protected void populateGradientElement(Element gradient, int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { - gradient.setAttribute("gradientUnits", "userSpaceOnUse"); - if (type == FILLSTYLE.LINEAR_GRADIENT) { - gradient.setAttribute("x1", "-819.2"); - gradient.setAttribute("x2", "819.2"); - } else { - gradient.setAttribute("r", "819.2"); - gradient.setAttribute("cx", "0"); - gradient.setAttribute("cy", "0"); - if (focalPointRatio != 0) { - gradient.setAttribute("fx", Double.toString(819.2 * focalPointRatio)); - gradient.setAttribute("fy", "0"); - } - } - switch (spreadMethod) { - case GRADIENT.SPREAD_PAD_MODE: - gradient.setAttribute("spreadMethod", "pad"); - break; - case GRADIENT.SPREAD_REFLECT_MODE: - gradient.setAttribute("spreadMethod", "reflect"); - break; - case GRADIENT.SPREAD_REPEAT_MODE: - gradient.setAttribute("spreadMethod", "repeat"); - break; - } - if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) { - gradient.setAttribute("color-interpolation", "linearRGB"); - } - if (matrix != null) { - gradient.setAttribute("gradientTransform", matrix.getSvgTransformationString(SWF.unitDivisor / zoom, 1)); - } - for (int i = 0; i < gradientRecords.length; i++) { - GRADRECORD record = gradientRecords[i]; - Element gradientEntry = exporter.createElement("stop"); - gradientEntry.setAttribute("offset", Double.toString(record.ratio / 255.0)); - RGB color = record.color; - //if(colors.get(i) != 0) { - gradientEntry.setAttribute("stop-color", color.toHexRGB()); - //} - if (color instanceof RGBA) { - RGBA colorA = (RGBA) color; - if (colorA.alpha != 255) { - gradientEntry.setAttribute("stop-opacity", Float.toString(colorA.getAlphaFloat())); - } - } - gradient.appendChild(gradientEntry); - } - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.exporters.shape; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; +import com.jpexs.decompiler.flash.tags.base.ImageTag; +import com.jpexs.decompiler.flash.tags.enums.ImageFormat; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.FILLSTYLE; +import com.jpexs.decompiler.flash.types.GRADIENT; +import com.jpexs.decompiler.flash.types.GRADRECORD; +import com.jpexs.decompiler.flash.types.LINESTYLE2; +import com.jpexs.decompiler.flash.types.RGB; +import com.jpexs.decompiler.flash.types.RGBA; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.helpers.Helper; +import com.jpexs.helpers.SerializableImage; +import java.awt.Color; +import org.w3c.dom.Element; + +/** + * + * @author JPEXS, Claus Wahlers + */ +public class SVGShapeExporter extends DefaultSVGShapeExporter { + + protected Element path; + + protected int id; + + protected int lastPatternId; + + private final Color defaultColor; + + private final SWF swf; + + private final SVGExporter exporter; + + public SVGShapeExporter(SWF swf, SHAPE shape, int id, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom) { + super(swf, shape, colorTransform, zoom); + this.swf = swf; + this.id = id; + this.defaultColor = defaultColor; + this.exporter = exporter; + } + + @Override + public void beginFill(RGB color) { + if (color == null && defaultColor != null) { + color = new RGB(defaultColor); + } + finalizePath(); + path.setAttribute("stroke", "none"); + if (color != null) { + path.setAttribute("fill", color.toHexRGB()); + } + path.setAttribute("fill-rule", "evenodd"); + if (color instanceof RGBA) { + RGBA colorA = (RGBA) color; + if (colorA.alpha != 255) { + path.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); + } + } + } + + @Override + public void beginGradientFill(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + finalizePath(); + Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) + ? exporter.createElement("linearGradient") + : exporter.createElement("radialGradient"); + populateGradientElement(gradient, type, gradientRecords, matrix, spreadMethod, interpolationMethod, focalPointRatio); + int id = exporter.gradients.indexOf(gradient); + if (id < 0) { + // todo: filter same gradients + id = exporter.gradients.size(); + exporter.gradients.add(gradient); + } + String gradientId = "gradient" + id; + gradient.setAttribute("id", gradientId); + path.setAttribute("stroke", "none"); + path.setAttribute("fill", "url(#" + gradientId + ")"); + path.setAttribute("fill-rule", "evenodd"); + exporter.addToDefs(gradient); + } + + @Override + public void beginBitmapFill(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { + finalizePath(); + ImageTag image = swf.getImage(bitmapId); + if (image != null) { + SerializableImage img = image.getImageCached(); + if (img != null) { + if (colorTransform != null) { + colorTransform.apply(img); + } + + int width = img.getWidth(); + int height = img.getHeight(); + lastPatternId++; + String patternId = "PatternID_" + id + "_" + lastPatternId; + ImageFormat format = image.getImageFormat(); + byte[] imageData = Helper.readStream(image.getImageData()); + String base64ImgData = Helper.byteArrayToBase64String(imageData); + path.setAttribute("style", "fill:url(#" + patternId + ")"); + Element pattern = exporter.createElement("pattern"); + pattern.setAttribute("id", patternId); + pattern.setAttribute("patternUnits", "userSpaceOnUse"); + pattern.setAttribute("overflow", "visible"); + pattern.setAttribute("width", "" + width); + pattern.setAttribute("height", "" + height); + pattern.setAttribute("viewBox", "0 0 " + width + " " + height); + if (matrix != null) { + pattern.setAttribute("patternTransform", matrix.getSvgTransformationString(SWF.unitDivisor / zoom, SWF.unitDivisor / zoom)); + } + Element imageElement = exporter.createElement("image"); + imageElement.setAttribute("width", "" + width); + imageElement.setAttribute("height", "" + height); + imageElement.setAttribute("xlink:href", "data:image/" + format + ";base64," + base64ImgData); + pattern.appendChild(imageElement); + exporter.addToGroup(pattern); + return; + } + } + + path.setAttribute("fill", "#ff0000"); + } + + @Override + public void lineStyle(double thickness, RGB color, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit) { + finalizePath(); + thickness *= zoom / SWF.unitDivisor; + path.setAttribute("fill", "none"); + if (color != null) { + path.setAttribute("stroke", color.toHexRGB()); + } + path.setAttribute("stroke-width", Double.toString(thickness == 0 ? 1 : thickness)); + if (color instanceof RGBA) { + RGBA colorA = (RGBA) color; + if (colorA.alpha != 255) { + path.setAttribute("stroke-opacity", Float.toString(colorA.getAlphaFloat())); + } + } + switch (startCaps) { + case LINESTYLE2.NO_CAP: + path.setAttribute("stroke-linecap", "butt"); + break; + case LINESTYLE2.SQUARE_CAP: + path.setAttribute("stroke-linecap", "square"); + break; + default: + path.setAttribute("stroke-linecap", "round"); + break; + } + switch (joints) { + case LINESTYLE2.BEVEL_JOIN: + path.setAttribute("stroke-linejoin", "bevel"); + break; + case LINESTYLE2.ROUND_JOIN: + path.setAttribute("stroke-linejoin", "round"); + break; + default: + path.setAttribute("stroke-linejoin", "miter"); + if (miterLimit >= 1 && miterLimit != 4f) { + path.setAttribute("stroke-miterlimit", Double.toString(miterLimit)); + } + break; + } + } + + @Override + public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + path.removeAttribute("stroke-opacity"); + Element gradient = (type == FILLSTYLE.LINEAR_GRADIENT) + ? exporter.createElement("linearGradient") + : exporter.createElement("radialGradient"); + populateGradientElement(gradient, type, gradientRecords, matrix, spreadMethod, interpolationMethod, focalPointRatio); + int id = exporter.gradients.indexOf(gradient); + if (id < 0) { + // todo: filter same gradients + id = exporter.gradients.size(); + exporter.gradients.add(gradient); + } + gradient.setAttribute("id", "gradient" + id); + path.setAttribute("stroke", "url(#gradient" + id + ")"); + path.setAttribute("fill", "none"); + exporter.addToDefs(gradient); + } + + @Override + protected void finalizePath() { + if (path != null && pathData != null && pathData.length() > 0) { + path.setAttribute("d", pathData.toString().trim()); + exporter.addToGroup(path); + } + path = exporter.createElement("path"); + super.finalizePath(); + } + + protected void populateGradientElement(Element gradient, int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + gradient.setAttribute("gradientUnits", "userSpaceOnUse"); + if (type == FILLSTYLE.LINEAR_GRADIENT) { + gradient.setAttribute("x1", "-819.2"); + gradient.setAttribute("x2", "819.2"); + } else { + gradient.setAttribute("r", "819.2"); + gradient.setAttribute("cx", "0"); + gradient.setAttribute("cy", "0"); + if (focalPointRatio != 0) { + gradient.setAttribute("fx", Double.toString(819.2 * focalPointRatio)); + gradient.setAttribute("fy", "0"); + } + } + switch (spreadMethod) { + case GRADIENT.SPREAD_PAD_MODE: + gradient.setAttribute("spreadMethod", "pad"); + break; + case GRADIENT.SPREAD_REFLECT_MODE: + gradient.setAttribute("spreadMethod", "reflect"); + break; + case GRADIENT.SPREAD_REPEAT_MODE: + gradient.setAttribute("spreadMethod", "repeat"); + break; + } + if (interpolationMethod == GRADIENT.INTERPOLATION_LINEAR_RGB_MODE) { + gradient.setAttribute("color-interpolation", "linearRGB"); + } + if (matrix != null) { + gradient.setAttribute("gradientTransform", matrix.getSvgTransformationString(SWF.unitDivisor / zoom, 1)); + } + for (int i = 0; i < gradientRecords.length; i++) { + GRADRECORD record = gradientRecords[i]; + Element gradientEntry = exporter.createElement("stop"); + gradientEntry.setAttribute("offset", Double.toString(record.ratio / 255.0)); + RGB color = record.color; + //if(colors.get(i) != 0) { + gradientEntry.setAttribute("stop-color", color.toHexRGB()); + //} + if (color instanceof RGBA) { + RGBA colorA = (RGBA) color; + if (colorA.alpha != 255) { + gradientEntry.setAttribute("stop-opacity", Float.toString(colorA.getAlphaFloat())); + } + } + gradient.appendChild(gradientEntry); + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ImageTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ImageTag.java index bf3f8b40c..b5e9386e8 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ImageTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ImageTag.java @@ -1,281 +1,281 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.tags.base; - -import com.jpexs.decompiler.flash.SWF; -import com.jpexs.decompiler.flash.configuration.Configuration; -import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; -import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; -import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; -import com.jpexs.decompiler.flash.exporters.shape.CanvasShapeExporter; -import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; -import com.jpexs.decompiler.flash.helpers.ImageHelper; -import com.jpexs.decompiler.flash.tags.TagInfo; -import com.jpexs.decompiler.flash.tags.enums.ImageFormat; -import com.jpexs.decompiler.flash.types.BasicType; -import com.jpexs.decompiler.flash.types.ColorTransform; -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.LINESTYLEARRAY; -import com.jpexs.decompiler.flash.types.MATRIX; -import com.jpexs.decompiler.flash.types.RECT; -import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; -import com.jpexs.decompiler.flash.types.annotations.SWFType; -import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; -import com.jpexs.helpers.ByteArrayRange; -import com.jpexs.helpers.SerializableImage; -import java.awt.Dimension; -import java.awt.Shape; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Set; - -/** - * - * @author JPEXS - */ -public abstract class ImageTag extends DrawableTag { - - @SWFType(BasicType.UI16) - public int characterID; - - protected SerializableImage cachedImage; - - public ImageTag(SWF swf, int id, String name, ByteArrayRange data) { - super(swf, id, name, data); - } - - public abstract InputStream getOriginalImageData(); - - protected abstract SerializableImage getImage(); - - public abstract Dimension getImageDimension(); - - public abstract void setImage(byte[] data) throws IOException; - - public abstract ImageFormat getImageFormat(); - - public abstract ImageFormat getOriginalImageFormat(); - - public boolean importSupported() { - return true; - } - - public static ImageFormat getImageFormat(byte[] data) { - return getImageFormat(new ByteArrayRange(data)); - } - - public static ImageFormat getImageFormat(ByteArrayRange data) { - if (hasErrorHeader(data)) { - return ImageFormat.JPEG; - } - - if (data.getLength() > 2 && ((data.get(0) & 0xff) == 0xff) && ((data.get(1) & 0xff) == 0xd8)) { - return ImageFormat.JPEG; - } - - if (data.getLength() > 6 && ((data.get(0) & 0xff) == 0x47) && ((data.get(1) & 0xff) == 0x49) && ((data.get(2) & 0xff) == 0x46) && ((data.get(3) & 0xff) == 0x38) && ((data.get(4) & 0xff) == 0x39) && ((data.get(5) & 0xff) == 0x61)) { - return ImageFormat.GIF; - } - - if (data.getLength() > 8 && ((data.get(0) & 0xff) == 0x89) && ((data.get(1) & 0xff) == 0x50) && ((data.get(2) & 0xff) == 0x4e) && ((data.get(3) & 0xff) == 0x47) && ((data.get(4) & 0xff) == 0x0d) && ((data.get(5) & 0xff) == 0x0a) && ((data.get(6) & 0xff) == 0x1a) && ((data.get(7) & 0xff) == 0x0a)) { - return ImageFormat.PNG; - } - - return ImageFormat.UNKNOWN; - } - - public SerializableImage getImageCached() { - if (cachedImage != null) { - return cachedImage; - } - - SerializableImage image = getImage(); - if (Configuration.cacheImages.get()) { - cachedImage = image; - } - - return image; - } - - public InputStream getImageData() { - InputStream is = getOriginalImageData(); - if (is != null) { - return is; - } - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ImageHelper.write(getImage().getBufferedImage(), getImageFormat(), baos); - return new ByteArrayInputStream(baos.toByteArray()); - } - - public static boolean hasErrorHeader(byte[] data) { - return hasErrorHeader(new ByteArrayRange(data)); - } - - public static boolean hasErrorHeader(ByteArrayRange data) { - if (data.getLength() > 4) { - if ((data.get(0) & 0xff) == 0xff && (data.get(1) & 0xff) == 0xd9 - && (data.get(2) & 0xff) == 0xff && (data.get(3) & 0xff) == 0xd8) { - return true; - } - } - return false; - } - - private SHAPEWITHSTYLE getShape() { - RECT rect = getRect(); - return getShape(rect, false); - } - - public SHAPEWITHSTYLE getShape(RECT rect, boolean fill) { - boolean translated = rect.Xmin != 0 || rect.Ymin != 0; - SHAPEWITHSTYLE shape = new SHAPEWITHSTYLE(); - shape.fillStyles = new FILLSTYLEARRAY(); - shape.fillStyles.fillStyles = new FILLSTYLE[1]; - FILLSTYLE fillStyle = new FILLSTYLE(); - fillStyle.fillStyleType = Configuration.shapeImportUseNonSmoothedFill.get() - ? FILLSTYLE.NON_SMOOTHED_REPEATING_BITMAP : FILLSTYLE.REPEATING_BITMAP; - fillStyle.bitmapId = getCharacterId(); - MATRIX matrix = new MATRIX(); - matrix.hasScale = true; - if (fill) { - RECT imageRect = getRect(); - matrix.scaleX = (int) ((((long) SWF.unitDivisor) << 16) * rect.getWidth() / imageRect.getWidth()); - matrix.scaleY = (int) ((((long) SWF.unitDivisor) << 16) * rect.getHeight() / imageRect.getHeight()); - } else { - matrix.scaleX = ((int) SWF.unitDivisor) << 16; - matrix.scaleY = matrix.scaleX; - } - if (translated) { - matrix.translateX = rect.Xmin; - matrix.translateY = rect.Ymin; - } - fillStyle.bitmapMatrix = matrix; - shape.fillStyles.fillStyles[0] = fillStyle; - - shape.lineStyles = new LINESTYLEARRAY(); - shape.lineStyles.lineStyles = new LINESTYLE[0]; - shape.shapeRecords = new ArrayList<>(); - StyleChangeRecord style = new StyleChangeRecord(); - style.stateFillStyle0 = true; - style.fillStyle0 = 1; - style.stateMoveTo = true; - if (translated) { - style.moveDeltaX = rect.Xmin; - style.moveDeltaY = rect.Ymin; - } - shape.shapeRecords.add(style); - StraightEdgeRecord top = new StraightEdgeRecord(); - top.generalLineFlag = true; - top.deltaX = rect.getWidth(); - StraightEdgeRecord right = new StraightEdgeRecord(); - right.generalLineFlag = true; - right.deltaY = rect.getHeight(); - StraightEdgeRecord bottom = new StraightEdgeRecord(); - bottom.generalLineFlag = true; - bottom.deltaX = -rect.getWidth(); - StraightEdgeRecord left = new StraightEdgeRecord(); - left.generalLineFlag = true; - left.deltaY = -rect.getHeight(); - shape.shapeRecords.add(top); - shape.shapeRecords.add(right); - shape.shapeRecords.add(bottom); - shape.shapeRecords.add(left); - shape.shapeRecords.add(new EndShapeRecord()); - return shape; - } - - @Override - public RECT getRect() { - return getRect(null); // parameter not used - } - - @Override - public RECT getRect(Set added) { - Dimension dimension = getImageDimension(); - int widthInTwips = (int) (dimension.getWidth() * SWF.unitDivisor); - int heightInTwips = (int) (dimension.getHeight() * SWF.unitDivisor); - return new RECT(0, widthInTwips, 0, heightInTwips); - } - - @Override - public int getUsedParameters() { - return 0; - } - - @Override - public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { - return transformation.toTransform().createTransformedShape(getShape().getOutline(swf, stroked)); - } - - @Override - public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { - BitmapExporter.export(swf, getShape(), null, image, transformation, strokeTransformation, colorTransform); - } - - @Override - public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) throws IOException { - SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, getShape(), exporter, null, colorTransform, 1); - shapeExporter.export(); - } - - @Override - public void toHtmlCanvas(StringBuilder result, double unitDivisor) { - CanvasShapeExporter cse = new CanvasShapeExporter(null, unitDivisor, swf, getShape(), null, 0, 0); - cse.export(); - result.append(cse.getShapeData()); - } - - @Override - public int getNumFrames() { - return 1; - } - - @Override - public boolean isSingleFrame() { - return true; - } - - public void clearCache() { - cachedImage = null; - } - - @Override - public void getTagInfo(TagInfo tagInfo) { - super.getTagInfo(tagInfo); - Dimension dimension = getImageDimension(); - tagInfo.addInfo("general", "width", dimension.getWidth()); - tagInfo.addInfo("general", "height", dimension.getHeight()); - } - - @Override - public int getCharacterId() { - return characterID; - } - - @Override - public void setCharacterId(int characterId) { - this.characterID = characterId; - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.base; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; +import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; +import com.jpexs.decompiler.flash.exporters.shape.CanvasShapeExporter; +import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; +import com.jpexs.decompiler.flash.helpers.ImageHelper; +import com.jpexs.decompiler.flash.tags.TagInfo; +import com.jpexs.decompiler.flash.tags.enums.ImageFormat; +import com.jpexs.decompiler.flash.types.BasicType; +import com.jpexs.decompiler.flash.types.ColorTransform; +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.LINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.MATRIX; +import com.jpexs.decompiler.flash.types.RECT; +import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; +import com.jpexs.decompiler.flash.types.annotations.SWFType; +import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import com.jpexs.helpers.ByteArrayRange; +import com.jpexs.helpers.SerializableImage; +import java.awt.Dimension; +import java.awt.Shape; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Set; + +/** + * + * @author JPEXS + */ +public abstract class ImageTag extends DrawableTag { + + @SWFType(BasicType.UI16) + public int characterID; + + protected SerializableImage cachedImage; + + public ImageTag(SWF swf, int id, String name, ByteArrayRange data) { + super(swf, id, name, data); + } + + public abstract InputStream getOriginalImageData(); + + protected abstract SerializableImage getImage(); + + public abstract Dimension getImageDimension(); + + public abstract void setImage(byte[] data) throws IOException; + + public abstract ImageFormat getImageFormat(); + + public abstract ImageFormat getOriginalImageFormat(); + + public boolean importSupported() { + return true; + } + + public static ImageFormat getImageFormat(byte[] data) { + return getImageFormat(new ByteArrayRange(data)); + } + + public static ImageFormat getImageFormat(ByteArrayRange data) { + if (hasErrorHeader(data)) { + return ImageFormat.JPEG; + } + + if (data.getLength() > 2 && ((data.get(0) & 0xff) == 0xff) && ((data.get(1) & 0xff) == 0xd8)) { + return ImageFormat.JPEG; + } + + if (data.getLength() > 6 && ((data.get(0) & 0xff) == 0x47) && ((data.get(1) & 0xff) == 0x49) && ((data.get(2) & 0xff) == 0x46) && ((data.get(3) & 0xff) == 0x38) && ((data.get(4) & 0xff) == 0x39) && ((data.get(5) & 0xff) == 0x61)) { + return ImageFormat.GIF; + } + + if (data.getLength() > 8 && ((data.get(0) & 0xff) == 0x89) && ((data.get(1) & 0xff) == 0x50) && ((data.get(2) & 0xff) == 0x4e) && ((data.get(3) & 0xff) == 0x47) && ((data.get(4) & 0xff) == 0x0d) && ((data.get(5) & 0xff) == 0x0a) && ((data.get(6) & 0xff) == 0x1a) && ((data.get(7) & 0xff) == 0x0a)) { + return ImageFormat.PNG; + } + + return ImageFormat.UNKNOWN; + } + + public SerializableImage getImageCached() { + if (cachedImage != null) { + return cachedImage; + } + + SerializableImage image = getImage(); + if (Configuration.cacheImages.get()) { + cachedImage = image; + } + + return image; + } + + public InputStream getImageData() { + InputStream is = getOriginalImageData(); + if (is != null) { + return is; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageHelper.write(getImage().getBufferedImage(), getImageFormat(), baos); + return new ByteArrayInputStream(baos.toByteArray()); + } + + public static boolean hasErrorHeader(byte[] data) { + return hasErrorHeader(new ByteArrayRange(data)); + } + + public static boolean hasErrorHeader(ByteArrayRange data) { + if (data.getLength() > 4) { + if ((data.get(0) & 0xff) == 0xff && (data.get(1) & 0xff) == 0xd9 + && (data.get(2) & 0xff) == 0xff && (data.get(3) & 0xff) == 0xd8) { + return true; + } + } + return false; + } + + private SHAPEWITHSTYLE getShape() { + RECT rect = getRect(); + return getShape(rect, false); + } + + public SHAPEWITHSTYLE getShape(RECT rect, boolean fill) { + boolean translated = rect.Xmin != 0 || rect.Ymin != 0; + SHAPEWITHSTYLE shape = new SHAPEWITHSTYLE(); + shape.fillStyles = new FILLSTYLEARRAY(); + shape.fillStyles.fillStyles = new FILLSTYLE[1]; + FILLSTYLE fillStyle = new FILLSTYLE(); + fillStyle.fillStyleType = Configuration.shapeImportUseNonSmoothedFill.get() + ? FILLSTYLE.NON_SMOOTHED_REPEATING_BITMAP : FILLSTYLE.REPEATING_BITMAP; + fillStyle.bitmapId = getCharacterId(); + MATRIX matrix = new MATRIX(); + matrix.hasScale = true; + if (fill) { + RECT imageRect = getRect(); + matrix.scaleX = (int) ((((long) SWF.unitDivisor) << 16) * rect.getWidth() / imageRect.getWidth()); + matrix.scaleY = (int) ((((long) SWF.unitDivisor) << 16) * rect.getHeight() / imageRect.getHeight()); + } else { + matrix.scaleX = ((int) SWF.unitDivisor) << 16; + matrix.scaleY = matrix.scaleX; + } + if (translated) { + matrix.translateX = rect.Xmin; + matrix.translateY = rect.Ymin; + } + fillStyle.bitmapMatrix = matrix; + shape.fillStyles.fillStyles[0] = fillStyle; + + shape.lineStyles = new LINESTYLEARRAY(); + shape.lineStyles.lineStyles = new LINESTYLE[0]; + shape.shapeRecords = new ArrayList<>(); + StyleChangeRecord style = new StyleChangeRecord(); + style.stateFillStyle0 = true; + style.fillStyle0 = 1; + style.stateMoveTo = true; + if (translated) { + style.moveDeltaX = rect.Xmin; + style.moveDeltaY = rect.Ymin; + } + shape.shapeRecords.add(style); + StraightEdgeRecord top = new StraightEdgeRecord(); + top.generalLineFlag = true; + top.deltaX = rect.getWidth(); + StraightEdgeRecord right = new StraightEdgeRecord(); + right.generalLineFlag = true; + right.deltaY = rect.getHeight(); + StraightEdgeRecord bottom = new StraightEdgeRecord(); + bottom.generalLineFlag = true; + bottom.deltaX = -rect.getWidth(); + StraightEdgeRecord left = new StraightEdgeRecord(); + left.generalLineFlag = true; + left.deltaY = -rect.getHeight(); + shape.shapeRecords.add(top); + shape.shapeRecords.add(right); + shape.shapeRecords.add(bottom); + shape.shapeRecords.add(left); + shape.shapeRecords.add(new EndShapeRecord()); + return shape; + } + + @Override + public RECT getRect() { + return getRect(null); // parameter not used + } + + @Override + public RECT getRect(Set added) { + Dimension dimension = getImageDimension(); + int widthInTwips = (int) (dimension.getWidth() * SWF.unitDivisor); + int heightInTwips = (int) (dimension.getHeight() * SWF.unitDivisor); + return new RECT(0, widthInTwips, 0, heightInTwips); + } + + @Override + public int getUsedParameters() { + return 0; + } + + @Override + public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { + return transformation.toTransform().createTransformedShape(getShape().getOutline(swf, stroked)); + } + + @Override + public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { + BitmapExporter.export(swf, getShape(), null, image, transformation, strokeTransformation, colorTransform); + } + + @Override + public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) throws IOException { + SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, getShape(), getCharacterId(), exporter, null, colorTransform, 1); + shapeExporter.export(); + } + + @Override + public void toHtmlCanvas(StringBuilder result, double unitDivisor) { + CanvasShapeExporter cse = new CanvasShapeExporter(null, unitDivisor, swf, getShape(), null, 0, 0); + cse.export(); + result.append(cse.getShapeData()); + } + + @Override + public int getNumFrames() { + return 1; + } + + @Override + public boolean isSingleFrame() { + return true; + } + + public void clearCache() { + cachedImage = null; + } + + @Override + public void getTagInfo(TagInfo tagInfo) { + super.getTagInfo(tagInfo); + Dimension dimension = getImageDimension(); + tagInfo.addInfo("general", "width", dimension.getWidth()); + tagInfo.addInfo("general", "height", dimension.getHeight()); + } + + @Override + public int getCharacterId() { + return characterID; + } + + @Override + public void setCharacterId(int characterId) { + this.characterID = characterId; + } +} 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 23276fc5b..a1061bed5 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 @@ -1,327 +1,327 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.tags.base; - -import com.jpexs.decompiler.flash.SWF; -import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; -import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; -import com.jpexs.decompiler.flash.exporters.morphshape.CanvasMorphShapeExporter; -import com.jpexs.decompiler.flash.exporters.morphshape.SVGMorphShapeExporter; -import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; -import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; -import com.jpexs.decompiler.flash.types.BasicType; -import com.jpexs.decompiler.flash.types.ColorTransform; -import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; -import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; -import com.jpexs.decompiler.flash.types.MORPHFILLSTYLEARRAY; -import com.jpexs.decompiler.flash.types.MORPHLINESTYLEARRAY; -import com.jpexs.decompiler.flash.types.RECT; -import com.jpexs.decompiler.flash.types.SHAPE; -import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; -import com.jpexs.decompiler.flash.types.annotations.SWFType; -import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; -import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; -import com.jpexs.helpers.ByteArrayRange; -import com.jpexs.helpers.SerializableImage; -import java.awt.Shape; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * - * @author JPEXS - */ -public abstract class MorphShapeTag extends DrawableTag { - - public static final int MAX_RATIO = 65535; - - @SWFType(BasicType.UI16) - public int characterId; - - public RECT startBounds; - - public RECT endBounds; - - public MORPHFILLSTYLEARRAY morphFillStyles; - - public MORPHLINESTYLEARRAY morphLineStyles; - - public SHAPE startEdges; - - public SHAPE endEdges; - - public MorphShapeTag(SWF swf, int id, String name, ByteArrayRange data) { - super(swf, id, name, data); - } - - public abstract int getShapeNum(); - - @Override - public RECT getRect() { - return getRect(new HashSet<>()); - } - - @Override - public void getNeededCharacters(Set needed) { - morphFillStyles.getNeededCharacters(needed); - startEdges.getNeededCharacters(needed); - endEdges.getNeededCharacters(needed); - } - - @Override - public boolean replaceCharacter(int oldCharacterId, int newCharacterId) { - boolean modified = false; - modified |= morphFillStyles.replaceCharacter(oldCharacterId, newCharacterId); - modified |= startEdges.replaceCharacter(oldCharacterId, newCharacterId); - modified |= endEdges.replaceCharacter(oldCharacterId, newCharacterId); - if (modified) { - setModified(true); - } - return modified; - } - - @Override - public boolean removeCharacter(int characterId) { - boolean modified = false; - modified |= morphFillStyles.removeCharacter(characterId); - modified |= startEdges.removeCharacter(characterId); - modified |= endEdges.removeCharacter(characterId); - if (modified) { - setModified(true); - } - return modified; - } - - @Override - public int getCharacterId() { - return characterId; - } - - @Override - public void setCharacterId(int characterId) { - this.characterId = characterId; - } - - @Override - public RECT getRect(Set added) { - RECT rect = new RECT(); - rect.Xmin = Math.min(startBounds.Xmin, endBounds.Xmin); - rect.Ymin = Math.min(startBounds.Ymin, endBounds.Ymin); - rect.Xmax = Math.max(startBounds.Xmax, endBounds.Xmax); - rect.Ymax = Math.max(startBounds.Ymax, endBounds.Ymax); - return rect; - } - - public RECT getStartBounds() { - return startBounds; - } - - public RECT getEndBounds() { - return endBounds; - } - - public MORPHFILLSTYLEARRAY getFillStyles() { - return morphFillStyles; - } - - public MORPHLINESTYLEARRAY getLineStyles() { - return morphLineStyles; - } - - public SHAPE getStartEdges() { - return startEdges; - } - - public SHAPE getEndEdges() { - return endEdges; - } - - public SHAPEWITHSTYLE getShapeAtRatio(int ratio) { - List finalRecords = new ArrayList<>(); - FILLSTYLEARRAY fillStyles = morphFillStyles.getFillStylesAt(ratio); - LINESTYLEARRAY lineStyles = morphLineStyles.getLineStylesAt(getShapeNum(), ratio); - - int startPosX = 0, startPosY = 0; - int endPosX = 0, endPosY = 0; - int posX = 0, posY = 0; - - for (int startIndex = 0, endIndex = 0; - startIndex < startEdges.shapeRecords.size() - && endIndex < endEdges.shapeRecords.size(); startIndex++, endIndex++) { - - SHAPERECORD edge1 = startEdges.shapeRecords.get(startIndex); - SHAPERECORD edge2 = endEdges.shapeRecords.get(endIndex); - if (edge1 instanceof StyleChangeRecord || edge2 instanceof StyleChangeRecord) { - StyleChangeRecord scr1; - if (edge1 instanceof StyleChangeRecord) { - scr1 = (StyleChangeRecord) edge1; - if (scr1.stateMoveTo) { - startPosX = scr1.moveDeltaX; - startPosY = scr1.moveDeltaY; - } - } else { - scr1 = new StyleChangeRecord(); - startIndex--; - } - StyleChangeRecord scr2; - if (edge2 instanceof StyleChangeRecord) { - scr2 = (StyleChangeRecord) edge2; - if (scr2.stateMoveTo) { - endPosX = scr2.moveDeltaX; - endPosY = scr2.moveDeltaY; - } - } else { - scr2 = new StyleChangeRecord(); - endIndex--; - } - 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.stateMoveTo = scr.moveDeltaX != posX || scr.moveDeltaY != posY; - } - finalRecords.add(scr); - continue; - } - - if (edge1 instanceof EndShapeRecord) { - finalRecords.add(edge1); - break; - } - if (edge2 instanceof EndShapeRecord) { - finalRecords.add(edge2); - break; - } - - if (edge1 instanceof CurvedEdgeRecord || edge2 instanceof CurvedEdgeRecord) { - CurvedEdgeRecord cer1 = null; - if (edge1 instanceof CurvedEdgeRecord) { - cer1 = (CurvedEdgeRecord) edge1; - } else if (edge1 instanceof StraightEdgeRecord) { - cer1 = SHAPERECORD.straightToCurve((StraightEdgeRecord) edge1); - } - CurvedEdgeRecord cer2 = null; - if (edge2 instanceof CurvedEdgeRecord) { - cer2 = (CurvedEdgeRecord) edge2; - } else if (edge2 instanceof StraightEdgeRecord) { - cer2 = SHAPERECORD.straightToCurve((StraightEdgeRecord) edge2); - } - if ((cer2 == null) || (cer1 == null)) { - 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; - startPosX += cer1.controlDeltaX + cer1.anchorDeltaX; - startPosY += cer1.controlDeltaY + cer1.anchorDeltaY; - endPosX += cer2.controlDeltaX + cer2.anchorDeltaX; - endPosY += cer2.controlDeltaY + cer2.anchorDeltaY; - posX += cer.controlDeltaX + cer.anchorDeltaX; - posY += cer.controlDeltaY + cer.anchorDeltaY; - finalRecords.add(cer); - } else { - StraightEdgeRecord ser1 = null; - if (edge1 instanceof StraightEdgeRecord) { - ser1 = (StraightEdgeRecord) edge1; - } - StraightEdgeRecord ser2 = null; - if (edge2 instanceof StraightEdgeRecord) { - ser2 = (StraightEdgeRecord) edge2; - } - if ((ser2 == null) || (ser1 == null)) { - continue; - } - 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; - startPosX += ser1.deltaX; - startPosY += ser1.deltaY; - endPosX += ser2.deltaX; - endPosY += ser2.deltaY; - posX += ser.deltaX; - posY += ser.deltaX; - finalRecords.add(ser); - } - } - SHAPEWITHSTYLE shape = new SHAPEWITHSTYLE(); - shape.fillStyles = fillStyles; - shape.lineStyles = lineStyles; - shape.shapeRecords = finalRecords; - return shape; - } - - @Override - public int getUsedParameters() { - return PARAMETER_RATIO; - } - - @Override - public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { - SHAPEWITHSTYLE shape = getShapeAtRatio(ratio); - // morphShape using shapeNum=3, morphShape2 using shapeNum=4 - // todo: Currently the generated image is not cached, because the cache - // key contains the hashCode of the finalRecord object, and it is always - // recreated - BitmapExporter.export(swf, shape, null, image, transformation, strokeTransformation, colorTransform); - } - - @Override - public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) { - if (ratio == -2) { - SHAPEWITHSTYLE beginShapes = getShapeAtRatio(0); - SHAPEWITHSTYLE endShapes = getShapeAtRatio(65535); - SVGMorphShapeExporter shapeExporter = new SVGMorphShapeExporter(swf, beginShapes, endShapes, exporter, null, colorTransform, 1); - shapeExporter.export(); - } else { - SHAPEWITHSTYLE shapes = getShapeAtRatio(ratio); - SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, shapes, exporter, null, colorTransform, 1); - shapeExporter.export(); - } - } - - @Override - public int getNumFrames() { - return 65536; - } - - @Override - public boolean isSingleFrame() { - // Morpshape is a single frame specified with the ratio - return true; - } - - @Override - public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { - return transformation.toTransform().createTransformedShape(getShapeAtRatio(ratio).getOutline(swf, stroked)); - } - - @Override - public void toHtmlCanvas(StringBuilder result, double unitDivisor) { - CanvasMorphShapeExporter cmse = new CanvasMorphShapeExporter(swf, getShapeAtRatio(0), getShapeAtRatio(MAX_RATIO), null, unitDivisor, 0, 0); - cmse.export(); - result.append(cmse.getShapeData()); - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.base; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; +import com.jpexs.decompiler.flash.exporters.morphshape.CanvasMorphShapeExporter; +import com.jpexs.decompiler.flash.exporters.morphshape.SVGMorphShapeExporter; +import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; +import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; +import com.jpexs.decompiler.flash.types.BasicType; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.MORPHFILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.MORPHLINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.RECT; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; +import com.jpexs.decompiler.flash.types.annotations.SWFType; +import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; +import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import com.jpexs.helpers.ByteArrayRange; +import com.jpexs.helpers.SerializableImage; +import java.awt.Shape; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * + * @author JPEXS + */ +public abstract class MorphShapeTag extends DrawableTag { + + public static final int MAX_RATIO = 65535; + + @SWFType(BasicType.UI16) + public int characterId; + + public RECT startBounds; + + public RECT endBounds; + + public MORPHFILLSTYLEARRAY morphFillStyles; + + public MORPHLINESTYLEARRAY morphLineStyles; + + public SHAPE startEdges; + + public SHAPE endEdges; + + public MorphShapeTag(SWF swf, int id, String name, ByteArrayRange data) { + super(swf, id, name, data); + } + + public abstract int getShapeNum(); + + @Override + public RECT getRect() { + return getRect(new HashSet<>()); + } + + @Override + public void getNeededCharacters(Set needed) { + morphFillStyles.getNeededCharacters(needed); + startEdges.getNeededCharacters(needed); + endEdges.getNeededCharacters(needed); + } + + @Override + public boolean replaceCharacter(int oldCharacterId, int newCharacterId) { + boolean modified = false; + modified |= morphFillStyles.replaceCharacter(oldCharacterId, newCharacterId); + modified |= startEdges.replaceCharacter(oldCharacterId, newCharacterId); + modified |= endEdges.replaceCharacter(oldCharacterId, newCharacterId); + if (modified) { + setModified(true); + } + return modified; + } + + @Override + public boolean removeCharacter(int characterId) { + boolean modified = false; + modified |= morphFillStyles.removeCharacter(characterId); + modified |= startEdges.removeCharacter(characterId); + modified |= endEdges.removeCharacter(characterId); + if (modified) { + setModified(true); + } + return modified; + } + + @Override + public int getCharacterId() { + return characterId; + } + + @Override + public void setCharacterId(int characterId) { + this.characterId = characterId; + } + + @Override + public RECT getRect(Set added) { + RECT rect = new RECT(); + rect.Xmin = Math.min(startBounds.Xmin, endBounds.Xmin); + rect.Ymin = Math.min(startBounds.Ymin, endBounds.Ymin); + rect.Xmax = Math.max(startBounds.Xmax, endBounds.Xmax); + rect.Ymax = Math.max(startBounds.Ymax, endBounds.Ymax); + return rect; + } + + public RECT getStartBounds() { + return startBounds; + } + + public RECT getEndBounds() { + return endBounds; + } + + public MORPHFILLSTYLEARRAY getFillStyles() { + return morphFillStyles; + } + + public MORPHLINESTYLEARRAY getLineStyles() { + return morphLineStyles; + } + + public SHAPE getStartEdges() { + return startEdges; + } + + public SHAPE getEndEdges() { + return endEdges; + } + + public SHAPEWITHSTYLE getShapeAtRatio(int ratio) { + List finalRecords = new ArrayList<>(); + FILLSTYLEARRAY fillStyles = morphFillStyles.getFillStylesAt(ratio); + LINESTYLEARRAY lineStyles = morphLineStyles.getLineStylesAt(getShapeNum(), ratio); + + int startPosX = 0, startPosY = 0; + int endPosX = 0, endPosY = 0; + int posX = 0, posY = 0; + + for (int startIndex = 0, endIndex = 0; + startIndex < startEdges.shapeRecords.size() + && endIndex < endEdges.shapeRecords.size(); startIndex++, endIndex++) { + + SHAPERECORD edge1 = startEdges.shapeRecords.get(startIndex); + SHAPERECORD edge2 = endEdges.shapeRecords.get(endIndex); + if (edge1 instanceof StyleChangeRecord || edge2 instanceof StyleChangeRecord) { + StyleChangeRecord scr1; + if (edge1 instanceof StyleChangeRecord) { + scr1 = (StyleChangeRecord) edge1; + if (scr1.stateMoveTo) { + startPosX = scr1.moveDeltaX; + startPosY = scr1.moveDeltaY; + } + } else { + scr1 = new StyleChangeRecord(); + startIndex--; + } + StyleChangeRecord scr2; + if (edge2 instanceof StyleChangeRecord) { + scr2 = (StyleChangeRecord) edge2; + if (scr2.stateMoveTo) { + endPosX = scr2.moveDeltaX; + endPosY = scr2.moveDeltaY; + } + } else { + scr2 = new StyleChangeRecord(); + endIndex--; + } + 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.stateMoveTo = scr.moveDeltaX != posX || scr.moveDeltaY != posY; + } + finalRecords.add(scr); + continue; + } + + if (edge1 instanceof EndShapeRecord) { + finalRecords.add(edge1); + break; + } + if (edge2 instanceof EndShapeRecord) { + finalRecords.add(edge2); + break; + } + + if (edge1 instanceof CurvedEdgeRecord || edge2 instanceof CurvedEdgeRecord) { + CurvedEdgeRecord cer1 = null; + if (edge1 instanceof CurvedEdgeRecord) { + cer1 = (CurvedEdgeRecord) edge1; + } else if (edge1 instanceof StraightEdgeRecord) { + cer1 = SHAPERECORD.straightToCurve((StraightEdgeRecord) edge1); + } + CurvedEdgeRecord cer2 = null; + if (edge2 instanceof CurvedEdgeRecord) { + cer2 = (CurvedEdgeRecord) edge2; + } else if (edge2 instanceof StraightEdgeRecord) { + cer2 = SHAPERECORD.straightToCurve((StraightEdgeRecord) edge2); + } + if ((cer2 == null) || (cer1 == null)) { + 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; + startPosX += cer1.controlDeltaX + cer1.anchorDeltaX; + startPosY += cer1.controlDeltaY + cer1.anchorDeltaY; + endPosX += cer2.controlDeltaX + cer2.anchorDeltaX; + endPosY += cer2.controlDeltaY + cer2.anchorDeltaY; + posX += cer.controlDeltaX + cer.anchorDeltaX; + posY += cer.controlDeltaY + cer.anchorDeltaY; + finalRecords.add(cer); + } else { + StraightEdgeRecord ser1 = null; + if (edge1 instanceof StraightEdgeRecord) { + ser1 = (StraightEdgeRecord) edge1; + } + StraightEdgeRecord ser2 = null; + if (edge2 instanceof StraightEdgeRecord) { + ser2 = (StraightEdgeRecord) edge2; + } + if ((ser2 == null) || (ser1 == null)) { + continue; + } + 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; + startPosX += ser1.deltaX; + startPosY += ser1.deltaY; + endPosX += ser2.deltaX; + endPosY += ser2.deltaY; + posX += ser.deltaX; + posY += ser.deltaX; + finalRecords.add(ser); + } + } + SHAPEWITHSTYLE shape = new SHAPEWITHSTYLE(); + shape.fillStyles = fillStyles; + shape.lineStyles = lineStyles; + shape.shapeRecords = finalRecords; + return shape; + } + + @Override + public int getUsedParameters() { + return PARAMETER_RATIO; + } + + @Override + public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { + SHAPEWITHSTYLE shape = getShapeAtRatio(ratio); + // morphShape using shapeNum=3, morphShape2 using shapeNum=4 + // todo: Currently the generated image is not cached, because the cache + // key contains the hashCode of the finalRecord object, and it is always + // recreated + BitmapExporter.export(swf, shape, null, image, transformation, strokeTransformation, colorTransform); + } + + @Override + public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) { + if (ratio == -2) { + SHAPEWITHSTYLE beginShapes = getShapeAtRatio(0); + SHAPEWITHSTYLE endShapes = getShapeAtRatio(65535); + SVGMorphShapeExporter shapeExporter = new SVGMorphShapeExporter(swf, beginShapes, endShapes, getCharacterId(), exporter, null, colorTransform, 1); + shapeExporter.export(); + } else { + SHAPEWITHSTYLE shapes = getShapeAtRatio(ratio); + SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, shapes, getCharacterId(), exporter, null, colorTransform, 1); + shapeExporter.export(); + } + } + + @Override + public int getNumFrames() { + return 65536; + } + + @Override + public boolean isSingleFrame() { + // Morpshape is a single frame specified with the ratio + return true; + } + + @Override + public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { + return transformation.toTransform().createTransformedShape(getShapeAtRatio(ratio).getOutline(swf, stroked)); + } + + @Override + public void toHtmlCanvas(StringBuilder result, double unitDivisor) { + CanvasMorphShapeExporter cmse = new CanvasMorphShapeExporter(swf, getShapeAtRatio(0), getShapeAtRatio(MAX_RATIO), null, unitDivisor, 0, 0); + cmse.export(); + result.append(cmse.getShapeData()); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ShapeTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ShapeTag.java index 531426565..e8aacc204 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ShapeTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/ShapeTag.java @@ -1,213 +1,213 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.tags.base; - -import com.jpexs.decompiler.flash.SWF; -import com.jpexs.decompiler.flash.SWFInputStream; -import com.jpexs.decompiler.flash.configuration.Configuration; -import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; -import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; -import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; -import com.jpexs.decompiler.flash.exporters.shape.CanvasShapeExporter; -import com.jpexs.decompiler.flash.exporters.shape.PathExporter; -import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; -import com.jpexs.decompiler.flash.helpers.LazyObject; -import com.jpexs.decompiler.flash.types.BasicType; -import com.jpexs.decompiler.flash.types.ColorTransform; -import com.jpexs.decompiler.flash.types.RECT; -import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; -import com.jpexs.decompiler.flash.types.annotations.SWFType; -import com.jpexs.helpers.ByteArrayRange; -import com.jpexs.helpers.SerializableImage; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Shape; -import java.awt.geom.AffineTransform; -import java.awt.geom.GeneralPath; -import java.awt.geom.PathIterator; -import java.io.IOException; -import java.util.List; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * - * @author JPEXS - */ -public abstract class ShapeTag extends DrawableTag implements LazyObject { - - @SWFType(BasicType.UI16) - public int shapeId; - - public RECT shapeBounds; - - public SHAPEWITHSTYLE shapes; - - protected ByteArrayRange shapeData; - - private final int markerSize = 10; - - public ShapeTag(SWF swf, int id, String name, ByteArrayRange data) { - super(swf, id, name, data); - } - - @Override - public void load() { - getShapes(); - } - - public abstract int getShapeNum(); - - public SHAPEWITHSTYLE getShapes() { - if (shapes == null && shapeData != null) { - try { - SWFInputStream sis = new SWFInputStream(swf, shapeData.getArray(), 0, shapeData.getPos() + shapeData.getLength()); - sis.seek(shapeData.getPos()); - shapes = sis.readSHAPEWITHSTYLE(getShapeNum(), false, "shapes"); - shapeData = null; // not needed anymore, give it to GC - } catch (IOException ex) { - Logger.getLogger(ShapeTag.class.getName()).log(Level.SEVERE, null, ex); - } - } - - return shapes; - } - - @Override - public void getNeededCharacters(Set needed) { - SHAPEWITHSTYLE shapes = getShapes(); - if (shapes != null) { - getShapes().getNeededCharacters(needed); - } - } - - @Override - public boolean replaceCharacter(int oldCharacterId, int newCharacterId) { - boolean modified = getShapes().replaceCharacter(oldCharacterId, newCharacterId); - if (modified) { - setModified(true); - } - return modified; - } - - @Override - public boolean removeCharacter(int characterId) { - boolean modified = getShapes().removeCharacter(characterId); - if (modified) { - setModified(true); - } - return modified; - } - - @Override - public RECT getRect(Set added) { - return shapeBounds; - } - - @Override - public RECT getRect() { - return getRect(null); // parameter not used - } - - @Override - public int getUsedParameters() { - return 0; - } - - @Override - public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { - return transformation.toTransform().createTransformedShape(getShapes().getOutline(swf, stroked)); - } - - @Override - public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { - BitmapExporter.export(swf, getShapes(), null, image, transformation, strokeTransformation, colorTransform); - if (Configuration._debugMode.get()) { // show control points - List paths = PathExporter.export(swf, getShapes()); - double[] coords = new double[6]; - AffineTransform at = transformation.toTransform(); - at.preConcatenate(AffineTransform.getScaleInstance(1 / SWF.unitDivisor, 1 / SWF.unitDivisor)); - - // get the graphics from the inner image object, because it creates a new Graphics object - Graphics2D graphics = (Graphics2D) image.getBufferedImage().getGraphics(); - graphics.setPaint(Color.black); - for (GeneralPath path : paths) { - PathIterator iterator = path.getPathIterator(at); - while (!iterator.isDone()) { - int type = iterator.currentSegment(coords); - double x = coords[0]; - double y = coords[1]; - switch (type) { - case PathIterator.SEG_MOVETO: - graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); - break; - case PathIterator.SEG_LINETO: - graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); - break; - case PathIterator.SEG_QUADTO: - graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); - x = coords[2]; - y = coords[3]; - graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); - break; - case PathIterator.SEG_CUBICTO: - System.out.print("CUBICTO NOT SUPPORTED. "); - break; - case PathIterator.SEG_CLOSE: - System.out.print("CLOSE NOT SUPPORTED. "); - break; - } - iterator.next(); - } - } - } - } - - @Override - public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) throws IOException { - SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, getShapes(), exporter, null, colorTransform, 1); - shapeExporter.export(); - } - - @Override - public void toHtmlCanvas(StringBuilder result, double unitDivisor) { - CanvasShapeExporter cse = new CanvasShapeExporter(null, unitDivisor, swf, getShapes(), null, 0, 0); - cse.export(); - result.append(cse.getShapeData()); - } - - @Override - public int getNumFrames() { - return 1; - } - - @Override - public boolean isSingleFrame() { - return true; - } - - @Override - public int getCharacterId() { - return shapeId; - } - - @Override - public void setCharacterId(int characterId) { - this.shapeId = characterId; - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.base; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.SWFInputStream; +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; +import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; +import com.jpexs.decompiler.flash.exporters.shape.CanvasShapeExporter; +import com.jpexs.decompiler.flash.exporters.shape.PathExporter; +import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; +import com.jpexs.decompiler.flash.helpers.LazyObject; +import com.jpexs.decompiler.flash.types.BasicType; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.RECT; +import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; +import com.jpexs.decompiler.flash.types.annotations.SWFType; +import com.jpexs.helpers.ByteArrayRange; +import com.jpexs.helpers.SerializableImage; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public abstract class ShapeTag extends DrawableTag implements LazyObject { + + @SWFType(BasicType.UI16) + public int shapeId; + + public RECT shapeBounds; + + public SHAPEWITHSTYLE shapes; + + protected ByteArrayRange shapeData; + + private final int markerSize = 10; + + public ShapeTag(SWF swf, int id, String name, ByteArrayRange data) { + super(swf, id, name, data); + } + + @Override + public void load() { + getShapes(); + } + + public abstract int getShapeNum(); + + public SHAPEWITHSTYLE getShapes() { + if (shapes == null && shapeData != null) { + try { + SWFInputStream sis = new SWFInputStream(swf, shapeData.getArray(), 0, shapeData.getPos() + shapeData.getLength()); + sis.seek(shapeData.getPos()); + shapes = sis.readSHAPEWITHSTYLE(getShapeNum(), false, "shapes"); + shapeData = null; // not needed anymore, give it to GC + } catch (IOException ex) { + Logger.getLogger(ShapeTag.class.getName()).log(Level.SEVERE, null, ex); + } + } + + return shapes; + } + + @Override + public void getNeededCharacters(Set needed) { + SHAPEWITHSTYLE shapes = getShapes(); + if (shapes != null) { + getShapes().getNeededCharacters(needed); + } + } + + @Override + public boolean replaceCharacter(int oldCharacterId, int newCharacterId) { + boolean modified = getShapes().replaceCharacter(oldCharacterId, newCharacterId); + if (modified) { + setModified(true); + } + return modified; + } + + @Override + public boolean removeCharacter(int characterId) { + boolean modified = getShapes().removeCharacter(characterId); + if (modified) { + setModified(true); + } + return modified; + } + + @Override + public RECT getRect(Set added) { + return shapeBounds; + } + + @Override + public RECT getRect() { + return getRect(null); // parameter not used + } + + @Override + public int getUsedParameters() { + return 0; + } + + @Override + public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { + return transformation.toTransform().createTransformedShape(getShapes().getOutline(swf, stroked)); + } + + @Override + public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, ColorTransform colorTransform) { + BitmapExporter.export(swf, getShapes(), null, image, transformation, strokeTransformation, colorTransform); + if (Configuration._debugMode.get()) { // show control points + List paths = PathExporter.export(swf, getShapes()); + double[] coords = new double[6]; + AffineTransform at = transformation.toTransform(); + at.preConcatenate(AffineTransform.getScaleInstance(1 / SWF.unitDivisor, 1 / SWF.unitDivisor)); + + // get the graphics from the inner image object, because it creates a new Graphics object + Graphics2D graphics = (Graphics2D) image.getBufferedImage().getGraphics(); + graphics.setPaint(Color.black); + for (GeneralPath path : paths) { + PathIterator iterator = path.getPathIterator(at); + while (!iterator.isDone()) { + int type = iterator.currentSegment(coords); + double x = coords[0]; + double y = coords[1]; + switch (type) { + case PathIterator.SEG_MOVETO: + graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); + break; + case PathIterator.SEG_LINETO: + graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); + break; + case PathIterator.SEG_QUADTO: + graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); + x = coords[2]; + y = coords[3]; + graphics.drawRect((int) (x - markerSize / 2), (int) (y - markerSize / 2), markerSize, markerSize); + break; + case PathIterator.SEG_CUBICTO: + System.out.print("CUBICTO NOT SUPPORTED. "); + break; + case PathIterator.SEG_CLOSE: + System.out.print("CLOSE NOT SUPPORTED. "); + break; + } + iterator.next(); + } + } + } + } + + @Override + public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level) throws IOException { + SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, getShapes(), getCharacterId(), exporter, null, colorTransform, 1); + shapeExporter.export(); + } + + @Override + public void toHtmlCanvas(StringBuilder result, double unitDivisor) { + CanvasShapeExporter cse = new CanvasShapeExporter(null, unitDivisor, swf, getShapes(), null, 0, 0); + cse.export(); + result.append(cse.getShapeData()); + } + + @Override + public int getNumFrames() { + return 1; + } + + @Override + public boolean isSingleFrame() { + return true; + } + + @Override + public int getCharacterId() { + return shapeId; + } + + @Override + public void setCharacterId(int characterId) { + this.shapeId = characterId; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java index 8908be962..a5229a614 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/TextTag.java @@ -1,780 +1,780 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.tags.base; - -import com.jpexs.decompiler.flash.SWF; -import com.jpexs.decompiler.flash.configuration.Configuration; -import com.jpexs.decompiler.flash.exporters.FontExporter; -import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle; -import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; -import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; -import com.jpexs.decompiler.flash.exporters.modes.FontExportMode; -import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; -import com.jpexs.decompiler.flash.exporters.shape.CanvasShapeExporter; -import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; -import com.jpexs.decompiler.flash.helpers.HighlightedText; -import com.jpexs.decompiler.flash.importers.TextImportResizeTextBoundsMode; -import com.jpexs.decompiler.flash.tags.text.JustifyAlignGlyphEntry; -import com.jpexs.decompiler.flash.tags.text.TextAlign; -import com.jpexs.decompiler.flash.tags.text.TextParseException; -import com.jpexs.decompiler.flash.types.ColorTransform; -import com.jpexs.decompiler.flash.types.DynamicTextGlyphEntry; -import com.jpexs.decompiler.flash.types.FILLSTYLE; -import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; -import com.jpexs.decompiler.flash.types.GLYPHENTRY; -import com.jpexs.decompiler.flash.types.LINESTYLE; -import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; -import com.jpexs.decompiler.flash.types.MATRIX; -import com.jpexs.decompiler.flash.types.RECT; -import com.jpexs.decompiler.flash.types.RGB; -import com.jpexs.decompiler.flash.types.RGBA; -import com.jpexs.decompiler.flash.types.SHAPE; -import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; -import com.jpexs.decompiler.flash.types.TEXTRECORD; -import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; -import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; -import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; -import com.jpexs.helpers.ByteArrayRange; -import com.jpexs.helpers.Helper; -import com.jpexs.helpers.SerializableImage; -import java.awt.Color; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.Shape; -import java.awt.font.LineMetrics; -import java.awt.font.TextAttribute; -import java.awt.image.BufferedImage; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.w3c.dom.Element; - -/** - * - * @author JPEXS - */ -public abstract class TextTag extends DrawableTag { - - public TextTag(SWF swf, int id, String name, ByteArrayRange data) { - super(swf, id, name, data); - } - - public abstract MATRIX getTextMatrix(); - - public abstract List getTexts(); - - public abstract List getFontIds(); - - public abstract HighlightedText getFormattedText(boolean ignoreLetterSpacing); - - // use the texts from the "texts" argument when it is not null - public abstract boolean setFormattedText(MissingCharacterHandler missingCharHandler, String formattedText, String[] texts) throws TextParseException; - - public abstract void updateTextBounds(); - - public abstract boolean alignText(TextAlign textAlign); - - public abstract boolean translateText(int diff); - - public abstract RECT getBounds(); - - public abstract void setBounds(RECT r); - - public abstract ExportRectangle calculateTextBounds(); - - @Override - public RECT getRect() { - return getRect(null); // parameter not used - } - - private static void updateRect(RECT ret, int x, int y) { - if (x < ret.Xmin) { - ret.Xmin = x; - } - if (x > ret.Xmax) { - ret.Xmax = x; - } - if (y < ret.Ymin) { - ret.Ymin = y; - } - if (y > ret.Ymax) { - ret.Ymax = y; - } - } - - public static void alignText(SWF swf, List textRecords, TextAlign textAlign) { - // Remove Justify align entries - for (TEXTRECORD tr : textRecords) { - for (int i = 0; i < tr.glyphEntries.size(); i++) { - GLYPHENTRY ge = tr.glyphEntries.get(i); - if (ge instanceof JustifyAlignGlyphEntry) { - JustifyAlignGlyphEntry jge = (JustifyAlignGlyphEntry) ge; - ge = new GLYPHENTRY(); - ge.glyphAdvance = jge.originalAdvance; - ge.glyphIndex = jge.glyphIndex; - tr.glyphEntries.set(i, ge); - } - } - } - - int xMin = Integer.MAX_VALUE; - int maxWidth = 0; - for (TEXTRECORD tr : textRecords) { - int xOffset = tr.styleFlagsHasXOffset ? tr.xOffset : 0; - if (xOffset < xMin) { - xMin = xOffset; - } - - int width = tr.getTotalAdvance(); - - if (width > maxWidth) { - maxWidth = width; - } - } - - FontTag font = null; - - for (TEXTRECORD tr : textRecords) { - if (tr.styleFlagsHasFont) { - FontTag font2 = swf.getFont(tr.fontId); - if (font2 != null) { - font = font2; - } - } - - int width = tr.getTotalAdvance(); - switch (textAlign) { - case LEFT: - tr.xOffset = xMin; - tr.styleFlagsHasXOffset = true; - break; - case CENTER: - tr.xOffset = xMin + (maxWidth - width) / 2; - tr.styleFlagsHasXOffset = true; - break; - case RIGHT: - tr.xOffset = xMin + maxWidth - width; - tr.styleFlagsHasXOffset = true; - break; - case JUSTIFY: - tr.xOffset = xMin; - tr.styleFlagsHasXOffset = true; - - if (font != null) { - int diff = maxWidth - width; - if (diff > 0) { - int spaces = 0; - int spaces2 = 0; - int state = 0; - List glyphEntries = new ArrayList<>(); - List glyphEntries2 = new ArrayList<>(); - for (GLYPHENTRY ge : tr.glyphEntries) { - char ch = font.glyphToChar(ge.glyphIndex); - boolean whitespace = Character.isWhitespace(ch); - switch (state) { - case 0: - if (!whitespace) { - state = 1; - } - break; - case 1: - if (whitespace) { - spaces2++; - glyphEntries2.add(ge); - } else { - spaces += spaces2; - spaces2 = 0; - glyphEntries.addAll(glyphEntries2); - glyphEntries2.clear(); - } - break; - } - } - - if (spaces > 0 && glyphEntries.size() > 0) { - int fix = diff / spaces; - int remaining = diff - fix * spaces; - for (GLYPHENTRY ge : glyphEntries) { - int diff2 = fix; - if (remaining-- > 0) { - diff2++; - } - - JustifyAlignGlyphEntry jge = new JustifyAlignGlyphEntry(); - jge.originalAdvance = ge.glyphAdvance; - jge.glyphAdvance = ge.glyphAdvance + diff2; - jge.glyphIndex = ge.glyphIndex; - int idx = tr.glyphEntries.indexOf(ge); - tr.glyphEntries.set(idx, jge); - } - } - } - } - break; - } - } - } - - public static Map getTextRecordsAttributes(List list, SWF swf) { - Map att = new HashMap<>(); - RECT textBounds = new RECT(Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE); - FontTag font = null; - int x = 0; - int y = 0; - int textHeight = 12; - int lineSpacing = 0; - double leading = 0; - double ascent = 0; - double descent = 0; - double lineDistance = 0; - - List glyphs = null; - boolean firstLine = true; - double top = 0; - List allLeftMargins = new ArrayList<>(); - List allLetterSpacings = new ArrayList<>(); - FontMetrics fontMetrics; - BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); - Graphics graphics = bi.getGraphics(); - Font aFont = null; - int currentLeftMargin; - for (int r = 0; r < list.size(); r++) { - TEXTRECORD rec = list.get(r); - if (rec.styleFlagsHasFont) { - FontTag font2 = swf.getFont(rec.fontId); - if (font2 != null) { - font = font2; - } - textHeight = rec.textHeight; - if (font == null) { - Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, "Font with id=" + rec.fontId + " was not found."); - continue; - } - - glyphs = font.getGlyphShapeTable(); - - if (!font.hasLayout()) { - String fontName = FontTag.getFontNameWithFallback(font.getFontNameIntag()); - aFont = new Font(fontName, font.getFontStyle(), (int) (textHeight / SWF.unitDivisor)); - - Map attr = new HashMap<>(); - - attr.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); - attr.put(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON); - aFont = aFont.deriveFont(attr); - - fontMetrics = graphics.getFontMetrics(aFont); - LineMetrics lm = fontMetrics.getLineMetrics("A", graphics); - ascent = lm.getAscent(); - descent = lm.getDescent(); - leading = lm.getLeading(); - lineDistance = ascent + descent; - } else { - leading = ((double) font.getLeading() * textHeight / 1024.0 / font.getDivider() / SWF.unitDivisor); - ascent = ((double) font.getAscent() * textHeight / 1024.0 / font.getDivider() / SWF.unitDivisor); - descent = ((double) font.getDescent() * textHeight / 1024.0 / font.getDivider() / SWF.unitDivisor); - lineDistance = ascent + descent; - } - } - currentLeftMargin = 0; - if (rec.styleFlagsHasXOffset) { - x = rec.xOffset; - currentLeftMargin = x; - } - if (rec.styleFlagsHasYOffset) { - if (!firstLine) { - top += ascent + descent; - int topint = (int) (Math.round(top) * SWF.unitDivisor); - lineSpacing = rec.yOffset - topint; - top += lineSpacing / SWF.unitDivisor; - } else { - top = ascent; - } - y = rec.yOffset; - } - - firstLine = false; - allLeftMargins.add(currentLeftMargin); - - if (glyphs == null) { - Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, "Glyphs not found."); - continue; - } - - int letterSpacing = Integer.MAX_VALUE; - for (int e = 0; e < rec.glyphEntries.size(); e++) { - GLYPHENTRY entry = rec.glyphEntries.get(e); - GLYPHENTRY nextEntry = null; - if (e < rec.glyphEntries.size() - 1) { - nextEntry = rec.glyphEntries.get(e + 1); - } - RECT rect = SHAPERECORD.getBounds(glyphs.get(entry.glyphIndex).shapeRecords); - rect.Xmax = (int) Math.round(((double) rect.Xmax * textHeight) / (font.getDivider() * 1024)); - rect.Xmin = (int) Math.round(((double) rect.Xmin * textHeight) / (font.getDivider() * 1024)); - rect.Ymax = (int) Math.round(((double) rect.Ymax * textHeight) / (font.getDivider() * 1024)); - rect.Ymin = (int) Math.round(((double) rect.Ymin * textHeight) / (font.getDivider() * 1024)); - updateRect(textBounds, x + rect.Xmin, y + rect.Ymin); - updateRect(textBounds, x + rect.Xmax, y + rect.Ymax); - int adv = entry.glyphAdvance; - - int defaultAdvance; - if (font.hasLayout()) { - int kerningAdjustment = 0; - if (nextEntry != null) { - kerningAdjustment = font.getGlyphKerningAdjustment(entry.glyphIndex, nextEntry.glyphIndex); - } - defaultAdvance = (int) (Math.round(textHeight * (font.getGlyphAdvance(entry.glyphIndex) + kerningAdjustment) / (1024.0 * font.getDivider()))); - } else { - defaultAdvance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(aFont, font.glyphToChar(entry.glyphIndex), nextEntry == null ? null : font.glyphToChar(nextEntry.glyphIndex))); - } - int newLetterSpacing = adv - defaultAdvance; - if (e == 0 || e == rec.glyphEntries.size() - 1) { - if (rec.glyphEntries.size() == 1) { - letterSpacing = 0; - } - } else if (newLetterSpacing < letterSpacing) { - letterSpacing = newLetterSpacing; - } - x += adv; - } - allLetterSpacings.add(letterSpacing); - } - att.put("indent", 0); //? - att.put("rightMargin", 0); //? - att.put("lineSpacing", lineSpacing); - att.put("textBounds", textBounds); - att.put("allLeftMargins", allLeftMargins); - att.put("allLetterSpacings", allLetterSpacings); - return att; - } - - public static SHAPE getBorderShape(RGB borderColor, RGB fillColor, RECT rect) { - SHAPEWITHSTYLE shape = new SHAPEWITHSTYLE(); - shape.fillStyles = new FILLSTYLEARRAY(); - if (fillColor != null) { - shape.fillStyles.fillStyles = new FILLSTYLE[1]; - FILLSTYLE fillStyle = new FILLSTYLE(); - fillStyle.fillStyleType = FILLSTYLE.SOLID; - fillStyle.color = fillColor; - shape.fillStyles.fillStyles[0] = fillStyle; - } else { - shape.fillStyles.fillStyles = new FILLSTYLE[0]; - } - shape.lineStyles = new LINESTYLEARRAY(); - shape.lineStyles.lineStyles = new LINESTYLE[1]; - LINESTYLE lineStyle = new LINESTYLE(); - lineStyle.color = borderColor; - lineStyle.width = 20; - shape.lineStyles.lineStyles[0] = lineStyle; - shape.shapeRecords = new ArrayList<>(); - StyleChangeRecord style = new StyleChangeRecord(); - style.lineStyle = 1; - style.stateLineStyle = true; - if (fillColor != null) { - style.stateFillStyle0 = true; - style.fillStyle0 = 1; - } - style.stateMoveTo = true; - shape.shapeRecords.add(style); - StraightEdgeRecord top = new StraightEdgeRecord(); - top.generalLineFlag = true; - top.deltaX = rect.getWidth(); - StraightEdgeRecord right = new StraightEdgeRecord(); - right.generalLineFlag = true; - right.deltaY = rect.getHeight(); - StraightEdgeRecord bottom = new StraightEdgeRecord(); - bottom.generalLineFlag = true; - bottom.deltaX = -rect.getWidth(); - StraightEdgeRecord left = new StraightEdgeRecord(); - left.generalLineFlag = true; - left.deltaY = -rect.getHeight(); - shape.shapeRecords.add(top); - shape.shapeRecords.add(right); - shape.shapeRecords.add(bottom); - shape.shapeRecords.add(left); - shape.shapeRecords.add(new EndShapeRecord()); - return shape; - } - - public static void drawBorder(SWF swf, SerializableImage image, RGB borderColor, RGB fillColor, RECT rect, MATRIX textMatrix, Matrix transformation, ColorTransform colorTransform) { - Graphics2D g = (Graphics2D) image.getGraphics(); - Matrix mat = transformation.clone(); - mat = mat.concatenate(new Matrix(textMatrix)); - BitmapExporter.export(swf, getBorderShape(borderColor, fillColor, rect), null, image, mat, mat, colorTransform); - } - - public static void drawBorderHtmlCanvas(SWF swf, StringBuilder result, RGB borderColor, RGB fillColor, RECT rect, MATRIX textMatrix, ColorTransform colorTransform, double unitDivisor) { - Matrix mat = new Matrix(textMatrix); - result.append("\tctx.save();\r\n"); - result.append("\tctx.transform(").append(mat.scaleX).append(",").append(mat.rotateSkew0).append(",").append(mat.rotateSkew1).append(",").append(mat.scaleY).append(",").append(mat.translateX).append(",").append(mat.translateY).append(");\r\n"); - SHAPE shape = getBorderShape(borderColor, fillColor, rect); - CanvasShapeExporter cse = new CanvasShapeExporter(null, unitDivisor, swf, shape, colorTransform, 0, 0); - cse.export(); - result.append(cse.getShapeData()); - result.append("\tctx.restore();\r\n"); - } - - public static void drawBorderSVG(SWF swf, SVGExporter exporter, RGB borderColor, RGB fillColor, RECT rect, MATRIX textMatrix, ColorTransform colorTransform, double zoom) { - exporter.createSubGroup(new Matrix(textMatrix), null); - SHAPE shape = getBorderShape(borderColor, fillColor, rect); - SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, shape, exporter, null, colorTransform, zoom); - shapeExporter.export(); - exporter.endGroup(); - } - - public static void staticTextToImage(SWF swf, List textRecords, int numText, SerializableImage image, MATRIX textMatrix, Matrix transformation, ColorTransform colorTransform) { - int textColor = 0; - FontTag font = null; - int textHeight = 12; - int x = 0; - int y = 0; - List glyphs = null; - Matrix mat0 = transformation.clone(); - mat0 = mat0.concatenate(new Matrix(textMatrix)); - for (TEXTRECORD rec : textRecords) { - if (rec.styleFlagsHasColor) { - if (numText == 2) { - textColor = rec.textColorA.toInt(); - } else { - textColor = rec.textColor.toInt(); - } - - if (colorTransform != null) { - textColor = colorTransform.apply(textColor); - } - } - if (rec.styleFlagsHasFont) { - FontTag font2 = swf.getFont(rec.fontId); - if (font2 != null) { - font = font2; - } - glyphs = font == null ? null : font.getGlyphShapeTable(); - textHeight = rec.textHeight; - } - if (rec.styleFlagsHasXOffset) { - x = rec.xOffset; - } - if (rec.styleFlagsHasYOffset) { - y = rec.yOffset; - } - - double divider = font == null ? 1 : font.getDivider(); - double rat = textHeight / 1024.0 / divider; - - Matrix matScale = Matrix.getScaleInstance(rat); - Color textColor2 = new Color(textColor, true); - for (GLYPHENTRY entry : rec.glyphEntries) { - matScale.translateX = x; - matScale.translateY = y; - - Matrix mat = mat0.concatenate(matScale); - SHAPE shape = null; - if (entry.glyphIndex != -1 && glyphs != null) { - // shapeNum: 1 - shape = glyphs.get(entry.glyphIndex); - } else if (entry instanceof DynamicTextGlyphEntry) { - DynamicTextGlyphEntry dynamicEntry = (DynamicTextGlyphEntry) entry; - if (dynamicEntry.fontFace != null) { - FontTag fnt = swf.getFontByName(dynamicEntry.fontFace); - if (fnt != null && entry.glyphIndex != -1) { - shape = fnt.getGlyphShapeTable().get(entry.glyphIndex); - } else { - shape = SHAPERECORD.fontCharacterToSHAPE(new Font(dynamicEntry.fontFace, dynamicEntry.fontStyle, 12), (int) Math.round(divider * 1024), dynamicEntry.character); - } - } - } - - if (shape != null) { - BitmapExporter.export(swf, shape, textColor2, image, mat, mat, colorTransform); - if (SHAPERECORD.DRAW_BOUNDING_BOX) { - RGB borderColor = new RGBA(Color.black); - RGB fillColor = new RGBA(new Color(255, 255, 255, 0)); - RECT bounds = shape.getBounds(); - mat = Matrix.getTranslateInstance(bounds.Xmin, bounds.Ymin).preConcatenate(mat); - TextTag.drawBorder(swf, image, borderColor, fillColor, bounds, new MATRIX(), mat, colorTransform); - } - } - - x += entry.glyphAdvance; - } - } - } - - public static ExportRectangle calculateTextBounds(SWF swf, List textRecords, MATRIX textMatrix) { - FontTag font = null; - int textHeight = 12; - int x = 0; - int y = 0; - List glyphs = null; - ExportRectangle result = null; - for (TEXTRECORD rec : textRecords) { - if (rec.styleFlagsHasFont) { - font = swf.getFont(rec.fontId); - glyphs = font == null ? null : font.getGlyphShapeTable(); - textHeight = rec.textHeight; - } - if (rec.styleFlagsHasXOffset) { - x = rec.xOffset; - } - if (rec.styleFlagsHasYOffset) { - y = rec.yOffset; - } - - double rat = textHeight / 1024.0 / (font == null ? 1 : font.getDivider()); - - for (GLYPHENTRY entry : rec.glyphEntries) { - Matrix mat = new Matrix(); - mat = mat.concatenate(new Matrix(textMatrix)); - Matrix matTr = Matrix.getTranslateInstance(x, y); - mat = mat.concatenate(matTr); - mat = mat.concatenate(Matrix.getScaleInstance(rat)); - if (entry.glyphIndex != -1 && glyphs != null) { - // shapeNum: 1 - SHAPE shape = glyphs.get(entry.glyphIndex); - RECT glyphBounds = shape.getBounds(); - int glyphWidth = glyphBounds.getWidth(); - int glyphHeight = glyphBounds.getHeight(); - glyphBounds.Xmin -= glyphWidth / 2; - glyphBounds.Ymin -= glyphHeight / 2; - glyphBounds.Xmax += glyphWidth; - glyphBounds.Ymax += glyphHeight; - if (glyphBounds.Xmax > glyphBounds.Xmin && glyphBounds.Ymax > glyphBounds.Ymin) { - ExportRectangle rect = mat.transform(new ExportRectangle(glyphBounds)); - if (result == null) { - result = rect; - } else { - result.xMin = Math.min(result.xMin, rect.xMin); - result.yMin = Math.min(result.yMin, rect.yMin); - result.xMax = Math.max(result.xMax, rect.xMax); - result.yMax = Math.max(result.yMax, rect.yMax); - } - } - x += entry.glyphAdvance; - } - } - } - - return result; - } - - protected void updateTextBounds(RECT textBounds) { - TextImportResizeTextBoundsMode resizeMode = Configuration.textImportResizeTextBoundsMode.get(); - if (resizeMode != null && (resizeMode.equals(TextImportResizeTextBoundsMode.GROW_ONLY) || resizeMode.equals(TextImportResizeTextBoundsMode.GROW_AND_SHRINK))) { - ExportRectangle newBounds = calculateTextBounds(); - if (newBounds != null) { - int xMin = (int) Math.floor(newBounds.xMin); - int yMin = (int) Math.floor(newBounds.yMin); - int xMax = (int) Math.ceil(newBounds.xMax); - int yMax = (int) Math.ceil(newBounds.yMax); - if (resizeMode.equals(TextImportResizeTextBoundsMode.GROW_ONLY)) { - textBounds.Xmin = Math.min(xMin, textBounds.Xmin); - textBounds.Ymin = Math.min(yMin, textBounds.Ymin); - textBounds.Xmax = Math.max(xMax, textBounds.Xmax); - textBounds.Ymax = Math.max(yMax, textBounds.Ymax); - } else if (resizeMode.equals(TextImportResizeTextBoundsMode.GROW_AND_SHRINK)) { - textBounds.Xmin = xMin; - textBounds.Ymin = yMin; - textBounds.Xmax = xMax; - textBounds.Ymax = yMax; - } - } - } - } - - public static void staticTextToHtmlCanvas(double unitDivisor, SWF swf, List textRecords, int numText, StringBuilder result, RECT bounds, MATRIX textMatrix, ColorTransform colorTransform) { - int textColor = 0; - FontTag font = null; - int fontId = -1; - int textHeight = 12; - int x = 0; - int y = 0; - - List glyphs = new ArrayList<>(); - for (TEXTRECORD rec : textRecords) { - if (rec.styleFlagsHasColor) { - if (numText == 2) { - textColor = rec.textColorA.toInt(); - } else { - textColor = rec.textColor.toInt(); - } - - if (colorTransform != null) { - textColor = colorTransform.apply(textColor); - } - } - if (rec.styleFlagsHasFont) { - font = swf.getFont(rec.fontId); - fontId = rec.fontId; - glyphs = font.getGlyphShapeTable(); - textHeight = rec.textHeight; - } - if (rec.styleFlagsHasXOffset) { - x = rec.xOffset; - } - if (rec.styleFlagsHasYOffset) { - y = rec.yOffset; - } - - double rat = textHeight / 1024.0 / font.getDivider(); - - result.append("\tvar textColor = ").append(CanvasShapeExporter.color(textColor)).append(";\r\n"); - for (GLYPHENTRY entry : rec.glyphEntries) { - Matrix mat = (new Matrix(textMatrix).concatenate(Matrix.getTranslateInstance(x, y))).concatenate(Matrix.getScaleInstance(rat)); - if (entry.glyphIndex != -1) { - // shapeNum: 1 - result.append("\tctx.save();\r\n"); - result.append("\tctx.transform(").append(mat.scaleX).append(",").append(mat.rotateSkew0).append(",").append(mat.rotateSkew1).append(",").append(mat.scaleY).append(",").append(mat.translateX).append(",").append(mat.translateY).append(");\r\n"); - result.append("\tfont").append(fontId).append("(ctx,\"").append(("" + font.glyphToChar(entry.glyphIndex)).replace("\\", "\\\\").replace("\"", "\\\"")).append("\",textColor);\r\n"); - result.append("\tctx.restore();\r\n"); - x += entry.glyphAdvance; - } - } - } - } - - public static void staticTextToSVG(SWF swf, List textRecords, int numText, SVGExporter exporter, RECT bounds, MATRIX textMatrix, ColorTransform colorTransform, double zoom) { - int textColor = 0; - FontTag font = null; - int textHeight = 12; - int x = 0; - int y = 0; - List glyphs = new ArrayList<>(); - for (TEXTRECORD rec : textRecords) { - if (rec.styleFlagsHasColor) { - if (numText == 2) { - textColor = rec.textColorA.toInt(); - } else { - textColor = rec.textColor.toInt(); - } - - if (colorTransform != null) { - textColor = colorTransform.apply(textColor); - } - } - if (rec.styleFlagsHasFont) { - font = swf.getFont(rec.fontId); - glyphs = font.getGlyphShapeTable(); - textHeight = rec.textHeight; - } - int offsetX = 0; - int offsetY = 0; - if (rec.styleFlagsHasXOffset) { - offsetX = rec.xOffset; - x = offsetX; - } - if (rec.styleFlagsHasYOffset) { - offsetY = rec.yOffset; - y = offsetY; - } - - double rat = textHeight / 1024.0 / font.getDivider(); - - exporter.createSubGroup(new Matrix(textMatrix), null); - if (exporter.useTextTag) { - StringBuilder text = new StringBuilder(); - int totalAdvance = 0; - for (GLYPHENTRY entry : rec.glyphEntries) { - if (entry.glyphIndex != -1) { - char ch = font.glyphToChar(entry.glyphIndex); - text.append(ch); - totalAdvance += entry.glyphAdvance; - } - } - - boolean hasOffset = offsetX != 0 || offsetY != 0; - if (hasOffset) { - exporter.createSubGroup(Matrix.getTranslateInstance(offsetX, offsetY), null); - } - - Element textElement = exporter.createElement("text"); - textElement.setAttribute("font-size", Double.toString(rat * 1024)); - textElement.setAttribute("font-family", font.getFontNameIntag()); - textElement.setAttribute("textLength", Double.toString(totalAdvance / SWF.unitDivisor)); - textElement.setAttribute("lengthAdjust", "spacing"); - textElement.setTextContent(text.toString()); - - RGBA colorA = new RGBA(textColor); - textElement.setAttribute("fill", colorA.toHexRGB()); - if (colorA.alpha != 255) { - textElement.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); - } - - exporter.addToGroup(textElement); - FontExportMode fontExportMode = FontExportMode.WOFF; - exporter.addStyle(font.getFontNameIntag(), new FontExporter().exportFont(font, fontExportMode), fontExportMode); - - if (hasOffset) { - exporter.endGroup(); - } - } else { - for (GLYPHENTRY entry : rec.glyphEntries) { - Matrix mat = Matrix.getTranslateInstance(x, y).concatenate(Matrix.getScaleInstance(rat)); - if (entry.glyphIndex != -1) { - // shapeNum: 1 - SHAPE shape = glyphs.get(entry.glyphIndex); - char ch = font.glyphToChar(entry.glyphIndex); - - String charId = null; - Map chs; - if (exporter.exportedChars.containsKey(font)) { - chs = exporter.exportedChars.get(font); - if (chs.containsKey(entry.glyphIndex)) { - charId = chs.get(entry.glyphIndex); - } - } else { - chs = new HashMap<>(); - exporter.exportedChars.put(font, chs); - } - - if (charId == null) { - charId = exporter.getUniqueId(Helper.getValidHtmlId("font_" + font.getFontNameIntag() + "_" + ch)); - exporter.createDefGroup(null, charId); - SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, shape, exporter, null, colorTransform, zoom); - shapeExporter.export(); - exporter.endGroup(); - chs.put(entry.glyphIndex, charId); - } - - Element charImage = exporter.addUse(mat, bounds, charId, null); - RGBA colorA = new RGBA(textColor); - charImage.setAttribute("fill", colorA.toHexRGB()); - if (colorA.alpha != 255) { - charImage.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); - } - - x += entry.glyphAdvance; - } - } - } - exporter.endGroup(); - } - } - - @Override - public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { - RECT r = getBounds(); - Shape shp = new Rectangle.Double(r.Xmin, r.Ymin, r.getWidth(), r.getHeight()); - return transformation.toTransform().createTransformedShape(shp); //TODO: match character shapes (?) - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.tags.base; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.FontExporter; +import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; +import com.jpexs.decompiler.flash.exporters.modes.FontExportMode; +import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter; +import com.jpexs.decompiler.flash.exporters.shape.CanvasShapeExporter; +import com.jpexs.decompiler.flash.exporters.shape.SVGShapeExporter; +import com.jpexs.decompiler.flash.helpers.HighlightedText; +import com.jpexs.decompiler.flash.importers.TextImportResizeTextBoundsMode; +import com.jpexs.decompiler.flash.tags.text.JustifyAlignGlyphEntry; +import com.jpexs.decompiler.flash.tags.text.TextAlign; +import com.jpexs.decompiler.flash.tags.text.TextParseException; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.DynamicTextGlyphEntry; +import com.jpexs.decompiler.flash.types.FILLSTYLE; +import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.GLYPHENTRY; +import com.jpexs.decompiler.flash.types.LINESTYLE; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.MATRIX; +import com.jpexs.decompiler.flash.types.RECT; +import com.jpexs.decompiler.flash.types.RGB; +import com.jpexs.decompiler.flash.types.RGBA; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; +import com.jpexs.decompiler.flash.types.TEXTRECORD; +import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; +import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import com.jpexs.helpers.ByteArrayRange; +import com.jpexs.helpers.Helper; +import com.jpexs.helpers.SerializableImage; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.font.LineMetrics; +import java.awt.font.TextAttribute; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.w3c.dom.Element; + +/** + * + * @author JPEXS + */ +public abstract class TextTag extends DrawableTag { + + public TextTag(SWF swf, int id, String name, ByteArrayRange data) { + super(swf, id, name, data); + } + + public abstract MATRIX getTextMatrix(); + + public abstract List getTexts(); + + public abstract List getFontIds(); + + public abstract HighlightedText getFormattedText(boolean ignoreLetterSpacing); + + // use the texts from the "texts" argument when it is not null + public abstract boolean setFormattedText(MissingCharacterHandler missingCharHandler, String formattedText, String[] texts) throws TextParseException; + + public abstract void updateTextBounds(); + + public abstract boolean alignText(TextAlign textAlign); + + public abstract boolean translateText(int diff); + + public abstract RECT getBounds(); + + public abstract void setBounds(RECT r); + + public abstract ExportRectangle calculateTextBounds(); + + @Override + public RECT getRect() { + return getRect(null); // parameter not used + } + + private static void updateRect(RECT ret, int x, int y) { + if (x < ret.Xmin) { + ret.Xmin = x; + } + if (x > ret.Xmax) { + ret.Xmax = x; + } + if (y < ret.Ymin) { + ret.Ymin = y; + } + if (y > ret.Ymax) { + ret.Ymax = y; + } + } + + public static void alignText(SWF swf, List textRecords, TextAlign textAlign) { + // Remove Justify align entries + for (TEXTRECORD tr : textRecords) { + for (int i = 0; i < tr.glyphEntries.size(); i++) { + GLYPHENTRY ge = tr.glyphEntries.get(i); + if (ge instanceof JustifyAlignGlyphEntry) { + JustifyAlignGlyphEntry jge = (JustifyAlignGlyphEntry) ge; + ge = new GLYPHENTRY(); + ge.glyphAdvance = jge.originalAdvance; + ge.glyphIndex = jge.glyphIndex; + tr.glyphEntries.set(i, ge); + } + } + } + + int xMin = Integer.MAX_VALUE; + int maxWidth = 0; + for (TEXTRECORD tr : textRecords) { + int xOffset = tr.styleFlagsHasXOffset ? tr.xOffset : 0; + if (xOffset < xMin) { + xMin = xOffset; + } + + int width = tr.getTotalAdvance(); + + if (width > maxWidth) { + maxWidth = width; + } + } + + FontTag font = null; + + for (TEXTRECORD tr : textRecords) { + if (tr.styleFlagsHasFont) { + FontTag font2 = swf.getFont(tr.fontId); + if (font2 != null) { + font = font2; + } + } + + int width = tr.getTotalAdvance(); + switch (textAlign) { + case LEFT: + tr.xOffset = xMin; + tr.styleFlagsHasXOffset = true; + break; + case CENTER: + tr.xOffset = xMin + (maxWidth - width) / 2; + tr.styleFlagsHasXOffset = true; + break; + case RIGHT: + tr.xOffset = xMin + maxWidth - width; + tr.styleFlagsHasXOffset = true; + break; + case JUSTIFY: + tr.xOffset = xMin; + tr.styleFlagsHasXOffset = true; + + if (font != null) { + int diff = maxWidth - width; + if (diff > 0) { + int spaces = 0; + int spaces2 = 0; + int state = 0; + List glyphEntries = new ArrayList<>(); + List glyphEntries2 = new ArrayList<>(); + for (GLYPHENTRY ge : tr.glyphEntries) { + char ch = font.glyphToChar(ge.glyphIndex); + boolean whitespace = Character.isWhitespace(ch); + switch (state) { + case 0: + if (!whitespace) { + state = 1; + } + break; + case 1: + if (whitespace) { + spaces2++; + glyphEntries2.add(ge); + } else { + spaces += spaces2; + spaces2 = 0; + glyphEntries.addAll(glyphEntries2); + glyphEntries2.clear(); + } + break; + } + } + + if (spaces > 0 && glyphEntries.size() > 0) { + int fix = diff / spaces; + int remaining = diff - fix * spaces; + for (GLYPHENTRY ge : glyphEntries) { + int diff2 = fix; + if (remaining-- > 0) { + diff2++; + } + + JustifyAlignGlyphEntry jge = new JustifyAlignGlyphEntry(); + jge.originalAdvance = ge.glyphAdvance; + jge.glyphAdvance = ge.glyphAdvance + diff2; + jge.glyphIndex = ge.glyphIndex; + int idx = tr.glyphEntries.indexOf(ge); + tr.glyphEntries.set(idx, jge); + } + } + } + } + break; + } + } + } + + public static Map getTextRecordsAttributes(List list, SWF swf) { + Map att = new HashMap<>(); + RECT textBounds = new RECT(Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE); + FontTag font = null; + int x = 0; + int y = 0; + int textHeight = 12; + int lineSpacing = 0; + double leading = 0; + double ascent = 0; + double descent = 0; + double lineDistance = 0; + + List glyphs = null; + boolean firstLine = true; + double top = 0; + List allLeftMargins = new ArrayList<>(); + List allLetterSpacings = new ArrayList<>(); + FontMetrics fontMetrics; + BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + Graphics graphics = bi.getGraphics(); + Font aFont = null; + int currentLeftMargin; + for (int r = 0; r < list.size(); r++) { + TEXTRECORD rec = list.get(r); + if (rec.styleFlagsHasFont) { + FontTag font2 = swf.getFont(rec.fontId); + if (font2 != null) { + font = font2; + } + textHeight = rec.textHeight; + if (font == null) { + Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, "Font with id=" + rec.fontId + " was not found."); + continue; + } + + glyphs = font.getGlyphShapeTable(); + + if (!font.hasLayout()) { + String fontName = FontTag.getFontNameWithFallback(font.getFontNameIntag()); + aFont = new Font(fontName, font.getFontStyle(), (int) (textHeight / SWF.unitDivisor)); + + Map attr = new HashMap<>(); + + attr.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); + attr.put(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON); + aFont = aFont.deriveFont(attr); + + fontMetrics = graphics.getFontMetrics(aFont); + LineMetrics lm = fontMetrics.getLineMetrics("A", graphics); + ascent = lm.getAscent(); + descent = lm.getDescent(); + leading = lm.getLeading(); + lineDistance = ascent + descent; + } else { + leading = ((double) font.getLeading() * textHeight / 1024.0 / font.getDivider() / SWF.unitDivisor); + ascent = ((double) font.getAscent() * textHeight / 1024.0 / font.getDivider() / SWF.unitDivisor); + descent = ((double) font.getDescent() * textHeight / 1024.0 / font.getDivider() / SWF.unitDivisor); + lineDistance = ascent + descent; + } + } + currentLeftMargin = 0; + if (rec.styleFlagsHasXOffset) { + x = rec.xOffset; + currentLeftMargin = x; + } + if (rec.styleFlagsHasYOffset) { + if (!firstLine) { + top += ascent + descent; + int topint = (int) (Math.round(top) * SWF.unitDivisor); + lineSpacing = rec.yOffset - topint; + top += lineSpacing / SWF.unitDivisor; + } else { + top = ascent; + } + y = rec.yOffset; + } + + firstLine = false; + allLeftMargins.add(currentLeftMargin); + + if (glyphs == null) { + Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, "Glyphs not found."); + continue; + } + + int letterSpacing = Integer.MAX_VALUE; + for (int e = 0; e < rec.glyphEntries.size(); e++) { + GLYPHENTRY entry = rec.glyphEntries.get(e); + GLYPHENTRY nextEntry = null; + if (e < rec.glyphEntries.size() - 1) { + nextEntry = rec.glyphEntries.get(e + 1); + } + RECT rect = SHAPERECORD.getBounds(glyphs.get(entry.glyphIndex).shapeRecords); + rect.Xmax = (int) Math.round(((double) rect.Xmax * textHeight) / (font.getDivider() * 1024)); + rect.Xmin = (int) Math.round(((double) rect.Xmin * textHeight) / (font.getDivider() * 1024)); + rect.Ymax = (int) Math.round(((double) rect.Ymax * textHeight) / (font.getDivider() * 1024)); + rect.Ymin = (int) Math.round(((double) rect.Ymin * textHeight) / (font.getDivider() * 1024)); + updateRect(textBounds, x + rect.Xmin, y + rect.Ymin); + updateRect(textBounds, x + rect.Xmax, y + rect.Ymax); + int adv = entry.glyphAdvance; + + int defaultAdvance; + if (font.hasLayout()) { + int kerningAdjustment = 0; + if (nextEntry != null) { + kerningAdjustment = font.getGlyphKerningAdjustment(entry.glyphIndex, nextEntry.glyphIndex); + } + defaultAdvance = (int) (Math.round(textHeight * (font.getGlyphAdvance(entry.glyphIndex) + kerningAdjustment) / (1024.0 * font.getDivider()))); + } else { + defaultAdvance = (int) Math.round(SWF.unitDivisor * FontTag.getSystemFontAdvance(aFont, font.glyphToChar(entry.glyphIndex), nextEntry == null ? null : font.glyphToChar(nextEntry.glyphIndex))); + } + int newLetterSpacing = adv - defaultAdvance; + if (e == 0 || e == rec.glyphEntries.size() - 1) { + if (rec.glyphEntries.size() == 1) { + letterSpacing = 0; + } + } else if (newLetterSpacing < letterSpacing) { + letterSpacing = newLetterSpacing; + } + x += adv; + } + allLetterSpacings.add(letterSpacing); + } + att.put("indent", 0); //? + att.put("rightMargin", 0); //? + att.put("lineSpacing", lineSpacing); + att.put("textBounds", textBounds); + att.put("allLeftMargins", allLeftMargins); + att.put("allLetterSpacings", allLetterSpacings); + return att; + } + + public static SHAPE getBorderShape(RGB borderColor, RGB fillColor, RECT rect) { + SHAPEWITHSTYLE shape = new SHAPEWITHSTYLE(); + shape.fillStyles = new FILLSTYLEARRAY(); + if (fillColor != null) { + shape.fillStyles.fillStyles = new FILLSTYLE[1]; + FILLSTYLE fillStyle = new FILLSTYLE(); + fillStyle.fillStyleType = FILLSTYLE.SOLID; + fillStyle.color = fillColor; + shape.fillStyles.fillStyles[0] = fillStyle; + } else { + shape.fillStyles.fillStyles = new FILLSTYLE[0]; + } + shape.lineStyles = new LINESTYLEARRAY(); + shape.lineStyles.lineStyles = new LINESTYLE[1]; + LINESTYLE lineStyle = new LINESTYLE(); + lineStyle.color = borderColor; + lineStyle.width = 20; + shape.lineStyles.lineStyles[0] = lineStyle; + shape.shapeRecords = new ArrayList<>(); + StyleChangeRecord style = new StyleChangeRecord(); + style.lineStyle = 1; + style.stateLineStyle = true; + if (fillColor != null) { + style.stateFillStyle0 = true; + style.fillStyle0 = 1; + } + style.stateMoveTo = true; + shape.shapeRecords.add(style); + StraightEdgeRecord top = new StraightEdgeRecord(); + top.generalLineFlag = true; + top.deltaX = rect.getWidth(); + StraightEdgeRecord right = new StraightEdgeRecord(); + right.generalLineFlag = true; + right.deltaY = rect.getHeight(); + StraightEdgeRecord bottom = new StraightEdgeRecord(); + bottom.generalLineFlag = true; + bottom.deltaX = -rect.getWidth(); + StraightEdgeRecord left = new StraightEdgeRecord(); + left.generalLineFlag = true; + left.deltaY = -rect.getHeight(); + shape.shapeRecords.add(top); + shape.shapeRecords.add(right); + shape.shapeRecords.add(bottom); + shape.shapeRecords.add(left); + shape.shapeRecords.add(new EndShapeRecord()); + return shape; + } + + public static void drawBorder(SWF swf, SerializableImage image, RGB borderColor, RGB fillColor, RECT rect, MATRIX textMatrix, Matrix transformation, ColorTransform colorTransform) { + Graphics2D g = (Graphics2D) image.getGraphics(); + Matrix mat = transformation.clone(); + mat = mat.concatenate(new Matrix(textMatrix)); + BitmapExporter.export(swf, getBorderShape(borderColor, fillColor, rect), null, image, mat, mat, colorTransform); + } + + public static void drawBorderHtmlCanvas(SWF swf, StringBuilder result, RGB borderColor, RGB fillColor, RECT rect, MATRIX textMatrix, ColorTransform colorTransform, double unitDivisor) { + Matrix mat = new Matrix(textMatrix); + result.append("\tctx.save();\r\n"); + result.append("\tctx.transform(").append(mat.scaleX).append(",").append(mat.rotateSkew0).append(",").append(mat.rotateSkew1).append(",").append(mat.scaleY).append(",").append(mat.translateX).append(",").append(mat.translateY).append(");\r\n"); + SHAPE shape = getBorderShape(borderColor, fillColor, rect); + CanvasShapeExporter cse = new CanvasShapeExporter(null, unitDivisor, swf, shape, colorTransform, 0, 0); + cse.export(); + result.append(cse.getShapeData()); + result.append("\tctx.restore();\r\n"); + } + + public static void drawBorderSVG(SWF swf, SVGExporter exporter, RGB borderColor, RGB fillColor, RECT rect, MATRIX textMatrix, ColorTransform colorTransform, double zoom) { + exporter.createSubGroup(new Matrix(textMatrix), null); + SHAPE shape = getBorderShape(borderColor, fillColor, rect); + SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, shape, 0, exporter, null, colorTransform, zoom); + shapeExporter.export(); + exporter.endGroup(); + } + + public static void staticTextToImage(SWF swf, List textRecords, int numText, SerializableImage image, MATRIX textMatrix, Matrix transformation, ColorTransform colorTransform) { + int textColor = 0; + FontTag font = null; + int textHeight = 12; + int x = 0; + int y = 0; + List glyphs = null; + Matrix mat0 = transformation.clone(); + mat0 = mat0.concatenate(new Matrix(textMatrix)); + for (TEXTRECORD rec : textRecords) { + if (rec.styleFlagsHasColor) { + if (numText == 2) { + textColor = rec.textColorA.toInt(); + } else { + textColor = rec.textColor.toInt(); + } + + if (colorTransform != null) { + textColor = colorTransform.apply(textColor); + } + } + if (rec.styleFlagsHasFont) { + FontTag font2 = swf.getFont(rec.fontId); + if (font2 != null) { + font = font2; + } + glyphs = font == null ? null : font.getGlyphShapeTable(); + textHeight = rec.textHeight; + } + if (rec.styleFlagsHasXOffset) { + x = rec.xOffset; + } + if (rec.styleFlagsHasYOffset) { + y = rec.yOffset; + } + + double divider = font == null ? 1 : font.getDivider(); + double rat = textHeight / 1024.0 / divider; + + Matrix matScale = Matrix.getScaleInstance(rat); + Color textColor2 = new Color(textColor, true); + for (GLYPHENTRY entry : rec.glyphEntries) { + matScale.translateX = x; + matScale.translateY = y; + + Matrix mat = mat0.concatenate(matScale); + SHAPE shape = null; + if (entry.glyphIndex != -1 && glyphs != null) { + // shapeNum: 1 + shape = glyphs.get(entry.glyphIndex); + } else if (entry instanceof DynamicTextGlyphEntry) { + DynamicTextGlyphEntry dynamicEntry = (DynamicTextGlyphEntry) entry; + if (dynamicEntry.fontFace != null) { + FontTag fnt = swf.getFontByName(dynamicEntry.fontFace); + if (fnt != null && entry.glyphIndex != -1) { + shape = fnt.getGlyphShapeTable().get(entry.glyphIndex); + } else { + shape = SHAPERECORD.fontCharacterToSHAPE(new Font(dynamicEntry.fontFace, dynamicEntry.fontStyle, 12), (int) Math.round(divider * 1024), dynamicEntry.character); + } + } + } + + if (shape != null) { + BitmapExporter.export(swf, shape, textColor2, image, mat, mat, colorTransform); + if (SHAPERECORD.DRAW_BOUNDING_BOX) { + RGB borderColor = new RGBA(Color.black); + RGB fillColor = new RGBA(new Color(255, 255, 255, 0)); + RECT bounds = shape.getBounds(); + mat = Matrix.getTranslateInstance(bounds.Xmin, bounds.Ymin).preConcatenate(mat); + TextTag.drawBorder(swf, image, borderColor, fillColor, bounds, new MATRIX(), mat, colorTransform); + } + } + + x += entry.glyphAdvance; + } + } + } + + public static ExportRectangle calculateTextBounds(SWF swf, List textRecords, MATRIX textMatrix) { + FontTag font = null; + int textHeight = 12; + int x = 0; + int y = 0; + List glyphs = null; + ExportRectangle result = null; + for (TEXTRECORD rec : textRecords) { + if (rec.styleFlagsHasFont) { + font = swf.getFont(rec.fontId); + glyphs = font == null ? null : font.getGlyphShapeTable(); + textHeight = rec.textHeight; + } + if (rec.styleFlagsHasXOffset) { + x = rec.xOffset; + } + if (rec.styleFlagsHasYOffset) { + y = rec.yOffset; + } + + double rat = textHeight / 1024.0 / (font == null ? 1 : font.getDivider()); + + for (GLYPHENTRY entry : rec.glyphEntries) { + Matrix mat = new Matrix(); + mat = mat.concatenate(new Matrix(textMatrix)); + Matrix matTr = Matrix.getTranslateInstance(x, y); + mat = mat.concatenate(matTr); + mat = mat.concatenate(Matrix.getScaleInstance(rat)); + if (entry.glyphIndex != -1 && glyphs != null) { + // shapeNum: 1 + SHAPE shape = glyphs.get(entry.glyphIndex); + RECT glyphBounds = shape.getBounds(); + int glyphWidth = glyphBounds.getWidth(); + int glyphHeight = glyphBounds.getHeight(); + glyphBounds.Xmin -= glyphWidth / 2; + glyphBounds.Ymin -= glyphHeight / 2; + glyphBounds.Xmax += glyphWidth; + glyphBounds.Ymax += glyphHeight; + if (glyphBounds.Xmax > glyphBounds.Xmin && glyphBounds.Ymax > glyphBounds.Ymin) { + ExportRectangle rect = mat.transform(new ExportRectangle(glyphBounds)); + if (result == null) { + result = rect; + } else { + result.xMin = Math.min(result.xMin, rect.xMin); + result.yMin = Math.min(result.yMin, rect.yMin); + result.xMax = Math.max(result.xMax, rect.xMax); + result.yMax = Math.max(result.yMax, rect.yMax); + } + } + x += entry.glyphAdvance; + } + } + } + + return result; + } + + protected void updateTextBounds(RECT textBounds) { + TextImportResizeTextBoundsMode resizeMode = Configuration.textImportResizeTextBoundsMode.get(); + if (resizeMode != null && (resizeMode.equals(TextImportResizeTextBoundsMode.GROW_ONLY) || resizeMode.equals(TextImportResizeTextBoundsMode.GROW_AND_SHRINK))) { + ExportRectangle newBounds = calculateTextBounds(); + if (newBounds != null) { + int xMin = (int) Math.floor(newBounds.xMin); + int yMin = (int) Math.floor(newBounds.yMin); + int xMax = (int) Math.ceil(newBounds.xMax); + int yMax = (int) Math.ceil(newBounds.yMax); + if (resizeMode.equals(TextImportResizeTextBoundsMode.GROW_ONLY)) { + textBounds.Xmin = Math.min(xMin, textBounds.Xmin); + textBounds.Ymin = Math.min(yMin, textBounds.Ymin); + textBounds.Xmax = Math.max(xMax, textBounds.Xmax); + textBounds.Ymax = Math.max(yMax, textBounds.Ymax); + } else if (resizeMode.equals(TextImportResizeTextBoundsMode.GROW_AND_SHRINK)) { + textBounds.Xmin = xMin; + textBounds.Ymin = yMin; + textBounds.Xmax = xMax; + textBounds.Ymax = yMax; + } + } + } + } + + public static void staticTextToHtmlCanvas(double unitDivisor, SWF swf, List textRecords, int numText, StringBuilder result, RECT bounds, MATRIX textMatrix, ColorTransform colorTransform) { + int textColor = 0; + FontTag font = null; + int fontId = -1; + int textHeight = 12; + int x = 0; + int y = 0; + + List glyphs = new ArrayList<>(); + for (TEXTRECORD rec : textRecords) { + if (rec.styleFlagsHasColor) { + if (numText == 2) { + textColor = rec.textColorA.toInt(); + } else { + textColor = rec.textColor.toInt(); + } + + if (colorTransform != null) { + textColor = colorTransform.apply(textColor); + } + } + if (rec.styleFlagsHasFont) { + font = swf.getFont(rec.fontId); + fontId = rec.fontId; + glyphs = font.getGlyphShapeTable(); + textHeight = rec.textHeight; + } + if (rec.styleFlagsHasXOffset) { + x = rec.xOffset; + } + if (rec.styleFlagsHasYOffset) { + y = rec.yOffset; + } + + double rat = textHeight / 1024.0 / font.getDivider(); + + result.append("\tvar textColor = ").append(CanvasShapeExporter.color(textColor)).append(";\r\n"); + for (GLYPHENTRY entry : rec.glyphEntries) { + Matrix mat = (new Matrix(textMatrix).concatenate(Matrix.getTranslateInstance(x, y))).concatenate(Matrix.getScaleInstance(rat)); + if (entry.glyphIndex != -1) { + // shapeNum: 1 + result.append("\tctx.save();\r\n"); + result.append("\tctx.transform(").append(mat.scaleX).append(",").append(mat.rotateSkew0).append(",").append(mat.rotateSkew1).append(",").append(mat.scaleY).append(",").append(mat.translateX).append(",").append(mat.translateY).append(");\r\n"); + result.append("\tfont").append(fontId).append("(ctx,\"").append(("" + font.glyphToChar(entry.glyphIndex)).replace("\\", "\\\\").replace("\"", "\\\"")).append("\",textColor);\r\n"); + result.append("\tctx.restore();\r\n"); + x += entry.glyphAdvance; + } + } + } + } + + public static void staticTextToSVG(SWF swf, List textRecords, int numText, SVGExporter exporter, RECT bounds, MATRIX textMatrix, ColorTransform colorTransform, double zoom) { + int textColor = 0; + FontTag font = null; + int textHeight = 12; + int x = 0; + int y = 0; + List glyphs = new ArrayList<>(); + for (TEXTRECORD rec : textRecords) { + if (rec.styleFlagsHasColor) { + if (numText == 2) { + textColor = rec.textColorA.toInt(); + } else { + textColor = rec.textColor.toInt(); + } + + if (colorTransform != null) { + textColor = colorTransform.apply(textColor); + } + } + if (rec.styleFlagsHasFont) { + font = swf.getFont(rec.fontId); + glyphs = font.getGlyphShapeTable(); + textHeight = rec.textHeight; + } + int offsetX = 0; + int offsetY = 0; + if (rec.styleFlagsHasXOffset) { + offsetX = rec.xOffset; + x = offsetX; + } + if (rec.styleFlagsHasYOffset) { + offsetY = rec.yOffset; + y = offsetY; + } + + double rat = textHeight / 1024.0 / font.getDivider(); + + exporter.createSubGroup(new Matrix(textMatrix), null); + if (exporter.useTextTag) { + StringBuilder text = new StringBuilder(); + int totalAdvance = 0; + for (GLYPHENTRY entry : rec.glyphEntries) { + if (entry.glyphIndex != -1) { + char ch = font.glyphToChar(entry.glyphIndex); + text.append(ch); + totalAdvance += entry.glyphAdvance; + } + } + + boolean hasOffset = offsetX != 0 || offsetY != 0; + if (hasOffset) { + exporter.createSubGroup(Matrix.getTranslateInstance(offsetX, offsetY), null); + } + + Element textElement = exporter.createElement("text"); + textElement.setAttribute("font-size", Double.toString(rat * 1024)); + textElement.setAttribute("font-family", font.getFontNameIntag()); + textElement.setAttribute("textLength", Double.toString(totalAdvance / SWF.unitDivisor)); + textElement.setAttribute("lengthAdjust", "spacing"); + textElement.setTextContent(text.toString()); + + RGBA colorA = new RGBA(textColor); + textElement.setAttribute("fill", colorA.toHexRGB()); + if (colorA.alpha != 255) { + textElement.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); + } + + exporter.addToGroup(textElement); + FontExportMode fontExportMode = FontExportMode.WOFF; + exporter.addStyle(font.getFontNameIntag(), new FontExporter().exportFont(font, fontExportMode), fontExportMode); + + if (hasOffset) { + exporter.endGroup(); + } + } else { + for (GLYPHENTRY entry : rec.glyphEntries) { + Matrix mat = Matrix.getTranslateInstance(x, y).concatenate(Matrix.getScaleInstance(rat)); + if (entry.glyphIndex != -1) { + // shapeNum: 1 + SHAPE shape = glyphs.get(entry.glyphIndex); + char ch = font.glyphToChar(entry.glyphIndex); + + String charId = null; + Map chs; + if (exporter.exportedChars.containsKey(font)) { + chs = exporter.exportedChars.get(font); + if (chs.containsKey(entry.glyphIndex)) { + charId = chs.get(entry.glyphIndex); + } + } else { + chs = new HashMap<>(); + exporter.exportedChars.put(font, chs); + } + + if (charId == null) { + charId = exporter.getUniqueId(Helper.getValidHtmlId("font_" + font.getFontNameIntag() + "_" + ch)); + exporter.createDefGroup(null, charId); + SVGShapeExporter shapeExporter = new SVGShapeExporter(swf, shape, 0, exporter, null, colorTransform, zoom); + shapeExporter.export(); + exporter.endGroup(); + chs.put(entry.glyphIndex, charId); + } + + Element charImage = exporter.addUse(mat, bounds, charId, null); + RGBA colorA = new RGBA(textColor); + charImage.setAttribute("fill", colorA.toHexRGB()); + if (colorA.alpha != 255) { + charImage.setAttribute("fill-opacity", Float.toString(colorA.getAlphaFloat())); + } + + x += entry.glyphAdvance; + } + } + } + exporter.endGroup(); + } + } + + @Override + public Shape getOutline(int frame, int time, int ratio, RenderContext renderContext, Matrix transformation, boolean stroked) { + RECT r = getBounds(); + Shape shp = new Rectangle.Double(r.Xmin, r.Ymin, r.getWidth(), r.getHeight()); + return transformation.toTransform().createTransformedShape(shp); //TODO: match character shapes (?) + } +} diff --git a/src/com/jpexs/decompiler/flash/gui/DebuggerHandler.java b/src/com/jpexs/decompiler/flash/gui/DebuggerHandler.java index ce311cd28..b9dcbc0ff 100644 --- a/src/com/jpexs/decompiler/flash/gui/DebuggerHandler.java +++ b/src/com/jpexs/decompiler/flash/gui/DebuggerHandler.java @@ -1,902 +1,902 @@ -/* - * Copyright (C) 2010-2016 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash.gui; - -import com.jpexs.debugger.flash.DebugConnectionListener; -import com.jpexs.debugger.flash.DebugMessageListener; -import com.jpexs.debugger.flash.Debugger; -import com.jpexs.debugger.flash.DebuggerCommands; -import com.jpexs.debugger.flash.DebuggerConnection; -import com.jpexs.debugger.flash.Variable; -import com.jpexs.debugger.flash.VariableFlags; -import com.jpexs.debugger.flash.VariableType; -import com.jpexs.debugger.flash.messages.in.InAskBreakpoints; -import com.jpexs.debugger.flash.messages.in.InBreakAt; -import com.jpexs.debugger.flash.messages.in.InBreakAtExt; -import com.jpexs.debugger.flash.messages.in.InBreakReason; -import com.jpexs.debugger.flash.messages.in.InCallFunction; -import com.jpexs.debugger.flash.messages.in.InConstantPool; -import com.jpexs.debugger.flash.messages.in.InContinue; -import com.jpexs.debugger.flash.messages.in.InFrame; -import com.jpexs.debugger.flash.messages.in.InGetSwd; -import com.jpexs.debugger.flash.messages.in.InGetSwf; -import com.jpexs.debugger.flash.messages.in.InGetVariable; -import com.jpexs.debugger.flash.messages.in.InNumScript; -import com.jpexs.debugger.flash.messages.in.InProcessTag; -import com.jpexs.debugger.flash.messages.in.InScript; -import com.jpexs.debugger.flash.messages.in.InSetBreakpoint; -import com.jpexs.debugger.flash.messages.in.InSwfInfo; -import com.jpexs.debugger.flash.messages.in.InTrace; -import com.jpexs.debugger.flash.messages.in.InVersion; -import com.jpexs.debugger.flash.messages.out.OutAddWatch2; -import com.jpexs.debugger.flash.messages.out.OutGetBreakReason; -import com.jpexs.debugger.flash.messages.out.OutGetSwd; -import com.jpexs.debugger.flash.messages.out.OutGetSwf; -import com.jpexs.debugger.flash.messages.out.OutPlay; -import com.jpexs.debugger.flash.messages.out.OutProcessedTag; -import com.jpexs.debugger.flash.messages.out.OutRewind; -import com.jpexs.debugger.flash.messages.out.OutSwfInfo; -import com.jpexs.decompiler.flash.configuration.Configuration; -import com.jpexs.decompiler.graph.DottedChain; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.logging.ConsoleHandler; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * - * @author JPEXS - */ -public class DebuggerHandler implements DebugConnectionListener { - - private boolean connected = false; - - private DebuggerCommands commands = null; - - private List swfs = new ArrayList<>(); - - private boolean paused = true; - - private Map modulePaths = new HashMap<>(); - - private Map moduleToSwfIndex = new HashMap<>(); - - private Map scriptToModule = new HashMap<>(); - - private Map moduleToTraitIndex = new HashMap<>(); - - private Map moduleToClassIndex = new HashMap<>(); - - private Map moduleToMethodIndex = new HashMap<>(); - - private Map> toAddBPointMap = new HashMap<>(); - - private Map> confirmedPointMap = new HashMap<>(); - - private Map> invalidBreakPointMap = new HashMap<>(); - - private Map> toRemoveBPointMap = new HashMap<>(); - - private int breakIp = -1; - - private String breakScriptName = null; - - public static class ActionScriptException extends Exception { - - private String errorClass; - - public ActionScriptException(String errorClass, String message) { - super(message); - this.errorClass = errorClass; - } - - public ActionScriptException(String errorClsMessage) { - this(errorClsMessage.substring(0, errorClsMessage.indexOf(": ")), errorClsMessage.substring(errorClsMessage.indexOf(": ") + 2)); - } - - public String getErrorClass() { - return errorClass; - } - } - - public int getBreakIp() { - if (!isPaused()) { - return -1; - } - return breakIp; - } - - public String getBreakScriptName() { - if (!isPaused()) { - return "-"; - } - return breakScriptName; - } - - public InGetVariable getVariable(long parentId, String varName, boolean children) { - try { - return commands.getVariable(parentId, varName, true, children); - } catch (IOException ex) { - return null; - } - } - - public void setVariable(long parentId, String varName, int valueType, Object value) { - try { - String svalue = ""; - switch (valueType) { - case VariableType.STRING: - svalue = "" + value; - break; - case VariableType.NUMBER: - svalue = "" + value; - break; - case VariableType.BOOLEAN: - svalue = ((Boolean) value) ? "true" : "false"; - break; - case VariableType.UNDEFINED: - svalue = "undefined"; - break; - case VariableType.NULL: - svalue = "undefined"; - break; - } - commands.setVariable(parentId, varName, valueType, svalue); - } catch (IOException ex) { - //ignore - } - } - - public synchronized void removeBreakPoint(String scriptName, int line) { - if (isBreakpointInvalid(scriptName, line)) { - invalidBreakPointMap.get(scriptName).remove(line); - if (invalidBreakPointMap.get(scriptName).isEmpty()) { - invalidBreakPointMap.remove(scriptName); - } - return; - } - if (isBreakpointToAdd(scriptName, line)) { - toAddBPointMap.get(scriptName).remove(line); - if (toAddBPointMap.get(scriptName).isEmpty()) { - toAddBPointMap.remove(scriptName); - } - } else if (isBreakpointConfirmed(scriptName, line)) { - if (!toRemoveBPointMap.containsKey(scriptName)) { - toRemoveBPointMap.put(scriptName, new TreeSet<>()); - } - toRemoveBPointMap.get(scriptName).add(line); - } - try { - sendBreakPoints(false); - } catch (IOException ex) { - //ignore - } - } - - private int watchTag = 1; - - public synchronized com.jpexs.debugger.flash.DebuggerCommands.Watch addWatch(Variable v, long v_id, boolean watchRead, boolean watchWrite) { - int tag = watchTag++; - try { - return commands.addWatch(v_id, v.name, (watchRead ? OutAddWatch2.FLAG_READ : 0) | (watchWrite ? OutAddWatch2.FLAG_WRITE : 0), tag); - } catch (IOException ex) { - return null; - } - } - - public synchronized Set getBreakPoints(String scriptName, boolean onlyValid) { - Set lines = new TreeSet<>(); - if (confirmedPointMap.containsKey(scriptName)) { - lines.addAll(confirmedPointMap.get(scriptName)); - } - if (toAddBPointMap.containsKey(scriptName)) { - lines.addAll(toAddBPointMap.get(scriptName)); - } - if (!onlyValid && invalidBreakPointMap.containsKey(scriptName)) { - lines.addAll(invalidBreakPointMap.get(scriptName)); - } - return lines; - } - - public synchronized void clearBreakPoints() { - for (String scriptName : confirmedPointMap.keySet()) { - if (!toAddBPointMap.containsKey(scriptName)) { - toAddBPointMap.put(scriptName, new TreeSet<>()); - } - toAddBPointMap.get(scriptName).addAll(confirmedPointMap.get(scriptName)); - } - for (String scriptName : invalidBreakPointMap.keySet()) { - if (!toAddBPointMap.containsKey(scriptName)) { - toAddBPointMap.put(scriptName, new TreeSet<>()); - } - toAddBPointMap.get(scriptName).addAll(invalidBreakPointMap.get(scriptName)); - } - confirmedPointMap.clear(); - invalidBreakPointMap.clear(); - } - - public synchronized Map> getAllBreakPoints(boolean validOnly) { - Map> ret = new HashMap<>(); - for (String scriptName : confirmedPointMap.keySet()) { - Set lines = new TreeSet<>(); - lines.addAll(confirmedPointMap.get(scriptName)); - ret.put(scriptName, lines); - } - for (String scriptName : toAddBPointMap.keySet()) { - if (!ret.containsKey(scriptName)) { - ret.put(scriptName, new TreeSet<>()); - } - ret.get(scriptName).addAll(toAddBPointMap.get(scriptName)); - } - if (!validOnly) { - for (String scriptName : invalidBreakPointMap.keySet()) { - if (!ret.containsKey(scriptName)) { - ret.put(scriptName, new TreeSet<>()); - } - ret.get(scriptName).addAll(invalidBreakPointMap.get(scriptName)); - } - } - return ret; - } - - public boolean addBreakPoint(String scriptName, int line) { - synchronized (this) { - Logger.getLogger(DebuggerHandler.class - .getName()).log(Level.FINE, "adding bp " + scriptName + ":" + line); - if (isBreakpointToRemove(scriptName, line)) { - toRemoveBPointMap.get(scriptName).remove(line); - if (toRemoveBPointMap.get(scriptName).isEmpty()) { - toRemoveBPointMap.remove(scriptName); - - } - } - - if (isBreakpointConfirmed(scriptName, line)) { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp " + scriptName + ":" + line + " already confirmed"); - return true; - - } - if (isBreakpointInvalid(scriptName, line)) { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp " + scriptName + ":" + line + " already invalid"); - return false; - } - if (!toAddBPointMap.containsKey(scriptName)) { - toAddBPointMap.put(scriptName, new TreeSet<>()); - } - toAddBPointMap.get(scriptName).add(line); - Logger - .getLogger(DebuggerHandler.class - .getName()).log(Level.FINE, "bp " + scriptName + ":" + line + " added to todo"); - } - try { - sendBreakPoints(false); - } catch (IOException ex) { - //ignored - } - - return true; - } - - public synchronized boolean isBreakpointConfirmed(String scriptName, int line) { - return confirmedPointMap.containsKey(scriptName) && confirmedPointMap.get(scriptName).contains(line); - } - - public synchronized boolean isBreakpointToAdd(String scriptName, int line) { - return toAddBPointMap.containsKey(scriptName) && toAddBPointMap.get(scriptName).contains(line); - } - - public synchronized boolean isBreakpointToRemove(String scriptName, int line) { - return toRemoveBPointMap.containsKey(scriptName) && toRemoveBPointMap.get(scriptName).contains(line); - } - - public synchronized boolean isBreakpointInvalid(String scriptName, int line) { - return invalidBreakPointMap.containsKey(scriptName) && invalidBreakPointMap.get(scriptName).contains(line); - } - - private synchronized void markBreakPointInvalid(String scriptName, int line) { - if (!invalidBreakPointMap.containsKey(scriptName)) { - invalidBreakPointMap.put(scriptName, new TreeSet<>()); - } - invalidBreakPointMap.get(scriptName).add(line); - } - - private InFrame frame; - - private InConstantPool pool; - - private InBreakAtExt breakInfo; - - private InBreakReason breakReason; - - private final List breakListeners = new ArrayList<>(); - - private final List traceListeners = new ArrayList<>(); - - private final List clisteners = new ArrayList<>(); - - public String moduleToString(int file) { - if (!modulePaths.containsKey(file)) { - return "unknown"; - } - return modulePaths.get(file); - } - - public synchronized InBreakAtExt getBreakInfo() { - if (!paused) { - return null; - } - return breakInfo; - } - - public synchronized InBreakReason getBreakReason() { - if (!paused) { - return null; - } - return breakReason; - } - - public static interface ConnectionListener { - - public void connected(); - - public void disconnected(); - } - - public static interface TraceListener { - - public void trace(String... val); - } - - public static interface BreakListener { - - public void breakAt(String scriptName, int line, int classIndex, int traitIndex, int methodIndex); - - public void doContinue(); - } - - public void addBreakListener(BreakListener l) { - breakListeners.add(l); - } - - public void addTraceListener(TraceListener l) { - traceListeners.add(l); - } - - public void removeTraceListener(TraceListener l) { - traceListeners.remove(l); - } - - public void removeBreakListener(BreakListener l) { - breakListeners.remove(l); - } - - public void addConnectionListener(ConnectionListener l) { - clisteners.add(l); - } - - public void removeConnectionListener(ConnectionListener l) { - clisteners.remove(l); - } - - public synchronized void refreshFrame() { - if (!paused) { - return; - } - try { - frame = commands.getFrame(0); - pool = commands.getConstantPool(0); - } catch (IOException ex) { - //ignore - } - } - - public synchronized InFrame getFrame() { - if (!paused) { - return null; - } - return frame; - } - - public synchronized int moduleIdOf(String pack) { - if (scriptToModule.containsKey(pack)) { - return scriptToModule.get(pack); - } - return -1; - } - - public boolean isPaused() { - if (!isConnected()) { - return false; - } - synchronized (this) { - return paused; - } - } - - public List getSwfs() { - return swfs; - } - - public void disconnect() { - frame = null; - pool = null; - breakInfo = null; - breakReason = null; - connected = false; - if (commands != null) { - commands.disconnect(); - } - commands = null; - synchronized (this) { - for (String scriptName : confirmedPointMap.keySet()) { - if (!toAddBPointMap.containsKey(scriptName)) { - toAddBPointMap.put(scriptName, new TreeSet<>()); - } - toAddBPointMap.get(scriptName).addAll(confirmedPointMap.get(scriptName)); - } - confirmedPointMap.clear(); - for (String scriptName : invalidBreakPointMap.keySet()) { - if (!toAddBPointMap.containsKey(scriptName)) { - toAddBPointMap.put(scriptName, new TreeSet<>()); - } - toAddBPointMap.get(scriptName).addAll(invalidBreakPointMap.get(scriptName)); - } - invalidBreakPointMap.clear(); - } - for (ConnectionListener l : clisteners) { - l.disconnected(); - } - } - - public synchronized boolean isConnected() { - return connected; - } - - public DebuggerCommands getCommands() throws IOException { - if (!isConnected() || commands == null) { - throw new IOException("Not connected"); - } - return commands; - } - - private static void enlog(Class cls) { - Level level = Level.FINEST; - - Logger mylog = Logger.getLogger(cls.getName()); - mylog.setLevel(level); - ConsoleHandler ch = new ConsoleHandler(); - ch.setLevel(level); - mylog.addHandler(ch); - } - - @Override - public void failedListen(IOException ex) { - View.execInEventDispatch(new Runnable() { - @Override - public void run() { - disconnect(); - Main.stopRun(); - Main.stopWork(); - View.showMessageDialog(Main.getMainFrame().getPanel(), AppStrings.translate("error.debug.listen").replace("%port%", "" + Debugger.DEBUG_PORT)); - Main.getMainFrame().getPanel().updateMenu(); - } - }); - - } - - @Override - public void connected(DebuggerConnection con) { - clearBreakPoints(); - - Main.startWork(AppStrings.translate("work.debugging"), null); - - synchronized (this) { - paused = false; - } - - Main.getMainFrame().getPanel().updateMenu(); - - //enlog(DebuggerConnection.class); - //enlog(DebuggerCommands.class); - //enlog(DebuggerHandler.class); - try { - con.getMessage(InVersion.class); - } catch (IOException ex) { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.SEVERE, null, ex); - } - - //Respon to InProcessTag with OutProcessedTag - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InProcessTag message) { - try { - con.writeMessage(new OutProcessedTag(con)); - } catch (IOException ex) { - //disconnect(); - //ignore - } - } - }); - - swfs.clear(); - - Map moduleNames = new HashMap<>(); - - final Pattern patAS3 = Pattern.compile("^(.*);(.*);(.*)\\.as$"); - final Pattern patAS3PCode = Pattern.compile("^#PCODE abc:([0-9]+),script:([0-9]+),class:(-?[0-9]+),trait:(-?[0-9]+),method:([0-9]+),body:([0-9]+);(.*)$"); - - try { - - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InNumScript t) { - con.dropMessage(t); - } - }); - - modulePaths = new HashMap<>(); - scriptToModule = new HashMap<>(); - - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InScript sc) { - moduleNames.put(sc.module, sc.name); - moduleToSwfIndex.put(sc.module, sc.swfIndex); - int file = sc.module; - String name = sc.name; - String[] parts = name.split(";"); - - Matcher m; - if ((m = patAS3.matcher(name)).matches()) { - String clsNameWithSuffix = m.group(3); - String pkg = m.group(2).replace("\\", "."); - m = patAS3PCode.matcher(name); - - if (m.matches()) { - moduleToClassIndex.put(file, Integer.parseInt(m.group(3))); - moduleToTraitIndex.put(file, Integer.parseInt(m.group(4))); - moduleToMethodIndex.put(file, Integer.parseInt(m.group(5))); - name = DottedChain.parseWithSuffix(pkg).addWithSuffix(clsNameWithSuffix).toString(); - name = "#PCODE abc:" + m.group(1) + ",body:" + m.group(6) + ";" + name; - } else { - name = DottedChain.parseWithSuffix(pkg).addWithSuffix(clsNameWithSuffix).toString(); - } - } - modulePaths.put(file, name); - scriptToModule.put(name, file); - con.dropMessage(sc); - } - }); - - /*int numScript = con.getMessage(InNumScript.class).num; - for (int i = 0; i < numScript; i++) { - InScript sc = con.getMessage(InScript.class); - moduleNames.put(sc.module, sc.name); - }*/ - //Pattern patMainFrame = Pattern.compile("^Actions for Scene ([0-9]+): Frame ([0-9]+) of Layer Name .*$"); - //Pattern patSymbol = Pattern.compile("^Actions for Symbol ([0-9]+): Frame ([0-9]+) of Layer Name .*$"); - //Pattern patAS2 = Pattern.compile("^([^:]+): .*\\.as$"); - //con.getMessage(InSetBreakpoint.class); - commands = new DebuggerCommands(con); - - commands.stopWarning(); - commands.setStopOnFault(); - commands.setEnumerateOverride(); - commands.setNotifyFailure(); - commands.setInvokeSetters(); - commands.setSwfLoadNotify(); - commands.setGetterTimeout(1500); - commands.setSetterTimeout(5000); - - boolean isAS3 = (Main.getMainFrame().getPanel().getCurrentSwf().isAS3()); - con.isAS3 = isAS3; - - //Widelines - only AS3, it hangs in AS1/2 and SWD does not support UI32 lines - if (isAS3) { - con.wideLines = commands.getOption("wide_line_player", "false").equals("true"); - if (con.wideLines) { - commands.setOption("wide_line_debugger", "on"); - } - } - commands.squelch(true); - - con.writeMessage(new OutSwfInfo(con, 0)); - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InSwfInfo t) { - for (InSwfInfo.SwfInfo s : t.swfInfos) { - swfs.add(s); - View.execInEventDispatch(new Runnable() { - @Override - public void run() { - try { - con.sendMessage(new OutGetSwf(con, (int) s.index), InGetSwf.class); - con.sendMessage(new OutGetSwd(con, (int) s.index), InGetSwd.class); - } catch (IOException ex) { - //ignore - } - } - }); - } - con.dropMessage(t); - } - }); - - InSetBreakpoint isb = con.getMessage(InSetBreakpoint.class); - synchronized (this) { - for (int i = 0; i < isb.files.size(); i++) { - String sname = moduleNames.get(isb.files.get(i)); - if (!confirmedPointMap.containsKey(sname)) { - confirmedPointMap.put(sname, new TreeSet<>()); - } - if (toAddBPointMap.containsKey(sname)) { - toAddBPointMap.get(sname).remove(isb.lines.get(i)); - if (toAddBPointMap.get(sname).isEmpty()) { - toAddBPointMap.remove(sname); - } - } - confirmedPointMap.get(sname).add(isb.lines.get(i)); - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} submitted successfully", new Object[]{sname, isb.lines.get(i)}); - } - } - - synchronized (this) { - connected = true; - } - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InAskBreakpoints message) { - - } - }); - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InContinue msg) { - synchronized (DebuggerHandler.this) { - paused = false; - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "continued"); - } - for (BreakListener bl : breakListeners) { - bl.doContinue(); - } - } - }); - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InBreakAt message) { - synchronized (DebuggerHandler.this) { - paused = true; - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "paused"); - - } - - try { - breakInfo = con.getMessage(InBreakAtExt.class); - breakReason = con.sendMessage(new OutGetBreakReason(con), InBreakReason.class); - - String newBreakScriptName = "unknown"; - if (modulePaths.containsKey(message.file)) { - newBreakScriptName = modulePaths.get(message.file); - - } else if (breakReason.reason != InBreakReason.REASON_SCRIPT_LOADED) { - Logger.getLogger(DebuggerCommands.class.getName()).log(Level.SEVERE, "Invalid file: " + message.file); - return; - } - - final String[] reasonNames = new String[]{"unknown", "breakpoint", "watch", "fault", "stopRequest", "step", "halt", "scriptLoaded"}; - String reason = breakReason.reason < reasonNames.length ? reasonNames[breakReason.reason] : reasonNames[0]; - - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "break at {0}:{1}, reason: {2}", new Object[]{newBreakScriptName, message.line, reason}); - - sendBreakPoints(false); - synchronized (DebuggerHandler.this) { - breakScriptName = newBreakScriptName; - breakIp = message.line; - } - - if (breakReason.reason == InBreakReason.REASON_SCRIPT_LOADED) { - if (!Configuration.debugHalt.get()) { - commands.sendContinue(); - return; - } - Main.startWork(AppStrings.translate("work.halted"), null); - } else { - Main.startWork(AppStrings.translate("work.breakat") + newBreakScriptName + ":" + message.line + " " + AppStrings.translate("debug.break.reason." + reason), null); - } - frame = commands.getFrame(0); - pool = commands.getConstantPool(0); - - for (BreakListener l : breakListeners) { - l.breakAt(newBreakScriptName, message.line, - moduleToClassIndex.containsKey(message.file) ? moduleToClassIndex.get(message.file) : -1, - moduleToTraitIndex.containsKey(message.file) ? moduleToTraitIndex.get(message.file) : -1, - moduleToMethodIndex.containsKey(message.file) ? moduleToMethodIndex.get(message.file) : -1 - ); - } - - } catch (IOException ex) { - //ignore - } - - } - }); - - for (ConnectionListener l : clisteners) { - l.connected(); - } - - con.addMessageListener(new DebugMessageListener() { - @Override - public void message(InTrace tr) { - for (TraceListener l : traceListeners) { - l.trace(tr.text); - } - } - }); - - if (!isAS3) { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINER, "End of connect - sending continue"); - con.writeMessage(new OutRewind(con)); - con.writeMessage(new OutPlay(con)); - commands.sendContinue(); - } - - } catch (IOException ex) { - - synchronized (this) { - connected = false; - } - } - } - - private void sendBreakPoints(boolean force) throws IOException { - if (!force && !isPaused()) { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINEST, "not sending bps, not paused"); - return; - } - synchronized (this) { - for (String scriptName : toRemoveBPointMap.keySet()) { - int file = moduleIdOf(scriptName); - if (file > -1) { - for (int line : toRemoveBPointMap.get(scriptName)) { - if (isBreakpointConfirmed(scriptName, line)) { - commands.removeBreakPoint(file, line); - confirmedPointMap.get(scriptName).remove(line); - if (confirmedPointMap.get(scriptName).isEmpty()) { - confirmedPointMap.remove(scriptName); - } - } - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} removed", new Object[]{scriptName, line}); - } - } - } - toRemoveBPointMap.clear(); - - for (String scriptName : toAddBPointMap.keySet()) { - int file = moduleIdOf(scriptName); - if (file > -1) { - for (int line : toAddBPointMap.get(scriptName)) { - if (commands.addBreakPoint(file, line)) { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} submitted successfully", new Object[]{scriptName, line}); - if (!confirmedPointMap.containsKey(scriptName)) { - confirmedPointMap.put(scriptName, new TreeSet<>()); - } - confirmedPointMap.get(scriptName).add(line); - } else { - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} unable to submit", new Object[]{scriptName, line}); - markBreakPointInvalid(scriptName, line); - } - } - } else { - for (int line : toAddBPointMap.get(scriptName)) { - markBreakPointInvalid(scriptName, line); - } - } - } - toAddBPointMap.clear(); - - } - Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINEST, "sending bps finished"); - - } - - public synchronized InConstantPool getConstantPool() { - if (!paused) { - return null; - } - return pool; - } - - public synchronized InCallFunction callMethod(Variable object, String methodName, List args) throws ActionScriptException { - return callFunction(false, methodName, object, args); - } - - public synchronized InCallFunction callMethod(String object, String methodName, List args) throws ActionScriptException { - InGetVariable igv = getVariable(0, object, false); - return callMethod(igv.parent, methodName, args); - } - - private static String typeAsStr(Object value) { - if (value == null) { - return "null"; - } - if ((value instanceof Long) || (value instanceof Integer)) { - return "int"; - } - if (value instanceof Number) { - return "Number"; - } - if (value instanceof String) { - return "String"; - } - if (value instanceof Variable) { - Variable v = (Variable) value; - return v.getTypeAsStr(); - } - return "String"; - } - - private static String valueAsStr(Object value) { - if (value == null) { - return "null"; - } - if (value instanceof Double) { - double doubleValue = ((Double) value).doubleValue(); - long longValue = (long) doubleValue; - if (doubleValue == longValue) { - return Long.toString(longValue); - } - } - - if (value instanceof Variable) { - Variable v = (Variable) value; - return valueAsStr(v.value); - } - return "" + value; - } - - public synchronized InCallFunction callFunction(boolean isConstructor, String funcName, Variable thisValue, List args) throws ActionScriptException { - List argTypes = new ArrayList<>(); - List argValues = new ArrayList<>(); - for (Object value : args) { - argTypes.add(typeAsStr(value)); - argValues.add(valueAsStr(value)); - } - String thisType = typeAsStr(thisValue); - String thisValueStr = valueAsStr(thisValue); - try { - InCallFunction icf = commands.callFunction(isConstructor, funcName, thisType, thisValueStr, argTypes, argValues); - if (!icf.variables.isEmpty()) { - if ((icf.variables.get(0).flags & VariableFlags.IS_EXCEPTION) > 0) { - throw new ActionScriptException("" + icf.variables.get(0).value); - } - } - return icf; - } catch (IOException e) { - - } - return null; - } -} +/* + * Copyright (C) 2010-2016 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.gui; + +import com.jpexs.debugger.flash.DebugConnectionListener; +import com.jpexs.debugger.flash.DebugMessageListener; +import com.jpexs.debugger.flash.Debugger; +import com.jpexs.debugger.flash.DebuggerCommands; +import com.jpexs.debugger.flash.DebuggerConnection; +import com.jpexs.debugger.flash.Variable; +import com.jpexs.debugger.flash.VariableFlags; +import com.jpexs.debugger.flash.VariableType; +import com.jpexs.debugger.flash.messages.in.InAskBreakpoints; +import com.jpexs.debugger.flash.messages.in.InBreakAt; +import com.jpexs.debugger.flash.messages.in.InBreakAtExt; +import com.jpexs.debugger.flash.messages.in.InBreakReason; +import com.jpexs.debugger.flash.messages.in.InCallFunction; +import com.jpexs.debugger.flash.messages.in.InConstantPool; +import com.jpexs.debugger.flash.messages.in.InContinue; +import com.jpexs.debugger.flash.messages.in.InFrame; +import com.jpexs.debugger.flash.messages.in.InGetSwd; +import com.jpexs.debugger.flash.messages.in.InGetSwf; +import com.jpexs.debugger.flash.messages.in.InGetVariable; +import com.jpexs.debugger.flash.messages.in.InNumScript; +import com.jpexs.debugger.flash.messages.in.InProcessTag; +import com.jpexs.debugger.flash.messages.in.InScript; +import com.jpexs.debugger.flash.messages.in.InSetBreakpoint; +import com.jpexs.debugger.flash.messages.in.InSwfInfo; +import com.jpexs.debugger.flash.messages.in.InTrace; +import com.jpexs.debugger.flash.messages.in.InVersion; +import com.jpexs.debugger.flash.messages.out.OutAddWatch2; +import com.jpexs.debugger.flash.messages.out.OutGetBreakReason; +import com.jpexs.debugger.flash.messages.out.OutGetSwd; +import com.jpexs.debugger.flash.messages.out.OutGetSwf; +import com.jpexs.debugger.flash.messages.out.OutPlay; +import com.jpexs.debugger.flash.messages.out.OutProcessedTag; +import com.jpexs.debugger.flash.messages.out.OutRewind; +import com.jpexs.debugger.flash.messages.out.OutSwfInfo; +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.graph.DottedChain; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * @author JPEXS + */ +public class DebuggerHandler implements DebugConnectionListener { + + private boolean connected = false; + + private DebuggerCommands commands = null; + + private List swfs = new ArrayList<>(); + + private boolean paused = true; + + private Map modulePaths = new HashMap<>(); + + private Map moduleToSwfIndex = new HashMap<>(); + + private Map scriptToModule = new HashMap<>(); + + private Map moduleToTraitIndex = new HashMap<>(); + + private Map moduleToClassIndex = new HashMap<>(); + + private Map moduleToMethodIndex = new HashMap<>(); + + private Map> toAddBPointMap = new HashMap<>(); + + private Map> confirmedPointMap = new HashMap<>(); + + private Map> invalidBreakPointMap = new HashMap<>(); + + private Map> toRemoveBPointMap = new HashMap<>(); + + private int breakIp = -1; + + private String breakScriptName = null; + + public static class ActionScriptException extends Exception { + + private String errorClass; + + public ActionScriptException(String errorClass, String message) { + super(message); + this.errorClass = errorClass; + } + + public ActionScriptException(String errorClsMessage) { + this(errorClsMessage.substring(0, errorClsMessage.indexOf(": ")), errorClsMessage.substring(errorClsMessage.indexOf(": ") + 2)); + } + + public String getErrorClass() { + return errorClass; + } + } + + public int getBreakIp() { + if (!isPaused()) { + return -1; + } + return breakIp; + } + + public String getBreakScriptName() { + if (!isPaused()) { + return "-"; + } + return breakScriptName; + } + + public InGetVariable getVariable(long parentId, String varName, boolean children) { + try { + return commands.getVariable(parentId, varName, true, children); + } catch (IOException ex) { + return null; + } + } + + public void setVariable(long parentId, String varName, int valueType, Object value) { + try { + String svalue = ""; + switch (valueType) { + case VariableType.STRING: + svalue = "" + value; + break; + case VariableType.NUMBER: + svalue = "" + value; + break; + case VariableType.BOOLEAN: + svalue = ((Boolean) value) ? "true" : "false"; + break; + case VariableType.UNDEFINED: + svalue = "undefined"; + break; + case VariableType.NULL: + svalue = "undefined"; + break; + } + commands.setVariable(parentId, varName, valueType, svalue); + } catch (IOException ex) { + //ignore + } + } + + public synchronized void removeBreakPoint(String scriptName, int line) { + if (isBreakpointInvalid(scriptName, line)) { + invalidBreakPointMap.get(scriptName).remove(line); + if (invalidBreakPointMap.get(scriptName).isEmpty()) { + invalidBreakPointMap.remove(scriptName); + } + return; + } + if (isBreakpointToAdd(scriptName, line)) { + toAddBPointMap.get(scriptName).remove(line); + if (toAddBPointMap.get(scriptName).isEmpty()) { + toAddBPointMap.remove(scriptName); + } + } else if (isBreakpointConfirmed(scriptName, line)) { + if (!toRemoveBPointMap.containsKey(scriptName)) { + toRemoveBPointMap.put(scriptName, new TreeSet<>()); + } + toRemoveBPointMap.get(scriptName).add(line); + } + try { + sendBreakPoints(false); + } catch (IOException ex) { + //ignore + } + } + + private int watchTag = 1; + + public synchronized com.jpexs.debugger.flash.DebuggerCommands.Watch addWatch(Variable v, long v_id, boolean watchRead, boolean watchWrite) { + int tag = watchTag++; + try { + return commands.addWatch(v_id, v.name, (watchRead ? OutAddWatch2.FLAG_READ : 0) | (watchWrite ? OutAddWatch2.FLAG_WRITE : 0), tag); + } catch (IOException ex) { + return null; + } + } + + public synchronized Set getBreakPoints(String scriptName, boolean onlyValid) { + Set lines = new TreeSet<>(); + if (confirmedPointMap.containsKey(scriptName)) { + lines.addAll(confirmedPointMap.get(scriptName)); + } + if (toAddBPointMap.containsKey(scriptName)) { + lines.addAll(toAddBPointMap.get(scriptName)); + } + if (!onlyValid && invalidBreakPointMap.containsKey(scriptName)) { + lines.addAll(invalidBreakPointMap.get(scriptName)); + } + return lines; + } + + public synchronized void clearBreakPoints() { + for (String scriptName : confirmedPointMap.keySet()) { + if (!toAddBPointMap.containsKey(scriptName)) { + toAddBPointMap.put(scriptName, new TreeSet<>()); + } + toAddBPointMap.get(scriptName).addAll(confirmedPointMap.get(scriptName)); + } + for (String scriptName : invalidBreakPointMap.keySet()) { + if (!toAddBPointMap.containsKey(scriptName)) { + toAddBPointMap.put(scriptName, new TreeSet<>()); + } + toAddBPointMap.get(scriptName).addAll(invalidBreakPointMap.get(scriptName)); + } + confirmedPointMap.clear(); + invalidBreakPointMap.clear(); + } + + public synchronized Map> getAllBreakPoints(boolean validOnly) { + Map> ret = new HashMap<>(); + for (String scriptName : confirmedPointMap.keySet()) { + Set lines = new TreeSet<>(); + lines.addAll(confirmedPointMap.get(scriptName)); + ret.put(scriptName, lines); + } + for (String scriptName : toAddBPointMap.keySet()) { + if (!ret.containsKey(scriptName)) { + ret.put(scriptName, new TreeSet<>()); + } + ret.get(scriptName).addAll(toAddBPointMap.get(scriptName)); + } + if (!validOnly) { + for (String scriptName : invalidBreakPointMap.keySet()) { + if (!ret.containsKey(scriptName)) { + ret.put(scriptName, new TreeSet<>()); + } + ret.get(scriptName).addAll(invalidBreakPointMap.get(scriptName)); + } + } + return ret; + } + + public boolean addBreakPoint(String scriptName, int line) { + synchronized (this) { + Logger.getLogger(DebuggerHandler.class + .getName()).log(Level.FINE, "adding bp " + scriptName + ":" + line); + if (isBreakpointToRemove(scriptName, line)) { + toRemoveBPointMap.get(scriptName).remove(line); + if (toRemoveBPointMap.get(scriptName).isEmpty()) { + toRemoveBPointMap.remove(scriptName); + + } + } + + if (isBreakpointConfirmed(scriptName, line)) { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp " + scriptName + ":" + line + " already confirmed"); + return true; + + } + if (isBreakpointInvalid(scriptName, line)) { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "bp " + scriptName + ":" + line + " already invalid"); + return false; + } + if (!toAddBPointMap.containsKey(scriptName)) { + toAddBPointMap.put(scriptName, new TreeSet<>()); + } + toAddBPointMap.get(scriptName).add(line); + Logger + .getLogger(DebuggerHandler.class + .getName()).log(Level.FINE, "bp " + scriptName + ":" + line + " added to todo"); + } + try { + sendBreakPoints(false); + } catch (IOException ex) { + //ignored + } + + return true; + } + + public synchronized boolean isBreakpointConfirmed(String scriptName, int line) { + return confirmedPointMap.containsKey(scriptName) && confirmedPointMap.get(scriptName).contains(line); + } + + public synchronized boolean isBreakpointToAdd(String scriptName, int line) { + return toAddBPointMap.containsKey(scriptName) && toAddBPointMap.get(scriptName).contains(line); + } + + public synchronized boolean isBreakpointToRemove(String scriptName, int line) { + return toRemoveBPointMap.containsKey(scriptName) && toRemoveBPointMap.get(scriptName).contains(line); + } + + public synchronized boolean isBreakpointInvalid(String scriptName, int line) { + return invalidBreakPointMap.containsKey(scriptName) && invalidBreakPointMap.get(scriptName).contains(line); + } + + private synchronized void markBreakPointInvalid(String scriptName, int line) { + if (!invalidBreakPointMap.containsKey(scriptName)) { + invalidBreakPointMap.put(scriptName, new TreeSet<>()); + } + invalidBreakPointMap.get(scriptName).add(line); + } + + private InFrame frame; + + private InConstantPool pool; + + private InBreakAtExt breakInfo; + + private InBreakReason breakReason; + + private final List breakListeners = new ArrayList<>(); + + private final List traceListeners = new ArrayList<>(); + + private final List clisteners = new ArrayList<>(); + + public String moduleToString(int file) { + if (!modulePaths.containsKey(file)) { + return "unknown"; + } + return modulePaths.get(file); + } + + public synchronized InBreakAtExt getBreakInfo() { + if (!paused) { + return null; + } + return breakInfo; + } + + public synchronized InBreakReason getBreakReason() { + if (!paused) { + return null; + } + return breakReason; + } + + public static interface ConnectionListener { + + public void connected(); + + public void disconnected(); + } + + public static interface TraceListener { + + public void trace(String... val); + } + + public static interface BreakListener { + + public void breakAt(String scriptName, int line, int classIndex, int traitIndex, int methodIndex); + + public void doContinue(); + } + + public void addBreakListener(BreakListener l) { + breakListeners.add(l); + } + + public void addTraceListener(TraceListener l) { + traceListeners.add(l); + } + + public void removeTraceListener(TraceListener l) { + traceListeners.remove(l); + } + + public void removeBreakListener(BreakListener l) { + breakListeners.remove(l); + } + + public void addConnectionListener(ConnectionListener l) { + clisteners.add(l); + } + + public void removeConnectionListener(ConnectionListener l) { + clisteners.remove(l); + } + + public synchronized void refreshFrame() { + if (!paused) { + return; + } + try { + frame = commands.getFrame(0); + pool = commands.getConstantPool(0); + } catch (IOException ex) { + //ignore + } + } + + public synchronized InFrame getFrame() { + if (!paused) { + return null; + } + return frame; + } + + public synchronized int moduleIdOf(String pack) { + if (scriptToModule.containsKey(pack)) { + return scriptToModule.get(pack); + } + return -1; + } + + public boolean isPaused() { + if (!isConnected()) { + return false; + } + synchronized (this) { + return paused; + } + } + + public List getSwfs() { + return swfs; + } + + public void disconnect() { + frame = null; + pool = null; + breakInfo = null; + breakReason = null; + connected = false; + if (commands != null) { + commands.disconnect(); + } + commands = null; + synchronized (this) { + for (String scriptName : confirmedPointMap.keySet()) { + if (!toAddBPointMap.containsKey(scriptName)) { + toAddBPointMap.put(scriptName, new TreeSet<>()); + } + toAddBPointMap.get(scriptName).addAll(confirmedPointMap.get(scriptName)); + } + confirmedPointMap.clear(); + for (String scriptName : invalidBreakPointMap.keySet()) { + if (!toAddBPointMap.containsKey(scriptName)) { + toAddBPointMap.put(scriptName, new TreeSet<>()); + } + toAddBPointMap.get(scriptName).addAll(invalidBreakPointMap.get(scriptName)); + } + invalidBreakPointMap.clear(); + } + for (ConnectionListener l : clisteners) { + l.disconnected(); + } + } + + public synchronized boolean isConnected() { + return connected; + } + + public DebuggerCommands getCommands() throws IOException { + if (!isConnected() || commands == null) { + throw new IOException("Not connected"); + } + return commands; + } + + private static void enlog(Class cls) { + Level level = Level.FINEST; + + Logger mylog = Logger.getLogger(cls.getName()); + mylog.setLevel(level); + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(level); + mylog.addHandler(ch); + } + + @Override + public void failedListen(IOException ex) { + View.execInEventDispatch(new Runnable() { + @Override + public void run() { + disconnect(); + Main.stopRun(); + Main.stopWork(); + View.showMessageDialog(Main.getMainFrame().getPanel(), AppStrings.translate("error.debug.listen").replace("%port%", "" + Debugger.DEBUG_PORT)); + Main.getMainFrame().getPanel().updateMenu(); + } + }); + + } + + @Override + public void connected(DebuggerConnection con) { + clearBreakPoints(); + + Main.startWork(AppStrings.translate("work.debugging"), null); + + synchronized (this) { + paused = false; + } + + Main.getMainFrame().getPanel().updateMenu(); + + //enlog(DebuggerConnection.class); + //enlog(DebuggerCommands.class); + //enlog(DebuggerHandler.class); + try { + con.getMessage(InVersion.class); + } catch (IOException ex) { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.SEVERE, null, ex); + } + + //Respon to InProcessTag with OutProcessedTag + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InProcessTag message) { + try { + con.writeMessage(new OutProcessedTag(con)); + } catch (IOException ex) { + //disconnect(); + //ignore + } + } + }); + + swfs.clear(); + + Map moduleNames = new HashMap<>(); + + final Pattern patAS3 = Pattern.compile("^(.*);(.*);(.*)\\.as$"); + final Pattern patAS3PCode = Pattern.compile("^#PCODE abc:([0-9]+),script:([0-9]+),class:(-?[0-9]+),trait:(-?[0-9]+),method:([0-9]+),body:([0-9]+);(.*)$"); + + try { + + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InNumScript t) { + con.dropMessage(t); + } + }); + + modulePaths = new HashMap<>(); + scriptToModule = new HashMap<>(); + + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InScript sc) { + moduleNames.put(sc.module, sc.name); + moduleToSwfIndex.put(sc.module, sc.swfIndex); + int file = sc.module; + String name = sc.name; + String[] parts = name.split(";"); + + Matcher m; + if ((m = patAS3.matcher(name)).matches()) { + String clsNameWithSuffix = m.group(3); + String pkg = m.group(2).replace("\\", "."); + m = patAS3PCode.matcher(name); + + if (m.matches()) { + moduleToClassIndex.put(file, Integer.parseInt(m.group(3))); + moduleToTraitIndex.put(file, Integer.parseInt(m.group(4))); + moduleToMethodIndex.put(file, Integer.parseInt(m.group(5))); + name = DottedChain.parseWithSuffix(pkg).addWithSuffix(clsNameWithSuffix).toString(); + name = "#PCODE abc:" + m.group(1) + ",body:" + m.group(6) + ";" + name; + } else { + name = DottedChain.parseWithSuffix(pkg).addWithSuffix(clsNameWithSuffix).toString(); + } + } + modulePaths.put(file, name); + scriptToModule.put(name, file); + con.dropMessage(sc); + } + }); + + /*int numScript = con.getMessage(InNumScript.class).num; + for (int i = 0; i < numScript; i++) { + InScript sc = con.getMessage(InScript.class); + moduleNames.put(sc.module, sc.name); + }*/ + //Pattern patMainFrame = Pattern.compile("^Actions for Scene ([0-9]+): Frame ([0-9]+) of Layer Name .*$"); + //Pattern patSymbol = Pattern.compile("^Actions for Symbol ([0-9]+): Frame ([0-9]+) of Layer Name .*$"); + //Pattern patAS2 = Pattern.compile("^([^:]+): .*\\.as$"); + //con.getMessage(InSetBreakpoint.class); + commands = new DebuggerCommands(con); + + commands.stopWarning(); + commands.setStopOnFault(); + commands.setEnumerateOverride(); + commands.setNotifyFailure(); + commands.setInvokeSetters(); + commands.setSwfLoadNotify(); + commands.setGetterTimeout(1500); + commands.setSetterTimeout(5000); + + boolean isAS3 = (Main.getMainFrame().getPanel().getCurrentSwf().isAS3()); + con.isAS3 = isAS3; + + //Widelines - only AS3, it hangs in AS1/2 and SWD does not support UI32 lines + if (isAS3) { + con.wideLines = commands.getOption("wide_line_player", "false").equals("true"); + if (con.wideLines) { + commands.setOption("wide_line_debugger", "on"); + } + } + commands.squelch(true); + + con.writeMessage(new OutSwfInfo(con, 0)); + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InSwfInfo t) { + for (InSwfInfo.SwfInfo s : t.swfInfos) { + swfs.add(s); + View.execInEventDispatch(new Runnable() { + @Override + public void run() { + try { + con.sendMessage(new OutGetSwf(con, (int) s.index), InGetSwf.class); + con.sendMessage(new OutGetSwd(con, (int) s.index), InGetSwd.class); + } catch (IOException ex) { + //ignore + } + } + }); + } + con.dropMessage(t); + } + }); + + InSetBreakpoint isb = con.getMessage(InSetBreakpoint.class); + synchronized (this) { + for (int i = 0; i < isb.files.size(); i++) { + String sname = moduleNames.get(isb.files.get(i)); + if (!confirmedPointMap.containsKey(sname)) { + confirmedPointMap.put(sname, new TreeSet<>()); + } + if (toAddBPointMap.containsKey(sname)) { + toAddBPointMap.get(sname).remove(isb.lines.get(i)); + if (toAddBPointMap.get(sname).isEmpty()) { + toAddBPointMap.remove(sname); + } + } + confirmedPointMap.get(sname).add(isb.lines.get(i)); + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} submitted successfully", new Object[]{sname, isb.lines.get(i)}); + } + } + + synchronized (this) { + connected = true; + } + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InAskBreakpoints message) { + + } + }); + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InContinue msg) { + synchronized (DebuggerHandler.this) { + paused = false; + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "continued"); + } + for (BreakListener bl : breakListeners) { + bl.doContinue(); + } + } + }); + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InBreakAt message) { + synchronized (DebuggerHandler.this) { + paused = true; + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "paused"); + + } + + try { + breakInfo = con.getMessage(InBreakAtExt.class); + breakReason = con.sendMessage(new OutGetBreakReason(con), InBreakReason.class); + + String newBreakScriptName = "unknown"; + if (modulePaths.containsKey(message.file)) { + newBreakScriptName = modulePaths.get(message.file); + + } else if (breakReason.reason != InBreakReason.REASON_SCRIPT_LOADED) { + Logger.getLogger(DebuggerCommands.class.getName()).log(Level.SEVERE, "Invalid file: {0}", message.file); + return; + } + + final String[] reasonNames = new String[]{"unknown", "breakpoint", "watch", "fault", "stopRequest", "step", "halt", "scriptLoaded"}; + String reason = breakReason.reason < reasonNames.length ? reasonNames[breakReason.reason] : reasonNames[0]; + + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINE, "break at {0}:{1}, reason: {2}", new Object[]{newBreakScriptName, message.line, reason}); + + sendBreakPoints(false); + synchronized (DebuggerHandler.this) { + breakScriptName = newBreakScriptName; + breakIp = message.line; + } + + if (breakReason.reason == InBreakReason.REASON_SCRIPT_LOADED) { + if (!Configuration.debugHalt.get()) { + commands.sendContinue(); + return; + } + Main.startWork(AppStrings.translate("work.halted"), null); + } else { + Main.startWork(AppStrings.translate("work.breakat") + newBreakScriptName + ":" + message.line + " " + AppStrings.translate("debug.break.reason." + reason), null); + } + frame = commands.getFrame(0); + pool = commands.getConstantPool(0); + + for (BreakListener l : breakListeners) { + l.breakAt(newBreakScriptName, message.line, + moduleToClassIndex.containsKey(message.file) ? moduleToClassIndex.get(message.file) : -1, + moduleToTraitIndex.containsKey(message.file) ? moduleToTraitIndex.get(message.file) : -1, + moduleToMethodIndex.containsKey(message.file) ? moduleToMethodIndex.get(message.file) : -1 + ); + } + + } catch (IOException ex) { + //ignore + } + + } + }); + + for (ConnectionListener l : clisteners) { + l.connected(); + } + + con.addMessageListener(new DebugMessageListener() { + @Override + public void message(InTrace tr) { + for (TraceListener l : traceListeners) { + l.trace(tr.text); + } + } + }); + + if (!isAS3) { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINER, "End of connect - sending continue"); + con.writeMessage(new OutRewind(con)); + con.writeMessage(new OutPlay(con)); + commands.sendContinue(); + } + + } catch (IOException ex) { + + synchronized (this) { + connected = false; + } + } + } + + private void sendBreakPoints(boolean force) throws IOException { + if (!force && !isPaused()) { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINEST, "not sending bps, not paused"); + return; + } + synchronized (this) { + for (String scriptName : toRemoveBPointMap.keySet()) { + int file = moduleIdOf(scriptName); + if (file > -1) { + for (int line : toRemoveBPointMap.get(scriptName)) { + if (isBreakpointConfirmed(scriptName, line)) { + commands.removeBreakPoint(file, line); + confirmedPointMap.get(scriptName).remove(line); + if (confirmedPointMap.get(scriptName).isEmpty()) { + confirmedPointMap.remove(scriptName); + } + } + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} removed", new Object[]{scriptName, line}); + } + } + } + toRemoveBPointMap.clear(); + + for (String scriptName : toAddBPointMap.keySet()) { + int file = moduleIdOf(scriptName); + if (file > -1) { + for (int line : toAddBPointMap.get(scriptName)) { + if (commands.addBreakPoint(file, line)) { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} submitted successfully", new Object[]{scriptName, line}); + if (!confirmedPointMap.containsKey(scriptName)) { + confirmedPointMap.put(scriptName, new TreeSet<>()); + } + confirmedPointMap.get(scriptName).add(line); + } else { + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.INFO, "Breakpoint {0}:{1} unable to submit", new Object[]{scriptName, line}); + markBreakPointInvalid(scriptName, line); + } + } + } else { + for (int line : toAddBPointMap.get(scriptName)) { + markBreakPointInvalid(scriptName, line); + } + } + } + toAddBPointMap.clear(); + + } + Logger.getLogger(DebuggerHandler.class.getName()).log(Level.FINEST, "sending bps finished"); + + } + + public synchronized InConstantPool getConstantPool() { + if (!paused) { + return null; + } + return pool; + } + + public synchronized InCallFunction callMethod(Variable object, String methodName, List args) throws ActionScriptException { + return callFunction(false, methodName, object, args); + } + + public synchronized InCallFunction callMethod(String object, String methodName, List args) throws ActionScriptException { + InGetVariable igv = getVariable(0, object, false); + return callMethod(igv.parent, methodName, args); + } + + private static String typeAsStr(Object value) { + if (value == null) { + return "null"; + } + if ((value instanceof Long) || (value instanceof Integer)) { + return "int"; + } + if (value instanceof Number) { + return "Number"; + } + if (value instanceof String) { + return "String"; + } + if (value instanceof Variable) { + Variable v = (Variable) value; + return v.getTypeAsStr(); + } + return "String"; + } + + private static String valueAsStr(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof Double) { + double doubleValue = ((Double) value).doubleValue(); + long longValue = (long) doubleValue; + if (doubleValue == longValue) { + return Long.toString(longValue); + } + } + + if (value instanceof Variable) { + Variable v = (Variable) value; + return valueAsStr(v.value); + } + return "" + value; + } + + public synchronized InCallFunction callFunction(boolean isConstructor, String funcName, Variable thisValue, List args) throws ActionScriptException { + List argTypes = new ArrayList<>(); + List argValues = new ArrayList<>(); + for (Object value : args) { + argTypes.add(typeAsStr(value)); + argValues.add(valueAsStr(value)); + } + String thisType = typeAsStr(thisValue); + String thisValueStr = valueAsStr(thisValue); + try { + InCallFunction icf = commands.callFunction(isConstructor, funcName, thisType, thisValueStr, argTypes, argValues); + if (!icf.variables.isEmpty()) { + if ((icf.variables.get(0).flags & VariableFlags.IS_EXCEPTION) > 0) { + throw new ActionScriptException("" + icf.variables.get(0).value); + } + } + return icf; + } catch (IOException e) { + + } + return null; + } +}