From f04fcb151efc2c0f1523076340ecd74ceb9ccc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Mon, 27 Oct 2025 21:50:10 +0100 Subject: [PATCH] Fixed: #2545 FLA export - normalize font em square --- CHANGELOG.md | 2 + .../decompiler/flash/FontNormalizer.java | 639 ++++++++++++++---- .../flash/exporters/DualPdfGraphics2D.java | 7 +- .../flash/exporters/FrameExporter.java | 5 +- .../exporters/RequiresNormalizedFonts.java | 5 +- .../exporters/commonshape/SVGExporter.java | 7 +- .../flash/tags/DefineEditTextTag.java | 28 +- .../decompiler/flash/tags/base/FontTag.java | 33 +- .../flash/tags/base/StaticTextTag.java | 10 +- .../decompiler/flash/tags/base/TextTag.java | 11 +- .../decompiler/flash/xfl/XFLConverter.java | 77 ++- 11 files changed, 645 insertions(+), 179 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9646fd9f2..8c5584e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - [PR224] Font CLI replace throwing NullPointer - [#2546] AS3 findProperty vs temporary registers - [#2547] SVG import - improper linearGradient with userSpaceOnUse gradient units +- [#2545] FLA export - normalize font em square ## [24.1.0] - 2025-09-28 ### Added @@ -4027,6 +4028,7 @@ Major version of SWF to XML export changed to 2. [#2540]: https://www.free-decompiler.com/flash/issues/2540 [#2546]: https://www.free-decompiler.com/flash/issues/2546 [#2547]: https://www.free-decompiler.com/flash/issues/2547 +[#2545]: https://www.free-decompiler.com/flash/issues/2545 [#2477]: https://www.free-decompiler.com/flash/issues/2477 [#2478]: https://www.free-decompiler.com/flash/issues/2478 [#2485]: https://www.free-decompiler.com/flash/issues/2485 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java index ec5bad68b..43e44912b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java @@ -17,9 +17,17 @@ package com.jpexs.decompiler.flash; import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.tags.DefineEditTextTag; import com.jpexs.decompiler.flash.tags.base.CharacterTag; import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.tags.base.StaticTextTag; +import com.jpexs.decompiler.flash.tags.base.TextTag; +import com.jpexs.decompiler.flash.tags.dynamictext.CharacterWithStyle; +import com.jpexs.decompiler.flash.tags.dynamictext.TextStyle; +import com.jpexs.decompiler.flash.tags.text.xml.XmlException; +import com.jpexs.decompiler.flash.tags.text.xml.XmlLexer; +import com.jpexs.decompiler.flash.tags.text.xml.XmlParsedSymbol; +import com.jpexs.decompiler.flash.tags.text.xml.XmlSymbolType; import com.jpexs.decompiler.flash.types.GLYPHENTRY; import com.jpexs.decompiler.flash.types.RECT; import com.jpexs.decompiler.flash.types.SHAPE; @@ -28,60 +36,67 @@ import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import java.awt.Font; import java.awt.Point; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Stack; /** - * Font size / orientation normalizer. - * Does following: - * - shrinks oversized fonts to 1024 em - * - fixes vertically flipped fonts / texts - * - fixes zero/1unit spaces font glyph advance - * - fixes zero last glyph advance in texts - * + * Font size / orientation normalizer. Does following: - shrinks oversized fonts + * to 1024 em - fixes vertically flipped fonts / texts - fixes zero/1unit spaces + * font glyph advance - fixes zero last glyph advance in texts + * * @author JPEXS */ public class FontNormalizer { /** * Normalizes fonts in the SWF file in place. + * * @param swf SWF */ public void normalizeFonts(SWF swf) { normalizeFonts(swf, true, new LinkedHashMap<>(), new LinkedHashMap<>()); } - + /** * Normalizes fonts in the SWF file creating clones of font/text tags. + * * @param swf SWF file * @param outFonts Modified fonts (clone) - fontId to fontTag * @param outTexts Modified texts (clone) - textId to textTag */ - public void normalizeFonts(SWF swf, Map outFonts, Map outTexts) { + public void normalizeFonts(SWF swf, Map outFonts, Map outTexts) { normalizeFonts(swf, false, outFonts, outTexts); } - + /** * Normalizes fonts in the SWF file. + * * @param swf SWF file * @param inPlace Modify tags in SWF file (true) or create clones (false) * @param outFonts Modified fonts - fontId to fontTag * @param outTexts Modified texts - textId to textTag */ - public void normalizeFonts(SWF swf, boolean inPlace, Map outFonts, Map outTexts) { + public void normalizeFonts(SWF swf, boolean inPlace, Map outFonts, Map outTexts) { Map characters = swf.getCharacters(!inPlace); - Map texts = new LinkedHashMap<>(); + Map texts = new LinkedHashMap<>(); for (int characterId : characters.keySet()) { CharacterTag character = characters.get(characterId); - if (character instanceof StaticTextTag) { - texts.put(characterId, (StaticTextTag) character); + if (character instanceof TextTag) { + texts.put(characterId, (TextTag) character); } } @@ -89,21 +104,29 @@ public class FontNormalizer { Set notInvertedFontIds = new LinkedHashSet<>(); Set fontIds = new LinkedHashSet<>(); - for (StaticTextTag text : texts.values()) { - boolean inverted = false; - if (text.textMatrix != null) { - if (text.textMatrix.scaleY < 0) { - inverted = true; - } + for (TextTag text : texts.values()) { + if (text instanceof DefineEditTextTag) { + DefineEditTextTag detext = (DefineEditTextTag) text; + fontIds.addAll(getDefineEditTextFonts(detext)); } - for (TEXTRECORD rec : text.textRecords) { - if (rec.styleFlagsHasFont) { - if (inverted) { - invertedFontIds.add(rec.fontId); - } else { - notInvertedFontIds.add(rec.fontId); + + if (text instanceof StaticTextTag) { + StaticTextTag stext = (StaticTextTag) text; + boolean inverted = false; + if (stext.textMatrix != null) { + if (stext.textMatrix.scaleY < 0) { + inverted = true; + } + } + for (TEXTRECORD rec : stext.textRecords) { + if (rec.styleFlagsHasFont) { + if (inverted) { + invertedFontIds.add(rec.fontId); + } else { + notInvertedFontIds.add(rec.fontId); + } + fontIds.add(rec.fontId); } - fontIds.add(rec.fontId); } } } @@ -121,61 +144,71 @@ public class FontNormalizer { } FontTag font = (FontTag) fontCharacter; - int minY = Integer.MAX_VALUE; - int maxY = Integer.MIN_VALUE; - for (SHAPE shp : font.getGlyphShapeTable()) { - RECT b = shp.getBounds(1); - if (b.Ymin < minY) { - minY = b.Ymin; - } - if (b.Ymax > maxY) { - maxY = b.Ymax; - } - } - int maxH = maxY - minY; - - int originalEmSize = (int) Math.round(maxH / font.getDivider()); - double scale = 1.0; boolean willModify = false; - if (originalEmSize > 1024) { - scale = 1024.0 / originalEmSize; - willModify = true; - } if (invertedFontIds.contains(fontId)) { willModify = true; } - + + double newScale = 1; + + String systemFont = font.getSystemFontName(); + + List shapes1 = font.getGlyphShapeTable(); + Double h = null; + Double systemH = null; + for (int i = 0; i < shapes1.size(); i++) { + RECT b = shapes1.get(i).getBounds(1); + h = b.getHeight() / font.getDivider(); + if (h <= 0) { + continue; + } + char c = font.glyphToChar(i); + Font f = new Font(systemFont, (font.isBold() ? Font.BOLD : 0) | (font.isItalic() ? Font.ITALIC : 0), 1000); + if (!f.canDisplay(c)) { + continue; + } + FontRenderContext frc = new FontRenderContext(null, true, true); + GlyphVector gv = f.createGlyphVector(frc, new char[]{c}); + systemH = gv.getGlyphOutline(0).getBounds2D().getHeight(); + break; + } + + if (h != null && systemH != null) { + newScale = systemH / h; + willModify = true; + } + + final double scale = newScale; + int spaceGlyph = font.charToGlyph(' '); int nonBreakingSpaceGlyph = font.charToGlyph((char) 0xA0); - + if (spaceGlyph != -1 && font.getGlyphAdvance(spaceGlyph) <= 1) { willModify = true; } - + if (nonBreakingSpaceGlyph != -1 && font.getGlyphAdvance(nonBreakingSpaceGlyph) <= 1) { willModify = true; } - + if (!willModify) { continue; } - - + //scale = 1; - fontNewScale.put(fontId, scale); FontTag font2; if (inPlace) { font2 = font; - } else { + } else { try { font2 = (FontTag) font.cloneTag(); } catch (InterruptedException | IOException ex) { continue; } } - outFonts.put(fontId, font2); - + outFonts.put(fontId, font2); + List shapes = font2.getGlyphShapeTable(); Matrix matrix = new Matrix(); @@ -199,110 +232,460 @@ public class FontNormalizer { font2.setAscent((int) Math.round(font2.getAscent() * scale)); font2.setDescent((int) Math.round(font2.getDescent() * scale)); font2.setLeading((int) Math.round(font2.getLeading() * scale)); - + if (invertedFontIds.contains(fontId)) { int ascent = font2.getAscent(); int descent = font2.getDescent(); //switch ascent and descent font2.setAscent(descent); font2.setDescent(ascent); - + //what to do with leading? - } + } } - + if (spaceGlyph != -1 && font.getGlyphAdvance(spaceGlyph) <= 1) { font2.setGlyphAdvance(spaceGlyph, 512); } - + if (nonBreakingSpaceGlyph != -1 && font.getGlyphAdvance(nonBreakingSpaceGlyph) <= 1) { font2.setGlyphAdvance(nonBreakingSpaceGlyph, 512); } - + font2.setModified(true); } for (int textId : texts.keySet()) { int fontId = -1; int textHeight = 12 * 20; - StaticTextTag text = texts.get(textId); - StaticTextTag text2 = null; - for (int i = 0; i < text.textRecords.size(); i++) { - TEXTRECORD rec = text.textRecords.get(i); - if (rec.styleFlagsHasFont) { - fontId = rec.fontId; - if (fontNewScale.containsKey(fontId) || invertedFontIds.contains(fontId)) { - if (text2 == null) { - if (inPlace) { - text2 = text; - } else { - try { - text2 = (StaticTextTag) text.cloneTag(); - } catch (InterruptedException | IOException ex) { - break; + if (texts.get(textId) instanceof DefineEditTextTag) { + DefineEditTextTag text = (DefineEditTextTag) texts.get(textId); + scaleDefineEditTextFonts(text, fontNewScale, inPlace, outTexts); + } else if (texts.get(textId) instanceof StaticTextTag) { + StaticTextTag text = (StaticTextTag) texts.get(textId); + StaticTextTag text2 = null; + for (int i = 0; i < text.textRecords.size(); i++) { + TEXTRECORD rec = text.textRecords.get(i); + if (rec.styleFlagsHasFont) { + fontId = rec.fontId; + if (fontNewScale.containsKey(fontId) || invertedFontIds.contains(fontId)) { + if (text2 == null) { + if (inPlace) { + text2 = text; + } else { + try { + text2 = (StaticTextTag) text.cloneTag(); + } catch (InterruptedException | IOException ex) { + break; + } } + outTexts.put(textId, text2); + text2.setModified(true); } - outTexts.put(textId, text2); - text2.setModified(true); } - } - textHeight = text.textRecords.get(i).textHeight; - - if (fontNewScale.containsKey(fontId)) { - text2.textRecords.get(i).textHeight /= fontNewScale.get(fontId); - textHeight = text2.textRecords.get(i).textHeight; - } - if (invertedFontIds.contains(fontId)) { - if (text2.textMatrix != null && text2.textMatrix.scaleY < 0) { - text2.textMatrix.scaleY *= -1; - } - } - - } - - if (invertedFontIds.contains(fontId)) { - if (rec.styleFlagsHasYOffset) { - text2.textRecords.get(i).yOffset = - rec.yOffset; - } - } - if (!rec.glyphEntries.isEmpty() && rec.glyphEntries.get(rec.glyphEntries.size() - 1).glyphAdvance == 0) { - FontTag font; - if (outFonts.containsKey(fontId)) { - font = outFonts.get(fontId); - } else { - font = swf.getFont(fontId); - } - if (font != null) { - if (text2 == null) { - if (inPlace) { - text2 = text; - } else { - try { - text2 = (StaticTextTag) text.cloneTag(); - } catch (InterruptedException | IOException ex) { - break; - } - } - outTexts.put(textId, text2); - text2.setModified(true); - } - - GLYPHENTRY lastGlyphEntry = text2.textRecords.get(i).glyphEntries.get(rec.glyphEntries.size() - 1); - lastGlyphEntry.glyphAdvance = (int) Math.round(font.getGlyphAdvance(lastGlyphEntry.glyphIndex) * textHeight / (1024.0 * font.getDivider())); + textHeight = text.textRecords.get(i).textHeight; - if (i + 1 < text.textRecords.size()) { - TEXTRECORD nextRec = text2.textRecords.get(i + 1); - if (!nextRec.styleFlagsHasXOffset && !nextRec.glyphEntries.isEmpty()) { - nextRec.glyphEntries.get(0).glyphAdvance -= lastGlyphEntry.glyphAdvance; + if (fontNewScale.containsKey(fontId)) { + text2.textRecords.get(i).textHeight = round20(text2.textRecords.get(i).textHeight / fontNewScale.get(fontId)); + textHeight = text2.textRecords.get(i).textHeight; + } + if (invertedFontIds.contains(fontId)) { + if (text2.textMatrix != null && text2.textMatrix.scaleY < 0) { + text2.textMatrix.scaleY *= -1; } } - } + + } + + if (invertedFontIds.contains(fontId)) { + if (rec.styleFlagsHasYOffset) { + text2.textRecords.get(i).yOffset = -rec.yOffset; + } + } + if (!rec.glyphEntries.isEmpty() && rec.glyphEntries.get(rec.glyphEntries.size() - 1).glyphAdvance == 0) { + FontTag font; + if (outFonts.containsKey(fontId)) { + font = outFonts.get(fontId); + } else { + font = swf.getFont(fontId); + } + if (font != null) { + if (text2 == null) { + if (inPlace) { + text2 = text; + } else { + try { + text2 = (StaticTextTag) text.cloneTag(); + } catch (InterruptedException | IOException ex) { + break; + } + } + outTexts.put(textId, text2); + text2.setModified(true); + } + + GLYPHENTRY lastGlyphEntry = text2.textRecords.get(i).glyphEntries.get(rec.glyphEntries.size() - 1); + lastGlyphEntry.glyphAdvance = (int) Math.round(font.getGlyphAdvance(lastGlyphEntry.glyphIndex) * textHeight / (1024.0 * font.getDivider())); + + if (i + 1 < text.textRecords.size()) { + TEXTRECORD nextRec = text2.textRecords.get(i + 1); + if (!nextRec.styleFlagsHasXOffset && !nextRec.glyphEntries.isEmpty()) { + nextRec.glyphEntries.get(0).glyphAdvance -= lastGlyphEntry.glyphAdvance; + } + } + } + } } } } swf.clearShapeCache(); } + private static int round20(double val) { + return (int) Math.floor(val / 20.0) * 20; + } + + private Set getDefineEditTextFonts(DefineEditTextTag text) { + Set ret = new LinkedHashSet<>(); + TextStyle style = new TextStyle(); + if (text.fontClass != null) { + style.font = text.getSwf().getFontByClass(text.fontClass); + } else { + style.font = text.getSwf().getFont(text.fontId); + } + int fontId = text.getSwf().getCharacterId(style.font); + ret.add(fontId); + + if (text.html) { + final Stack styles = new Stack<>(); + styles.add(style); + XmlLexer lexer = new XmlLexer(new StringReader(text.initialText)); + try { + XmlParsedSymbol s = lexer.yylex(); + boolean inOpenTag = false; + String attributeName = null; + String tagName = null; + Map attributes = new LinkedHashMap<>(); + loops: + while (s.type != XmlSymbolType.EOF) { + switch (s.type) { + case TAG_OPEN: + inOpenTag = true; + attributeName = null; + tagName = (String) s.value; + attributes.clear(); + break; + case ATTRIBUTE: + attributeName = (String) s.value; + break; + case ATTRIBUTE_VALUE: + if (attributeName == null) { + //Error + break loops; + } + attributes.put(attributeName, (String) s.value); + break; + case TAG_OPEN_END: + style = styles.peek(); + switch (tagName) { + case "p": + // todo: parse the following attribute: + // align + break; + case "a": + // todo: handle link - href, target attributes + break; + case "b": + style = style.clone(); + style.bold = true; + styles.add(style); + break; + case "i": + style = style.clone(); + style.italic = true; + styles.add(style); + break; + case "u": + style = style.clone(); + style.underlined = true; + styles.add(style); + break; + case "font": + style = style.clone(); + String face = attributes.get("face"); + + if (face != null && face.length() > 0) { + style.fontFace = face; + } + + String size = attributes.get("size"); + if (size != null && size.length() > 0) { + + if (style.fontFace != null && text.useOutlines) { + CharacterTag ct = text.getSwf().getCharacterByExportName(style.fontFace); + if (ct != null && (ct instanceof FontTag)) { + style.font = (FontTag) ct; + } else { + style.font = text.getSwf().getFontByNameInTag(style.fontFace, style.bold, style.italic); + } + if (style.font == null) { + style.fontFace = null; + } else { + fontId = text.getSwf().getCharacterId(style.font); + ret.add(fontId); + } + } + + } + + styles.add(style); + break; + } + tagName = null; + break; + case TAG_CLOSE: + tagName = (String) s.value; + switch (tagName) { + case "b": + case "i": + case "u": + case "font": + styles.pop(); + break; + } + tagName = null; + break; + } + s = lexer.yylex(); + } + } catch (IOException ex) { + //Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex); + } catch (XmlException ex) { + //Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex); + //ex.printStackTrace(); + } + } + return ret; + } + + private void scaleDefineEditTextFonts(DefineEditTextTag text, Map fontNewScale, boolean inPlace, Map outTexts) { + String str = ""; + TextStyle style = new TextStyle(); + if (text.fontClass != null) { + style.font = text.getSwf().getFontByClass(text.fontClass); + } else { + style.font = text.getSwf().getFont(text.fontId); + } + int textId = text.getSwf().getCharacterId(text); + int fontId = text.getSwf().getCharacterId(style.font); + DefineEditTextTag text2 = null; + if (fontNewScale.containsKey(fontId)) { + if (inPlace) { + text2 = text; + } else { + try { + text2 = (DefineEditTextTag) text.cloneTag(); + } catch (InterruptedException | IOException ex) { + return; + } + } + text2.fontHeight = round20(text2.fontHeight / fontNewScale.get(fontId)); + outTexts.put(textId, text2); + text2.setModified(true); + } + + style.fontHeight = text.fontHeight; + style.fontLeading = text.leading; + if (text.hasTextColor) { + style.textColor = text.textColor; + } + if (text.hasText) { + str = text.initialText; + } + style.leftMargin = text.leftMargin; + final List ret = new ArrayList<>(); + if (text.html) { + StringBuilder sb = new StringBuilder(); + //SAXParserFactory factory = SAXParserFactory.newInstance(); + //SAXParser saxParser; + final Stack styles = new Stack<>(); + styles.add(style); + XmlLexer lexer = new XmlLexer(new StringReader(text.initialText)); + try { + XmlParsedSymbol s = lexer.yylex(); + boolean inOpenTag = false; + String attributeName = null; + String tagName = null; + Map attributes = new LinkedHashMap<>(); + loops: + while (s.type != XmlSymbolType.EOF) { + switch (s.type) { + case TAG_OPEN: + inOpenTag = true; + attributeName = null; + tagName = (String) s.value; + attributes.clear(); + break; + case ATTRIBUTE: + attributeName = (String) s.value; + break; + case ATTRIBUTE_VALUE: + if (attributeName == null) { + //Error + break loops; + } + attributes.put(attributeName, (String) s.value); + break; + case TAG_OPEN_END: + style = styles.peek(); + switch (tagName) { + case "p": + // todo: parse the following attribute: + // align + break; + case "a": + // todo: handle link - href, target attributes + break; + case "b": + style = style.clone(); + style.bold = true; + styles.add(style); + break; + case "i": + style = style.clone(); + style.italic = true; + styles.add(style); + break; + case "u": + style = style.clone(); + style.underlined = true; + styles.add(style); + break; + case "font": + style = style.clone(); + String face = attributes.get("face"); + + if (face != null && face.length() > 0) { + style.fontFace = face; + } + + String size = attributes.get("size"); + if (size != null && size.length() > 0) { + + if (style.fontFace != null && text.useOutlines) { + CharacterTag ct = text.getSwf().getCharacterByExportName(style.fontFace); + if (ct != null && (ct instanceof FontTag)) { + style.font = (FontTag) ct; + } else { + style.font = text.getSwf().getFontByNameInTag(style.fontFace, style.bold, style.italic); + } + if (style.font == null) { + style.fontFace = null; + } else { + fontId = text.getSwf().getCharacterId(style.font); + + if (fontNewScale.containsKey(fontId)) { + if (text2 == null) { + if (inPlace) { + text2 = text; + } else { + try { + text2 = (DefineEditTextTag) text.cloneTag(); + } catch (InterruptedException | IOException ex) { + return; + } + } + outTexts.put(textId, text2); + text2.setModified(true); + } + + try { + char firstChar = size.charAt(0); + if (firstChar != '+' && firstChar != '-') { + int fontSize = Integer.parseInt(size); + //style.fontHeight = (int) Math.round(fontSize * SWF.unitDivisor); + attributes.put("size", "" + Math.round(fontSize / fontNewScale.get(fontId))); + } else { + int fontSizeDelta = (int) Math.round(Integer.parseInt(size.substring(1)) * SWF.unitDivisor); + attributes.put("size", "" + firstChar + Math.round(fontSizeDelta / fontNewScale.get(fontId))); + /*if (firstChar == '+') { + style.fontHeight = style.fontHeight + fontSizeDelta; + } else { + style.fontHeight = style.fontHeight - fontSizeDelta; + }*/ + } + style.fontLeading = text.leading; + } catch (NumberFormatException nfe) { + //do not change fontHeight or leading + } + } + } + + } + + } + + styles.add(style); + break; + } + sb.append("<").append(tagName); + for (String key : attributes.keySet()) { + sb.append(" ").append(key).append("=").append("\"").append(attributes.get(key)).append("\""); + } + sb.append(">"); + tagName = null; + break; + case TAG_CLOSE: + tagName = (String) s.value; + switch (tagName) { + case "b": + case "i": + case "u": + case "font": + styles.pop(); + break; + } + sb.append(""); + tagName = null; + break; + + case ENTITY: + sb.append("&").append(s.value).append(";"); + break; + case CHARACTER: + sb.append(s.value); + break; + } + s = lexer.yylex(); + } + if (text2 != null) { + text2.initialText = sb.toString(); + } + } catch (IOException ex) { + //Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex); + } catch (XmlException ex) { + //Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, null, ex); + //ex.printStackTrace(); + } + } + } + + // Percentile helper (p in <0,100>) + private static double percentile(List values, double p) { + if (values.isEmpty()) { + return 0.0; + } + Collections.sort(values); + double rank = (p / 100.0) * (values.size() - 1); + int lo = (int) Math.floor(rank); + int hi = (int) Math.ceil(rank); + if (lo == hi) { + return values.get(lo); + } + double w = rank - lo; + return values.get(lo) * (1.0 - w) + values.get(hi) * w; + } + private void transformSHAPE(Matrix matrix, SHAPE shape) { int x = 0; int y = 0; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/DualPdfGraphics2D.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/DualPdfGraphics2D.java index 6aea7dc18..5a3469f7c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/DualPdfGraphics2D.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/DualPdfGraphics2D.java @@ -21,6 +21,7 @@ import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.exporters.modes.FontExportMode; import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.tags.base.StaticTextTag; +import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.DynamicTextGlyphEntry; import com.jpexs.decompiler.flash.types.GLYPHENTRY; @@ -71,7 +72,7 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, private final Map existingFonts; private Map normalizedFonts = new LinkedHashMap<>(); - private Map normalizedTexts = new LinkedHashMap<>(); + private Map normalizedTexts = new LinkedHashMap<>(); public DualPdfGraphics2D(Graphics2D first, PDFGraphics second, Map existingFonts) { this.imageGraphics = first; @@ -694,7 +695,7 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, } @Override - public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts) { + public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts) { this.normalizedFonts = normalizedFonts; this.normalizedTexts = normalizedTexts; } @@ -705,7 +706,7 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, } @Override - public Map getNormalizedTexts() { + public Map getNormalizedTexts() { return normalizedTexts; } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java index 58847505d..13db946d8 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java @@ -42,6 +42,7 @@ import com.jpexs.decompiler.flash.tags.base.CharacterTag; import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.tags.base.RenderContext; import com.jpexs.decompiler.flash.tags.base.StaticTextTag; +import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.tags.enums.ImageFormat; import com.jpexs.decompiler.flash.timeline.DepthState; import com.jpexs.decompiler.flash.timeline.Frame; @@ -324,7 +325,7 @@ public class FrameExporter { FontNormalizer normalizer = new FontNormalizer(); Map normalizedFonts = new LinkedHashMap<>(); - Map normalizedTexts = new LinkedHashMap<>(); + Map normalizedTexts = new LinkedHashMap<>(); normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts); @@ -608,7 +609,7 @@ public class FrameExporter { FontNormalizer normalizer = new FontNormalizer(); Map normalizedFonts = new LinkedHashMap<>(); - Map normalizedTexts = new LinkedHashMap<>(); + Map normalizedTexts = new LinkedHashMap<>(); normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts); File f = new File(foutdir + File.separator + "frames.pdf"); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/RequiresNormalizedFonts.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/RequiresNormalizedFonts.java index 666909a77..cefa741ab 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/RequiresNormalizedFonts.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/RequiresNormalizedFonts.java @@ -18,6 +18,7 @@ package com.jpexs.decompiler.flash.exporters; import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.tags.base.StaticTextTag; +import com.jpexs.decompiler.flash.tags.base.TextTag; import java.util.Map; /** @@ -26,10 +27,10 @@ import java.util.Map; * @author JPEXS */ public interface RequiresNormalizedFonts { - public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts); + public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts); public Map getNormalizedFonts(); - public Map getNormalizedTexts(); + public Map getNormalizedTexts(); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java index 5d7aeeacf..08f758b88 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/commonshape/SVGExporter.java @@ -23,6 +23,7 @@ import com.jpexs.decompiler.flash.exporters.modes.FontExportMode; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.base.FontTag; import com.jpexs.decompiler.flash.tags.base.StaticTextTag; +import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.types.BlendMode; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.RECT; @@ -98,10 +99,10 @@ public class SVGExporter implements RequiresNormalizedFonts { private double zoom; private Map normalizedFonts = new LinkedHashMap<>(); - private Map normalizedTexts = new LinkedHashMap<>(); + private Map normalizedTexts = new LinkedHashMap<>(); @Override - public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts) { + public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts) { this.normalizedFonts = normalizedFonts; this.normalizedTexts = normalizedTexts; } @@ -112,7 +113,7 @@ public class SVGExporter implements RequiresNormalizedFonts { } @Override - public Map getNormalizedTexts() { + public Map getNormalizedTexts() { return normalizedTexts; } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java index 1dfeaa742..82cdc2a40 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineEditTextTag.java @@ -20,6 +20,7 @@ import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.SWFInputStream; import com.jpexs.decompiler.flash.SWFOutputStream; import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.RequiresNormalizedFonts; import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle; import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter; @@ -326,8 +327,8 @@ public class DefineEditTextTag extends TextTag { case '\'': str = "'"; break; - */ - + */ + int lastHtmlSourcePos = -1; for (TEXTRECORD r : recs) { if (r instanceof AdvancedTextRecord) { @@ -616,7 +617,7 @@ public class DefineEditTextTag extends TextTag { addCharacters(ret, txt, style); } }; - */ + */ XmlLexer lexer = new XmlLexer(new StringReader(str)); try { @@ -803,7 +804,7 @@ public class DefineEditTextTag extends TextTag { } catch (ParserConfigurationException | SAXException | IOException ex) { Logger.getLogger(DefineEditTextTag.class.getName()).log(Level.SEVERE, "Error parsing text " + getCharacterId(), ex); } - */ + */ } else { addCharacters(ret, str, style, 0); } @@ -1283,6 +1284,12 @@ public class DefineEditTextTag extends TextTag { @Override public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level, Matrix transformation, Matrix strokeTransformation) { + int realTextId = getSwf().getCharacterId(this); + if (exporter.getNormalizedTexts().containsKey(realTextId) && exporter.getNormalizedTexts().get(realTextId) instanceof DefineEditTextTag) { + DefineEditTextTag normalizedText = (DefineEditTextTag) exporter.getNormalizedTexts().get(realTextId); + normalizedText.render(TextRenderMode.SVG, null, exporter, null, new Matrix(), colorTransform, 1, 0, 0); + return; + } render(TextRenderMode.SVG, null, exporter, null, new Matrix(), colorTransform, 1, 0, 0); } @@ -1292,6 +1299,16 @@ public class DefineEditTextTag extends TextTag { } private void render(TextRenderMode renderMode, SerializableImage image, SVGExporter svgExporter, StringBuilder htmlCanvasBuilder, Matrix transformation, ColorTransform colorTransform, double zoom, int selectionStart, int selectionEnd) { + if (image.getGraphics() instanceof RequiresNormalizedFonts) { + RequiresNormalizedFonts g = (RequiresNormalizedFonts) image.getGraphics(); + Map normalizedTexts = g.getNormalizedTexts(); + int realTextId = getSwf().getCharacterId(this); + if (normalizedTexts.containsKey(realTextId) && normalizedTexts.get(realTextId) instanceof DefineEditTextTag && normalizedTexts.get(realTextId) != this) { + DefineEditTextTag normalizedText = (DefineEditTextTag) normalizedTexts.get(realTextId); + normalizedText.render(renderMode, image, svgExporter, htmlCanvasBuilder, transformation, colorTransform, zoom, selectionStart, selectionEnd); + return; + } + } if (border) { // border is always black, fill color is always white? RGB borderColor = new RGBA(Color.black); @@ -1606,7 +1623,7 @@ public class DefineEditTextTag extends TextTag { txt = txt.replace("'", "'"); return txt; } - + @Override public Map getNameProperties() { Map ret = super.getNameProperties(); @@ -1616,4 +1633,5 @@ public class DefineEditTextTag extends TextTag { } return ret; } + } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/FontTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/FontTag.java index 706bd7f67..03e0e233d 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/FontTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/FontTag.java @@ -423,6 +423,18 @@ public abstract class FontTag extends DrawableTag implements AloneTag { * @return System font name */ public String getSystemFontName() { + String ret = getSystemFontNameNoDefault(); + if (ret == null) { + return defaultFontName; + } + return ret; + } + + /** + * Gets system font name without backup to default font + * @return System font name + */ + public String getSystemFontNameNoDefault() { FontTag.ensureLoaded(); int fontId = getCharacterId(); String selectedFont = swf.sourceFontNamesMap.get(fontId); @@ -443,7 +455,7 @@ public abstract class FontTag extends DrawableTag implements AloneTag { } // findInstalledFontName always returns an available font name - return FontTag.findInstalledFontName(getFontName()); + return FontTag.findInstalledFontNameNoDefault(getFontName()); } /** @@ -664,11 +676,11 @@ public abstract class FontTag extends DrawableTag implements AloneTag { } /** - * Finds installed font name. + * Finds installed font name without backup to default font name * @param fontName Font name * @return Installed font name */ - public static String findInstalledFontName(String fontName) { + public static String findInstalledFontNameNoDefault(String fontName) { ensureLoaded(); if (installedFontsByName.containsKey(fontName)) { return fontName; @@ -679,7 +691,20 @@ public abstract class FontTag extends DrawableTag implements AloneTag { return beforeUnderscore; } } - return defaultFontName; + return null; + } + + /** + * Finds installed font name. + * @param fontName Font name + * @return Installed font name + */ + public static String findInstalledFontName(String fontName) { + String ret = findInstalledFontNameNoDefault(fontName); + if (ret == null) { + return defaultFontName; + } + return ret; } @Override diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java index 8411e7c53..f64af118a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/StaticTextTag.java @@ -1030,10 +1030,10 @@ public abstract class StaticTextTag extends TextTag { public void toImage(int frame, int time, int ratio, RenderContext renderContext, SerializableImage image, SerializableImage fullImage, boolean isClip, Matrix transformation, Matrix strokeTransformation, Matrix absoluteTransformation, Matrix fullTransformation, ColorTransform colorTransform, double unzoom, boolean sameImage, ExportRectangle viewRect, ExportRectangle viewRectRaw, boolean scaleStrokes, int drawMode, int blendMode, boolean canUseSmoothing) { if (image.getGraphics() instanceof RequiresNormalizedFonts) { RequiresNormalizedFonts g = (RequiresNormalizedFonts) image.getGraphics(); - Map normalizedTexts = g.getNormalizedTexts(); + Map normalizedTexts = g.getNormalizedTexts(); int realTextId = getSwf().getCharacterId(this); - if (normalizedTexts.containsKey(realTextId)) { - StaticTextTag normalizedText = normalizedTexts.get(realTextId); + if (normalizedTexts.containsKey(realTextId) && normalizedTexts.get(realTextId) instanceof StaticTextTag) { + StaticTextTag normalizedText = (StaticTextTag) normalizedTexts.get(realTextId); staticTextToImage(swf, normalizedText.textRecords, getTextNum(), image, normalizedText.textMatrix, transformation, colorTransform, renderContext.selectionText == this ? renderContext.selectionStart : 0, renderContext.selectionText == this ? renderContext.selectionEnd : 0); return; } @@ -1053,8 +1053,8 @@ public abstract class StaticTextTag extends TextTag { @Override public void toSVG(SVGExporter exporter, int ratio, ColorTransform colorTransform, int level, Matrix transformation, Matrix strokeTransformation) { int realTextId = getSwf().getCharacterId(this); - if (exporter.getNormalizedTexts().containsKey(realTextId)) { - StaticTextTag normalizedText = exporter.getNormalizedTexts().get(realTextId); + if (exporter.getNormalizedTexts().containsKey(realTextId) && exporter.getNormalizedTexts().get(realTextId) instanceof StaticTextTag) { + StaticTextTag normalizedText = (StaticTextTag) exporter.getNormalizedTexts().get(realTextId); staticTextToSVG(swf, normalizedText.textRecords, getTextNum(), exporter, getRect(), normalizedText.textMatrix, colorTransform, exporter.getZoom(), transformation); return; } 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 1f73ceb93..76612adfa 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 @@ -386,9 +386,10 @@ public abstract class TextTag extends DrawableTag { * Gets text records attributes. * @param list Text records * @param swf SWF + * @param normalizedFonts Normalized fonts * @return Text records attributes */ - public static Map getTextRecordsAttributes(List list, SWF swf) { + public static Map getTextRecordsAttributes(List list, SWF swf, Map normalizedFonts) { Map att = new HashMap<>(); RECT textBounds = new RECT(Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE); FontTag font = null; @@ -416,8 +417,14 @@ public abstract class TextTag extends DrawableTag { if (rec.styleFlagsHasFont) { FontTag font2 = rec.getFont(swf); if (font2 != null) { + + int fontId = swf.getCharacterId(font2); + if (normalizedFonts.containsKey(fontId)) { + font2 = normalizedFonts.get(fontId); + } + font = font2; - } + } textHeight = rec.textHeight; if (font == null) { Logger.getLogger(TextTag.class.getName()).log(Level.SEVERE, "Font with id={0} was not found.", rec.fontId); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java index b72f787dc..59e3574b2 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java @@ -18,6 +18,7 @@ package com.jpexs.decompiler.flash.xfl; import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler; import com.jpexs.decompiler.flash.FlashPlayerVersion; +import com.jpexs.decompiler.flash.FontNormalizer; import com.jpexs.decompiler.flash.ReadOnlyTagList; import com.jpexs.decompiler.flash.RetryTask; import com.jpexs.decompiler.flash.SWF; @@ -1574,16 +1575,16 @@ public class XFLConverter { return date.getTime() / 1000; } - private void convertLibrary(Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, HashMap datfiles, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Set smallShapes) throws XMLStreamException { + private void convertLibrary(Map normalizedFonts, Map normalizedTexts, Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, HashMap datfiles, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Set smallShapes) throws XMLStreamException { statusStack.pushStatus("media"); convertMedia(lastItemIdNumber, charactersExportedInFirstFrame, lastImportedId, characterNameMap, characterImportLinkageURL, characters, swf, characterVariables, characterClasses, tags, files, datfiles, writer, statusStack); statusStack.popStatus(); statusStack.pushStatus("symbols"); - convertSymbols(lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, tags, files, flaVersion, writer, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, smallShapes); + convertSymbols(normalizedFonts, normalizedTexts, lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, tags, files, flaVersion, writer, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, smallShapes); statusStack.popStatus(); } - private void convertSymbols(Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Set smallShapes) throws XMLStreamException { + private void convertSymbols(Map normalizedFonts, Map normalizedTexts, Reference lastItemIdNumber, Set charactersExportedInFirstFrame, Map characterImportLinkageURL, Set characters, Reference lastImportedId, Map characterNameMap, SWF swf, Map characterVariables, Map characterClasses, Map characterScriptPacks, List nonLibraryShapes, String backgroundColor, ReadOnlyTagList tags, HashMap files, FLAVersion flaVersion, XFLXmlWriter writer, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Set smallShapes) throws XMLStreamException { //boolean hasSymbol = false; Reference nextClipId = new Reference<>(-1); writer.writeStartElement("symbols"); @@ -1759,7 +1760,11 @@ public class XFLConverter { statusStack.popStatus(); } else if (character instanceof TextTag) { statusStack.pushStatus(character.toString()); - convertText(frame, new AccessibilityBag() /*???*/, null, (TextTag) character, matrix, filters, recCharWriter, characterImportLinkageURL, lastImportedId, characterNameMap, characters); + int realCharacterId = swf.getCharacterId(character); + if (normalizedTexts.containsKey(realCharacterId)) { + character = normalizedTexts.get(realCharacterId); + } + convertText(normalizedFonts, frame, new AccessibilityBag() /*???*/, null, (TextTag) character, matrix, filters, recCharWriter, characterImportLinkageURL, lastImportedId, characterNameMap, characters); statusStack.popStatus(); } else if (character instanceof DefineVideoStreamTag) { statusStack.pushStatus(character.toString()); @@ -1827,9 +1832,9 @@ public class XFLConverter { } final ScriptPack spriteScriptPack = characterScriptPacks.containsKey(sprite) ? characterScriptPacks.get(sprite) : null; - extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, sprite.getTags(), swf.getCharacterId(sprite), writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + extractMultilevelClips(normalizedFonts, normalizedTexts, characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, sprite.getTags(), swf.getCharacterId(sprite), writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), sprite, swf.getCharacterId(sprite), characterVariables.get(sprite), nonLibraryShapes, tags, sprite.getTags(), getSymbolName(lastImportedId, characterNameMap, swf, symbol), flaVersion, files, symbolStr, spriteScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + convertTimelines(normalizedFonts, normalizedTexts, characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), sprite, swf.getCharacterId(sprite), characterVariables.get(sprite), nonLibraryShapes, tags, sprite.getTags(), getSymbolName(lastImportedId, characterNameMap, swf, symbol), flaVersion, files, symbolStr, spriteScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); } else if (symbol instanceof ShapeTag) { symbolStr.writeStartElement("timeline"); @@ -1870,11 +1875,11 @@ public class XFLConverter { } statusStack.pushStatus("extracting multilevel clips"); - extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, swf.getTags(), -1, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + extractMultilevelClips(normalizedFonts, normalizedTexts, characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, swf.getTags(), -1, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); statusStack.pushStatus("converting multiusage morphshapes"); - extractMultiUsageMorphShapes(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, writer, swf, nonLibraryShapes, flaVersion, files, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + extractMultiUsageMorphShapes(normalizedFonts, normalizedTexts, characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, writer, swf, nonLibraryShapes, flaVersion, files, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); /*if (hasSymbol) { @@ -2522,7 +2527,7 @@ public class XFLConverter { writer.writeEndElement(); } - private static void convertFrames(AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, List onlyFrames, int startFrame, int endFrame, String prevStr, String afterStr, List nonLibraryShapes, ReadOnlyTagList timelineTags, int depth, FLAVersion flaVersion, XFLXmlWriter writer, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { + private static void convertFrames(Map normalizedFonts, Map normalizedTexts, AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, List onlyFrames, int startFrame, int endFrame, String prevStr, String afterStr, List nonLibraryShapes, ReadOnlyTagList timelineTags, int depth, FLAVersion flaVersion, XFLXmlWriter writer, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { Logger.getLogger(XFLConverter.class.getName()).log(Level.FINE, "Converting frames of {0}", symbolName); boolean lastIn = false; XFLXmlWriter writer2 = new XFLXmlWriter(); @@ -2771,7 +2776,11 @@ public class XFLConverter { shapeTween = false; if (character instanceof TextTag) { statusStack.pushStatus(character.toString()); - convertText(frame, accessibility, instanceName, (TextTag) character, matrix, filters, elementsWriter, characterImportLinkageURL, lastImportedId, characterNameMap, characters); + int realCharacterId = swf.getCharacterId(character); + if (normalizedTexts.containsKey(realCharacterId)) { + character = normalizedTexts.get(realCharacterId); + } + convertText(normalizedFonts, frame, accessibility, instanceName, (TextTag) character, matrix, filters, elementsWriter, characterImportLinkageURL, lastImportedId, characterNameMap, characters); statusStack.popStatus(); } else if (character instanceof DefineVideoStreamTag) { convertVideoInstance(instanceName, matrix, (DefineVideoStreamTag) character, elementsWriter); @@ -3956,6 +3965,8 @@ public class XFLConverter { } private void addExtractedClip( + Map normalizedFonts, + Map normalizedTexts, Map characterScriptPacks, Reference lastItemIdNumber, Reference lastImportedId, @@ -3978,7 +3989,7 @@ public class XFLConverter { ) throws XMLStreamException { XFLXmlWriter symbolStr = new XFLXmlWriter(); - extractMultilevelClips(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, timelineTags, spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + extractMultilevelClips(normalizedFonts, normalizedTexts, characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, timelineTags, spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); if (nextClipId.getVal() < 0) { nextClipId.setVal(swf.getNextCharacterId()); @@ -3996,7 +4007,7 @@ public class XFLConverter { "lastModified", Long.toString(getTimestamp(swf))}); symbolStr.writeAttribute("symbolType", "graphic"); - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, timelineTags, timelineTags, getMaskedSymbolName(objectId), flaVersion, files, symbolStr, null, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + convertTimelines(normalizedFonts, normalizedTexts, characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, timelineTags, timelineTags, getMaskedSymbolName(objectId), flaVersion, files, symbolStr, null, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); symbolStr.writeEndElement(); // DOMSymbolItem String symbolStr2 = prettyFormatXML(symbolStr.toString()); @@ -4072,6 +4083,8 @@ public class XFLConverter { } private void extractMultiUsageMorphShapes( + Map normalizedFonts, + Map normalizedTexts, Map characterScriptPacks, Reference lastItemIdNumber, Reference lastImportedId, @@ -4117,7 +4130,7 @@ public class XFLConverter { } } - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, swf.getTags(), new ReadOnlyTagList(timelineTags), getSymbolName(lastImportedId, characterNameMap, swf, swf.getCharacter(objectId)), flaVersion, files, symbolStr, null, new HashMap<>(), new ArrayList<>(), statusStack, characterImportLinkageURL, characters, smallShapes); + convertTimelines(normalizedFonts, normalizedTexts, characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, objectId, "", nonLibraryShapes, swf.getTags(), new ReadOnlyTagList(timelineTags), getSymbolName(lastImportedId, characterNameMap, swf, swf.getCharacter(objectId)), flaVersion, files, symbolStr, null, new HashMap<>(), new ArrayList<>(), statusStack, characterImportLinkageURL, characters, smallShapes); symbolStr.writeEndElement(); // DOMSymbolItem String symbolStr2 = prettyFormatXML(symbolStr.toString()); @@ -4138,6 +4151,8 @@ public class XFLConverter { } private void extractMultilevelClips( + Map normalizedFonts, + Map normalizedTexts, Map characterScriptPacks, Reference lastItemIdNumber, Reference lastImportedId, @@ -4369,7 +4384,7 @@ public class XFLConverter { //set timelined? delegatedTimeline.add(showFrame); } - addExtractedClip(characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, new ReadOnlyTagList(delegatedTimeline), spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + addExtractedClip(normalizedFonts, normalizedTexts, characterScriptPacks, lastItemIdNumber, lastImportedId, characterNameMap, new ReadOnlyTagList(delegatedTimeline), spriteId, writer, swf, nextClipId, nonLibraryShapes, backgroundColor, flaVersion, files, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); placeToMaskedSymbol.put(secondPlace, new MultiLevelClip(secondPlace, nextClipId.getVal(), numFrames)); } } @@ -4391,7 +4406,7 @@ public class XFLConverter { } //Note: symbolId argument might be a virtual symbol like MaskedSymbol - private void convertTimelines(Map characterScriptPacks, Reference lastImportedId, Map characterNameMap, SWF swf, AbcIndexing abcIndex, CharacterTag sprite, int symbolId, String linkageIdentifier, List nonLibraryShapes, ReadOnlyTagList tags, ReadOnlyTagList timelineTags, String spriteName, FLAVersion flaVersion, HashMap files, XFLXmlWriter writer, ScriptPack scriptPack, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { + private void convertTimelines(Map normalizedFonts, Map normalizedTexts, Map characterScriptPacks, Reference lastImportedId, Map characterNameMap, SWF swf, AbcIndexing abcIndex, CharacterTag sprite, int symbolId, String linkageIdentifier, List nonLibraryShapes, ReadOnlyTagList tags, ReadOnlyTagList timelineTags, String spriteName, FLAVersion flaVersion, HashMap files, XFLXmlWriter writer, ScriptPack scriptPack, Map placeToMaskedSymbol, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { ScriptPack characterScriptPack = sprite == null ? null : characterScriptPacks.containsKey(sprite) ? characterScriptPacks.get(sprite) : null; if (sprite == null && symbolId == -1) { @@ -4782,7 +4797,7 @@ public class XFLConverter { "color", randomOutlineColor(), "layerType", "mask", "locked", "true"}); - convertFrames(accessibility, symbolName, lastImportedId, characterNameMap, swf, depthToFramesList.get(po.getDepth()), clipFrame, lastFrame, "", "", nonLibraryShapes, sceneTimelineTags, po.getDepth(), flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + convertFrames(normalizedFonts, normalizedTexts, accessibility, symbolName, lastImportedId, characterNameMap, swf, depthToFramesList.get(po.getDepth()), clipFrame, lastFrame, "", "", nonLibraryShapes, sceneTimelineTags, po.getDepth(), flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); writer.writeEndElement(); int parentIndex = index; @@ -4804,7 +4819,7 @@ public class XFLConverter { handledClips.add(po2); for (int ndx = po.getClipDepth() - 1; ndx > po2.getClipDepth(); ndx--) { - boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(ndx), ndx, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + boolean nonEmpty = writeLayer(normalizedFonts, normalizedTexts, accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(ndx), ndx, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); for (int i = clipFrame; i <= lastFrame; i++) { depthToFramesList.get(ndx).remove((Integer) i); } @@ -4883,7 +4898,7 @@ public class XFLConverter { } for (int nd = po.getClipDepth() - 1; nd > po.getDepth(); nd--) { - boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(nd), nd, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + boolean nonEmpty = writeLayer(normalizedFonts, normalizedTexts, accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(nd), nd, clipFrame, lastFrame, parentIndex, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); for (int i = clipFrame; i <= lastFrame; i++) { depthToFramesList.get(nd).remove((Integer) i); } @@ -4920,7 +4935,7 @@ public class XFLConverter { } } - boolean nonEmpty = writeLayer(accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(d), d, 0, Integer.MAX_VALUE, -1, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + boolean nonEmpty = writeLayer(normalizedFonts, normalizedTexts, accessibility, symbolName, lastImportedId, characterNameMap, swf, index, depthToFramesList.get(d), d, 0, Integer.MAX_VALUE, -1, writer, nonLibraryShapes, sceneTimelineTags, flaVersion, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); if (nonEmpty) { index++; } @@ -4959,7 +4974,7 @@ public class XFLConverter { writer.writeEndElement(); //DOMLayer } - private boolean writeLayer(AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, int index, List onlyFrames, int d, int startFrame, int endFrame, int parentLayer, XFLXmlWriter writer, List nonLibraryShapes, ReadOnlyTagList timelineTags, FLAVersion flaVersion, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { + private boolean writeLayer(Map normalizedFonts, Map normalizedTexts, AccessibilityBag accessibility, String symbolName, Reference lastImportedId, Map characterNameMap, SWF swf, int index, List onlyFrames, int d, int startFrame, int endFrame, int parentLayer, XFLXmlWriter writer, List nonLibraryShapes, ReadOnlyTagList timelineTags, FLAVersion flaVersion, List multiUsageMorphShapes, StatusStack statusStack, Map characterImportLinkageURL, Set characters, Set smallShapes) throws XMLStreamException { XFLXmlWriter layerPrev = new XFLXmlWriter(); statusStack.pushStatus("layer " + (index + 1)); //System.err.println("- writing layer " + (index + 1) + (startFrame == 0 && endFrame == Integer.MAX_VALUE ? ", all frames": ", frame " + startFrame + " to " + endFrame)); @@ -4978,7 +4993,7 @@ public class XFLConverter { layerPrev.writeCharacters(""); // todo honfika: hack to close start tag String layerAfter = ""; int prevLength = writer.length(); - convertFrames(accessibility, symbolName, lastImportedId, characterNameMap, swf, onlyFrames, startFrame, endFrame, layerPrev.toString(), layerAfter, nonLibraryShapes, timelineTags, d, flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + convertFrames(normalizedFonts, normalizedTexts, accessibility, symbolName, lastImportedId, characterNameMap, swf, onlyFrames, startFrame, endFrame, layerPrev.toString(), layerAfter, nonLibraryShapes, timelineTags, d, flaVersion, writer, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); return writer.length() != prevLength; } @@ -5058,7 +5073,7 @@ public class XFLConverter { return ret; } - private static void convertText(int frame, AccessibilityBag accessibility, String instanceName, TextTag tag, MATRIX m, List filters, XFLXmlWriter writer, Map characterImportLinkageURL, Reference lastImportedId, Map characterNameMap, Set characters) throws XMLStreamException { + private static void convertText(Map normalizedFonts, int frame, AccessibilityBag accessibility, String instanceName, TextTag tag, MATRIX m, List filters, XFLXmlWriter writer, Map characterImportLinkageURL, Reference lastImportedId, Map characterNameMap, Set characters) throws XMLStreamException { MATRIX matrix = new MATRIX(m); CSMSettingsTag csmts = null; XFLXmlWriter filterStr = new XFLXmlWriter(); @@ -5131,7 +5146,7 @@ public class XFLConverter { writer.writeAttribute("antiAliasSharpness", antiAliasSharpness); writer.writeAttribute("antiAliasThickness", antiAliasThickness); } - Map attrs = TextTag.getTextRecordsAttributes(textRecords, swf); + Map attrs = TextTag.getTextRecordsAttributes(textRecords, swf, normalizedFonts); writer.writeAttribute("width", tag.getBounds().getWidth() / 2); writer.writeAttribute("height", tag.getBounds().getHeight()); writer.writeAttribute("autoExpand", true); @@ -5168,6 +5183,9 @@ public class XFLConverter { fontName = null; textHeight = rec.textHeight; font = ((Tag) tag).getSwf().getFont(fontId); + if (normalizedFonts.containsKey(fontId)) { + font = normalizedFonts.get(fontId); + } if (font != null) { DefineFontNameTag dfn = (DefineFontNameTag) font.getSwf().getCharacterIdTag(font.getCharacterId(), DefineFontNameTag.ID); @@ -5240,6 +5258,10 @@ public class XFLConverter { DefineEditTextTag det = (DefineEditTextTag) tag; String tagName; FontTag ft = det.getSwf().getFont(det.fontId); + + if (normalizedFonts.containsKey(det.fontId)) { + ft = normalizedFonts.get(det.fontId); + } if (ft != null && ft.isSmall()) { fontRenderingMode = "bitmap"; } @@ -5644,12 +5666,17 @@ public class XFLConverter { convertFonts(lastItemIdNumber, lastImportedId, characterNameMap, swf, characters, domDocument, statusStack, characterVariables, characterClasses, charactersExportedInFirstFrame, characterImportLinkageURL); Set smallShapes = getSmallShapes(swf); + + Map normalizedFonts = new HashMap<>(); + Map normalizedTexts = new HashMap<>(); + FontNormalizer fontNormalizer = new FontNormalizer(); + fontNormalizer.normalizeFonts(swf, normalizedFonts, normalizedTexts); - convertLibrary(lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, swf.getTags(), files, datfiles, flaVersion, domDocument, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, smallShapes); + convertLibrary(normalizedFonts, normalizedTexts, lastItemIdNumber, charactersExportedInFirstFrame, characterImportLinkageURL, characters, lastImportedId, characterNameMap, swf, characterVariables, characterClasses, characterScriptPacks, nonLibraryShapes, backgroundColor, swf.getTags(), files, datfiles, flaVersion, domDocument, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, smallShapes); //domDocument.writeStartElement("timelines"); statusStack.pushStatus("main timeline"); - convertTimelines(characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, -1, null, nonLibraryShapes, swf.getTags(), swf.getTags(), null, flaVersion, files, domDocument, documentScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); + convertTimelines(normalizedFonts, normalizedTexts, characterScriptPacks, lastImportedId, characterNameMap, swf, swf.getAbcIndex(), null, -1, null, nonLibraryShapes, swf.getTags(), swf.getTags(), null, flaVersion, files, domDocument, documentScriptPack, placeToMaskedSymbol, multiUsageMorphShapes, statusStack, characterImportLinkageURL, characters, smallShapes); statusStack.popStatus(); //domDocument.writeEndElement();