diff --git a/CHANGELOG.md b/CHANGELOG.md index aa83fef14..5cb3f6e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added - [#2090] Support for Mochicrypt packed binarydata tags - loading SWF as subtree -- [#2079] Replace DefineSprite with GIF +- [#2079] Replace DefineSprite with GIF, Bulk import sprites from GIFs ## [19.0.0] - 2023-10-01 ### Added 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 273b8b665..76749bded 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 @@ -924,6 +924,9 @@ public final class Configuration { @ConfigurationDefaultBoolean(true) public static ConfigurationItem lastFlaExportCompressed = null; + @ConfigurationDefaultBoolean(true) + @ConfigurationCategory("ui") + public static ConfigurationItem showImportSpriteInfo = null; private enum OSId { WINDOWS, OSX, UNIX } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SpriteImporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SpriteImporter.java index d84387ec9..dba1885c8 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SpriteImporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/SpriteImporter.java @@ -22,20 +22,27 @@ import com.jpexs.decompiler.flash.SWF; import com.jpexs.decompiler.flash.tags.DefineShape2Tag; import com.jpexs.decompiler.flash.tags.DefineSpriteTag; import com.jpexs.decompiler.flash.tags.PlaceObject2Tag; -import com.jpexs.decompiler.flash.tags.RemoveObjectTag; import com.jpexs.decompiler.flash.tags.ShowFrameTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.base.CharacterIdTag; import com.jpexs.decompiler.flash.tags.base.CharacterTag; import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag; -import com.jpexs.decompiler.flash.timeline.Timelined; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.imageio.ImageIO; /** @@ -145,4 +152,70 @@ public class SpriteImporter { return true; } + + public int bulkImport(File spritesDir, SWF swf, boolean printOut) { + Map characters = swf.getCharacters(); + int spriteCount = 0; + List extensions = Arrays.asList("gif"); + File allFiles[] = spritesDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + String nameLower = name.toLowerCase(); + for (String ext : extensions) { + if (nameLower.endsWith("." + ext)) { + return true; + } + } + return false; + } + }); + for (int characterId : characters.keySet()) { + CharacterTag tag = characters.get(characterId); + if (tag instanceof DefineSpriteTag) { + DefineSpriteTag spriteTag = (DefineSpriteTag) tag; + List existingFilesForSpriteTag = new ArrayList<>(); + for (File f : allFiles) { + if (f.getName().startsWith("" + characterId + ".") || f.getName().startsWith("" + characterId + "_")) { + existingFilesForSpriteTag.add(f); + } + } + existingFilesForSpriteTag.sort(new Comparator() { + @Override + public int compare(File o1, File o2) { + String ext1 = o1.getName().substring(o1.getName().lastIndexOf(".") + 1); + String ext2 = o2.getName().substring(o2.getName().lastIndexOf(".") + 1); + int ret = extensions.indexOf(ext1) - extensions.indexOf(ext2); + if (ret == 0) { + return o1.getName().compareTo(o2.getName()); + } + return ret; + } + }); + + if (existingFilesForSpriteTag.isEmpty()) { + continue; + } + + if (existingFilesForSpriteTag.size() > 1) { + Logger.getLogger(SpriteImporter.class.getName()).log(Level.WARNING, "Multiple matching files for sprite tag {0} exists, {1} selected", new Object[]{characterId, existingFilesForSpriteTag.get(0).getName()}); + } + File sourceFile = existingFilesForSpriteTag.get(0); + + if (printOut) { + System.out.println("Importing character " + characterId + " from file " + sourceFile.getName()); + } + + try(FileInputStream fis = new FileInputStream(sourceFile.getAbsolutePath())) { + importSprite(spriteTag, fis); + spriteCount++; + } catch (IOException ex) { + Logger.getLogger(ShapeImporter.class.getName()).log(Level.WARNING, "Cannot import sprite " + characterId + " from file " + sourceFile.getName(), ex); + } + if (Thread.currentThread().isInterrupted()) { + break; + } + } + } + return spriteCount; + } } diff --git a/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java b/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java index 480f43439..c24e3c188 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/MainFrameMenu.java @@ -380,6 +380,16 @@ public abstract class MainFrameMenu implements MenuBuilder { } mainFrame.getPanel().importShape((SWF) openable, true); } + + protected void importSpritesActionPerformed(ActionEvent evt) { + if (Main.isWorking()) { + return; + } + if (mainFrame.getPanel().checkEdited()) { + return; + } + mainFrame.getPanel().importSprite((SWF) openable); + } protected void importMoviesActionPerformed(ActionEvent evt) { if (Main.isWorking()) { @@ -1031,6 +1041,7 @@ public abstract class MainFrameMenu implements MenuBuilder { setMenuEnabled("/import/importtab/importText", allSameSwf && swfSelected && !isWorking); setMenuEnabled("/import/importtab/importScript", allSameOpenable && openableSelected && !isWorking); setMenuEnabled("/import/importtab/importImages", allSameSwf && swfSelected && !isWorking); + setMenuEnabled("/import/importtab/importSprites", allSameSwf && swfSelected && !isWorking); setMenuEnabled("/import/importtab/importShapes", allSameSwf && swfSelected && !isWorking); setMenuEnabled("/import/importtab/importShapesNoFill", allSameSwf && swfSelected && !isWorking); setMenuEnabled("/import/importtab/importMovies", allSameSwf && swfSelected && !isWorking); @@ -1164,6 +1175,7 @@ public abstract class MainFrameMenu implements MenuBuilder { addMenuItem("/import/importtab/importImages", translate("menu.file.import.image"), "importimage32", this::importImagesActionPerformed, PRIORITY_MEDIUM, null, true, null, false); addMenuItem("/import/importtab/importShapes", translate("menu.file.import.shape"), "importshape32", this::importShapesActionPerformed, PRIORITY_MEDIUM, null, true, null, false); addMenuItem("/import/importtab/importShapesNoFill", translate("menu.file.import.shapeNoFill"), "importshape32", this::importShapesNoFillActionPerformed, PRIORITY_MEDIUM, null, true, null, false); + addMenuItem("/import/importtab/importSprites", translate("menu.file.import.sprite"), "importsprite32", this::importSpritesActionPerformed, PRIORITY_MEDIUM, null, true, null, false); addMenuItem("/import/importtab/importMovies", translate("menu.file.import.movie"), "importmovie32", this::importMoviesActionPerformed, PRIORITY_MEDIUM, null, true, null, false); addMenuItem("/import/importtab/importSounds", translate("menu.file.import.sound"), "importsound32", this::importSoundsActionPerformed, PRIORITY_MEDIUM, null, true, null, false); addMenuItem("/import/importtab/importSymbolClass", translate("menu.file.import.symbolClass"), "importsymbolclass32", this::importSymbolClassActionPerformed, PRIORITY_MEDIUM, null, true, null, false); diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index cc61afadc..1a2381efc 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -3389,6 +3389,65 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se }.execute(); } } + + public void importSprite(final SWF swf) { + ViewMessages.showMessageDialog(MainPanel.this, translate("message.info.importSprites"), translate("message.info"), JOptionPane.INFORMATION_MESSAGE, Configuration.showImportSpriteInfo); + JFileChooser chooser = new JFileChooser(); + chooser.setCurrentDirectory(new File(Configuration.lastExportDir.get())); + chooser.setDialogTitle(translate("import.select.directory")); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setAcceptAllFileFilterUsed(false); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + String selFile = Helper.fixDialogFile(chooser.getSelectedFile()).getAbsolutePath(); + File spritesDir = new File(Path.combine(selFile, SpriteExportSettings.EXPORT_FOLDER_NAME)); + if (!spritesDir.exists()) { + spritesDir = new File(selFile); + } + final File fSpritesDir = spritesDir; + SpriteImporter spriteImporter = new SpriteImporter(); + + final long timeBefore = System.currentTimeMillis(); + new CancellableWorker() { + + private int count = 0; + + @Override + public Void doInBackground() throws Exception { + try { + count = spriteImporter.bulkImport(fSpritesDir, swf, false); + swf.clearImageCache(); + swf.clearShapeCache(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error during import", ex); + ViewMessages.showMessageDialog(null, translate("error.import") + ": " + ex.getClass().getName() + " " + ex.getLocalizedMessage()); + } + return null; + } + + @Override + protected void onStart() { + Main.startWork(translate("work.importing") + "...", this); + } + + @Override + protected void done() { + Main.stopWork(); + long timeAfter = System.currentTimeMillis(); + final long timeMs = timeAfter - timeBefore; + + View.execInEventDispatch(() -> { + refreshTree(swf); + setStatus(translate("import.finishedin").replace("%time%", Helper.formatTimeSec(timeMs))); + + ViewMessages.showMessageDialog(MainPanel.this, translate("import.sprite.result").replace("%count%", Integer.toString(count))); + if (count != 0) { + reload(true); + } + }); + } + }.execute(); + } + } public void importShape(final SWF swf, boolean noFill) { ViewMessages.showMessageDialog(MainPanel.this, translate("message.info.importShapes2"), translate("message.info"), JOptionPane.INFORMATION_MESSAGE, Configuration.showImportShapeInfo); @@ -4439,7 +4498,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se } } - public void replaceWithGifButtonActionPerformed(TreeItem item) { + public void replaceSpriteWithGifButtonActionPerformed(TreeItem item) { if (item == null) { return; } diff --git a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java index 684184c8d..51a499b1d 100644 --- a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java @@ -403,7 +403,7 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel replaceSpriteButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - mainPanel.replaceWithGifButtonActionPerformed(mainPanel.getCurrentTree().getCurrentTreeItem()); + mainPanel.replaceSpriteWithGifButtonActionPerformed(mainPanel.getCurrentTree().getCurrentTreeItem()); } }); replaceSpriteButton.setVisible(false); diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/importsprite16.png b/src/com/jpexs/decompiler/flash/gui/graphics/importsprite16.png new file mode 100644 index 000000000..2edd3587e Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/importsprite16.png differ diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/importsprite32.png b/src/com/jpexs/decompiler/flash/gui/graphics/importsprite32.png new file mode 100644 index 000000000..b08ab1ea9 Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/importsprite32.png differ diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties index e105df715..cb88715c8 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties @@ -704,3 +704,7 @@ config.description.lastFlaExportVersion = Last exported FLA version config.name.lastFlaExportCompressed = Last FLA export compressed config.description.lastFlaExportCompressed = Last exported FLA version compressed + +#after 19.0.0 +config.name.showImportSpriteInfo = Show information before importing sprites +config.description.showImportSpriteInfo = Displays some info about how importing sprites works after clicking Import sprites in the menu. diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 2e36bd09b..bcb3e8b37 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1162,4 +1162,12 @@ button.abcexploretrait = Show trait in ABC Explorer #after 19.0.0 binarydata.swfInside.packer = It looks like there is SWF inside this binary data tag packed with %packer%. Click here to unpack the SWF and load it as subtree. -button.replaceWithGif = Replace with GIF... \ No newline at end of file +button.replaceWithGif = Replace with GIF... + +message.info.importSprites = During importing sprites, you need to select a FOLDER.\r\n \ + The folder must contain "sprites" subfolder and filenames inside must match existing sprites in current selected SWF.\r\n \ + Each file must have ".gif" extension. + +import.sprite.result = %count% sprites imported. + +menu.file.import.sprite = Import sprites from GIF \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java index 1bfa94a39..8a1408fc5 100644 --- a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java @@ -363,7 +363,7 @@ public class TagTreeContextMenu extends JPopupMenu { replaceWithGifMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - mainPanel.replaceWithGifButtonActionPerformed(getCurrentItem()); + mainPanel.replaceSpriteWithGifButtonActionPerformed(getCurrentItem()); } }); replaceWithGifMenuItem.setIcon(View.getIcon("replacesprite16"));