From 58eae43203bfd5a63d15d103e10a8ab6e20281f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Tue, 9 Sep 2025 21:19:37 +0200 Subject: [PATCH] Added: #2529, #2471 Font normalization Fixed: #2471 PDF export - ignore control characters Fixed: #2471 SVG export with typeface - white-space:pre style --- CHANGELOG.md | 13 +- .../decompiler/flash/FontNormalizer.java | 376 ++++++++++++++++++ .../flash/exporters/DualPdfGraphics2D.java | 39 +- .../flash/exporters/FontExporter.java | 4 +- .../flash/exporters/FrameExporter.java | 21 +- .../exporters/RequiresNormalizedFonts.java | 35 ++ .../exporters/commonshape/SVGExporter.java | 25 +- .../decompiler/flash/tags/DefineFont2Tag.java | 16 + .../decompiler/flash/tags/DefineFont3Tag.java | 16 + .../decompiler/flash/tags/DefineFontTag.java | 10 + .../decompiler/flash/tags/base/FontTag.java | 12 + .../flash/tags/base/StaticTextTag.java | 17 + .../decompiler/flash/tags/base/TextTag.java | 6 + .../flash/tags/gfx/DefineCompactedFont.java | 10 + .../flash/gui/locales/MainFrame.properties | 2 + .../flash/gui/locales/MainFrame_cs.properties | 7 +- .../flash/gui/tagtree/TagTreeContextMenu.java | 42 ++ 17 files changed, 639 insertions(+), 12 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/RequiresNormalizedFonts.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b911aa5aa..658fa2503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,13 @@ All notable changes to this project will be documented in this file. - view bytes length in bytes fields - [#2526] Option to disable drag & drop from Resources view to avoid problems on Mac OS - [#2497] AS3 - temp variables for better handling swap/dup instructions +- [#2529], [#2471] Font normalization + - available as context menu command on SWFs + - automatically executed on PDF/SVG export + - 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 ### Fixed - [#2474] Gotos incorrectly decompiled @@ -101,6 +108,9 @@ All notable changes to this project will be documented in this file. - AS Debugger - order of trace commands - AS1/2 print/printNum/printAsBitmap/printAsBitmapNum not correctly handling bmovie boundingBox parameter +- Font export - not setting ascent and descent +- [#2471] PDF export - ignore control characters +- [#2471] SVG export with typeface - white-space:pre style ### Changed - Icon of "Deobfuscation options" menu from pile of pills to medkit @@ -3999,6 +4009,8 @@ Major version of SWF to XML export changed to 2. [#2519]: https://www.free-decompiler.com/flash/issues/2519 [#2526]: https://www.free-decompiler.com/flash/issues/2526 [#2497]: https://www.free-decompiler.com/flash/issues/2497 +[#2529]: https://www.free-decompiler.com/flash/issues/2529 +[#2471]: https://www.free-decompiler.com/flash/issues/2471 [#2474]: https://www.free-decompiler.com/flash/issues/2474 [#2480]: https://www.free-decompiler.com/flash/issues/2480 [#2338]: https://www.free-decompiler.com/flash/issues/2338 @@ -4043,7 +4055,6 @@ Major version of SWF to XML export changed to 2. [#2405]: https://www.free-decompiler.com/flash/issues/2405 [#1646]: https://www.free-decompiler.com/flash/issues/1646 [#2469]: https://www.free-decompiler.com/flash/issues/2469 -[#2471]: https://www.free-decompiler.com/flash/issues/2471 [#2475]: https://www.free-decompiler.com/flash/issues/2475 [#2427]: https://www.free-decompiler.com/flash/issues/2427 [#1826]: https://www.free-decompiler.com/flash/issues/1826 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java new file mode 100644 index 000000000..ec5bad68b --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/FontNormalizer.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2010-2025 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash; + +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +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.types.GLYPHENTRY; +import com.jpexs.decompiler.flash.types.RECT; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.decompiler.flash.types.TEXTRECORD; +import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; +import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import java.awt.Point; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 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) { + 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) { + Map characters = swf.getCharacters(!inPlace); + + Map texts = new LinkedHashMap<>(); + + for (int characterId : characters.keySet()) { + CharacterTag character = characters.get(characterId); + if (character instanceof StaticTextTag) { + texts.put(characterId, (StaticTextTag) character); + } + } + + Set invertedFontIds = new LinkedHashSet<>(); + 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 (TEXTRECORD rec : text.textRecords) { + if (rec.styleFlagsHasFont) { + if (inverted) { + invertedFontIds.add(rec.fontId); + } else { + notInvertedFontIds.add(rec.fontId); + } + fontIds.add(rec.fontId); + } + } + } + + Map fontNewScale = new LinkedHashMap<>(); + + for (int fontId : fontIds) { + if (notInvertedFontIds.contains(fontId)) { + invertedFontIds.remove(fontId); + } + + CharacterTag fontCharacter = characters.get(fontId); + if (fontCharacter == null || !(fontCharacter instanceof FontTag)) { + continue; + } + 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; + } + + 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 { + try { + font2 = (FontTag) font.cloneTag(); + } catch (InterruptedException | IOException ex) { + continue; + } + } + outFonts.put(fontId, font2); + + List shapes = font2.getGlyphShapeTable(); + + Matrix matrix = new Matrix(); + + matrix = matrix.preConcatenate(Matrix.getScaleInstance(scale, scale)); + + if (invertedFontIds.contains(fontId)) { + matrix = matrix.preConcatenate(Matrix.getScaleInstance(1, -1)); + } + + for (int i = 0; i < shapes.size(); i++) { + SHAPE shp = shapes.get(i); + transformSHAPE(matrix, shp); + if (font2.hasLayout()) { + font2.setGlyphAdvance(i, font2.getGlyphAdvance(i) * scale); + } + } + if (font2.hasLayout()) { + font2.updateBounds(); + + 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; + } + } + 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())); + + 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 void transformSHAPE(Matrix matrix, SHAPE shape) { + int x = 0; + int y = 0; + StyleChangeRecord lastStyleChangeRecord = null; + boolean wasMoveTo = false; + for (SHAPERECORD rec : shape.shapeRecords) { + if (rec instanceof StyleChangeRecord) { + StyleChangeRecord scr = (StyleChangeRecord) rec; + lastStyleChangeRecord = scr; + if (scr.stateNewStyles) { + //transformStyles(matrix, scr.fillStyles, scr.lineStyles, shapeNum); + } + if (scr.stateMoveTo) { + Point nextPoint = new Point(scr.moveDeltaX, scr.moveDeltaY); + x = scr.changeX(x); + y = scr.changeY(y); + Point nextPoint2 = matrix.transform(nextPoint); + scr.moveDeltaX = nextPoint2.x; + scr.moveDeltaY = nextPoint2.y; + scr.calculateBits(); + wasMoveTo = true; + } + } + + if (((rec instanceof StraightEdgeRecord) || (rec instanceof CurvedEdgeRecord)) && !wasMoveTo) { + if (lastStyleChangeRecord != null) { + Point nextPoint2 = matrix.transform(new Point(x, y)); + if (nextPoint2.x != 0 || nextPoint2.y != 0) { + lastStyleChangeRecord.stateMoveTo = true; + lastStyleChangeRecord.moveDeltaX = nextPoint2.x; + lastStyleChangeRecord.moveDeltaY = nextPoint2.y; + lastStyleChangeRecord.calculateBits(); + wasMoveTo = true; + } + } + } + if (rec instanceof StraightEdgeRecord) { + StraightEdgeRecord ser = (StraightEdgeRecord) rec; + ser.generalLineFlag = true; + ser.vertLineFlag = false; + Point currentPoint = new Point(x, y); + Point nextPoint = new Point(x + ser.deltaX, y + ser.deltaY); + x = ser.changeX(x); + y = ser.changeY(y); + Point currentPoint2 = matrix.transform(currentPoint); + Point nextPoint2 = matrix.transform(nextPoint); + ser.deltaX = nextPoint2.x - currentPoint2.x; + ser.deltaY = nextPoint2.y - currentPoint2.y; + ser.simplify(); + } + if (rec instanceof CurvedEdgeRecord) { + CurvedEdgeRecord cer = (CurvedEdgeRecord) rec; + Point currentPoint = new Point(x, y); + Point controlPoint = new Point(x + cer.controlDeltaX, y + cer.controlDeltaY); + Point anchorPoint = new Point(x + cer.controlDeltaX + cer.anchorDeltaX, y + cer.controlDeltaY + cer.anchorDeltaY); + x = cer.changeX(x); + y = cer.changeY(y); + + Point currentPoint2 = matrix.transform(currentPoint); + Point controlPoint2 = matrix.transform(controlPoint); + Point anchorPoint2 = matrix.transform(anchorPoint); + + cer.controlDeltaX = controlPoint2.x - currentPoint2.x; + cer.controlDeltaY = controlPoint2.y - currentPoint2.y; + cer.anchorDeltaX = anchorPoint2.x - controlPoint2.x; + cer.anchorDeltaY = anchorPoint2.y - controlPoint2.y; + cer.calculateBits(); + } + } + } +} 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 3e56f0d9d..6aea7dc18 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 @@ -51,6 +51,7 @@ import java.awt.image.renderable.RenderableImage; import java.io.File; import java.io.IOException; import java.text.AttributedCharacterIterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -62,13 +63,16 @@ import java.util.logging.Logger; * * @author JPEXS */ -public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, GraphicsGroupable, GraphicsTextDrawable { +public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, GraphicsGroupable, GraphicsTextDrawable, RequiresNormalizedFonts { private final Graphics2D imageGraphics; private final PDFGraphics pdfGraphics; private final Map existingFonts; + private Map normalizedFonts = new LinkedHashMap<>(); + private Map normalizedTexts = new LinkedHashMap<>(); + public DualPdfGraphics2D(Graphics2D first, PDFGraphics second, Map existingFonts) { this.imageGraphics = first; this.pdfGraphics = second; @@ -563,6 +567,7 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, Matrix mat0 = mat.concatenate(textMatrix); Matrix trans = mat0.preConcatenate(Matrix.getScaleInstance(1 / SWF.unitDivisor)); FontTag font = null; + int fontId = -1; int textHeight = 12; int x = 0; int y = 0; @@ -582,7 +587,11 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, } if (rec.styleFlagsHasFont) { - font = rec.getFont(swf); + font = rec.getFont(swf); + fontId = swf.getCharacterId(font); + if (normalizedFonts.containsKey(fontId)) { + font = normalizedFonts.get(fontId); + } textHeight = rec.textHeight; } if (rec.styleFlagsHasXOffset) { @@ -612,7 +621,7 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, char ch = font.glyphToChar(entry.glyphIndex); if (spacing != 0) { text.append(currentChar); - drawText(swf, x, y, trans, textColor, existingFonts, font, text.toString(), textHeight, pdfGraphics); + drawText(swf, x, y, trans, textColor, existingFonts, fontId, font, text.toString(), textHeight, pdfGraphics); text = new StringBuilder(); x = x + deltaX + entry.glyphAdvance; deltaX = 0; @@ -628,14 +637,13 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, } } if (text.length() > 0) { - drawText(swf, x, y, trans, textColor, existingFonts, font, text.toString(), textHeight, pdfGraphics); + drawText(swf, x, y, trans, textColor, existingFonts, fontId, font, text.toString(), textHeight, pdfGraphics); } x = x + deltaX; } } - private static void drawText(SWF swf, float x, float y, Matrix trans, int textColor, Map existingFonts, FontTag font, String text, int textHeight, PDFGraphics g) { - int fontId = swf.getCharacterId(font); + private static void drawText(SWF swf, float x, float y, Matrix trans, int textColor, Map existingFonts, int fontId, FontTag font, String text, int textHeight, PDFGraphics g) { if (existingFonts.containsKey(fontId)) { g.setExistingTtfFont(existingFonts.get(fontId).deriveFont((float) textHeight)); } else { @@ -679,6 +687,25 @@ public class DualPdfGraphics2D extends Graphics2D implements BlendModeSettable, g.setTransform(trans.toTransform()); Color textColor2 = new Color(textColor, true); g.setColor(textColor2); + + text = text.replaceAll("\\p{Cc}", " "); //Replace control characters with space + g.drawString(text, (float) x, (float) y); } + + @Override + public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts) { + this.normalizedFonts = normalizedFonts; + this.normalizedTexts = normalizedTexts; + } + + @Override + public Map getNormalizedFonts() { + return normalizedFonts; + } + + @Override + public Map getNormalizedTexts() { + return normalizedTexts; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FontExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FontExporter.java index 735da3672..fb48a790f 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FontExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FontExporter.java @@ -193,7 +193,9 @@ public class FontExporter { f.setCreationDate(date); f.setModificationDate(date); } - + + f.setDefaultMetrics(); + int ascent = t.getAscent(); if (ascent != -1) { float value = Math.round(ascent / divider); 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 ce675957b..58847505d 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 @@ -19,6 +19,7 @@ package com.jpexs.decompiler.flash.exporters; import com.jpacker.JPacker; import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler; import com.jpexs.decompiler.flash.EventListener; +import com.jpexs.decompiler.flash.FontNormalizer; import com.jpexs.decompiler.flash.RetryTask; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.action.parser.ActionParseException; @@ -40,6 +41,7 @@ import com.jpexs.decompiler.flash.tags.Tag; 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.enums.ImageFormat; import com.jpexs.decompiler.flash.timeline.DepthState; import com.jpexs.decompiler.flash.timeline.Frame; @@ -86,6 +88,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -318,6 +321,13 @@ public class FrameExporter { } if (settings.mode == FrameExportMode.SVG) { + + FontNormalizer normalizer = new FontNormalizer(); + Map normalizedFonts = new LinkedHashMap<>(); + Map normalizedTexts = new LinkedHashMap<>(); + normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts); + + int max = frames.size(); if (subFramesLength > 1) { max = subFramesLength; @@ -341,7 +351,7 @@ public class FrameExporter { rect.xMin *= settings.zoom; rect.yMin *= settings.zoom; SVGExporter exporter = new SVGExporter(rect, settings.zoom, "frame", fbackgroundColor); - + exporter.setNormalizedFonts(normalizedFonts, normalizedTexts); tim.toSVG(frame, subFramesLength > 1 ? fi : 0, null, 0, exporter, null, 0, new Matrix(), new Matrix()); fos.write(Utf8Helper.getBytes(exporter.getSVG())); } @@ -595,6 +605,12 @@ public class FrameExporter { if (frameImages.hasNext()) { for (File foutdir : foutdirs) { new RetryTask(() -> { + + FontNormalizer normalizer = new FontNormalizer(); + Map normalizedFonts = new LinkedHashMap<>(); + Map normalizedTexts = new LinkedHashMap<>(); + normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts); + File f = new File(foutdir + File.separator + "frames.pdf"); PDFJob job = new PDFJob(new BufferedOutputStream(new FileOutputStream(f))); PageFormat pf = new PageFormat(); @@ -643,7 +659,8 @@ public class FrameExporter { return compositeGraphics; } final Graphics2D parentGraphics = (Graphics2D) super.getGraphics(); - compositeGraphics = new DualPdfGraphics2D(parentGraphics, (PDFGraphics) g, existingFonts); + compositeGraphics = new DualPdfGraphics2D(parentGraphics, (PDFGraphics) g, existingFonts); + ((DualPdfGraphics2D) compositeGraphics).setNormalizedFonts(normalizedFonts, normalizedTexts); return compositeGraphics; } 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 new file mode 100644 index 000000000..666909a77 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/RequiresNormalizedFonts.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010-2025 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.exporters; + +import com.jpexs.decompiler.flash.tags.base.FontTag; +import com.jpexs.decompiler.flash.tags.base.StaticTextTag; +import java.util.Map; + +/** + * This exporter requires normalized fonts. + * @see com.jpexs.decompiler.flash.FontNormalizer + * @author JPEXS + */ +public interface RequiresNormalizedFonts { + public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts); + + public Map getNormalizedFonts(); + + 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 add534d9d..5d7aeeacf 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 @@ -18,8 +18,11 @@ package com.jpexs.decompiler.flash.exporters.commonshape; import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.RequiresNormalizedFonts; 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.types.BlendMode; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.RECT; @@ -31,6 +34,7 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -59,7 +63,7 @@ import org.w3c.dom.NodeList; * * @author JPEXS */ -public class SVGExporter { +public class SVGExporter implements RequiresNormalizedFonts { protected static final String sNamespace = "http://www.w3.org/2000/svg"; @@ -92,6 +96,25 @@ public class SVGExporter { public boolean useTextTag = Configuration.textExportExportFontFace.get(); private double zoom; + + private Map normalizedFonts = new LinkedHashMap<>(); + private Map normalizedTexts = new LinkedHashMap<>(); + + @Override + public void setNormalizedFonts(Map normalizedFonts, Map normalizedTexts) { + this.normalizedFonts = normalizedFonts; + this.normalizedTexts = normalizedTexts; + } + + @Override + public Map getNormalizedFonts() { + return normalizedFonts; + } + + @Override + public Map getNormalizedTexts() { + return normalizedTexts; + } public static class ExportKey { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont2Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont2Tag.java index 832e5c82b..011390f77 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont2Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont2Tag.java @@ -358,6 +358,15 @@ public class DefineFont2Tag extends FontTag { } return super.getGlyphBounds(glyphIndex); } + + @Override + public void updateBounds() { + if (fontFlagsHasLayout) { + for (int i = 0; i < fontBoundsTable.size(); i++) { + fontBoundsTable.set(i, super.getGlyphBounds(i)); + } + } + } @Override public synchronized double getGlyphAdvance(int glyphIndex) { @@ -367,6 +376,13 @@ public class DefineFont2Tag extends FontTag { return -1; } } + + @Override + public synchronized void setGlyphAdvance(int glyphIndex, double advanceValue) { + if (fontFlagsHasLayout && glyphIndex != -1) { + fontAdvanceTable.set(glyphIndex, (int) Math.round(advanceValue)); + } + } @Override public synchronized List getGlyphShapeTable() { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont3Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont3Tag.java index d1308b7ac..b8172762b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont3Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFont3Tag.java @@ -349,6 +349,13 @@ public class DefineFont3Tag extends FontTag { return -1; } } + + @Override + public synchronized void setGlyphAdvance(int glyphIndex, double advanceValue) { + if (fontFlagsHasLayout && glyphIndex != -1) { + fontAdvanceTable.set(glyphIndex, (int) Math.round(advanceValue)); + } + } @Override public synchronized char glyphToChar(int glyphIndex) { @@ -646,6 +653,15 @@ public class DefineFont3Tag extends FontTag { return super.getGlyphBounds(glyphIndex); } + @Override + public void updateBounds() { + if (fontFlagsHasLayout) { + for (int i = 0; i < fontBoundsTable.size(); i++) { + fontBoundsTable.set(i, super.getGlyphBounds(i)); + } + } + } + @Override public synchronized int getGlyphKerningAdjustment(int glyphIndex, int nextGlyphIndex) { if (glyphIndex == -1 || nextGlyphIndex == -1) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFontTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFontTag.java index b7158590e..ce8ebc685 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFontTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineFontTag.java @@ -150,6 +150,16 @@ public class DefineFontTag extends FontTag { return -1; } + @Override + public void setGlyphAdvance(int glyphIndex, double advanceValue) { + + } + + @Override + public void updateBounds() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + @Override public synchronized int getGlyphWidth(int glyphIndex) { return glyphShapeTable.get(glyphIndex).getBounds(1).getWidth(); 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 5278786da..a057268b3 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 @@ -102,6 +102,13 @@ public abstract class FontTag extends DrawableTag implements AloneTag { */ public abstract void setAdvanceValues(Font font); + /** + * Sets advance value for glyph. + * @param glyphIndex Glyph index + * @param advanceValue Advance value + */ + public abstract void setGlyphAdvance(int glyphIndex, double advanceValue); + /** * Converts glyph to character. * @param glyphIndex Glyph index @@ -815,4 +822,9 @@ public abstract class FontTag extends DrawableTag implements AloneTag { */ public abstract String getCodesCharset(); + + /** + * Update tables of bounds + */ + public abstract void updateBounds(); } 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 b5c18b01a..8411e7c53 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 @@ -21,6 +21,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; @@ -1027,6 +1028,16 @@ public abstract class StaticTextTag extends TextTag { @Override 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(); + int realTextId = getSwf().getCharacterId(this); + if (normalizedTexts.containsKey(realTextId)) { + StaticTextTag normalizedText = 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; + } + } staticTextToImage(swf, textRecords, getTextNum(), image, textMatrix, transformation, colorTransform, renderContext.selectionText == this ? renderContext.selectionStart : 0, renderContext.selectionText == this ? renderContext.selectionEnd : 0); /*try { TextTag originalTag = (TextTag) getOriginalTag(); @@ -1041,6 +1052,12 @@ 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); + staticTextToSVG(swf, normalizedText.textRecords, getTextNum(), exporter, getRect(), normalizedText.textMatrix, colorTransform, exporter.getZoom(), transformation); + return; + } staticTextToSVG(swf, textRecords, getTextNum(), exporter, getRect(), textMatrix, colorTransform, exporter.getZoom(), transformation); } 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 5ce4749d4..1f73ceb93 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 @@ -990,6 +990,7 @@ public abstract class TextTag extends DrawableTag { public static void staticTextToSVG(SWF swf, List textRecords, int numText, SVGExporter exporter, RECT bounds, MATRIX textMatrix, ColorTransform colorTransform, double zoom, Matrix transformation) { int textColor = 0; FontTag font = null; + int fontId = -1; double textHeight = 12; int x = 0; int y = 0; @@ -1009,6 +1010,10 @@ public abstract class TextTag extends DrawableTag { } if (rec.styleFlagsHasFont) { font = rec.getFont(swf); + fontId = swf.getCharacterId(font); + if (exporter.getNormalizedFonts().containsKey(fontId)) { + font = exporter.getNormalizedFonts().get(fontId); + } glyphs = font.getGlyphShapeTable(); textHeight = rec.textHeight; } @@ -1049,6 +1054,7 @@ public abstract class TextTag extends DrawableTag { textElement.setAttribute("font-family", fontFamily); textElement.setAttribute("textLength", Double.toString(totalAdvance / SWF.unitDivisor)); textElement.setAttribute("lengthAdjust", "spacing"); + textElement.setAttribute("style", "white-space: pre"); textElement.setTextContent(text.toString()); RGBA colorA = new RGBA(textColor); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineCompactedFont.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineCompactedFont.java index 3383c1755..1c63ea497 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineCompactedFont.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineCompactedFont.java @@ -283,6 +283,11 @@ public final class DefineCompactedFont extends FontTag { public double getGlyphAdvance(int glyphIndex) { return resize(fonts.get(0).glyphInfo.get(glyphIndex).advanceX); } + + @Override + public synchronized void setGlyphAdvance(int glyphIndex, double advanceValue) { + throw new UnsupportedOperationException("Not implemented yet"); + } @Override public int getGlyphKerningAdjustment(int glyphIndex, int nextGlyphIndex) { @@ -401,6 +406,11 @@ public final class DefineCompactedFont extends FontTag { GlyphType gt = fonts.get(0).glyphs.get(glyphIndex); return new RECT(resize(gt.boundingBox[0]), resize(gt.boundingBox[1]), resize(gt.boundingBox[2]), resize(gt.boundingBox[3])); } + + @Override + public void updateBounds() { + throw new UnsupportedOperationException("Not implemented yet"); + } public SHAPE resizeShape(SHAPE shp) { SHAPE ret = new SHAPE(); diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 5861cd88c..e45b1f261 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1100,3 +1100,5 @@ node.errored = errored filter.swf_spl_swt = SWF files (*.swf, *.spl, *.swt) filter.swt = Generator templates (*.swt) + +contextmenu.normalizeFonts = Normalize fonts \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties index 25e8db058..d7cc94349 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties @@ -1094,4 +1094,9 @@ menu.settings.autoDeobfuscateIdentifiers = Deobfuskovat identifik\u00e1tory deobfuscate_options.deobfuscateIdentifiers = Deobfuskovat identifik\u00e1tory node.unknown = nezn\u00e1m\u00e9 -node.errored = chybov\u00e9 \ No newline at end of file +node.errored = chybov\u00e9 + +filter.swf_spl_swt = SWF soubory (*.swf, *.spl, *.swt) +filter.swt = \u0160ablony programu Generator (*.swt) + +contextmenu.normalizeFonts = Normalizovat p\u00edsma \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java index e3b01b6e5..9e31e5fd0 100644 --- a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java @@ -16,6 +16,7 @@ */ package com.jpexs.decompiler.flash.gui.tagtree; +import com.jpexs.decompiler.flash.FontNormalizer; import com.jpexs.decompiler.flash.IdentifiersDeobfuscation; import com.jpexs.decompiler.flash.ReadOnlyTagList; import com.jpexs.decompiler.flash.SWF; @@ -35,6 +36,8 @@ import com.jpexs.decompiler.flash.configuration.Configuration; import com.jpexs.decompiler.flash.configuration.CustomConfigurationKeys; import com.jpexs.decompiler.flash.configuration.SwfSpecificCustomConfiguration; import com.jpexs.decompiler.flash.exporters.PreviewExporter; +import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; import com.jpexs.decompiler.flash.exporters.swf.SwfFlashDevelopExporter; import com.jpexs.decompiler.flash.exporters.swf.SwfIntelliJIdeaExporter; import com.jpexs.decompiler.flash.exporters.swf.SwfVsCodeExporter; @@ -116,6 +119,7 @@ import com.jpexs.decompiler.flash.tags.base.RemoveTag; import com.jpexs.decompiler.flash.tags.base.ShapeTag; import com.jpexs.decompiler.flash.tags.base.SoundStreamHeadTypeTag; import com.jpexs.decompiler.flash.tags.base.SoundTag; +import com.jpexs.decompiler.flash.tags.base.StaticTextTag; import com.jpexs.decompiler.flash.tags.base.TextTag; import com.jpexs.decompiler.flash.tags.converters.PlaceObjectTypeConverter; import com.jpexs.decompiler.flash.tags.converters.ShapeTypeConverter; @@ -143,9 +147,17 @@ import com.jpexs.decompiler.flash.types.BUTTONRECORD; import com.jpexs.decompiler.flash.types.CLIPACTIONRECORD; import com.jpexs.decompiler.flash.types.CLIPACTIONS; import com.jpexs.decompiler.flash.types.CXFORMWITHALPHA; +import com.jpexs.decompiler.flash.types.GLYPHENTRY; import com.jpexs.decompiler.flash.types.HasCharacterId; import com.jpexs.decompiler.flash.types.MATRIX; +import com.jpexs.decompiler.flash.types.RECT; +import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.decompiler.flash.types.TEXTRECORD; import com.jpexs.decompiler.flash.types.annotations.SWFVersion; +import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; +import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; import com.jpexs.decompiler.graph.CompilationException; import com.jpexs.decompiler.graph.DottedChain; import com.jpexs.helpers.ByteArrayRange; @@ -155,6 +167,7 @@ import com.jpexs.helpers.Reference; import com.jpexs.helpers.utf8.Utf8Helper; import java.awt.Color; import java.awt.Component; +import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; @@ -169,6 +182,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -372,6 +386,8 @@ public class TagTreeContextMenu extends JPopupMenu { private JMenuItem convertShapeTypeMenuItem; private JMenuItem convertPlaceObjectTypeMenuItem; + + private JMenuItem normalizeFontsMenuItem; private List items = new ArrayList<>(); @@ -625,6 +641,12 @@ public class TagTreeContextMenu extends JPopupMenu { convertPlaceObjectTypeMenuItem.setIcon(View.getIcon("placeobject16")); add(convertPlaceObjectTypeMenuItem); + + normalizeFontsMenuItem = new JMenuItem(mainPanel.translate("contextmenu.normalizeFonts")); + normalizeFontsMenuItem.addActionListener(this::normalizeFontsActionPerformed); + normalizeFontsMenuItem.setIcon(View.getIcon("font16")); + add(normalizeFontsMenuItem); + addSeparator(); gotoDocumentClassMenuItem = new JMenuItem(mainPanel.translate("menu.tools.gotoDocumentClass")); @@ -1170,10 +1192,12 @@ public class TagTreeContextMenu extends JPopupMenu { boolean allSelectedIsShape = true; boolean allSelectedIsPlaceObject = true; + boolean allSelectedIsFont = true; if (items.isEmpty()) { allSelectedIsShape = false; allSelectedIsPlaceObject = false; + allSelectedIsFont = false; } for (TreeItem item : items) { @@ -1184,6 +1208,9 @@ public class TagTreeContextMenu extends JPopupMenu { if (!(item instanceof PlaceObjectTypeTag)) { allSelectedIsPlaceObject = false; } + if (!(item instanceof FontTag)) { + allSelectedIsFont = false; + } if (item instanceof Tag) { Tag tag = (Tag) item; @@ -1348,6 +1375,7 @@ public class TagTreeContextMenu extends JPopupMenu { replaceRefsWithTagMenuItem.setVisible(false); convertShapeTypeMenuItem.setVisible(false); convertPlaceObjectTypeMenuItem.setVisible(false); + normalizeFontsMenuItem.setVisible(false); abcExplorerMenuItem.setVisible(false); cleanAbcMenuItem.setVisible(false); rawEditMenuItem.setVisible(false); @@ -1839,6 +1867,9 @@ public class TagTreeContextMenu extends JPopupMenu { if (allSelectedIsPlaceObject) { convertPlaceObjectTypeMenuItem.setVisible(true); } + if (allSelectedIsSwf) { + normalizeFontsMenuItem.setVisible(true); + } moveTagToMenu.removeAll(); moveTagToWithDependenciesMenu.removeAll(); @@ -2859,6 +2890,17 @@ public class TagTreeContextMenu extends JPopupMenu { mainPanel.setTagTreeSelectedNode(mainPanel.getCurrentTree(), lastConverted); } } + + private void normalizeFontsActionPerformed(ActionEvent evt) { + for (TreeItem item : getSelectedItems()) { + SWF swf = (SWF) item; + FontNormalizer normalizer = new FontNormalizer(); + normalizer.normalizeFonts(swf); + } + mainPanel.getCurrentTree().repaint(); + } + + private void replaceRefsWithTagActionPerformed(ActionEvent evt) { TreeItem itemr = getCurrentItem();