diff --git a/CHANGELOG.md b/CHANGELOG.md index 8866663ab..56196d758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- [#2427] Commandline export with use of imported SWFs (importAssets tag) + ### Fixed - [#2424] DefineEditText handling of letterSpacing, font size on incorrect values - [#2391] Double not operator in ternar operator expression @@ -3691,6 +3694,7 @@ Major version of SWF to XML export changed to 2. [alpha 9]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha8...alpha9 [alpha 8]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha7...alpha8 [alpha 7]: https://github.com/jindrapetrik/jpexs-decompiler/releases/tag/alpha7 +[#2427]: https://www.free-decompiler.com/flash/issues/2427 [#2424]: https://www.free-decompiler.com/flash/issues/2424 [#2391]: https://www.free-decompiler.com/flash/issues/2391 [#2375]: https://www.free-decompiler.com/flash/issues/2375 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java index 66e94c0cf..886a63bd1 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java @@ -2330,7 +2330,7 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { if (importedSwfs.containsKey(url)) { iSwf = importedSwfs.get(url); } else { - iSwf = resolver.resolveUrl(url); + iSwf = resolver.resolveUrl(this.file, url); importedSwfs.put(url, iSwf); } if (iSwf != null) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/UrlResolver.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/UrlResolver.java index bf46f4885..76757416c 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/UrlResolver.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/UrlResolver.java @@ -16,6 +16,8 @@ */ package com.jpexs.decompiler.flash; +import java.io.File; + /** * URL resolver interface. * @@ -26,8 +28,9 @@ public interface UrlResolver { /** * Resolves URL to SWF object. * + * @param basePath Base SWF path * @param url URL * @return SWF object or null if not valid */ - public SWF resolveUrl(String url); + public SWF resolveUrl(String basePath, String url); } diff --git a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index 20d20cbe3..d0e243f55 100644 --- a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -27,6 +27,7 @@ import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.SWFCompression; import com.jpexs.decompiler.flash.SearchMode; import com.jpexs.decompiler.flash.SwfOpenException; +import com.jpexs.decompiler.flash.UrlResolver; import com.jpexs.decompiler.flash.ValueTooLargeException; import com.jpexs.decompiler.flash.abc.ABC; import com.jpexs.decompiler.flash.abc.ABCInputStream; @@ -228,6 +229,7 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -494,7 +496,9 @@ public class CommandLineArgumentParser { } AbortRetryIgnoreHandler handler = null; + UrlResolver urlResolver = new ConsoleUrlResolver(false, false, false, new HashMap<>()); Map format = new HashMap<>(); + Map changedImports = new LinkedHashMap<>(); double zoom = 1; String charset = Charset.defaultCharset().name(); boolean cliMode = false; @@ -598,6 +602,12 @@ public class CommandLineArgumentParser { } Configuration._debugMode.set(true); break; + case "-importassets": + urlResolver = parseImportAssets(args, changedImports); + break; + case "-changeimport": + parseChangeImport(args, changedImports); + break; default: break OUTER; } @@ -658,7 +668,7 @@ public class CommandLineArgumentParser { } else if (command.equals("proxy")) { parseProxy(args); } else if (command.equals("export")) { - parseExport(selectionClasses, selection, selectionIds, args, handler, traceLevel, format, zoom, charset, exportEmbed, resampleWav, transparentBackground); + parseExport(selectionClasses, selection, selectionIds, args, handler, traceLevel, format, zoom, charset, exportEmbed, resampleWav, transparentBackground, urlResolver); System.exit(0); } else if (command.equals("compress")) { parseCompress(args); @@ -1731,6 +1741,43 @@ public class CommandLineArgumentParser { System.err.println("Process affinity setting is only available on Windows platform."); } } + + private static void parseChangeImport(Stack args, Map changedImports) { + if (args.size() < 2) { + System.err.println("source and target of import expected"); + badArguments("changeimport"); + } + changedImports.put(args.pop(), args.pop()); + } + + private static UrlResolver parseImportAssets(Stack args, Map changedImports) { + if (args.isEmpty()) { + System.err.println("importassets options expected"); + badArguments("importassets"); + } + String impOptionsStr = args.pop(); + String[] impOptionsArr = impOptionsStr.split(",", -1); + boolean doResolve = false; + boolean doAsk = false; + boolean localOnly = false; + for (String impOption:impOptionsArr) { + switch (impOption) { + case "yes": + doResolve = true; + break; + case "ask": + doAsk = true; + break; + case "local": + localOnly = true; + break; + default: + System.err.println("incorrect importassets option: " + impOption); + badArguments("importassets"); + } + } + return new ConsoleUrlResolver(doResolve, doAsk, localOnly, changedImports); + } private static void parsePriority(Stack args) { if (Platform.isWindows()) { @@ -1930,7 +1977,7 @@ public class CommandLineArgumentParser { } - private static void parseExport(List selectionClasses, Selection selection, Selection selectionIds, Stack args, AbortRetryIgnoreHandler handler, Level traceLevel, Map formats, double zoom, String charset, boolean exportEmbed, boolean resampleWav, boolean transparentBackground) { + private static void parseExport(List selectionClasses, Selection selection, Selection selectionIds, Stack args, AbortRetryIgnoreHandler handler, Level traceLevel, Map formats, double zoom, String charset, boolean exportEmbed, boolean resampleWav, boolean transparentBackground, UrlResolver urlResolver) { if (args.size() < 3) { badArguments("export"); } @@ -2021,11 +2068,11 @@ public class CommandLineArgumentParser { startTimeSwf = System.currentTimeMillis(); System.out.println("Start exporting " + inFile.getName()); } - + OpenableSourceInfo sourceInfo = new OpenableSourceInfo(null, inFile.getAbsolutePath(), inFile.getName()); SWF swf; try { - swf = new SWF(new BufferedInputStream(new StdInAwareFileInputStream(inFile)), sourceInfo.getFile(), sourceInfo.getFileTitle(), null, Configuration.parallelSpeedUp.get(), false, true, charset); + swf = new SWF(new BufferedInputStream(new StdInAwareFileInputStream(inFile)), sourceInfo.getFile(), sourceInfo.getFileTitle(), null, Configuration.parallelSpeedUp.get(), false, true, urlResolver, charset); } catch (FileNotFoundException | SwfOpenException ex) { // FileNotFoundException when anti virus software blocks to open the file logger.log(Level.SEVERE, "Failed to open swf: " + inFile.getName(), ex); diff --git a/src/com/jpexs/decompiler/flash/console/ConsoleUrlResolver.java b/src/com/jpexs/decompiler/flash/console/ConsoleUrlResolver.java new file mode 100644 index 000000000..cf73bb2fd --- /dev/null +++ b/src/com/jpexs/decompiler/flash/console/ConsoleUrlResolver.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2025 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.console; + +import com.jpexs.decompiler.flash.OpenableSourceInfo; +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.UrlResolver; +import com.jpexs.decompiler.flash.configuration.Configuration; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Scanner; + +/** + * Console Url resolver + * @author JPEXS + */ +public class ConsoleUrlResolver implements UrlResolver { + + private final boolean doResolve; + private final boolean doAsk; + private final boolean localOnly; + + private boolean ignoredInfoPrinted = false; + private Character toAll = null; + + private Map importSources = new HashMap<>(); + + /** + * + * @param doResolve Do resolve? + * @param doAsk Do ask? + * @param localOnly Local only? + * @param importSources Import sources + */ + public ConsoleUrlResolver(boolean doResolve, boolean doAsk, boolean localOnly, Map importSources) { + this.doResolve = doResolve; + this.doAsk = doAsk; + this.localOnly = localOnly; + this.importSources = importSources; + } + + @Override + public SWF resolveUrl(String basePath, String url) { + + Character choice = toAll; + String currentUrl = url; + + if (importSources.containsKey(url)) { + currentUrl = importSources.get(url); + if (currentUrl.isEmpty()) { + return null; + } + choice = 'y'; + } else { + if (choice == null && doResolve) { + choice = 'y'; + } + if (!doResolve && !doAsk) { + if (!ignoredInfoPrinted) { + System.out.println("WARNING: SWF tried to import assets from external SWF, the request was ignored. (See --help -importAssets for more options)"); + } + ignoredInfoPrinted = true; + return null; + } + if (choice == null && doAsk) { + choice = ask(url); + } + } + + + while(choice != 'n') { + if (choice == 'c') { + System.out.println("Enter new location instead (empty to cancel):"); + Scanner sc = new Scanner(System.in); + String newUrl = sc.nextLine(); + importSources.put(url, newUrl); + currentUrl = newUrl; + if (currentUrl.isEmpty()) { + return null; + } + } + + if (currentUrl.startsWith("http://") || currentUrl.startsWith("https://")) { + if (localOnly && choice != 'c') { + return null; + } + try { + URL u = new URL(currentUrl); + SWF ret = open(u.openStream(), null, currentUrl); //? + return ret; + } catch (Exception ex) { + //ignore + } + } else { + File swf = new File(new File(basePath).getParentFile(), currentUrl); + if (swf.exists()) { + try { + SWF ret = open(new FileInputStream(swf), swf.getAbsolutePath(), swf.getName()); + return ret; + } catch (Exception ex) { + //ignore + } + } + // try .gfx if .swf failed + if (currentUrl.endsWith(".swf")) { + File gfx = new File(new File(basePath).getParentFile(), url.substring(0, url.length() - 4) + ".gfx"); + if (gfx.exists()) { + try { + SWF ret = open(new FileInputStream(gfx), gfx.getAbsolutePath(), gfx.getName()); + return ret; + } catch (Exception ex) { + //ignore + } + } + } + } + + System.err.println("Cannot load imported SWF " + currentUrl); + if (doAsk) { + choice = 'c'; + } else { + choice = 'n'; + } + } + return null; + } + + private SWF open(InputStream is, String file, String fileTitle) throws Exception { + OpenableSourceInfo sourceInfo = new OpenableSourceInfo(is, file, fileTitle); + return new SWF(new BufferedInputStream(is), sourceInfo.getFile(), sourceInfo.getFileTitle(), null, Configuration.parallelSpeedUp.get(), false, true, this, null /*??*/); + } + + private String prompt() { + Scanner sc = new Scanner(System.in); + String s; + System.out.print("Select action: (Y)es, (N)o, (C)ustom, (YA) Yes to all, (NA) No to all: "); + s = sc.nextLine(); + s = s.toLowerCase(Locale.ENGLISH); + return s; + } + + private char ask(String url) { + String s; + if (toAll != null) { + s = "" + toAll; + } else { + System.out.println("The SWF file is trying to import assets from the following location:"); + System.out.println(url); + s = prompt(); + } + + while (true) { + switch (s) { + case "ya": + toAll = 'y'; + return 'y'; + case "na": + toAll = 'n'; + return 'n'; + case "y": + return 'y'; + case "n": + return 'n'; + case "c": + return 'c'; + } + System.out.println("Invalid action."); + s = prompt(); + } + } +} diff --git a/src/com/jpexs/decompiler/flash/console/help.txt b/src/com/jpexs/decompiler/flash/console/help.txt index 8b1368f9b..d966d641e 100644 --- a/src/com/jpexs/decompiler/flash/console/help.txt +++ b/src/com/jpexs/decompiler/flash/console/help.txt @@ -444,3 +444,17 @@ Pre-options: Ignores SWF background color when exporting frames and thus make background transparent. +-importAssets + Applies to: -export + Specifies options for importAssets tag - using external SWFs. + format: (combination of yes,ask,local) + yes = use external SWFs when available + ask = ask whether to use external SWFs + yes,ask = use external SWFs, ask for alternatives when not available + yes,local = ignores URLs - SWFs starting with http/s:// prefix + +-changeImport + Applies to: -export + Replaces URL in importAssets tag with + for purposes of export. Can be used multiple times. + diff --git a/src/com/jpexs/decompiler/flash/gui/Main.java b/src/com/jpexs/decompiler/flash/gui/Main.java index 9e98e4d19..8ed014914 100644 --- a/src/com/jpexs/decompiler/flash/gui/Main.java +++ b/src/com/jpexs/decompiler/flash/gui/Main.java @@ -1235,7 +1235,7 @@ public class Main { } }, Configuration.parallelSpeedUp.get(), false, true, new UrlResolver() { @Override - public SWF resolveUrl(final String url) { + public SWF resolveUrl(String file, final String url) { loadedUrls.add(url); File selFile = null; int opt = -1;