feat(xml export): allow external as1/2 scripts, images and defineSounds (#2707)

This commit is contained in:
Jindra Petřík
2026-05-10 16:09:46 +02:00
parent f8652d236a
commit 5bb8a2c23d
24 changed files with 995 additions and 115 deletions

View File

@@ -1240,6 +1240,10 @@ public final class Configuration {
@ConfigurationDefaultBoolean(true)
@ConfigurationCategory("display")
public static ConfigurationItem<Boolean> showLoadingSpinner = null;
@ConfigurationDefaultString("")
@ConfigurationName("xmlExport.formats")
public static ConfigurationItem<String> lastSelectedXmlExportFormats = null;
private static Map<String, String> configurationDescriptions = new LinkedHashMap<>();
private static Map<String, String> configurationTitles = new LinkedHashMap<>();

View File

@@ -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<File> exportImages(AbortRetryIgnoreHandler handler, String outdir, ReadOnlyTagList tags, ImageExportSettings settings, EventListener evl) throws IOException, InterruptedException {
List<File> 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)));

View File

@@ -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<File> exportSounds(AbortRetryIgnoreHandler handler, String outdir, ReadOnlyTagList tags, final SoundExportSettings settings, EventListener evl) throws IOException, InterruptedException {
List<SoundTag> sounds = new ArrayList<>();
for (Tag t : tags) {
@@ -72,7 +93,7 @@ public class SoundExporter {
}
}
return exportSounds(handler, outdir, sounds, settings, evl);
}
}
public List<File> exportSounds(AbortRetryIgnoreHandler handler, String outdir, List<SoundTag> tags, final SoundExportSettings settings, EventListener evl) throws IOException, InterruptedException {
List<File> 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))) {

View File

@@ -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;
}
}

View File

@@ -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<ASMSource, String> asmExternalFiles = new HashMap<>();
if (settings.as12ExportMode != null) {
Map<String, ASMSource> externalNameToAsm = swf.getASMs(true);
Set<String> 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<Tag, String> tagExternalFiles = new IdentityHashMap<>();
List<Tag> imagesList = new ArrayList<>();
if (settings.imageExportMode != null) {
ImageExportSettings imageExportSetttings = new ImageExportSettings(settings.imageExportMode);
Map<Integer, CharacterTag> 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<SoundTag> soundList = new ArrayList<>();
if (settings.defineSoundExportMode != null) {
SoundExportSettings soundExportSetttings = new SoundExportSettings(settings.defineSoundExportMode);
Map<Integer, CharacterTag> 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<ASMSource, String> asmExternalFiles,
Map<Tag, String> 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<Field> 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<ASMSource, String> asmExternalFiles,
Map<Tag, String> 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);
}

View File

@@ -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();
}
}
}

View File

@@ -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<IdentityKey<Object>, String> asmExternalActions = new LinkedHashMap<>();
Map<IdentityKey<Tag>, 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<Object> 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<Tag> 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<IdentityKey<Object>, String> asmExternalActions, Map<IdentityKey<Tag>, 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<IdentityKey<Object>, String> asmExternalActions, Map<IdentityKey<Tag>, 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");

View File

@@ -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);
}
}
}
}
}

View File

@@ -329,5 +329,5 @@ public class DefineButtonTag extends ButtonTag implements ASMSourceContainer {
for (BUTTONRECORD rec : characters) {
needed.add(rec.characterId);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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<T> {
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;
}
}

View File

@@ -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()) {