From 5bb8a2c23d367b8f8de8205146ecb2edea85def7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Sun, 10 May 2026 16:09:46 +0200 Subject: [PATCH] feat(xml export): allow external as1/2 scripts, images and defineSounds (#2707) --- .../flash/configuration/Configuration.java | 4 + .../flash/exporters/ImageExporter.java | 63 ++-- .../flash/exporters/SoundExporter.java | 42 +-- .../settings/XmlSwfExportSettings.java | 53 ++++ .../flash/exporters/swf/SwfXmlExporter.java | 213 ++++++++++++- .../flash/importers/AS2ScriptImporter.java | 88 +++--- .../flash/importers/SwfXmlImporter.java | 129 +++++++- .../flash/tags/DefineButton2Tag.java | 13 + .../flash/tags/DefineButtonTag.java | 2 +- .../flash/tags/base/PlaceObjectTypeTag.java | 16 + .../src/com/jpexs/helpers/IdentityKey.java | 44 +++ .../flash/SwfXmlExportImportTest.java | 2 +- .../console/CommandLineArgumentParser.java | 56 +++- .../jpexs/decompiler/flash/console/help.txt | 14 +- .../jpexs/decompiler/flash/gui/MainPanel.java | 20 +- .../decompiler/flash/gui/XmlExportDialog.java | 279 ++++++++++++++++++ .../locales/AdvancedSettingsDialog.properties | 3 + .../AdvancedSettingsDialog_cs.properties | 7 + .../AdvancedSettingsDialog_de.properties | 7 + .../AdvancedSettingsDialog_sk.properties | 7 + .../gui/locales/XmlExportDialog.properties | 12 + .../gui/locales/XmlExportDialog_cs.properties | 12 + .../gui/locales/XmlExportDialog_de.properties | 12 + .../gui/locales/XmlExportDialog_sk.properties | 12 + 24 files changed, 995 insertions(+), 115 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/XmlSwfExportSettings.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/helpers/IdentityKey.java create mode 100644 src/com/jpexs/decompiler/flash/gui/XmlExportDialog.java create mode 100644 src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog.properties create mode 100644 src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_cs.properties create mode 100644 src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_de.properties create mode 100644 src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_sk.properties diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java index 55492b5d3..08f9daefa 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java @@ -1240,6 +1240,10 @@ public final class Configuration { @ConfigurationDefaultBoolean(true) @ConfigurationCategory("display") public static ConfigurationItem showLoadingSpinner = null; + + @ConfigurationDefaultString("") + @ConfigurationName("xmlExport.formats") + public static ConfigurationItem lastSelectedXmlExportFormats = null; private static Map configurationDescriptions = new LinkedHashMap<>(); private static Map configurationTitles = new LinkedHashMap<>(); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java index aab7f03ff..5ad599577 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ImageExporter.java @@ -56,6 +56,41 @@ import javax.imageio.ImageIO; * @author JPEXS */ public class ImageExporter { + + + private static ImageFormat getExportFormat(ImageTag imageTag, ImageExportSettings settings) { + ImageFormat fileFormat = imageTag.getOriginalImageFormat(); + boolean hasSeparateAlpha = false; + if (imageTag instanceof HasSeparateAlphaChannel) { + HasSeparateAlphaChannel hsac = (HasSeparateAlphaChannel) imageTag; + hasSeparateAlpha = hsac.hasAlphaChannel(); + } + if (settings.mode == ImageExportMode.PNG_GIF_JPEG && hasSeparateAlpha) { + fileFormat = ImageFormat.PNG; + } + if (settings.mode == ImageExportMode.PNG) { + fileFormat = ImageFormat.PNG; + } + + if (settings.mode == ImageExportMode.JPEG) { + fileFormat = ImageFormat.JPEG; + } + + if (settings.mode == ImageExportMode.BMP) { + fileFormat = ImageFormat.BMP; + } + + if (settings.mode == ImageExportMode.WEBP) { + fileFormat = ImageFormat.WEBP; + } + return fileFormat; + } + + public static String getExportExtension(ImageTag imageTag, ImageExportSettings settings) { + ImageFormat fileFormat = getExportFormat(imageTag, settings); + + return ImageHelper.getImageFormatString(fileFormat); + } public List exportImages(AbortRetryIgnoreHandler handler, String outdir, ReadOnlyTagList tags, ImageExportSettings settings, EventListener evl) throws IOException, InterruptedException { List ret = new ArrayList<>(); @@ -90,31 +125,9 @@ public class ImageExporter { final ImageTag imageTag = (ImageTag) t; - ImageFormat fileFormat = imageTag.getOriginalImageFormat(); - ImageFormat originalFormat = fileFormat; - boolean hasSeparateAlpha = false; - if (imageTag instanceof HasSeparateAlphaChannel) { - HasSeparateAlphaChannel hsac = (HasSeparateAlphaChannel) imageTag; - hasSeparateAlpha = hsac.hasAlphaChannel(); - } - if (settings.mode == ImageExportMode.PNG_GIF_JPEG && hasSeparateAlpha) { - fileFormat = ImageFormat.PNG; - } - if (settings.mode == ImageExportMode.PNG) { - fileFormat = ImageFormat.PNG; - } - - if (settings.mode == ImageExportMode.JPEG) { - fileFormat = ImageFormat.JPEG; - } - - if (settings.mode == ImageExportMode.BMP) { - fileFormat = ImageFormat.BMP; - } - - if (settings.mode == ImageExportMode.WEBP) { - fileFormat = ImageFormat.WEBP; - } + + ImageFormat originalFormat = imageTag.getOriginalImageFormat(); + ImageFormat fileFormat = getExportFormat(imageTag, settings); final File file = new File(outdir + File.separator + Helper.makeFileName(imageTag.getCharacterExportFileName() + "." + ImageHelper.getImageFormatString(fileFormat))); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java index 2ca02c1d8..cb81c81b8 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java @@ -64,6 +64,27 @@ import java.util.Set; */ public class SoundExporter { + public static String getExportExtension(SoundTag soundTag, SoundExportSettings settings) { + String ext = "wav"; + SoundFormat fmt = soundTag.getSoundFormat(); + switch (fmt.getNativeExportFormat()) { + case MP3: + if (settings.mode.hasMP3()) { + ext = "mp3"; + } + break; + case FLV: + if (settings.mode.hasFlv()) { + ext = "flv"; + } + break; + } + if (settings.mode == SoundExportMode.FLV) { + ext = "flv"; + } + return ext; + } + public List exportSounds(AbortRetryIgnoreHandler handler, String outdir, ReadOnlyTagList tags, final SoundExportSettings settings, EventListener evl) throws IOException, InterruptedException { List sounds = new ArrayList<>(); for (Tag t : tags) { @@ -72,7 +93,7 @@ public class SoundExporter { } } return exportSounds(handler, outdir, sounds, settings, evl); - } + } public List exportSounds(AbortRetryIgnoreHandler handler, String outdir, List tags, final SoundExportSettings settings, EventListener evl) throws IOException, InterruptedException { List ret = new ArrayList<>(); @@ -97,24 +118,7 @@ public class SoundExporter { evl.handleExportingEvent("sound", currentIndex, tags.size(), st.getName()); } - String ext = ".wav"; - SoundFormat fmt = st.getSoundFormat(); - switch (fmt.getNativeExportFormat()) { - case MP3: - if (settings.mode.hasMP3()) { - ext = ".mp3"; - } - break; - case FLV: - if (settings.mode.hasFlv()) { - ext = ".flv"; - } - break; - } - if (settings.mode == SoundExportMode.FLV) { - ext = ".flv"; - } - + String ext = "." + getExportExtension(st, settings); final File file = new File(outdir + File.separator + Helper.makeFileName(st.getCharacterExportFileName()) + ext); new RetryTask(() -> { try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/XmlSwfExportSettings.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/XmlSwfExportSettings.java new file mode 100644 index 000000000..18368276c --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/XmlSwfExportSettings.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010-2026 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.settings; + +import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; +import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; +import com.jpexs.decompiler.flash.exporters.modes.SoundExportMode; + +/** + * + * @author JPEXS + */ +public class XmlSwfExportSettings { + public ScriptExportMode as12ExportMode; + public ImageExportMode imageExportMode; + public SoundExportMode defineSoundExportMode; + + public XmlSwfExportSettings() { + } + + public XmlSwfExportSettings(ScriptExportMode as12ExportMode, ImageExportMode imageExportMode, SoundExportMode defineSoundExportMode) { + if (as12ExportMode != null && as12ExportMode != ScriptExportMode.AS) { + throw new IllegalArgumentException("Unsupported script export mode"); + } + this.as12ExportMode = as12ExportMode; + if ( + imageExportMode != null + && imageExportMode != ImageExportMode.PNG_GIF_JPEG + && imageExportMode != ImageExportMode.PNG_GIF_JPEG_ALPHA + ) { + throw new IllegalArgumentException("Unsupported image export mode"); + } + this.imageExportMode = imageExportMode; + if (defineSoundExportMode != null && defineSoundExportMode != SoundExportMode.MP3_WAV_FLV) { + throw new IllegalArgumentException("Unsupported sound export mode"); + } + this.defineSoundExportMode = defineSoundExportMode; + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java index c77641b4d..c74470d80 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/swf/SwfXmlExporter.java @@ -16,12 +16,31 @@ */ package com.jpexs.decompiler.flash.exporters.swf; +import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler; import com.jpexs.decompiler.flash.ApplicationInfo; +import com.jpexs.decompiler.flash.EventListener; +import com.jpexs.decompiler.flash.ReadOnlyTagList; import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.exporters.ImageExporter; +import com.jpexs.decompiler.flash.exporters.SoundExporter; +import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; +import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; +import com.jpexs.decompiler.flash.exporters.script.AS2ScriptExporter; +import com.jpexs.decompiler.flash.exporters.settings.ImageExportSettings; +import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings; +import com.jpexs.decompiler.flash.exporters.settings.SoundExportSettings; +import com.jpexs.decompiler.flash.exporters.settings.XmlSwfExportSettings; import com.jpexs.decompiler.flash.helpers.InternalClass; import com.jpexs.decompiler.flash.helpers.LazyObject; +import com.jpexs.decompiler.flash.tags.DefineButtonTag; +import com.jpexs.decompiler.flash.tags.DefineSoundTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.UnknownTag; +import com.jpexs.decompiler.flash.tags.base.ASMSource; +import com.jpexs.decompiler.flash.tags.base.ButtonAction; +import com.jpexs.decompiler.flash.tags.base.CharacterTag; +import com.jpexs.decompiler.flash.tags.base.ImageTag; +import com.jpexs.decompiler.flash.tags.base.SoundTag; import com.jpexs.decompiler.flash.types.annotations.Conditional; import com.jpexs.decompiler.flash.types.annotations.Internal; import com.jpexs.decompiler.flash.types.annotations.parser.AnnotationParseException; @@ -39,8 +58,12 @@ import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; @@ -62,9 +85,11 @@ public class SwfXmlExporter { */ public static final int XML_EXPORT_VERSION_MAJOR = 2; + public static final int XML_EXPORT_VERSION_MAJOR_WITH_EXTERNAL_FILES = 3; + /** * XML export version minor. - * + * * Version 2 - export only fields that meet conditions */ public static final int XML_EXPORT_VERSION_MINOR = 2; @@ -81,16 +106,100 @@ public class SwfXmlExporter { * @throws IOException On I/O error */ public void exportXml(SWF swf, File outFile) throws IOException { + exportXml(swf, outFile, new XmlSwfExportSettings(), null, new AbortRetryIgnoreHandler() { + @Override + public int handle(Throwable thrown) { + return AbortRetryIgnoreHandler.ABORT; + } + + @Override + public AbortRetryIgnoreHandler getNewInstance() { + return this; + } + }); + } + + /** + * Exports SWF to XML. + * + * @param swf SWF to export + * @param outFile Target file to save to + * @param settings Export settings + * @param evl Event listener + * @param handler Abort/Retry/Ignore handler + * + * @throws IOException On I/O error + */ + public void exportXml(SWF swf, File outFile, XmlSwfExportSettings settings, EventListener evl, AbortRetryIgnoreHandler handler) throws IOException { try { File tmp = File.createTempFile("FFDEC", "XML"); + String assetsDirName = outFile.getName(); + if (assetsDirName.contains(".")) { + assetsDirName = assetsDirName.substring(0, assetsDirName.lastIndexOf(".")); + } + assetsDirName = assetsDirName + "_assets"; + + Map asmExternalFiles = new HashMap<>(); + if (settings.as12ExportMode != null) { + Map externalNameToAsm = swf.getASMs(true); + Set existingNames = new HashSet<>(); + for (String key : externalNameToAsm.keySet()) { + ASMSource asm = externalNameToAsm.get(key); + String currentOutDir = key + "/"; + currentOutDir = new File(currentOutDir).getParentFile().toString(); + currentOutDir = currentOutDir.replace("\\", "/"); + if (!"/".equals(currentOutDir)) { + currentOutDir += "/"; + } + String name = Helper.makeFileName(asm.getExportFileName()); + int i = 1; + String baseName = name; + while (existingNames.contains(currentOutDir + name)) { + i++; + name = baseName + "_" + i; + } + existingNames.add(currentOutDir + name); + asmExternalFiles.put(asm, assetsDirName + "/scripts" + currentOutDir + name + ".as"); + } + } + + Map tagExternalFiles = new IdentityHashMap<>(); + List imagesList = new ArrayList<>(); + if (settings.imageExportMode != null) { + ImageExportSettings imageExportSetttings = new ImageExportSettings(settings.imageExportMode); + Map chars = swf.getCharacters(false); + for (int charId : chars.keySet()) { + CharacterTag ch = chars.get(charId); + if (ch instanceof ImageTag) { + ImageTag imageTag = (ImageTag) ch; + tagExternalFiles.put(imageTag, assetsDirName + "/images/" + Helper.makeFileName(imageTag.getCharacterExportFileName()) + "." + ImageExporter.getExportExtension(imageTag, imageExportSetttings)); + imagesList.add(imageTag); + } + } + } + + List soundList = new ArrayList<>(); + if (settings.defineSoundExportMode != null) { + SoundExportSettings soundExportSetttings = new SoundExportSettings(settings.defineSoundExportMode); + Map chars = swf.getCharacters(false); + for (int charId : chars.keySet()) { + CharacterTag ch = chars.get(charId); + if (ch instanceof DefineSoundTag) { + DefineSoundTag soundTag = (DefineSoundTag) ch; + tagExternalFiles.put(soundTag, assetsDirName + "/sounds/" + Helper.makeFileName(soundTag.getCharacterExportFileName()) + "." + SoundExporter.getExportExtension(soundTag, soundExportSetttings)); + soundList.add(soundTag); + } + } + } + try (Writer writer = new Utf8OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(tmp)))) { XMLStreamWriter xmlWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(writer); xmlWriter.writeStartDocument(); xmlWriter.writeComment("\r\nWARNING: The structure of this XML is not final.\r\nIn later versions of FFDec it can be changed.\r\nMake sure you use compatible reader/writer based on _xmlExportMajor/_xmlExportMinor keys.\r\n"); - exportXml(swf, xmlWriter); + exportXml(asmExternalFiles, tagExternalFiles, swf, xmlWriter); xmlWriter.writeEndDocument(); xmlWriter.flush(); @@ -106,6 +215,27 @@ public class SwfXmlExporter { logger.log(Level.SEVERE, "Cannot prettyformat XML"); } tmp.delete(); + + if (settings.as12ExportMode != null) { + AS2ScriptExporter exporter = new AS2ScriptExporter(); + exporter.exportActionScript2(swf, handler, outFile.getParentFile().toPath().resolve(assetsDirName + "/scripts").toFile().getAbsolutePath(), new ScriptExportSettings(settings.as12ExportMode, false, false, false, false), true, evl); + } + if (settings.imageExportMode != null) { + ImageExporter exporter = new ImageExporter(); + try { + exporter.exportImages(handler, outFile.getParentFile().toPath().resolve(assetsDirName + "/images").toFile().getAbsolutePath(), new ReadOnlyTagList(imagesList), new ImageExportSettings(settings.imageExportMode), evl); + } catch (InterruptedException ex) { + return; + } + } + if (settings.defineSoundExportMode != null) { + SoundExporter exporter = new SoundExporter(); + try { + exporter.exportSounds(handler, outFile.getParentFile().toPath().resolve(assetsDirName + "/sounds").toFile().getAbsolutePath(), soundList, new SoundExportSettings(settings.defineSoundExportMode), evl); + } catch (InterruptedException ex) { + return; + } + } } catch (XMLStreamException ex) { logger.log(Level.SEVERE, null, ex); } @@ -114,13 +244,30 @@ public class SwfXmlExporter { /** * Exports SWF to XML. * + * @param asmExternalFiles ASM external files + * @param tagExternalFiles Tag external files * @param swf SWF to export * @param writer XML writer * @throws IOException On I/O error * @throws XMLStreamException On XML error */ - public void exportXml(SWF swf, XMLStreamWriter writer) throws IOException, XMLStreamException { - generateXml(swf, null, writer, "swf", swf, false); + private void exportXml( + Map asmExternalFiles, + Map tagExternalFiles, + SWF swf, + XMLStreamWriter writer + ) throws IOException, XMLStreamException { + generateXml( + asmExternalFiles.isEmpty() && tagExternalFiles.isEmpty() ? XML_EXPORT_VERSION_MAJOR : XML_EXPORT_VERSION_MAJOR_WITH_EXTERNAL_FILES, + asmExternalFiles, + tagExternalFiles, + swf, + null, + writer, + "swf", + swf, + false + ); } public List getSwfFieldsCached(Class cls) { @@ -173,7 +320,17 @@ public class SwfXmlExporter { return cls != null && (cls.isArray() || List.class.isAssignableFrom(cls)); } - private void generateXml(SWF swf, Tag currentTag, XMLStreamWriter writer, String name, Object obj, boolean isListItem) throws XMLStreamException { + private void generateXml( + int major, + Map asmExternalFiles, + Map tagExternalFiles, + SWF swf, + Tag currentTag, + XMLStreamWriter writer, + String name, + Object obj, + boolean isListItem + ) throws XMLStreamException { Class cls = obj != null ? obj.getClass() : null; /*if (obj != null && cls == String.class) { @@ -221,7 +378,7 @@ public class SwfXmlExporter { writer.writeStartElement(name); int length = Array.getLength(value); for (int i = 0; i < length; i++) { - generateXml(swf, currentTag, writer, "item", Array.get(value, i), true); + generateXml(major, asmExternalFiles, tagExternalFiles, swf, currentTag, writer, "item", Array.get(value, i), true); } writer.writeEndElement(); } else if (obj != null) { @@ -239,16 +396,12 @@ public class SwfXmlExporter { writer.writeStartElement(name); if (obj instanceof SWF) { - writer.writeAttribute("_xmlExportMajor", "" + XML_EXPORT_VERSION_MAJOR); + writer.writeAttribute("_xmlExportMajor", "" + major); writer.writeAttribute("_xmlExportMinor", "" + XML_EXPORT_VERSION_MINOR); writer.writeAttribute("_generator", ApplicationInfo.applicationVerName); swf = (SWF) obj; } - if (obj instanceof Tag) { - currentTag = (Tag) obj; - } - writer.writeAttribute("type", clazz.getSimpleName()); if (obj instanceof UnknownTag) { @@ -258,8 +411,23 @@ public class SwfXmlExporter { writer.writeAttribute("charset", ((SWF) obj).getCharset()); } + boolean isExternal = false; + + if (obj instanceof Tag) { + currentTag = (Tag) obj; + + if (tagExternalFiles.containsKey((Tag) obj)) { + writer.writeAttribute("_externalFile", tagExternalFiles.get((Tag) obj)); + isExternal = true; + } + } + for (Field f : fields) { - //Multiline multilineA = f.getAnnotation(Multiline.class); + //Multiline multilineA = f.getAnnotation(Multiline.class); + + if (isExternal && !"characterID".equals(f.getName()) && !"soundId".equals(f.getName())) { + continue; + } Conditional cond = f.getAnnotation(Conditional.class); if (cond != null) { @@ -297,7 +465,26 @@ public class SwfXmlExporter { try { f.setAccessible(true); - generateXml(swf, currentTag, writer, f.getName(), f.get(obj), false); + Object value = f.get(obj); + + if ("actionBytes".equals(f.getName())) { + if (obj instanceof ASMSource && asmExternalFiles.containsKey((ASMSource) obj)) { + value = new ByteArrayRange("00"); + writer.writeAttribute("_externalActions", asmExternalFiles.get((ASMSource) obj)); + } else if (obj instanceof DefineButtonTag) { + for (ASMSource s : asmExternalFiles.keySet()) { + if (s instanceof ButtonAction) { + ButtonAction ba = (ButtonAction) s; + if (ba.getSourceTag() == obj) { + value = new ByteArrayRange("00"); + writer.writeAttribute("_externalActions", asmExternalFiles.get(s)); + break; + } + } + } + } + } + generateXml(major, asmExternalFiles, tagExternalFiles, swf, currentTag, writer, f.getName(), value, false); } catch (IllegalArgumentException | IllegalAccessException ex) { logger.log(Level.SEVERE, null, ex); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/AS2ScriptImporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/AS2ScriptImporter.java index 36d28c954..abfa9925e 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/AS2ScriptImporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/AS2ScriptImporter.java @@ -44,7 +44,6 @@ public class AS2ScriptImporter { private static final Logger logger = Logger.getLogger(AS2ScriptImporter.class.getName()); - /** * Constructor. */ @@ -52,8 +51,58 @@ public class AS2ScriptImporter { } + /** + * Imports actionScript 1/2 (not P-code) from given file + * + * @param fileName File to import + * @param asm Target to import into + * @param listener Import listener + * @return True on success + * @throws InterruptedException + */ + public boolean importActionScript(String fileName, ASMSource asm, ScriptImporterProgressListener listener) throws InterruptedException { + asm.getSwf().informListeners("importing_as", fileName); + String txt = Helper.readTextFile(fileName); + + ActionScript2Parser par = new ActionScript2Parser(asm.getSwf(), asm); + boolean errored = false; + try { + asm.setActions(par.actionsFromString(txt, asm.getSwf().getCharset())); + } catch (ValueTooLargeException ex) { + logger.log(Level.SEVERE, "Script or some of its functions are too large, file: {0}", fileName); + errored = true; + } catch (ActionParseException ex) { + logger.log(Level.SEVERE, "%error% on line %line%, file: %file%".replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)).replace("%file%", fileName), ex); + errored = true; + } catch (CompilationException ex) { + logger.log(Level.SEVERE, "%error% on line %line%, file: %file%".replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)).replace("%file%", fileName), ex); + errored = true; + } catch (IOException ex) { + logger.log(Level.SEVERE, "error during script import, file: %file%".replace("%file%", fileName), ex); + errored = true; + } catch (InterruptedException ex) { + throw ex; + } catch (Exception ex) { + logger.log(Level.SEVERE, "error during script import, file: %file%".replace("%file%", fileName), ex); + errored = true; + } + + if (!errored) { + asm.setModified(); + if (listener != null) { + listener.scriptImported(); + } + } else { + if (listener != null) { + listener.scriptImportError(); + } + } + return !errored; + } + /** * Imports scripts from given folder. + * * @param scriptsFolder Folder with scripts * @param asms Map of ASMSource objects * @return Number of imported scripts @@ -65,6 +114,7 @@ public class AS2ScriptImporter { /** * Imports scripts from given folder. + * * @param scriptsFolder Folder with scripts * @param asms Map of ASMSource objects * @param listener Progress listener @@ -104,42 +154,12 @@ public class AS2ScriptImporter { String fileName = Path.combine(currentOutDir, name) + ".as"; if (new File(fileName).exists()) { - asm.getSwf().informListeners("importing_as", fileName); - String txt = Helper.readTextFile(fileName); - - ActionScript2Parser par = new ActionScript2Parser(asm.getSwf(), asm); - boolean errored = false; try { - asm.setActions(par.actionsFromString(txt, asm.getSwf().getCharset())); - } catch (ValueTooLargeException ex) { - logger.log(Level.SEVERE, "Script or some of its functions are too large, file: {0}", fileName); - errored = true; - } catch (ActionParseException ex) { - logger.log(Level.SEVERE, "%error% on line %line%, file: %file%".replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)).replace("%file%", fileName), ex); - errored = true; - } catch (CompilationException ex) { - logger.log(Level.SEVERE, "%error% on line %line%, file: %file%".replace("%error%", ex.text).replace("%line%", Long.toString(ex.line)).replace("%file%", fileName), ex); - errored = true; - } catch (IOException ex) { - logger.log(Level.SEVERE, "error during script import, file: %file%".replace("%file%", fileName), ex); - errored = true; + if (importActionScript(fileName, asm, listener)) { + importCount++; + } } catch (InterruptedException ex) { return importCount; - } catch (Exception ex) { - logger.log(Level.SEVERE, "error during script import, file: %file%".replace("%file%", fileName), ex); - errored = true; - } - - if (!errored) { - asm.setModified(); - importCount++; - if (listener != null) { - listener.scriptImported(); - } - } else { - if (listener != null) { - listener.scriptImportError(); - } } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java index aff1ad0ae..28ec39e52 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SwfXmlImporter.java @@ -37,11 +37,17 @@ import com.jpexs.decompiler.flash.abc.types.traits.TraitMethodGetterSetter; import com.jpexs.decompiler.flash.abc.types.traits.TraitSlotConst; import com.jpexs.decompiler.flash.abc.types.traits.Traits; import com.jpexs.decompiler.flash.amf.amf3.Amf3Value; +import com.jpexs.decompiler.flash.exporters.swf.SwfXmlExporter; import com.jpexs.decompiler.flash.tags.CSMSettingsTag; +import com.jpexs.decompiler.flash.tags.DefineButtonTag; +import com.jpexs.decompiler.flash.tags.DefineSoundTag; import com.jpexs.decompiler.flash.tags.DefineSpriteTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.TagTypeInfo; import com.jpexs.decompiler.flash.tags.UnknownTag; +import com.jpexs.decompiler.flash.tags.base.ASMSource; +import com.jpexs.decompiler.flash.tags.base.ImageTag; +import com.jpexs.decompiler.flash.tags.base.SoundImportException; import com.jpexs.decompiler.flash.types.ALPHABITMAPDATA; import com.jpexs.decompiler.flash.types.ALPHACOLORMAPDATA; import com.jpexs.decompiler.flash.types.ARGB; @@ -108,12 +114,16 @@ import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; +import com.jpexs.decompiler.flash.types.sound.SoundFormat; import com.jpexs.helpers.ByteArrayRange; import com.jpexs.helpers.HashArrayList; import com.jpexs.helpers.Helper; +import com.jpexs.helpers.IdentityKey; import com.jpexs.helpers.ReflectionTools; import com.jpexs.helpers.utf8.Utf8InputStreamReader; import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -124,7 +134,9 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -145,7 +157,12 @@ public class SwfXmlImporter { /** * Maximum XML import version major. */ - public static final int MAX_XML_IMPORT_VERSION_MAJOR = 2; + public static final int MAX_XML_IMPORT_VERSION_MAJOR = 3; + + /** + * Minimum version for using external files - attributes _externalActions, _externalFile + */ + public static final int XML_IMPORT_VERSION_MAJOR_WITH_EXTERNAL_FILES = 3; private static final Logger logger = Logger.getLogger(SwfXmlImporter.class.getName()); @@ -221,11 +238,15 @@ public class SwfXmlImporter { * Imports SWF from input stream. * @param swf SWF object * @param in Input stream + * @param directory Directory where XML resides for external files resolving * @throws IOException On I/O error */ - public void importSwf(SWF swf, InputStream in) throws IOException { + public void importSwf(SWF swf, InputStream in, File directory) throws IOException { XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); + Map, String> asmExternalActions = new LinkedHashMap<>(); + Map, String> tagExternalFiles = new LinkedHashMap<>(); + try { try (Reader reader = new Utf8InputStreamReader(new BufferedInputStream(in))) { XMLStreamReader xmlReader = xmlFactory.createXMLStreamReader(reader); @@ -233,7 +254,7 @@ public class SwfXmlImporter { xmlReader.nextTag(); xmlReader.require(XMLStreamConstants.START_ELEMENT, null, "swf"); - processElement(xmlReader, swf, swf, null, MAX_XML_IMPORT_VERSION_MAJOR); + processElement(xmlReader, swf, swf, null, MAX_XML_IMPORT_VERSION_MAJOR, asmExternalActions, tagExternalFiles); } swf.clearAllCache(); @@ -241,16 +262,78 @@ public class SwfXmlImporter { } catch (XMLStreamException ex) { logger.log(Level.SEVERE, null, ex); } + + if (!asmExternalActions.isEmpty()) { + for (IdentityKey objKey : asmExternalActions.keySet()) { + ASMSource asm = null; + String fileName = asmExternalActions.get(objKey); + Object obj = objKey.get(); + if (obj instanceof ASMSource) { + asm = (ASMSource) obj; + } + if (obj instanceof DefineButtonTag) { + DefineButtonTag defineButton = (DefineButtonTag) obj; + asm = defineButton.getSubItems().get(0); + } + if (asm != null) { + AS2ScriptImporter importer = new AS2ScriptImporter(); + try { + importer.importActionScript(directory.toPath().resolve(fileName).toFile().getAbsolutePath(), asm, null); + } catch (InterruptedException ex) { + break; + } + } + } + } + if (!tagExternalFiles.isEmpty()) { + for (IdentityKey tagKey : tagExternalFiles.keySet()) { + String fileName = tagExternalFiles.get(tagKey); + Tag tag = tagKey.get(); + if (tag == null) { + continue; + } + if (tag instanceof ImageTag) { + ImageTag imageTag = (ImageTag) tag; + ImageImporter importer = new ImageImporter(); + importer.importImage(imageTag, Helper.readFile(directory.toPath().resolve(fileName).toFile().getAbsolutePath()), -1); + String baseName = new File(fileName).getName(); + if (baseName.contains(".")) { + baseName = baseName.substring(0, baseName.lastIndexOf(".")); + } + String alphaFile = new File(fileName).getParentFile().getAbsolutePath() + "/" + baseName + ".alpha.png"; + + if (new File(alphaFile).exists()) { + importer.importImageAlpha(imageTag, Helper.readFile(alphaFile)); + } + } else if (tag instanceof DefineSoundTag) { + DefineSoundTag defineSoundTag = (DefineSoundTag) tag; + SoundImporter importer = new SoundImporter(); + int format = SoundFormat.FORMAT_UNCOMPRESSED_LITTLE_ENDIAN; + if (fileName.toLowerCase(Locale.ENGLISH).endsWith(".mp3")) { + format = SoundFormat.FORMAT_MP3; + } + try (FileInputStream fis = new FileInputStream(directory.toPath().resolve(fileName).toFile().getAbsolutePath())) { + importer.importDefineSound(defineSoundTag, fis, format); + } catch (SoundImportException ex) { + logger.log(Level.SEVERE, "Cannot import sound", ex); + } catch (IOException ex) { + logger.log(Level.SEVERE, "Cannot read sound", ex); + } + } else { + logger.log(Level.WARNING, "Unrecognized tag type for external file: {0}", tag.getTagName()); + } + } + } } private void setSwfAndTimelined(SWF swf) { for (Tag t : swf.getTags()) { - t.setSwf(swf); + t.setSwf(swf, true); t.setTimelined(swf); if (t instanceof DefineSpriteTag) { DefineSpriteTag s = (DefineSpriteTag) t; for (Tag st : s.getTags()) { - st.setSwf(swf); + st.setSwf(swf, true); st.setTimelined(s); } } @@ -269,7 +352,7 @@ public class SwfXmlImporter { XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); try { XMLStreamReader reader = xmlFactory.createXMLStreamReader(new StringReader(xml)); - return processObject(reader, requiredType, swf, null, 1); + return processObject(reader, requiredType, swf, null, 1, new HashMap<>(), new HashMap<>()); } catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException | InstantiationException | InvocationTargetException | XMLStreamException ex) { Logger.getLogger(SwfXmlImporter.class.getName()).log(Level.SEVERE, null, ex); @@ -311,7 +394,7 @@ public class SwfXmlImporter { }*/ } - private void processElement(XMLStreamReader reader, Object obj, SWF swf, Tag tag, int xmlExportMajor) throws XMLStreamException { + private void processElement(XMLStreamReader reader, Object obj, SWF swf, Tag tag, int xmlExportMajor, Map, String> asmExternalActions, Map, String> tagExternalFiles) throws XMLStreamException { // Check if element started and start if needed if (!reader.isStartElement()) { reader.nextTag(); @@ -369,6 +452,30 @@ public class SwfXmlImporter { if (name.equals("reserved3") && "FileAttributesTag".equals(attributes.get("type"))) { name = "reservedB"; } + + if (name.equals("_externalActions")) { + if (xmlExportMajor < XML_IMPORT_VERSION_MAJOR_WITH_EXTERNAL_FILES) { + logger.log(Level.WARNING, "For _externalActions attribute _xmlExportMajor must be >= 3. The attribute is ignored."); + continue; + } + asmExternalActions.put(new IdentityKey<>(obj), val); + continue; + } + + if (obj instanceof Tag && name.equals("_externalFile")) { + if (xmlExportMajor < XML_IMPORT_VERSION_MAJOR_WITH_EXTERNAL_FILES) { + logger.log(Level.WARNING, "For _externalFile attribute _xmlExportMajor must be >= 3. The attribute is ignored."); + continue; + } + tagExternalFiles.put(new IdentityKey<>((Tag) obj), val); + continue; + } + + if (name.equals("actionBytes") && attributes.containsKey("_externalActions")) { + if (xmlExportMajor >= XML_IMPORT_VERSION_MAJOR_WITH_EXTERNAL_FILES) { + continue; + } + } if (!name.equals("type")) { try { @@ -397,7 +504,7 @@ public class SwfXmlImporter { // Check for list item elements reader.nextTag(); while (reader.isStartElement()) { - Object childObj = processObject(reader, reqType, swf, tag, xmlExportMajor); + Object childObj = processObject(reader, reqType, swf, tag, xmlExportMajor, asmExternalActions, tagExternalFiles); list.add(childObj); reader.nextTag(); @@ -414,7 +521,7 @@ public class SwfXmlImporter { setFieldValue(field, obj, value); } else { - Object childObj = processObject(reader, null, swf, tag, xmlExportMajor); + Object childObj = processObject(reader, null, swf, tag, xmlExportMajor, asmExternalActions, tagExternalFiles); setFieldValue(field, obj, childObj); } } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException @@ -432,7 +539,7 @@ public class SwfXmlImporter { } } - private Object processObject(XMLStreamReader reader, Class requiredType, SWF swf, Tag tag, int xmlExportMajor) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, XMLStreamException { + private Object processObject(XMLStreamReader reader, Class requiredType, SWF swf, Tag tag, int xmlExportMajor, Map, String> asmExternalActions, Map, String> tagExternalFiles) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, XMLStreamException { // Check if element started and start if needed if (!reader.isStartElement()) { reader.nextTag(); @@ -465,7 +572,7 @@ public class SwfXmlImporter { tag = (Tag) childObj; } - processElement(reader, childObj, swf, tag, xmlExportMajor); + processElement(reader, childObj, swf, tag, xmlExportMajor, asmExternalActions, tagExternalFiles); ret = childObj; } else { String isNullAttr = attributes.get("isNull"); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java index 3632cc78c..4fccf35df 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButton2Tag.java @@ -377,4 +377,17 @@ public class DefineButton2Tag extends ButtonTag implements ASMSourceContainer { needed.add(rec.characterId); } } + + @Override + public void setSwf(SWF swf, boolean deep) { + super.setSwf(swf, deep); + + if (deep) { + if (actions != null) { + for (BUTTONCONDACTION action : actions) { + action.setSourceTag(this); + } + } + } + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java index 9dd1ab720..5a146daa0 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineButtonTag.java @@ -329,5 +329,5 @@ public class DefineButtonTag extends ButtonTag implements ASMSourceContainer { for (BUTTONRECORD rec : characters) { needed.add(rec.characterId); } - } + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java index fd29c97d6..c80886348 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/PlaceObjectTypeTag.java @@ -20,6 +20,7 @@ import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.SWFOutputStream; import com.jpexs.decompiler.flash.amf.amf3.Amf3Value; import com.jpexs.decompiler.flash.tags.Tag; +import com.jpexs.decompiler.flash.types.CLIPACTIONRECORD; import com.jpexs.decompiler.flash.types.CLIPACTIONS; import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.MATRIX; @@ -361,4 +362,19 @@ public abstract class PlaceObjectTypeTag extends Tag implements CharacterIdTag, result += "_" + getDepth(); return result; } + + @Override + public void setSwf(SWF swf, boolean deep) { + super.setSwf(swf, deep); + + if (deep) { + CLIPACTIONS clipActions = getClipActions(); + if (clipActions != null) { + for (CLIPACTIONRECORD rec : clipActions.clipActionRecords) { + rec.setParentClipActions(clipActions); + rec.setSourceTag(this); + } + } + } + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/helpers/IdentityKey.java b/libsrc/ffdec_lib/src/com/jpexs/helpers/IdentityKey.java new file mode 100644 index 000000000..dfb1f0f55 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/helpers/IdentityKey.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010-2026 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.helpers; + +/** + * + * @author JPEXS + */ +public class IdentityKey { + private final T value; + + public IdentityKey(T value) { + this.value = value; + } + + @Override + public int hashCode() { + return System.identityHashCode(value); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof IdentityKey + && ((IdentityKey) obj).value == value; + } + + public T get() { + return value; + } +} \ No newline at end of file diff --git a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/SwfXmlExportImportTest.java b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/SwfXmlExportImportTest.java index 52d0cf19c..74ff2bf61 100644 --- a/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/SwfXmlExportImportTest.java +++ b/libsrc/ffdec_lib/test/com/jpexs/decompiler/flash/SwfXmlExportImportTest.java @@ -98,7 +98,7 @@ public class SwfXmlExportImportTest extends FileTestBase { SWF swf2 = new SWF(); try ( FileInputStream fis = new FileInputStream(outFile)) { - new SwfXmlImporter().importSwf(swf2, fis); + new SwfXmlImporter().importSwf(swf2, fis, outFile.getParentFile()); } if (swf.getTags().size() != swf2.getTags().size()) { diff --git a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index 092ad2db0..e3f61cac9 100644 --- a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -102,6 +102,7 @@ import com.jpexs.decompiler.flash.exporters.settings.SoundExportSettings; import com.jpexs.decompiler.flash.exporters.settings.SpriteExportSettings; import com.jpexs.decompiler.flash.exporters.settings.SymbolClassExportSettings; import com.jpexs.decompiler.flash.exporters.settings.TextExportSettings; +import com.jpexs.decompiler.flash.exporters.settings.XmlSwfExportSettings; import com.jpexs.decompiler.flash.exporters.swf.SwfToSwcExporter; import com.jpexs.decompiler.flash.exporters.swf.SwfXmlExporter; import com.jpexs.decompiler.flash.flexsdk.MxmlcAs3ScriptReplacer; @@ -701,7 +702,7 @@ public class CommandLineArgumentParser { parseDecrypt(args); System.exit(0); } else if (command.equals("swf2xml")) { - parseSwf2Xml(args, charset); + parseSwf2Xml(args, charset, handler); System.exit(0); } else if (command.equals("xml2swf")) { parseXml2Swf(args, charset); @@ -2711,15 +2712,58 @@ public class CommandLineArgumentParser { System.exit(result ? 0 : 1); } - private static void parseSwf2Xml(Stack args, String charset) { + private static void parseSwf2Xml(Stack args, String charset, AbortRetryIgnoreHandler handler) { if (args.size() < 2) { badArguments("swf2xml"); } + ScriptExportMode scriptExportMode = null; + ImageExportMode imageExportMode = null; + SoundExportMode soundExportMode = null; + + String arg = args.pop(); + if ("-external".equals(arg.toLowerCase(Locale.ENGLISH))) { + if (args.size() < 3) { + badArguments("swf2xml"); + } + String ext = args.pop(); + String[] parts = ext.split(",", -1); + + for (String part : parts) { + switch (part) { + case "as12script": + case "as12script:as": + scriptExportMode = ScriptExportMode.AS; + break; + case "image:png_gif_jpeg": + imageExportMode = ImageExportMode.PNG_GIF_JPEG; + break; + case "image": + case "image:png_gif_jpeg_alpha": + imageExportMode = ImageExportMode.PNG_GIF_JPEG_ALPHA; + break; + case "definesound": + case "definesound:mp3_wav_flv": + soundExportMode = SoundExportMode.MP3_WAV_FLV; + break; + case "all": + scriptExportMode = ScriptExportMode.AS; + imageExportMode = ImageExportMode.PNG_GIF_JPEG_ALPHA; + soundExportMode = SoundExportMode.MP3_WAV_FLV; + break; + default: + System.err.println("Unsupported external value: \"" + part + "\""); + badArguments("swf2xml"); + } + } + } else { + args.push(arg); + } + try { try (StdInAwareFileInputStream is = new StdInAwareFileInputStream(args.pop())) { SWF swf = new SWF(is, Configuration.parallelSpeedUp.get(), charset); - new SwfXmlExporter().exportXml(swf, new File(args.pop())); + new SwfXmlExporter().exportXml(swf, new File(args.pop()), new XmlSwfExportSettings(scriptExportMode, imageExportMode, soundExportMode), null, handler); } catch (FileNotFoundException ex) { System.err.println("File not found."); System.exit(1); @@ -2738,8 +2782,10 @@ public class CommandLineArgumentParser { try { SWF swf = new SWF(charset); - try (StdInAwareFileInputStream in = new StdInAwareFileInputStream(args.pop())) { - new SwfXmlImporter().importSwf(swf, in); + String fileName = args.pop(); + try (StdInAwareFileInputStream in = new StdInAwareFileInputStream(fileName)) { + File file = new File(StdInAwareFileInputStream.STDIN_PATH.equals(fileName) ? "." : fileName); + new SwfXmlImporter().importSwf(swf, in, file.getParentFile()); } try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(new File(args.pop())))) { swf.saveTo(fos); diff --git a/src/com/jpexs/decompiler/flash/console/help.txt b/src/com/jpexs/decompiler/flash/console/help.txt index 608ca3775..2e0303e3f 100644 --- a/src/com/jpexs/decompiler/flash/console/help.txt +++ b/src/com/jpexs/decompiler/flash/console/help.txt @@ -72,9 +72,19 @@ alias /? Decrypt HARMAN Air encrypted file. Decrypt HARMAN Air encrypted file and saves it to . --swf2xml +-swf2xml [-external ] Convert SWF to XML. Convert the SWF to XML file. + -external parameter sets which items will be available externally + Values for : Use comma separated list of following: + as12script:as + as12script (same as "as12script:as") + image:png_gif_jpeg + image:png_gif_jpeg_alpha + image (same as "image:png_gif_jpeg_alpha") + definesound:mp3_wav_flv + definesound (same as "definesound:mp3_wav_flv") + all (same as "as12script,image,definesound") -xml2swf Convert XML to SWF. @@ -448,7 +458,7 @@ Pre-options: When it has .bin extension, legacy storage is used. -onerror (abort|retry |ignore) - Applies to: -export, -importScript + Applies to: -export, -importScript, -swf2xml Error handling mode. "abort" stops the exporting "retry" tries the exporting N times diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 6218c21b0..661dbece2 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -92,6 +92,7 @@ import com.jpexs.decompiler.flash.exporters.settings.SoundExportSettings; import com.jpexs.decompiler.flash.exporters.settings.SpriteExportSettings; import com.jpexs.decompiler.flash.exporters.settings.SymbolClassExportSettings; import com.jpexs.decompiler.flash.exporters.settings.TextExportSettings; +import com.jpexs.decompiler.flash.exporters.settings.XmlSwfExportSettings; import com.jpexs.decompiler.flash.exporters.swf.SwfFlashDevelopExporter; import com.jpexs.decompiler.flash.exporters.swf.SwfIntelliJIdeaExporter; import com.jpexs.decompiler.flash.exporters.swf.SwfJavaExporter; @@ -4618,6 +4619,12 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se public void exportSwfXml(List items) { View.checkAccess(); + + XmlExportDialog dialog = new XmlExportDialog(Main.getDefaultDialogsOwner()); + if (dialog.showExportDialog() != AppDialog.OK_OPTION) { + return; + } + Set usedOpenables = new LinkedHashSet<>(); Set usedOpenableLists = new HashSet<>(); @@ -4678,6 +4685,9 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se return; } selFile = Helper.fixDialogFile(fc.getSelectedFile()).getAbsolutePath(); + + Configuration.lastExportDir.set(new File(selFile).getParentFile().getAbsolutePath()); + if (!selFile.toLowerCase(Locale.ENGLISH).endsWith(".xml")) { selFile = selFile + ".xml"; } @@ -4714,7 +4724,8 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se try { new RetryTask(() -> { File outFile = new File(selFile2); - new SwfXmlExporter().exportXml(openable, outFile); + XmlSwfExportSettings settings = new XmlSwfExportSettings(dialog.getEnumValue(ScriptExportMode.class), dialog.getEnumValue(ImageExportMode.class), dialog.getEnumValue(SoundExportMode.class)); + new SwfXmlExporter().exportXml(openable, outFile, settings, openable.getExportEventListener(), new GuiAbortRetryIgnoreHandler()); }, handler).run(); } catch (IOException ex) { @@ -4760,7 +4771,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se File selfile = Helper.fixDialogFile(selectedFile); try { try (FileInputStream fis = new FileInputStream(selfile)) { - new SwfXmlImporter().importSwf(swf, fis); + new SwfXmlImporter().importSwf(swf, fis, selfile.getParentFile()); } swf.clearAllCache(); swf.assignExportNamesToSymbols(); @@ -4769,8 +4780,9 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se } catch (IOException ex) { logger.log(Level.SEVERE, null, ex); } - } - } + Main.stopWork(); + } + } } public void renameIdentifiers(final Openable openable) { diff --git a/src/com/jpexs/decompiler/flash/gui/XmlExportDialog.java b/src/com/jpexs/decompiler/flash/gui/XmlExportDialog.java new file mode 100644 index 000000000..4542c63e5 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/XmlExportDialog.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2010-2026 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.gui; + +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.modes.ImageExportMode; +import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode; +import com.jpexs.decompiler.flash.exporters.modes.SoundExportMode; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +/** + * + * @author JPEXS + */ +public class XmlExportDialog extends AppDialog { + private int result = ERROR_OPTION; + + private final Map, JComboBox> combos = new HashMap<>(); + + private final Map, String> enumNames = new HashMap<>(); + + private final List allDefaults = Arrays.asList(ScriptExportMode.AS, ImageExportMode.PNG_GIF_JPEG_ALPHA, SoundExportMode.MP3_WAV_FLV); + + public XmlExportDialog(Window owner) { + super(owner); + + enumNames.put(ScriptExportMode.class, "as12script"); + enumNames.put(ImageExportMode.class, "image"); + enumNames.put(SoundExportMode.class, "definesound"); + setTitle(translate("dialog.title")); + JPanel buttonsPanel = new JPanel(new FlowLayout()); + JButton okButton = new JButton(translate("button.ok")); + okButton.addActionListener(this::okButtonActionPerformed); + JButton cancelButton = new JButton(translate("button.cancel")); + cancelButton.addActionListener(this::cancelButtonActionPerformed); + buttonsPanel.add(okButton); + buttonsPanel.add(cancelButton); + + JPanel centralPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.insets = new Insets(1, 2, 1, 2); + + JLabel selectLabel = new JLabel(translate("selectExternal")); + gbc.gridwidth = 4; + centralPanel.add(selectLabel, gbc); + + gbc.gridwidth = 1; + gbc.gridy++; + + DefaultComboBoxModel as12ScriptsModel = new DefaultComboBoxModel<>(); + as12ScriptsModel.addElement(new ComboValue(null, translate("xml"))); + as12ScriptsModel.addElement(new ComboValue(ScriptExportMode.AS, translateExport("scripts.as"))); + + addComboRow(centralPanel, gbc, translate("as12scripts"), "as", as12ScriptsModel, ScriptExportMode.class); + + DefaultComboBoxModel imageModel = new DefaultComboBoxModel<>(); + imageModel.addElement(new ComboValue(null, translate("xml"))); + imageModel.addElement(new ComboValue(ImageExportMode.PNG_GIF_JPEG, translateExport("images.png_gif_jpeg"))); + imageModel.addElement(new ComboValue(ImageExportMode.PNG_GIF_JPEG_ALPHA, translateExport("images.png_gif_jpeg_alpha"))); + + addComboRow(centralPanel, gbc, translateExport("images"), "image", imageModel, ImageExportMode.class); + + DefaultComboBoxModel soundModel = new DefaultComboBoxModel<>(); + soundModel.addElement(new ComboValue(null, translate("xml"))); + soundModel.addElement(new ComboValue(SoundExportMode.MP3_WAV_FLV, translateExport("sounds.mp3_wav_flv"))); + + addComboRow(centralPanel, gbc, translate("defineSound"), "sound", soundModel, SoundExportMode.class); + + + + String config = Configuration.lastSelectedXmlExportFormats.get(); + if (!config.isEmpty()) { + String[] parts = config.split(",", -1); + for (String part : parts) { + String[] parts2 = part.split("\\.", -1); + if (parts2.length != 2) { + continue; + } + String name = parts2[0]; + String value = parts2[1]; + for (Class cls : combos.keySet()) { + if (enumNames.get(cls).equals(name)) { + JComboBox combo = combos.get(cls); + DefaultComboBoxModel model = (DefaultComboBoxModel) combo.getModel(); + for (int i = 0; i < model.getSize(); i++) { + ComboValue cv = model.getElementAt(i); + if (cv.value == null && value.equals("none")) { + combo.setSelectedIndex(0); + break; + } + if (cv.value != null && cv.value.toString().toLowerCase(Locale.ENGLISH).equals(value)) { + combo.setSelectedIndex(i); + break; + } + } + break; + } + } + } + } + + JButton allButton = new JButton(translate("all")); + allButton.addActionListener(this::allButtonActionPerformed); + + JButton noneButton = new JButton(translate("none")); + noneButton.addActionListener(this::noneButtonActionPerformed); + + + gbc.gridx = 2; + gbc.insets = new Insets(5, 2, 1, 2); + gbc.anchor = GridBagConstraints.CENTER; + centralPanel.add(noneButton, gbc); + gbc.gridx++; + centralPanel.add(allButton, gbc); + + Container cnt = getContentPane(); + cnt.setLayout(new BorderLayout()); + cnt.add(centralPanel, BorderLayout.CENTER); + cnt.add(buttonsPanel, BorderLayout.SOUTH); + + pack(); + View.centerScreen(this); + View.setWindowIcon(this, "exportxml"); + getRootPane().setDefaultButton(okButton); + setModal(true); + } + + private void addComboRow(JPanel centralPanel, GridBagConstraints gbc, String label, String icon, ComboBoxModel model, Class enumClass) { + gbc.fill = GridBagConstraints.NONE; + + JLabel scriptsLabel = new JLabel(label); + scriptsLabel.setIcon(View.getIcon(icon + "16")); + scriptsLabel.setHorizontalTextPosition(SwingConstants.LEFT); + gbc.anchor = GridBagConstraints.LINE_END; + centralPanel.add(scriptsLabel, gbc); + + gbc.gridx++; + JLabel arrowLabel = new JLabel(translateExport("arrow")); + gbc.insets = new Insets(1, 5, 1, 5); + gbc.anchor = GridBagConstraints.CENTER; + centralPanel.add(arrowLabel, gbc); + + gbc.insets = new Insets(1, 2, 1, 2); + + gbc.gridx++; + gbc.gridwidth = 2; + JComboBox combo = new JComboBox<>(model); + gbc.anchor = GridBagConstraints.LINE_START; + gbc.fill = GridBagConstraints.BOTH; + + combos.put(enumClass, combo); + + centralPanel.add(combo, gbc); + gbc.gridy++; + gbc.gridwidth = 1; + gbc.gridx = 0; + gbc.fill = GridBagConstraints.NONE; + } + + + public > T getEnumValue(Class cls) { + if (!combos.containsKey(cls)) { + return null; + } + ComboValue cv = (ComboValue) combos.get(cls).getSelectedItem(); + if (cv == null) { + return null; + } + @SuppressWarnings("unchecked") + T ret = (T) cv.value; + return ret; + } + + private void okButtonActionPerformed(ActionEvent evt) { + + List vals = new ArrayList<>(); + for (Class cls : combos.keySet()) { + String name = enumNames.get(cls); + ComboValue cv = (ComboValue) combos.get(cls).getSelectedItem(); + if (cv == null || cv.value == null) { + vals.add(name + "." + "none"); + } else { + vals.add(name + "." + cv.value.toString().toLowerCase()); + } + } + Configuration.lastSelectedXmlExportFormats.set(String.join(",", vals)); + + result = OK_OPTION; + setVisible(false); + } + + private void noneButtonActionPerformed(ActionEvent evt) { + for (Class cls : combos.keySet()) { + JComboBox combo = combos.get(cls); + combo.setSelectedIndex(0); + } + } + + private void allButtonActionPerformed(ActionEvent evt) { + for (Class cls : combos.keySet()) { + JComboBox combo = combos.get(cls); + DefaultComboBoxModel model = (DefaultComboBoxModel) combo.getModel(); + for (int i = 0; i < model.getSize(); i++) { + ComboValue value = model.getElementAt(i); + if (value.value != null && allDefaults.contains(value.value)) { + combo.setSelectedIndex(i); + break; + } + } + } + } + + private void cancelButtonActionPerformed(ActionEvent evt) { + result = CANCEL_OPTION; + setVisible(false); + } + + public int showExportDialog() { + setVisible(true); + return result; + } + + private String translateExport(String key) { + return AppDialog.translateForDialog(key, ExportDialog.class); + } + + private class ComboValue { + + public Object value; + public String text; + + public ComboValue(Object value, String text) { + this.value = value; + this.text = text; + } + + @Override + public String toString() { + return text; + } + } +} diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties index 56028d9b8..f01554800 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties @@ -698,3 +698,6 @@ config.description.useMinimumStrokeWidth1Px = Use 1 pixel as minimal stroke widt #after 26.0.0 config.name.showLoadingSpinner = Show loading spinner config.description.showLoadingSpinner = Displays animated loading indicator in status bar + +config.name.xmlExport.formats = (Internal) Xml export external formats +config.description.xmlExport.formats = Last used Xml export external formats. \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties index 90fd293e3..0463e6ea0 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties @@ -694,3 +694,10 @@ config.description.msaaGridForExport = Velikost m\u0159\u00ed\u017eky v\u00edcen config.name.useMinimumStrokeWidth1Px = Minim\u00e1ln\u00ed \u0161\u00ed\u0159ka tahu 1 pixel (jako ve Flashi) config.description.useMinimumStrokeWidth1Px = Pou\u017e\u00edt 1 pixel jako minim\u00e1ln\u00ed \u0161\u00ed\u0159ku tahu. Flash vykresluje tahy t\u00edmto zp\u016fsobem. Vypn\u011bte pro umo\u017en\u011bn\u00ed ten\u010d\u00edch tah\u016f. + +#after 26.0.0 +config.name.showLoadingSpinner = Zobrazit indik\u00e1tor na\u010d\u00edt\u00e1n\u00ed +config.description.showLoadingSpinner = Zobraz\u00ed animovan\u00fd indik\u00e1tor na\u010d\u00edt\u00e1n\u00ed ve stavov\u00e9m \u0159\u00e1dku + +config.name.xmlExport.formats = (Intern\u00ed) Form\u00e1ty extern\u00edho XML exportu +config.description.xmlExport.formats = Naposledy pou\u017eit\u00e9 form\u00e1ty extern\u00edho XML exportu. \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_de.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_de.properties index 676336e06..28461879e 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_de.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_de.properties @@ -623,3 +623,10 @@ config.description.msaaGridForExport = Rastergr\u00f6\u00dfe NxN pro Multisample config.name.useMinimumStrokeWidth1Px = Minimale Strichbreite von 1 Pixel (wie in Flash) config.description.useMinimumStrokeWidth1Px = 1 Pixel als minimale Strichbreite verwenden. Flash rendert Striche auf diese Weise. Deaktivieren, um d\u00fcnnere Striche zu erm\u00f6glichen. + +#after 26.0.0 +config.name.showLoadingSpinner = Ladeanzeige anzeigen +config.description.showLoadingSpinner = Zeigt eine animierte Ladeanzeige in der Statusleiste an + +config.name.xmlExport.formats = (Intern) Formate f\u00fcr externen XML-Export +config.description.xmlExport.formats = Zuletzt verwendete Formate f\u00fcr externen XML-Export. \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_sk.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_sk.properties index 58ac75343..5c7db62c9 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_sk.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_sk.properties @@ -694,3 +694,10 @@ config.description.msaaGridForExport = Ve\u013ekos\u0165 mrie\u017eky viacn\u00e config.name.useMinimumStrokeWidth1Px = Minim\u00e1lna \u0161\u00edrka \u0165ahu 1 pixel (ako vo Flashi) config.description.useMinimumStrokeWidth1Px = Pou\u017ei\u0165 1 pixel ako minim\u00e1lnu \u0161\u00edrku \u0165ahu. Flash vykres\u013euje \u0165ahy t\u00fdmto sp\u00f4sobom. Vypnite pre umo\u017enenie ten\u0161\u00edch \u0165ahov. + +#after 26.0.0 +config.name.showLoadingSpinner = Zobrazi\u0165 indik\u00e1tor na\u010d\u00edtania +config.description.showLoadingSpinner = Zobraz\u00ed animovan\u00fd indik\u00e1tor na\u010d\u00edtania v stavovom riadku + +config.name.xmlExport.formats = (Intern\u00e9) Form\u00e1ty extern\u00e9ho XML exportu +config.description.xmlExport.formats = Naposledy pou\u017eit\u00e9 form\u00e1ty extern\u00e9ho XML exportu. \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog.properties new file mode 100644 index 000000000..2b25421ff --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog.properties @@ -0,0 +1,12 @@ +dialog.title = Export XML +button.ok = OK +button.cancel = Cancel + +selectExternal = Select items which will be available externally: + +all = Select all +none = Select none + +as12scripts = AS1/2 scripts +defineSound = DefineSounds +xml = XML (no external) \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_cs.properties new file mode 100644 index 000000000..b073cdf45 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_cs.properties @@ -0,0 +1,12 @@ +dialog.title = Exportovat XML +button.ok = OK +button.cancel = Storno + +selectExternal = Vyberte polo\u017eky, kter\u00e9 budou dostupn\u00e9 extern\u011b: + +all = Vybrat v\u0161e +none = Nevybrat nic + +as12scripts = AS1/2 skripty +defineSound = DefineSounds +xml = XML (ne extern\u00ed) \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_de.properties b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_de.properties new file mode 100644 index 000000000..17e315ed1 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_de.properties @@ -0,0 +1,12 @@ +dialog.title = XML exportieren +button.ok = OK +button.cancel = Abbrechen + +selectExternal = W\u00e4hlen Sie die Elemente aus, die extern verf\u00fcgbar sein sollen: + +all = Alles ausw\u00e4hlen +none = Nichts ausw\u00e4hlen + +as12scripts = AS1/2-Skripte +defineSound = DefineSounds +xml = XML (nicht extern) \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_sk.properties b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_sk.properties new file mode 100644 index 000000000..6ba90b92a --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/XmlExportDialog_sk.properties @@ -0,0 +1,12 @@ +dialog.title = Exportova\u0165 XML +button.ok = OK +button.cancel = Zru\u0161i\u0165 + +selectExternal = Vyberte polo\u017eky, ktor\u00e9 bud\u00fa dostupn\u00e9 externe: + +all = Vybra\u0165 v\u0161etko +none = Nevybra\u0165 ni\u010d + +as12scripts = AS1/2 skripty +defineSound = DefineSounds +xml = XML (nie extern\u00e9) \ No newline at end of file