diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7e69edf..2c12358ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to this project will be documented in this file. - AS1/2 direct editation - hide P-code panel when editing - Disable AS1/2/3 direct editation when editing P-code - AS3 - navigation to definition in other SWF file and also player/airglobal +- [#2463] Export subsprites animation context menu on frames ### Changed - AS1/2 - Single DoAction tag inside frame is now displayed directly as frame node @@ -3845,6 +3846,7 @@ Major version of SWF to XML export changed to 2. [#289]: https://www.free-decompiler.com/flash/issues/289 [#2412]: https://www.free-decompiler.com/flash/issues/2412 [#1682]: https://www.free-decompiler.com/flash/issues/1682 +[#2463]: https://www.free-decompiler.com/flash/issues/2463 [#2456]: https://www.free-decompiler.com/flash/issues/2456 [#2459]: https://www.free-decompiler.com/flash/issues/2459 [#2460]: https://www.free-decompiler.com/flash/issues/2460 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java index 2266dd6f4..48337bdb7 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java @@ -131,10 +131,10 @@ public class FrameExporter { } FrameExportSettings fes = new FrameExportSettings(fem, settings.zoom, true); - return exportFrames(handler, outdir, swf, containerId, frames, fes, evl); + return exportFrames(handler, outdir, swf, containerId, frames, 1, fes, evl); } - public List exportSpriteFrames(AbortRetryIgnoreHandler handler, String outdir, SWF swf, int containerId, List frames, SpriteExportSettings settings, EventListener evl) throws IOException, InterruptedException { + public List exportSpriteFrames(AbortRetryIgnoreHandler handler, String outdir, SWF swf, int containerId, List frames, int subframesLength, SpriteExportSettings settings, EventListener evl) throws IOException, InterruptedException { FrameExportMode fem; switch (settings.mode) { case PNG: @@ -166,7 +166,7 @@ public class FrameExporter { } FrameExportSettings fes = new FrameExportSettings(fem, settings.zoom, true); - return exportFrames(handler, outdir, swf, containerId, frames, fes, evl); + return exportFrames(handler, outdir, swf, containerId, frames, subframesLength, fes, evl); } private class MyFrameIterator implements Iterator { @@ -178,6 +178,8 @@ public class FrameExporter { private final boolean usesTransparency; private final Color backgroundColor; private final FrameExportSettings settings; + private final int subframeLength; + private final boolean subFrameMode; public MyFrameIterator( Timeline tim, @@ -185,7 +187,8 @@ public class FrameExporter { final EventListener evl, boolean usesTransparency, Color backgroundColor, - FrameExportSettings settings + FrameExportSettings settings, + int subframeLength ) { this.tim = tim; this.fframes = fframes; @@ -193,6 +196,8 @@ public class FrameExporter { this.usesTransparency = usesTransparency; this.backgroundColor = backgroundColor; this.settings = settings; + this.subframeLength = subframeLength; + this.subFrameMode = this.subframeLength > 1; } public void reset() { @@ -204,6 +209,9 @@ public class FrameExporter { if (CancellableWorker.isInterrupted()) { return false; } + if (subframeLength > 1) { + return subframeLength > pos; + } return fframes.size() > pos; } @@ -224,21 +232,22 @@ public class FrameExporter { if (evl != null) { evl.handleExportingEvent("frame", pos + 1, fframes.size(), tagName); } + int max = subFrameMode ? subframeLength : fframes.size(); - int fframe = fframes.get(pos++); - BufferedImage result = SWF.frameToImageGet(tim, fframe, 0, null, 0, tim.displayRect, new Matrix(), null, backgroundColor == null && !usesTransparency ? Color.white : backgroundColor, settings.zoom, true).getBufferedImage(); + int fframe = subFrameMode ? fframes.get(0) : fframes.get(pos++); + BufferedImage result = SWF.frameToImageGet(tim, fframe, subFrameMode ? pos++ : 0, null, 0, tim.displayRect, new Matrix(), null, backgroundColor == null && !usesTransparency ? Color.white : backgroundColor, settings.zoom, true).getBufferedImage(); if (CancellableWorker.isInterrupted()) { return null; } if (evl != null) { - evl.handleExportedEvent("frame", pos, fframes.size(), tagName); + evl.handleExportedEvent("frame", pos, max, tagName); } return result; } } - public List exportFrames(AbortRetryIgnoreHandler handler, String outdir, final SWF swf, int containerId, List frames, final FrameExportSettings settings, final EventListener evl) throws IOException, InterruptedException { + public List exportFrames(AbortRetryIgnoreHandler handler, String outdir, final SWF swf, int containerId, List frames, int subFramesLength, final FrameExportSettings settings, final EventListener evl) throws IOException, InterruptedException { final List ret = new ArrayList<>(); if (CancellableWorker.isInterrupted()) { return ret; @@ -249,19 +258,24 @@ public class FrameExporter { } Timeline tim0; List paths = new ArrayList<>(); + String subPath = ""; + if (subFramesLength > 1) { + subPath = File.separator + frames.get(0); + } + if (containerId == 0) { tim0 = swf.getTimeline(); - paths.add(""); + paths.add(subPath); } else { tim0 = ((Timelined) swf.getCharacter(containerId)).getTimeline(); Set classNames = swf.getCharacter(containerId).getClassNames(); if (Configuration.as3ExportNamesUseClassNamesOnly.get() && !classNames.isEmpty()) { for (String className : classNames) { - paths.add(File.separator + Helper.makeFileName(className)); + paths.add(File.separator + Helper.makeFileName(className) + subPath); } } else { - paths.add(File.separator + Helper.makeFileName(swf.getCharacter(containerId).getExportFileName())); + paths.add(File.separator + Helper.makeFileName(swf.getCharacter(containerId).getExportFileName()) + subPath); } } @@ -292,18 +306,22 @@ public class FrameExporter { } if (settings.mode == FrameExportMode.SVG) { - for (int i = 0; i < frames.size(); i++) { + int max = frames.size(); + if (subFramesLength > 1) { + max = subFramesLength; + } + for (int i = 0; i < max; i++) { if (evl != null) { Tag parentTag = tim.getParentTag(); - evl.handleExportingEvent("frame", i + 1, frames.size(), parentTag == null ? "" : parentTag.getName()); + evl.handleExportingEvent("frame", i + 1, max, parentTag == null ? "" : parentTag.getName()); } final int fi = i; final Color fbackgroundColor = backgroundColor; for (File foutdir : foutdirs) { new RetryTask(() -> { - int frame = fframes.get(fi); - File f = new File(foutdir + File.separator + (frame + 1) + ".svg"); + int frame = subFramesLength > 1 ? fframes.get(0) : fframes.get(fi); + File f = new File(foutdir + File.separator + ((subFramesLength > 1 ? fi : fframes.get(fi)) + 1) + ".svg"); try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(f))) { ExportRectangle rect = new ExportRectangle(tim.displayRect); rect.xMax *= settings.zoom; @@ -312,7 +330,7 @@ public class FrameExporter { rect.yMin *= settings.zoom; SVGExporter exporter = new SVGExporter(rect, settings.zoom, "frame", fbackgroundColor); - tim.toSVG(frame, 0, null, 0, exporter, null, 0, new Matrix(), new Matrix()); + tim.toSVG(frame, subFramesLength > 1 ? fi : 0, null, 0, exporter, null, 0, new Matrix(), new Matrix()); fos.write(Utf8Helper.getBytes(exporter.getSVG())); } ret.add(f); @@ -325,7 +343,7 @@ public class FrameExporter { if (evl != null) { Tag parentTag = tim.getParentTag(); - evl.handleExportedEvent("frame", i + 1, frames.size(), parentTag == null ? "" : parentTag.getName()); + evl.handleExportedEvent("frame", i + 1, max, parentTag == null ? "" : parentTag.getName()); } } @@ -460,7 +478,7 @@ public class FrameExporter { try { new PreviewExporter().exportSwf(fos, swf.getCharacter(containerId), fBackgroundColor, 0, false); } catch (ActionParseException ex) { - Logger.getLogger(MorphShapeExporter.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(FrameExporter.class.getName()).log(Level.SEVERE, null, ex); } } @@ -478,7 +496,7 @@ public class FrameExporter { try { new PreviewExporter().exportSwf(fos, fn, fBackgroundColor, 0, false); } catch (ActionParseException ex) { - Logger.getLogger(MorphShapeExporter.class.getName()).log(Level.SEVERE, null, ex); + Logger.getLogger(FrameExporter.class.getName()).log(Level.SEVERE, null, ex); } } @@ -491,7 +509,7 @@ public class FrameExporter { final Color fbackgroundColor = backgroundColor; final boolean usesTransparency = settings.mode == FrameExportMode.PNG || settings.mode == FrameExportMode.GIF; - final MyFrameIterator frameImages = new MyFrameIterator(tim, fframes, evl, usesTransparency, backgroundColor, settings); + final MyFrameIterator frameImages = new MyFrameIterator(tim, fframes, evl, usesTransparency, backgroundColor, settings, subFramesLength); switch (settings.mode) { case GIF: @@ -510,7 +528,9 @@ public class FrameExporter { for (int i = 0; frameImages.hasNext(); i++) { final int fi = i; new RetryTask(() -> { - File f = new File(foutdir + File.separator + (fframes.get(fi) + 1) + ".bmp"); + int fileNum = subFramesLength > 1 ? fi + 1 : (fframes.get(fi) + 1); + + File f = new File(foutdir + File.separator + fileNum + ".bmp"); BufferedImage img = frameImages.next(); if (img != null) { BMPFile.saveBitmap(img, f); @@ -526,7 +546,8 @@ public class FrameExporter { for (int i = 0; frameImages.hasNext(); i++) { final int fi = i; new RetryTask(() -> { - File file = new File(foutdir + File.separator + (fframes.get(fi) + 1) + ".png"); + int fileNum = subFramesLength > 1 ? fi + 1 : (fframes.get(fi) + 1); + File file = new File(foutdir + File.separator + fileNum + ".png"); BufferedImage img = frameImages.next(); if (img != null) { ImageHelper.write(img, ImageFormat.PNG, file); @@ -562,14 +583,19 @@ public class FrameExporter { Matrix transformation = m; Map existingFonts = new HashMap<>(); - while (pos < fframes.size()) { + int maxPos = fframes.size(); + if (subFramesLength > 1) { + maxPos = subFramesLength; + } + + while (pos < maxPos) { if (evl != null) { Tag parentTag = tim.getParentTag(); String tagName = parentTag == null ? "" : parentTag.getName(); - evl.handleExportingEvent("frame", pos + 1, fframes.size(), tagName); + evl.handleExportingEvent("frame", pos + 1, maxPos, tagName); } - int fframe = fframes.get(pos); + int fframe = subFramesLength > 1 ? fframes.get(0) : fframes.get(pos); final Graphics2D g = (Graphics2D) job.getGraphics(pf); //g.drawImage(img, 5, 5, img.getWidth(), img.getHeight(), null); @@ -609,7 +635,7 @@ public class FrameExporter { renderContext.stateUnderCursor = new ArrayList<>(); try { - tim.toImage(fframe, fframe, renderContext, image, image, false, m, new Matrix(), m, null, zoom, true, new ExportRectangle(rect), new ExportRectangle(rect), m, true, Timeline.DRAW_MODE_ALL, 0, true, new ArrayList<>()); + tim.toImage(fframe, subFramesLength > 1 ? pos : 0, renderContext, image, image, false, m, new Matrix(), m, null, zoom, true, new ExportRectangle(rect), new ExportRectangle(rect), m, true, Timeline.DRAW_MODE_ALL, 0, true, new ArrayList<>()); } catch (Exception ex) { ex.printStackTrace(); } diff --git a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index 4bce9a5cb..adcf78ec7 100644 --- a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -2258,7 +2258,7 @@ public class CommandLineArgumentParser { } } FrameExportSettings fes = new FrameExportSettings(enumFromStr(formats.get("frame"), FrameExportMode.class), zoom, transparentBackground); - frameExporter.exportFrames(handler, outDir + (multipleExportTypes ? File.separator + FrameExportSettings.EXPORT_FOLDER_NAME : ""), swf, 0, frames, fes, evl); + frameExporter.exportFrames(handler, outDir + (multipleExportTypes ? File.separator + FrameExportSettings.EXPORT_FOLDER_NAME : ""), swf, 0, frames, 1, fes, evl); } if (exportAll || exportFormats.contains("sprite")) { @@ -2266,7 +2266,7 @@ public class CommandLineArgumentParser { SpriteExportSettings ses = new SpriteExportSettings(enumFromStr(formats.get("sprite"), SpriteExportMode.class), zoom); for (Tag t : extags) { if (t instanceof DefineSpriteTag) { - frameExporter.exportSpriteFrames(handler, outDir + (multipleExportTypes ? File.separator + SpriteExportSettings.EXPORT_FOLDER_NAME : ""), swf, ((DefineSpriteTag) t).getCharacterId(), null, ses, evl); + frameExporter.exportSpriteFrames(handler, outDir + (multipleExportTypes ? File.separator + SpriteExportSettings.EXPORT_FOLDER_NAME : ""), swf, ((DefineSpriteTag) t).getCharacterId(), null, 1, ses, evl); } } } diff --git a/src/com/jpexs/decompiler/flash/gui/ExportDialog.java b/src/com/jpexs/decompiler/flash/gui/ExportDialog.java index 0552924fe..2034c0991 100644 --- a/src/com/jpexs/decompiler/flash/gui/ExportDialog.java +++ b/src/com/jpexs/decompiler/flash/gui/ExportDialog.java @@ -189,7 +189,11 @@ public class ExportDialog extends AppDialog { } public double getZoom() { - return Double.parseDouble(zoomTextField.getText()) / 100; + try { + return Double.parseDouble(zoomTextField.getText()) / 100; + } catch (NumberFormatException nfe) { + return 1; + } } private void saveConfig() { diff --git a/src/com/jpexs/decompiler/flash/gui/ExportSubspriteAnimationDialog.java b/src/com/jpexs/decompiler/flash/gui/ExportSubspriteAnimationDialog.java new file mode 100644 index 000000000..380abe0aa --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/ExportSubspriteAnimationDialog.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2025 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.gui; + +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.modes.FrameExportMode; +import com.jpexs.decompiler.flash.treeitems.TreeItem; +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.List; +import java.util.Locale; +import java.util.Vector; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * + * @author JPEXS + */ +public class ExportSubspriteAnimationDialog extends AppDialog { + + private int result = ERROR_OPTION; + + private JTextField lengthTextField = new JTextField("1", 3); + private JComboBox formatComboBox; + private final MainPanel mainPanel; + private JButton okButton; + + private JTextField zoomTextField = new JTextField(4); + + private JCheckBox transparentFrameBackgroundCheckBox; + + private List modes = new ArrayList<>(); + + public ExportSubspriteAnimationDialog(MainPanel mainPanel, Window owner) { + super(owner); + + setTitle(translate("dialog.title")); + Container cnt = getContentPane(); + + setSize(800, 600); + + cnt.setLayout(new BorderLayout()); + + JPanel centralPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.insets = new Insets(2, 2, 2, 2); + gbc.anchor = GridBagConstraints.FIRST_LINE_START; + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + centralPanel.add(new JLabel(translate("format")), gbc); + Vector optionNames = new Vector<>(); + + String exportFormatsStr = Configuration.lastSelectedExportFormats.get(); + if ("".equals(exportFormatsStr)) { + exportFormatsStr = null; + } + + String[] exportFormatsArr = new String[0]; + if (exportFormatsStr != null) { + if (exportFormatsStr.contains(",")) { + exportFormatsArr = exportFormatsStr.split(","); + } else { + exportFormatsArr = new String[]{exportFormatsStr}; + } + + } + + List exportFormats = Arrays.asList(exportFormatsArr); + + int selectedFormat = -1; + for (FrameExportMode mode : FrameExportMode.values()) { + if (mode == FrameExportMode.SWF + || mode == FrameExportMode.CANVAS) { + continue; + } + String key = "frames." + mode.toString().toLowerCase(Locale.ENGLISH); + optionNames.add(AppStrings.translate(ExportDialog.class, key)); + if (exportFormats.contains(key)) { + selectedFormat = modes.size(); + } + modes.add(mode); + } + + gbc.gridx++; + gbc.gridwidth = 2; + gbc.fill = GridBagConstraints.NONE; + formatComboBox = new JComboBox<>(optionNames); + if (selectedFormat != -1) { + formatComboBox.setSelectedIndex(selectedFormat); + } + centralPanel.add(formatComboBox, gbc); + + + gbc.gridy++; + gbc.gridwidth = 2; + transparentFrameBackgroundCheckBox = new JCheckBox(AppStrings.translate(ExportDialog.class, "transparentFrameBackground")); + transparentFrameBackgroundCheckBox.setVisible(true); + centralPanel.add(transparentFrameBackgroundCheckBox, gbc); + if (Configuration.lastExportTransparentBackground.get()) { + transparentFrameBackgroundCheckBox.setSelected(true); + } + + JLabel zlab = new JLabel(translateTitle("zoom")); + JLabel pctLabel = new JLabel(AppStrings.translate(ExportDialog.class, "zoom.percent")); + zlab.setLabelFor(zoomTextField); + + String pct = "" + Configuration.lastSelectedExportZoom.get() * 100; + if (pct.endsWith(".0")) { + pct = pct.substring(0, pct.length() - 2); + } + + zoomTextField.setText(pct); + + gbc.gridy++; + gbc.gridx = 0; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.LINE_END; + centralPanel.add(zlab, gbc); + gbc.gridx++; + gbc.anchor = GridBagConstraints.LINE_START; + centralPanel.add(zoomTextField, gbc); + gbc.gridx++; + gbc.anchor = GridBagConstraints.LINE_START; + centralPanel.add(pctLabel, gbc); + + gbc.gridx = 0; + gbc.gridy++; + gbc.gridwidth = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + centralPanel.add(new JLabel(translate("length")), gbc); + + gbc.gridx++; + gbc.fill = GridBagConstraints.NONE; + lengthTextField.setMinimumSize(lengthTextField.getPreferredSize()); + lengthTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + check(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + check(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + check(); + } + + }); + centralPanel.add(lengthTextField, gbc); + + gbc.gridx++; + centralPanel.add(new JLabel(translate("frames")), gbc); + + JPanel buttonsPanel = new JPanel(new FlowLayout()); + 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); + + cnt.add(centralPanel, BorderLayout.CENTER); + cnt.add(buttonsPanel, BorderLayout.SOUTH); + + cnt.setMinimumSize(cnt.getPreferredSize()); + setSize(350, 200); + View.centerScreen(this); + View.setWindowIcon(this, "export"); + getRootPane().setDefaultButton(okButton); + setModal(true); + this.mainPanel = mainPanel; + } + + private void check() { + try { + int len = Integer.parseInt(lengthTextField.getText()); + okButton.setEnabled(len > 0); + } catch (NumberFormatException nfe) { + okButton.setEnabled(false); + } + } + + private void okButtonActionPerformed(ActionEvent evt) { + result = OK_OPTION; + try { + saveConfig(); + } catch (NumberFormatException nfe) { + JOptionPane.showMessageDialog(ExportSubspriteAnimationDialog.this, AppStrings.translate(ExportDialog.class, "zoom.invalid"), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + zoomTextField.requestFocusInWindow(); + return; + } + setVisible(false); + } + + private void saveConfig() { + String format = getFormat().toString().toLowerCase(Locale.ENGLISH); + String formats = Configuration.lastSelectedExportFormats.get(); + String[] parts = formats.split(",", -1); + List newFormats = new ArrayList<>(); + for (String part : parts) { + if (part.startsWith("frames.")) { + newFormats.add("frames." + format); + } else { + newFormats.add(part); + } + } + String cfg = String.join(",", newFormats); + Configuration.lastSelectedExportZoom.set(Double.parseDouble(zoomTextField.getText()) / 100); + Configuration.lastSelectedExportFormats.set(cfg); + Configuration.lastExportTransparentBackground.set(transparentFrameBackgroundCheckBox.isSelected()); + } + + private void cancelButtonActionPerformed(ActionEvent evt) { + result = CANCEL_OPTION; + setVisible(false); + } + + public int showDialog() { + setVisible(true); + return result; + } + + public int getLength() { + try { + return Integer.parseInt(lengthTextField.getText()); + } catch (NumberFormatException nfe) { + return 0; + } + } + + public FrameExportMode getFormat() { + return modes.get(formatComboBox.getSelectedIndex()); + } + + private String translateTitle(String title) { + return AppStrings.translate(ExportDialog.class, "titleFormat").replace("%title%", AppStrings.translate(ExportDialog.class, title)); + } + + public boolean isTransparentFrameBackgroundEnabled() { + return transparentFrameBackgroundCheckBox.isSelected(); + } + + public double getZoom() { + return Double.parseDouble(zoomTextField.getText()) / 100; + } +} diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index c5bda142c..73261bd3b 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -2296,7 +2296,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se FrameExportSettings fes = new FrameExportSettings(export.getValue(FrameExportMode.class), export.getZoom(), export.isTransparentFrameBackgroundEnabled()); if (frames.containsKey(0)) { String subFolder = FrameExportSettings.EXPORT_FOLDER_NAME; - ret.addAll(frameExporter.exportFrames(handler, selFile2 + File.separator + subFolder, swf, 0, frames.get(0), fes, evl)); + ret.addAll(frameExporter.exportFrames(handler, selFile2 + File.separator + subFolder, swf, 0, frames.get(0), 1, fes, evl)); } } @@ -2306,7 +2306,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se int containerId = entry.getKey(); if (containerId != 0) { String subFolder = SpriteExportSettings.EXPORT_FOLDER_NAME; - ret.addAll(frameExporter.exportSpriteFrames(handler, selFile2 + File.separator + subFolder, swf, containerId, entry.getValue(), ses, evl)); + ret.addAll(frameExporter.exportSpriteFrames(handler, selFile2 + File.separator + subFolder, swf, containerId, entry.getValue(), 1, ses, evl)); } } } @@ -2407,14 +2407,14 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se if (export.isOptionEnabled(FrameExportMode.class)) { FrameExportSettings fes = new FrameExportSettings(export.getValue(FrameExportMode.class), export.getZoom(), export.isTransparentFrameBackgroundEnabled()); - frameExporter.exportFrames(handler, Path.combine(selFile, FrameExportSettings.EXPORT_FOLDER_NAME), swf, 0, null, fes, evl); + frameExporter.exportFrames(handler, Path.combine(selFile, FrameExportSettings.EXPORT_FOLDER_NAME), swf, 0, null, 1, fes, evl); } if (export.isOptionEnabled(SpriteExportMode.class)) { SpriteExportSettings ses = new SpriteExportSettings(export.getValue(SpriteExportMode.class), export.getZoom()); for (CharacterTag c : swf.getCharacters(false).values()) { if (c instanceof DefineSpriteTag) { - frameExporter.exportSpriteFrames(handler, Path.combine(selFile, SpriteExportSettings.EXPORT_FOLDER_NAME), swf, c.getCharacterId(), null, ses, evl); + frameExporter.exportSpriteFrames(handler, Path.combine(selFile, SpriteExportSettings.EXPORT_FOLDER_NAME), swf, c.getCharacterId(), null, 1, ses, evl); } } } @@ -2518,7 +2518,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se if (export.isOptionEnabled(FrameExportMode.class)) { for (FrameExportMode exportMode : FrameExportMode.values()) { FrameExportSettings fes = new FrameExportSettings(exportMode, export.getZoom(), export.isTransparentFrameBackgroundEnabled()); - frameExporter.exportFrames(handler, Path.combine(selFile, FrameExportSettings.EXPORT_FOLDER_NAME, exportMode.name()), swf, 0, null, fes, evl); + frameExporter.exportFrames(handler, Path.combine(selFile, FrameExportSettings.EXPORT_FOLDER_NAME, exportMode.name()), swf, 0, null, 1, fes, evl); } } @@ -2527,7 +2527,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se SpriteExportSettings ses = new SpriteExportSettings(exportMode, export.getZoom()); for (CharacterTag c : swf.getCharacters(false).values()) { if (c instanceof DefineSpriteTag) { - frameExporter.exportSpriteFrames(handler, Path.combine(selFile, SpriteExportSettings.EXPORT_FOLDER_NAME, exportMode.name()), swf, c.getCharacterId(), null, ses, evl); + frameExporter.exportSpriteFrames(handler, Path.combine(selFile, SpriteExportSettings.EXPORT_FOLDER_NAME, exportMode.name()), swf, c.getCharacterId(), null, 1, ses, evl); } } } @@ -5392,6 +5392,67 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se export(true, selected); } + public void exportSubspriteAnimationActionPerformed(TreeItem frame) { + if (Main.isWorking()) { + return; + } + + int frameNum = ((Frame) frame).frame; + TreeItem parent = getCurrentTree().getFullModel().getParent(frame); + int containerId = 0; + if (parent instanceof DefineSpriteTag) { + containerId = ((DefineSpriteTag) parent).getCharacterId(); + } + final int fContainerId = containerId; + + ExportSubspriteAnimationDialog dialog = new ExportSubspriteAnimationDialog(this, Main.getDefaultDialogsOwner()); + if (dialog.showDialog() != JOptionPane.OK_OPTION) { + return; + } + + FrameExportMode mode = dialog.getFormat(); + int length = dialog.getLength(); + final String selFile = selectExportDir("export"); + if (selFile != null) { + final long timeBefore = System.currentTimeMillis(); + + new CancellableWorker("export") { + @Override + public Void doInBackground() throws Exception { + try { + AbortRetryIgnoreHandler errorHandler = new GuiAbortRetryIgnoreHandler(); + + FrameExporter frameExporter = new FrameExporter(); + FrameExportSettings fes = new FrameExportSettings(mode, dialog.getZoom(), dialog.isTransparentFrameBackgroundEnabled()); + String subFolder = FrameExportSettings.EXPORT_FOLDER_NAME; + frameExporter.exportFrames(errorHandler, selFile + File.separator + subFolder, (SWF) frame.getOpenable(), fContainerId, Arrays.asList(frameNum), length, fes, null); + + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error during export", ex); + ViewMessages.showMessageDialog(null, translate("error.export") + ": " + ex.getClass().getName() + " " + ex.getLocalizedMessage()); + } + return null; + } + + @Override + protected void onStart() { + Main.startWork(translate("work.exporting") + "...", this); + } + + @Override + protected void done() { + Main.stopWork(); + long timeAfter = System.currentTimeMillis(); + final long timeMs = timeAfter - timeBefore; + + View.execInEventDispatch(() -> { + setStatus(translate("export.finishedin").replace("%time%", Helper.formatTimeSec(timeMs))); + }); + } + }.execute(); + } + } + public File showImportFileChooser(String filter, boolean imagePreview, String icon) { return showImportFileChooser(filter, imagePreview, null, icon); } diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/exportsubsprites16.png b/src/com/jpexs/decompiler/flash/gui/graphics/exportsubsprites16.png new file mode 100644 index 000000000..24b0ef6a6 Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/exportsubsprites16.png differ diff --git a/src/com/jpexs/decompiler/flash/gui/locales/ExportSubspriteAnimationDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/ExportSubspriteAnimationDialog.properties new file mode 100644 index 000000000..127a0c654 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/ExportSubspriteAnimationDialog.properties @@ -0,0 +1,21 @@ +# Copyright (C) 2025 JPEXS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +dialog.title = Export subsprite animation +format = Format: +length = Length: +frames = frames +button.ok = OK +button.cancel = Cancel \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/ExportSubspriteAnimationDialog_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/ExportSubspriteAnimationDialog_cs.properties new file mode 100644 index 000000000..7bb0de920 --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/locales/ExportSubspriteAnimationDialog_cs.properties @@ -0,0 +1,21 @@ +# Copyright (C) 2025 JPEXS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +dialog.title = Export animace podsprit\u016f +format = Form\u00e1t: +length = D\u00e9lka: +frames = sn\u00edmk\u016f +button.ok = OK +button.cancel = Storno \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 7bbe3c418..c085ac1f2 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1071,4 +1071,6 @@ message.link.type.otherScript = Link to other script in this SWF message.link.type.otherScript.sample = OtherClass message.link.type.otherFile = Link to other SWF file (usually playerglobal.swc) message.link.type.otherFile.sample = String -message.link.reallyGo = Do you really want to go to another SWF? \ No newline at end of file +message.link.reallyGo = Do you really want to go to another SWF? + +contextmenu.exportSubspriteAnimation = Export subsprite animation \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties index 1f0f59a62..cc97b1d1f 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties @@ -1070,4 +1070,6 @@ message.link.type.otherScript = Odkaz na jin\u00fd skript v tomto SWF message.link.type.otherScript.sample = JinaTrida message.link.type.otherFile = Odkaz do jin\u00e9ho SWF souboru (obvykle playerglobal.swc) message.link.type.otherFile.sample = String -message.link.reallyGo = Opravdu chcete p\u0159ej\u00edt do jin\u00e9ho SWF? \ No newline at end of file +message.link.reallyGo = Opravdu chcete p\u0159ej\u00edt do jin\u00e9ho SWF? + +contextmenu.exportSubspriteAnimation = Export animace podsprit\u016f \ 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 343b9f573..f6a3076a5 100644 --- a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java @@ -202,6 +202,8 @@ public class TagTreeContextMenu extends JPopupMenu { private JMenuItem undoTagMenuItem; private JMenuItem exportSelectionMenuItem; + + private JMenuItem exportSubspriteAnimationMenuItem; private JMenuItem exportABCMenuItem; @@ -482,6 +484,17 @@ public class TagTreeContextMenu extends JPopupMenu { }); exportSelectionMenuItem.setIcon(View.getIcon("exportsel16")); add(exportSelectionMenuItem); + + exportSubspriteAnimationMenuItem = new JMenuItem(mainPanel.translate("contextmenu.exportSubspriteAnimation")); + exportSubspriteAnimationMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + mainPanel.exportSubspriteAnimationActionPerformed(getCurrentItem()); + } + }); + exportSubspriteAnimationMenuItem.setIcon(View.getIcon("exportsubsprites16")); + add(exportSubspriteAnimationMenuItem); + exportABCMenuItem = new JMenuItem(mainPanel.translate("contextmenu.exportAbc")); exportABCMenuItem.addActionListener(this::exportABCActionPerformed); @@ -1304,6 +1317,7 @@ public class TagTreeContextMenu extends JPopupMenu { cloneMenuItem.setVisible(allSelectedIsTagOrFrame && allSelectedSameParent); undoTagMenuItem.setVisible(allSelectedIsTag); exportSelectionMenuItem.setEnabled(hasExportableNodes); //? + exportSubspriteAnimationMenuItem.setVisible(false); exportABCMenuItem.setVisible(false); replaceMenuItem.setVisible(false); replaceNoFillMenuItem.setVisible(false); @@ -1775,6 +1789,10 @@ public class TagTreeContextMenu extends JPopupMenu { changeCharsetMenu.setVisible(true); } } + + if (firstItem instanceof Frame) { + exportSubspriteAnimationMenuItem.setVisible(true); + } } if (allSelectedIsShape) {