From 0f0d76ece5db1f82ac339d9d2ef9ecc449fb02ee Mon Sep 17 00:00:00 2001 From: MasterAlexS <40096192+MasterAlexS@users.noreply.github.com> Date: Sun, 17 May 2026 18:49:09 +0300 Subject: [PATCH 1/4] Add native SWF vector font extraction to XML --- scripts/ExportFonts.hx | 115 ++++++++++++++++++++++++ src/swf/exporters/NativeFontExporter.hx | 106 ++++++++++++++++++++++ src/swf/exporters/SWFVectorFont.hx | 23 +++++ 3 files changed, 244 insertions(+) create mode 100644 scripts/ExportFonts.hx create mode 100644 src/swf/exporters/NativeFontExporter.hx create mode 100644 src/swf/exporters/SWFVectorFont.hx diff --git a/scripts/ExportFonts.hx b/scripts/ExportFonts.hx new file mode 100644 index 0000000..4bdc9c4 --- /dev/null +++ b/scripts/ExportFonts.hx @@ -0,0 +1,115 @@ +package; + +import sys.io.File; +import sys.FileSystem; +import swf.SWF; +import openfl.utils.ByteArray; +import swf.exporters.NativeFontExporter; +import swf.exporters.SWFVectorFont; + +class ExportFonts +{ + public static function main() + { + var args = Sys.args(); + if (args.length < 2) + { + Sys.println("Usage: haxe --run ExportFonts "); + return; + } + + var swfPath = args[0]; + var outputDir = args[1]; + + Sys.println("Reading SWF: " + swfPath); + + var rawBytes = File.getBytes(swfPath); + + var bytes = ByteArray.fromBytes(rawBytes); + + bytes.position = 0; + bytes.endian = openfl.utils.Endian.LITTLE_ENDIAN; + + var swf = new SWF(bytes); + + Sys.println("Extracting Vector Fonts natively..."); + var fonts = NativeFontExporter.extract(swf); + + if (!FileSystem.exists(outputDir)) + { + FileSystem.createDirectory(outputDir); + } + + var nameCount = new Map(); + + for (font in fonts) + { + var baseName = font.name.split(" ").join("_"); + var safeName = baseName; + + if (nameCount.exists(baseName)) + { + var count = nameCount.get(baseName) + 1; + nameCount.set(baseName, count); + safeName = baseName + "_" + count; + } + else + { + nameCount.set(baseName, 1); + } + + var xmlContent = generateXML(font); + + var filePath = outputDir + "/" + safeName + ".xml"; + File.saveContent(filePath, xmlContent); + Sys.println("Saved: " + filePath); + } + + Sys.println("Done! Extracted " + fonts.length + " individual XML fonts."); + } + + private static function generateXML(font:SWFVectorFont):String + { + var xml = '\n'; + xml += '\n'; + + for (charCode in font.glyphs.keys()) + { + var glyph = font.glyphs.get(charCode); + xml += '\t\n'; + + var pathData = ""; + + for (cmd in glyph.commands) + { + switch (cmd) + { + case MoveTo(x, y): + pathData += 'M $x $y '; + case LineTo(x, y): + pathData += 'L $x $y '; + case CurveTo(cx, cy, ax, ay): + pathData += 'Q $cx $cy $ax $ay '; + } + } + + if (StringTools.trim(pathData) != "") + { + xml += '\t\t\n'; + } + + xml += '\t\n'; + } + + xml += ''; + return xml; + } +} diff --git a/src/swf/exporters/NativeFontExporter.hx b/src/swf/exporters/NativeFontExporter.hx new file mode 100644 index 0000000..ad585e0 --- /dev/null +++ b/src/swf/exporters/NativeFontExporter.hx @@ -0,0 +1,106 @@ +package swf.exporters; + +import swf.SWF; +import swf.tags.TagDefineFont2; +import swf.tags.TagDefineFont3; +import swf.exporters.ShapeCommandExporter; +import swf.exporters.core.ShapeCommand; +import swf.exporters.SWFVectorFont; + +class NativeFontExporter +{ + private static inline var TWIPS:Float = 20.0; + + public static function extract(swf:SWF):Array + { + var exportedFonts = new Array(); + + for (tag in swf.data.tags) + { + if (Std.isOfType(tag, TagDefineFont2)) + { + var fontTag:TagDefineFont2 = cast tag; + exportedFonts.push(processFontTag(fontTag)); + } + } + + return exportedFonts; + } + + private static function processFontTag(fontTag:TagDefineFont2):SWFVectorFont + { + var isFont3 = Std.isOfType(fontTag, TagDefineFont3); + var scale = isFont3 ? (TWIPS * 20.0) : TWIPS; + + var font:SWFVectorFont = { + name: fontTag.fontName != null ? fontTag.fontName : "Unknown_" + fontTag.characterId, + ascent: 0, + descent: 0, + leading: 0, + glyphs: new Map() + }; + + if (fontTag.hasLayout) + { + font.ascent = fontTag.ascent / scale; + font.descent = fontTag.descent / scale; + font.leading = fontTag.leading / scale; + } + + var shapeTable = fontTag.glyphShapeTable; + var codeTable = fontTag.codeTable; + + if (shapeTable != null && codeTable != null) + { + for (i in 0...shapeTable.length) + { + if (i >= codeTable.length) break; + + var charCode = codeTable[i]; + var shape = shapeTable[i]; + + var shapeExporter = new ShapeCommandExporter(null); + shape.export(shapeExporter); + + var glyphCommands = convertCommands(shapeExporter.commands, scale); + + var charAdvance = 0.0; + if (fontTag.hasLayout && fontTag.fontAdvanceTable != null && i < fontTag.fontAdvanceTable.length) + { + charAdvance = fontTag.fontAdvanceTable[i] / scale; + } + + font.glyphs.set(charCode, { + commands: glyphCommands, + advance: charAdvance + }); + } + } + + return font; + } + + private static function convertCommands(openflCommands:Array, scale:Float):Array + { + var result = new Array(); + + for (cmd in openflCommands) + { + switch (cmd) + { + case MoveTo(x, y): + result.push(GlyphCommand.MoveTo(x / scale, y / scale)); + + case LineTo(x, y): + result.push(GlyphCommand.LineTo(x / scale, y / scale)); + + case CurveTo(cx, cy, ax, ay): + result.push(GlyphCommand.CurveTo(cx / scale, cy / scale, ax / scale, ay / scale)); + + default: + } + } + + return result; + } +} diff --git a/src/swf/exporters/SWFVectorFont.hx b/src/swf/exporters/SWFVectorFont.hx new file mode 100644 index 0000000..59f89c7 --- /dev/null +++ b/src/swf/exporters/SWFVectorFont.hx @@ -0,0 +1,23 @@ +package swf.exporters; + +enum GlyphCommand +{ + MoveTo(x:Float, y:Float); + LineTo(x:Float, y:Float); + CurveTo(cx:Float, cy:Float, ax:Float, ay:Float); +} + +typedef GlyphData = +{ + var commands:Array; + var advance:Float; +} + +typedef SWFVectorFont = +{ + var name:String; + var ascent:Float; + var descent:Float; + var leading:Float; + var glyphs:Map; +} From 40515965f1f8fb4a3f1154ec9aad3b9763421c27 Mon Sep 17 00:00:00 2001 From: MasterAlexS <40096192+MasterAlexS@users.noreply.github.com> Date: Sun, 17 May 2026 21:26:02 +0300 Subject: [PATCH 2/4] Optimize XML generation and round TWIPS coordinates --- scripts/ExportFonts.hx | 46 ++++++++++++------------- src/swf/exporters/NativeFontExporter.hx | 19 ++++++---- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/scripts/ExportFonts.hx b/scripts/ExportFonts.hx index 4bdc9c4..84c2353 100644 --- a/scripts/ExportFonts.hx +++ b/scripts/ExportFonts.hx @@ -2,6 +2,7 @@ package; import sys.io.File; import sys.FileSystem; +import haxe.io.Path; import swf.SWF; import openfl.utils.ByteArray; import swf.exporters.NativeFontExporter; @@ -21,10 +22,14 @@ class ExportFonts var swfPath = args[0]; var outputDir = args[1]; - Sys.println("Reading SWF: " + swfPath); + if (!FileSystem.exists(swfPath)) + { + Sys.println("Error: Input SWF file not found at path: " + swfPath); + return; + } + Sys.println("Reading SWF: " + swfPath); var rawBytes = File.getBytes(swfPath); - var bytes = ByteArray.fromBytes(rawBytes); bytes.position = 0; @@ -60,7 +65,7 @@ class ExportFonts var xmlContent = generateXML(font); - var filePath = outputDir + "/" + safeName + ".xml"; + var filePath = Path.join([outputDir, safeName + ".xml"]); File.saveContent(filePath, xmlContent); Sys.println("Saved: " + filePath); } @@ -70,46 +75,41 @@ class ExportFonts private static function generateXML(font:SWFVectorFont):String { - var xml = '\n'; - xml += '\n'; + var buf = new StringBuf(); + + buf.add('\n'); + buf.add('\n'); for (charCode in font.glyphs.keys()) { var glyph = font.glyphs.get(charCode); - xml += '\t\n'; + buf.add('\t\n'); - var pathData = ""; + var pathBuf = new StringBuf(); for (cmd in glyph.commands) { switch (cmd) { case MoveTo(x, y): - pathData += 'M $x $y '; + pathBuf.add('M $x $y '); case LineTo(x, y): - pathData += 'L $x $y '; + pathBuf.add('L $x $y '); case CurveTo(cx, cy, ax, ay): - pathData += 'Q $cx $cy $ax $ay '; + pathBuf.add('Q $cx $cy $ax $ay '); } } - if (StringTools.trim(pathData) != "") + var pathString = StringTools.trim(pathBuf.toString()); + if (pathString != "") { - xml += '\t\t\n'; + buf.add('\t\t\n'); } - xml += '\t\n'; + buf.add('\t\n'); } - xml += ''; - return xml; + buf.add(''); + return buf.toString(); } } diff --git a/src/swf/exporters/NativeFontExporter.hx b/src/swf/exporters/NativeFontExporter.hx index ad585e0..4d41391 100644 --- a/src/swf/exporters/NativeFontExporter.hx +++ b/src/swf/exporters/NativeFontExporter.hx @@ -42,9 +42,9 @@ class NativeFontExporter if (fontTag.hasLayout) { - font.ascent = fontTag.ascent / scale; - font.descent = fontTag.descent / scale; - font.leading = fontTag.leading / scale; + font.ascent = roundFloat(fontTag.ascent / scale); + font.descent = roundFloat(fontTag.descent / scale); + font.leading = roundFloat(fontTag.leading / scale); } var shapeTable = fontTag.glyphShapeTable; @@ -67,7 +67,7 @@ class NativeFontExporter var charAdvance = 0.0; if (fontTag.hasLayout && fontTag.fontAdvanceTable != null && i < fontTag.fontAdvanceTable.length) { - charAdvance = fontTag.fontAdvanceTable[i] / scale; + charAdvance = roundFloat(fontTag.fontAdvanceTable[i] / scale); } font.glyphs.set(charCode, { @@ -89,13 +89,13 @@ class NativeFontExporter switch (cmd) { case MoveTo(x, y): - result.push(GlyphCommand.MoveTo(x / scale, y / scale)); + result.push(GlyphCommand.MoveTo(roundFloat(x / scale), roundFloat(y / scale))); case LineTo(x, y): - result.push(GlyphCommand.LineTo(x / scale, y / scale)); + result.push(GlyphCommand.LineTo(roundFloat(x / scale), roundFloat(y / scale))); case CurveTo(cx, cy, ax, ay): - result.push(GlyphCommand.CurveTo(cx / scale, cy / scale, ax / scale, ay / scale)); + result.push(GlyphCommand.CurveTo(roundFloat(cx / scale), roundFloat(cy / scale), roundFloat(ax / scale), roundFloat(ay / scale))); default: } @@ -103,4 +103,9 @@ class NativeFontExporter return result; } + + private static inline function roundFloat(val:Float):Float + { + return Math.round(val * 10000.0) / 10000.0; + } } From 1cbf97ed4c5ac5d88fdd8dbef31fef4552ff6b32 Mon Sep 17 00:00:00 2001 From: MasterAlexS <40096192+MasterAlexS@users.noreply.github.com> Date: Sun, 17 May 2026 21:41:27 +0300 Subject: [PATCH 3/4] Bypass enum allocations and generate SVG paths directly --- scripts/ExportFonts.hx | 29 +++++++----------- src/swf/exporters/NativeFontExporter.hx | 39 ++++++++++++++++--------- src/swf/exporters/SWFVectorFont.hx | 9 +----- 3 files changed, 37 insertions(+), 40 deletions(-) diff --git a/scripts/ExportFonts.hx b/scripts/ExportFonts.hx index 84c2353..8b88295 100644 --- a/scripts/ExportFonts.hx +++ b/scripts/ExportFonts.hx @@ -15,7 +15,12 @@ class ExportFonts var args = Sys.args(); if (args.length < 2) { - Sys.println("Usage: haxe --run ExportFonts "); + Sys.println("ExportFonts - Native SWF Vector Extractor\n"); + Sys.println("Usage (Recommended - Neko):"); + Sys.println(" 1. Build: haxe -cp src -cp scripts -lib openfl -lib lime -lib format -D optional-cffi -main ExportFonts -neko export_fonts.n"); + Sys.println(" 2. Run: neko export_fonts.n \n"); + Sys.println("Usage (Direct/Eval - May cause memory issues on large SWFs):"); + Sys.println(" haxe --run ExportFonts "); return; } @@ -85,25 +90,11 @@ class ExportFonts var glyph = font.glyphs.get(charCode); buf.add('\t\n'); - var pathBuf = new StringBuf(); - - for (cmd in glyph.commands) - { - switch (cmd) - { - case MoveTo(x, y): - pathBuf.add('M $x $y '); - case LineTo(x, y): - pathBuf.add('L $x $y '); - case CurveTo(cx, cy, ax, ay): - pathBuf.add('Q $cx $cy $ax $ay '); - } - } - - var pathString = StringTools.trim(pathBuf.toString()); - if (pathString != "") + if (glyph.pathData != "") { - buf.add('\t\t\n'); + buf.add('\t\t\n'); } buf.add('\t\n'); diff --git a/src/swf/exporters/NativeFontExporter.hx b/src/swf/exporters/NativeFontExporter.hx index 4d41391..22f3b74 100644 --- a/src/swf/exporters/NativeFontExporter.hx +++ b/src/swf/exporters/NativeFontExporter.hx @@ -62,7 +62,7 @@ class NativeFontExporter var shapeExporter = new ShapeCommandExporter(null); shape.export(shapeExporter); - var glyphCommands = convertCommands(shapeExporter.commands, scale); + var finalSvgPath = generateSVGPath(shapeExporter.commands, scale); var charAdvance = 0.0; if (fontTag.hasLayout && fontTag.fontAdvanceTable != null && i < fontTag.fontAdvanceTable.length) @@ -71,7 +71,7 @@ class NativeFontExporter } font.glyphs.set(charCode, { - commands: glyphCommands, + pathData: finalSvgPath, advance: charAdvance }); } @@ -80,28 +80,41 @@ class NativeFontExporter return font; } - private static function convertCommands(openflCommands:Array, scale:Float):Array + private static function generateSVGPath(commands:Array, scale:Float):String { - var result = new Array(); + if (commands == null || commands.length == 0) return ""; - for (cmd in openflCommands) + var buf = new StringBuf(); + for (cmd in commands) { switch (cmd) { case MoveTo(x, y): - result.push(GlyphCommand.MoveTo(roundFloat(x / scale), roundFloat(y / scale))); - + buf.add("M "); + buf.add(roundFloat(x / scale)); + buf.add(" "); + buf.add(roundFloat(y / scale)); + buf.add(" "); case LineTo(x, y): - result.push(GlyphCommand.LineTo(roundFloat(x / scale), roundFloat(y / scale))); - + buf.add("L "); + buf.add(roundFloat(x / scale)); + buf.add(" "); + buf.add(roundFloat(y / scale)); + buf.add(" "); case CurveTo(cx, cy, ax, ay): - result.push(GlyphCommand.CurveTo(roundFloat(cx / scale), roundFloat(cy / scale), roundFloat(ax / scale), roundFloat(ay / scale))); - + buf.add("Q "); + buf.add(roundFloat(cx / scale)); + buf.add(" "); + buf.add(roundFloat(cy / scale)); + buf.add(" "); + buf.add(roundFloat(ax / scale)); + buf.add(" "); + buf.add(roundFloat(ay / scale)); + buf.add(" "); default: } } - - return result; + return StringTools.trim(buf.toString()); } private static inline function roundFloat(val:Float):Float diff --git a/src/swf/exporters/SWFVectorFont.hx b/src/swf/exporters/SWFVectorFont.hx index 59f89c7..04366d4 100644 --- a/src/swf/exporters/SWFVectorFont.hx +++ b/src/swf/exporters/SWFVectorFont.hx @@ -1,15 +1,8 @@ package swf.exporters; -enum GlyphCommand -{ - MoveTo(x:Float, y:Float); - LineTo(x:Float, y:Float); - CurveTo(cx:Float, cy:Float, ax:Float, ay:Float); -} - typedef GlyphData = { - var commands:Array; + var pathData:String; var advance:Float; } From 93fb97729bd152f92e77e0538c39074eb816014e Mon Sep 17 00:00:00 2001 From: MasterAlexS <40096192+MasterAlexS@users.noreply.github.com> Date: Sun, 17 May 2026 22:03:08 +0300 Subject: [PATCH 4/4] Implement multithreading for parallel font extraction --- src/swf/exporters/NativeFontExporter.hx | 30 +++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/swf/exporters/NativeFontExporter.hx b/src/swf/exporters/NativeFontExporter.hx index 22f3b74..1078bcb 100644 --- a/src/swf/exporters/NativeFontExporter.hx +++ b/src/swf/exporters/NativeFontExporter.hx @@ -6,6 +6,9 @@ import swf.tags.TagDefineFont3; import swf.exporters.ShapeCommandExporter; import swf.exporters.core.ShapeCommand; import swf.exporters.SWFVectorFont; +import sys.thread.Thread; +import sys.thread.Mutex; +import sys.thread.Lock; class NativeFontExporter { @@ -14,16 +17,39 @@ class NativeFontExporter public static function extract(swf:SWF):Array { var exportedFonts = new Array(); + var fontTagsToProcess = new Array(); for (tag in swf.data.tags) { if (Std.isOfType(tag, TagDefineFont2)) { - var fontTag:TagDefineFont2 = cast tag; - exportedFonts.push(processFontTag(fontTag)); + fontTagsToProcess.push(cast tag); } } + if (fontTagsToProcess.length == 0) return exportedFonts; + + var mutex = new Mutex(); + var lock = new Lock(); + + for (fontTag in fontTagsToProcess) + { + Thread.create(function() + { + var parsedFont = processFontTag(fontTag); + + mutex.acquire(); + exportedFonts.push(parsedFont); + mutex.release(); + lock.release(); + }); + } + + for (i in 0...fontTagsToProcess.length) + { + lock.wait(); + } + return exportedFonts; }