From ab717cc51cc7a385723c914e702e09178893c220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Sun, 1 Feb 2026 02:35:36 +0100 Subject: [PATCH] Added: #2610 Export morphshapes - export individual frames, setting of morph duration and/or number of morph frames Added: Animated WEBP export for frames, sprites and morphshapes Fixed: HTML5 Canvas morphshape export --- CHANGELOG.md | 6 + .../flash/configuration/Configuration.java | 8 + .../flash/exporters/FrameExporter.java | 60 ++++- .../flash/exporters/MorphShapeExporter.java | 227 +++++++++++++++++- .../flash/exporters/PreviewExporter.java | 28 ++- .../flash/exporters/ShapeExporter.java | 3 +- .../exporters/modes/FrameExportMode.java | 6 +- .../exporters/modes/MorphShapeExportMode.java | 83 ++++++- .../exporters/modes/SpriteExportMode.java | 6 +- .../morphshape/CanvasMorphShapeExporter.java | 38 ++- .../morphshape/SVGMorphShapeExporter.java | 11 +- .../settings/MorphShapeExportSettings.java | 45 +++- .../flash/tags/base/MorphShapeTag.java | 11 +- .../console/CommandLineArgumentParser.java | 54 ++++- .../jpexs/decompiler/flash/console/help.txt | 14 ++ .../decompiler/flash/gui/ExportDialog.java | 193 +++++++++++++-- .../jpexs/decompiler/flash/gui/MainPanel.java | 6 +- .../decompiler/flash/gui/PreviewPanel.java | 4 +- .../decompiler/flash/gui/TimelinedMaker.java | 2 +- .../locales/AdvancedSettingsDialog.properties | 7 + .../AdvancedSettingsDialog_cs.properties | 7 + .../flash/gui/locales/ExportDialog.properties | 17 +- .../gui/locales/ExportDialog_cs.properties | 17 +- .../flash/gui/tagtree/TagTreeContextMenu.java | 2 +- 24 files changed, 766 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb8b9a05..2e1dfdc9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- [#2610] Export morphshapes - export individual frames, setting of morph + duration and/or number of morph frames +- Animated WEBP export for frames, sprites and morphshapes + ### Fixed - [#2603] Export Sprite as SWF - sprite frames as timeline frames, without sprite itself +- HTML5 Canvas morphshape export ### Changed - [#2610] Export as SWF - take SWF bounds from the exported item bounds 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 dbede4e33..266ecaafa 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 @@ -1185,6 +1185,14 @@ public final class Configuration { @ConfigurationCategory("ui") public static ConfigurationItem allowDragAndDropFromResourcesTree = null; + @ConfigurationDefaultDouble(2.0) + @ConfigurationCategory("export") + public static ConfigurationItem lastExportMorphDuration = null; + + @ConfigurationDefaultInt(10) + @ConfigurationCategory("export") + public static ConfigurationItem lastExportMorphNumberOfFrames = 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/FrameExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/FrameExporter.java index b75c9de45..f40b6c7f5 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 @@ -66,6 +66,8 @@ import com.jpexs.helpers.Path; import com.jpexs.helpers.SerializableImage; import com.jpexs.helpers.utf8.Utf8Helper; import dev.matrixlab.webp4j.WebPCodec; +import dev.matrixlab.webp4j.animation.AnimatedWebPEncoder; +import dev.matrixlab.webp4j.gif.GifToWebPConfig; import gnu.jpdf.PDFGraphics; import gnu.jpdf.PDFJob; import java.awt.AlphaComposite; @@ -170,6 +172,9 @@ public class FrameExporter { case WEBP: fem = FrameExportMode.WEBP; break; + case WEBP_ANIMATED: + fem = FrameExportMode.WEBP_ANIMATED; + break; case SWF: fem = FrameExportMode.SWF; break; @@ -323,13 +328,12 @@ public class FrameExporter { } if (settings.mode == FrameExportMode.SVG) { - + FontNormalizer normalizer = new FontNormalizer(); Map normalizedFonts = new LinkedHashMap<>(); Map normalizedTexts = new LinkedHashMap<>(); normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts); - int max = frames.size(); if (subFramesLength > 1) { max = subFramesLength; @@ -501,7 +505,7 @@ public class FrameExporter { try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(f))) { try { - new PreviewExporter().exportSwf(fos, swf.getCharacter(containerId), fBackgroundColor, 0, false); + new PreviewExporter().exportSwf(fos, swf.getCharacter(containerId), fBackgroundColor, 0, false, null); } catch (ActionParseException ex) { Logger.getLogger(FrameExporter.class.getName()).log(Level.SEVERE, null, ex); } @@ -519,7 +523,7 @@ public class FrameExporter { try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(f))) { try { - new PreviewExporter().exportSwf(fos, fn, fBackgroundColor, 0, false); + new PreviewExporter().exportSwf(fos, fn, fBackgroundColor, 0, false, null); } catch (ActionParseException ex) { Logger.getLogger(FrameExporter.class.getName()).log(Level.SEVERE, null, ex); } @@ -533,8 +537,8 @@ public class FrameExporter { } final Color fbackgroundColor = backgroundColor; - final boolean usesTransparency = settings.mode == FrameExportMode.PNG - || settings.mode == FrameExportMode.GIF + final boolean usesTransparency = settings.mode == FrameExportMode.PNG + || settings.mode == FrameExportMode.GIF || settings.mode == FrameExportMode.WEBP; final MyFrameIterator frameImages = new MyFrameIterator(tim, fframes, evl, usesTransparency, backgroundColor, settings, subFramesLength); @@ -549,6 +553,16 @@ public class FrameExporter { }, handler).run(); } break; + case WEBP_ANIMATED: + for (File foutdir : foutdirs) { + frameImages.reset(); + new RetryTask(() -> { + File f = new File(foutdir + File.separator + "frames.webp"); + makeAnimatedWebP(frameImages, swf.frameRate, f, evl); + ret.add(f); + }, handler).run(); + } + break; case BMP: for (File foutdir : foutdirs) { frameImages.reset(); @@ -608,12 +622,12 @@ public class FrameExporter { if (frameImages.hasNext()) { for (File foutdir : foutdirs) { new RetryTask(() -> { - + FontNormalizer normalizer = new FontNormalizer(); Map normalizedFonts = new LinkedHashMap<>(); Map normalizedTexts = new LinkedHashMap<>(); normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts); - + File f = new File(foutdir + File.separator + "frames.pdf"); PDFJob job = new PDFJob(new BufferedOutputStream(new FileOutputStream(f))); PageFormat pf = new PageFormat(); @@ -662,7 +676,7 @@ public class FrameExporter { return compositeGraphics; } final Graphics2D parentGraphics = (Graphics2D) super.getGraphics(); - compositeGraphics = new DualPdfGraphics2D(parentGraphics, (PDFGraphics) g, existingFonts); + compositeGraphics = new DualPdfGraphics2D(parentGraphics, (PDFGraphics) g, existingFonts); ((DualPdfGraphics2D) compositeGraphics).setNormalizedFonts(normalizedFonts, normalizedTexts); return compositeGraphics; } @@ -817,6 +831,34 @@ public class FrameExporter { encoder.finish(); } + public static void makeAnimatedWebP(Iterator images, float frameRate, File file, EventListener evl) throws IOException { + GifToWebPConfig config = GifToWebPConfig.createLosslessConfig(); + List allImages = new ArrayList<>(); + List delays = new ArrayList<>(); + int lastTime = 0; + while (images.hasNext()) { + BufferedImage img = images.next(); + if (img == null) { + break; + } + allImages.add(img); + int timeAfter = (int) Math.round(allImages.size() * frameRate); + int delay = timeAfter - lastTime; + lastTime = timeAfter; + delays.add(delay); + } + int[] delaysInt = new int[delays.size()]; + for (int i = 0; i < delays.size(); i++) { + delaysInt[i] = delays.get(i); + } + + try (FileOutputStream fos = new FileOutputStream(file)) { + byte[] data = WebPCodec.createAnimatedWebP(allImages, delaysInt, config); + fos.write(data); + fos.flush(); + } + } + public static void makeGIFOld(Iterator images, float frameRate, File file, EventListener evl) throws IOException { if (!images.hasNext()) { return; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MorphShapeExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MorphShapeExporter.java index 66964a2a1..938d1e1b3 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MorphShapeExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/MorphShapeExporter.java @@ -32,6 +32,7 @@ import com.jpexs.decompiler.flash.exporters.settings.MorphShapeExportSettings; import com.jpexs.decompiler.flash.helpers.BMPFile; import com.jpexs.decompiler.flash.helpers.ImageHelper; import com.jpexs.decompiler.flash.tags.DefineMorphShapeTag; +import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.base.CharacterTag; import com.jpexs.decompiler.flash.tags.base.MorphShapeTag; @@ -50,9 +51,8 @@ import com.jpexs.helpers.Path; import com.jpexs.helpers.SerializableImage; import com.jpexs.helpers.utf8.Utf8Helper; import dev.matrixlab.webp4j.WebPCodec; -import java.awt.Graphics; +import java.awt.Color; import java.awt.Graphics2D; -import java.awt.Image; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; @@ -65,6 +65,7 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -78,7 +79,6 @@ import java.util.logging.Logger; */ public class MorphShapeExporter { - //TODO: implement morphshape export. How to handle 65536 frames? public List exportMorphShapes(AbortRetryIgnoreHandler handler, final String outdir, ReadOnlyTagList tags, final MorphShapeExportSettings settings, EventListener evl) throws IOException, InterruptedException { List ret = new ArrayList<>(); if (CancellableWorker.isInterrupted()) { @@ -115,6 +115,8 @@ public class MorphShapeExporter { characterID = ((CharacterTag) t).getCharacterId(); } + final int fCharacterID = characterID; + final File file = new File(outdir + File.separator + characterID + settings.getFileExtension()); final File fileStart = new File(outdir + File.separator + characterID + ".start" + settings.getFileExtension()); final File fileEnd = new File(outdir + File.separator + characterID + ".end" + settings.getFileExtension()); @@ -158,7 +160,7 @@ public class MorphShapeExporter { rect2.xMin *= settings.zoom; rect2.yMin *= settings.zoom; SVGExporter exporter = new SVGExporter(rect2, settings.zoom, "morphshape"); - mst.toSVG(0, 0, exporter, -2, new CXFORMWITHALPHA(), 0, m, m); + mst.toSVG(exporter, new CXFORMWITHALPHA(), settings.duration); fos.write(Utf8Helper.getBytes(exporter.getSVG())); } break; @@ -245,7 +247,7 @@ public class MorphShapeExporter { try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(file))) { int deltaX = -Math.min(mst.getStartBounds().Xmin, mst.getEndBounds().Xmin); int deltaY = -Math.min(mst.getStartBounds().Ymin, mst.getEndBounds().Ymin); - CanvasMorphShapeExporter cse = new CanvasMorphShapeExporter(mst.getShapeNum(), ((Tag) mst).getSwf(), mst.getShapeAtRatio(0), mst.getShapeAtRatio(DefineMorphShapeTag.MAX_RATIO), new CXFORMWITHALPHA(), SWF.unitDivisor, deltaX, deltaY); + CanvasMorphShapeExporter cse = new CanvasMorphShapeExporter(mst.getShapeNum(), ((Tag) mst).getSwf(), mst.getShapeAtRatio(0), mst.getShapeAtRatio(DefineMorphShapeTag.MAX_RATIO), new CXFORMWITHALPHA(), SWF.unitDivisor, deltaX, deltaY, settings.duration); cse.export(); Set needed = new HashSet<>(); Set neededClasses = new HashSet<>(); @@ -254,6 +256,7 @@ public class MorphShapeExporter { ct.getNeededCharactersDeep(needed, neededClasses); ByteArrayOutputStream baos = new ByteArrayOutputStream(); //FIXME!!! Handle Library Classes + baos.write(Utf8Helper.getBytes("var scalingGrids = {};\r\nvar boundRects = {};\r\n")); SWF.libraryToHtmlCanvas(ct.getSwf(), needed, baos); fos.write(Utf8Helper.getBytes(cse.getHtml(new String(baos.toByteArray(), Utf8Helper.charset), SWF.getTypePrefix(mst) + mst.getCharacterId(), mst.getRect()))); } @@ -261,15 +264,121 @@ public class MorphShapeExporter { case SWF: try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(file))) { try { - new PreviewExporter().exportSwf(fos, mst, null, 0, false); + new PreviewExporter().exportSwf(fos, mst, null, 0, false, settings.duration); } catch (ActionParseException ex) { Logger.getLogger(MorphShapeExporter.class.getName()).log(Level.SEVERE, null, ex); } } - break; + break; } }, handler).run(); + + SetBackgroundColorTag bkgTag = mst.getSwf().getBackgroundColor(); + Color backgroundColor = null; + if (bkgTag != null) { + backgroundColor = bkgTag.backgroundColor.toColor(); + } + + int iteratorNumFrames = 2; + if (settings.mode.hasDuration()) { + iteratorNumFrames = (int) Math.round(settings.duration * PreviewExporter.MORPH_SHAPE_ANIMATION_FRAME_RATE); + } else if (settings.mode.hasFrames()) { + iteratorNumFrames = settings.numberOfFrames; + } + + final boolean usesTransparency = settings.mode == MorphShapeExportMode.PNG_FRAMES + || settings.mode == MorphShapeExportMode.GIF + || settings.mode == MorphShapeExportMode.WEBP_FRAMES; + + + MyFrameIterator frameImages = new MyFrameIterator(mst, iteratorNumFrames, evl, usesTransparency, backgroundColor, settings); + + + File dir; + switch (settings.mode) { + case SVG_FRAMES: + Matrix m; + RECT rect = mst.getRect(); + m = Matrix.getScaleInstance(settings.zoom); + m.translate(-rect.Xmin, -rect.Ymin); + + dir = new File(outdir + File.separator + fCharacterID); + Path.createDirectorySafe(dir); + + ExportRectangle rect2 = new ExportRectangle(mst.getRect()); + rect2.xMax *= settings.zoom; + rect2.yMax *= settings.zoom; + rect2.xMin *= settings.zoom; + rect2.yMin *= settings.zoom; + + + for (int f = 0; f < settings.numberOfFrames; f++) { + int ratio = (int) Math.round(f * 65535.0 / (settings.numberOfFrames - 1)); + File frameFile = new File(outdir + File.separator + fCharacterID + File.separator + (f + 1) + "_of_" + settings.numberOfFrames + settings.getFileExtension()); + + new RetryTask(() -> { + try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(frameFile))) { + SVGExporter exporter = new SVGExporter(rect2, settings.zoom, "shape"); + mst.getShapeTagAtRatio(ratio).toSVG(0, 0, exporter, -2, new CXFORMWITHALPHA(), 0, m, m); + fos.write(Utf8Helper.getBytes(exporter.getSVG())); + } + }, handler).run(); + } + break; + + case GIF: + frameImages.reset(); + new RetryTask(() -> { + FrameExporter.makeGIF(frameImages, PreviewExporter.MORPH_SHAPE_ANIMATION_FRAME_RATE, file, evl); + ret.add(file); + }, handler).run(); + break; + + case AVI: + frameImages.reset(); + new RetryTask(() -> { + FrameExporter.makeAVI(frameImages, PreviewExporter.MORPH_SHAPE_ANIMATION_FRAME_RATE, file, evl); + ret.add(file); + }, handler).run(); + break; + case WEBP: + frameImages.reset(); + new RetryTask(() -> { + FrameExporter.makeAnimatedWebP(frameImages, PreviewExporter.MORPH_SHAPE_ANIMATION_FRAME_RATE, file, evl); + ret.add(file); + }, handler).run(); + break; + case PNG_FRAMES: + case BMP_FRAMES: + case WEBP_FRAMES: + dir = new File(outdir + File.separator + fCharacterID); + Path.createDirectorySafe(dir); + + + frameImages.reset(); + for (int i = 0; frameImages.hasNext(); i++) { + final int fi = i; + new RetryTask(() -> { + File frameFile = new File(outdir + File.separator + fCharacterID + File.separator + (fi + 1) + "_of_" + settings.numberOfFrames + settings.getFileExtension()); + BufferedImage img = frameImages.next(); + if (img != null) { + if (settings.mode == MorphShapeExportMode.PNG_FRAMES) { + ImageHelper.write(img, ImageFormat.PNG, frameFile); + } else if (settings.mode == MorphShapeExportMode.WEBP_FRAMES) { + try (FileOutputStream fos = new FileOutputStream(frameFile)) { + fos.write(WebPCodec.encodeLosslessImage(img)); + } + } else { + BMPFile.saveBitmap(img, frameFile); + } + ret.add(frameFile); + } + }, handler).run(); + } + break; + } + Set classNames = mst.getClassNames(); if (Configuration.as3ExportNamesUseClassNamesOnly.get() && !classNames.isEmpty()) { for (String className : classNames) { @@ -325,4 +434,108 @@ public class MorphShapeExporter { } return ret; } + + private class MyFrameIterator implements Iterator { + + private int pos = 0; + private final MorphShapeTag mst; + private final int numFrames; + private final EventListener evl; + private final boolean usesTransparency; + private final Color backgroundColor; + private final MorphShapeExportSettings settings; + private final RECT rect; + private final int realAaScale; + private final int newWidth; + private final int newHeight; + + public MyFrameIterator( + MorphShapeTag mst, + int numFrames, + final EventListener evl, + boolean usesTransparency, + Color backgroundColor, + MorphShapeExportSettings settings + ) { + this.mst = mst; + this.numFrames = numFrames; + this.evl = evl; + this.usesTransparency = usesTransparency; + this.backgroundColor = backgroundColor; + this.settings = settings; + + + rect = mst.getRect(); + realAaScale = Configuration.calculateRealAaScale(rect.getWidth(), rect.getHeight(), settings.zoom, settings.aaScale); + newWidth = (int) (rect.getWidth() * settings.zoom * realAaScale / SWF.unitDivisor) + 1; + newHeight = (int) (rect.getHeight() * settings.zoom * realAaScale / SWF.unitDivisor) + 1; + } + + public void reset() { + pos = 0; + } + + @Override + public boolean hasNext() { + if (CancellableWorker.isInterrupted()) { + return false; + } + return numFrames > pos; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public BufferedImage next() { + if (!hasNext()) { + return null; + } + + String tagName = mst.getName(); + + if (evl != null) { + evl.handleExportingEvent("frame", pos + 1, numFrames, tagName); + } + + int ratio = (int) Math.round(pos * 65535.0 / (numFrames - 1)); + + ShapeTag st = mst.getShapeTagAtRatio(ratio); + + SerializableImage img = new SerializableImage(newWidth, newHeight, SerializableImage.TYPE_INT_ARGB_PRE); + img.fillTransparent(); + if (!usesTransparency) { + Graphics2D g = (Graphics2D) img.getGraphics(); + if (backgroundColor == null) { + g.setColor(Color.white); + } else { + g.setColor(backgroundColor); + } + g.fillRect(0, 0, img.getWidth(), img.getHeight()); + } + Matrix m = Matrix.getScaleInstance(settings.zoom * realAaScale); + m.translate(-rect.Xmin, -rect.Ymin); + st.toImage(0, 0, 0, new RenderContext(), img, img, false, m, m, m, m, new CXFORMWITHALPHA(), settings.zoom * realAaScale, false, new ExportRectangle(rect), new ExportRectangle(rect), true, Timeline.DRAW_MODE_ALL, 0, true, realAaScale); + + BufferedImage result = img.getBufferedImage(); + + if (realAaScale > 1) { + result = ImageResizer.resizeImage(result, ((newWidth - 1) / realAaScale) + 1, + ((newHeight - 1) / realAaScale) + 1, RenderingHints.VALUE_INTERPOLATION_BICUBIC, true); + } + + + if (CancellableWorker.isInterrupted()) { + return null; + } + pos++; + if (evl != null) { + evl.handleExportedEvent("frame", pos, numFrames, tagName); + } + + return result; + } + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/PreviewExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/PreviewExporter.java index af323bba8..bd35290ae 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/PreviewExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/PreviewExporter.java @@ -100,12 +100,14 @@ import java.util.logging.Logger; * @author JPEXS */ public class PreviewExporter { - - // play morph shape in 2 second(s) - public static final int MORPH_SHAPE_ANIMATION_LENGTH = 2; - + + // default: play morph shape in 2 second(s) + public static final double MORPH_SHAPE_DEFAULT_DURATION = 2; + public static final int MORPH_SHAPE_ANIMATION_FRAME_RATE = 30; + public static final int MORPH_SHAPE_ANIMATION_FRAME_NUM = (int) Math.round(MORPH_SHAPE_ANIMATION_FRAME_RATE * MORPH_SHAPE_DEFAULT_DURATION); + private static final double WIDTH_DIVISOR = 1000; @@ -255,7 +257,7 @@ public class PreviewExporter { doAction.writeTag(sos2); } - public SWFHeader exportSwf(OutputStream os, TreeItem treeItem, Color backgroundColor, int fontPageNum, boolean showControls) throws IOException, ActionParseException { + public SWFHeader exportSwf(OutputStream os, TreeItem treeItem, Color backgroundColor, int fontPageNum, boolean showControls, Double morphDuration) throws IOException, ActionParseException { SWF swf = (SWF) treeItem.getOpenable(); if (treeItem instanceof TagScript) { @@ -285,7 +287,7 @@ public class PreviewExporter { if ((treeItem instanceof DefineMorphShapeTag) || (treeItem instanceof DefineMorphShape2Tag)) { frameRate = MORPH_SHAPE_ANIMATION_FRAME_RATE; - frameCount = (int) (MORPH_SHAPE_ANIMATION_LENGTH * frameRate); + frameCount = (int) Math.round(frameRate * morphDuration); } if (treeItem instanceof DefineSoundTag) { @@ -567,10 +569,16 @@ public class PreviewExporter { } new PlaceObject2Tag(swf, false, 1, chtId, mat, null, 0, null, -1, null).writeTag(sos2); new ShowFrameTag(swf).writeTag(sos2); - for (int ratio = 0; ratio < 65536; ratio += 65536 / frameCount) { - new PlaceObject2Tag(swf, true, 1, chtId, mat, null, ratio, null, -1, null).writeTag(sos2); - if (showControls) { - updateProgressBar(rxmin, rymin, swf, sos2, width, height, progressBarHeight, ratio, 65536); + int lastRatio = -1; + for (int f = 0; f < frameCount; f++) { + int ratio = (int) Math.round(f * 65535.0 / (frameCount - 1)); + + if (lastRatio != ratio) { + new PlaceObject2Tag(swf, true, 1, chtId, mat, null, ratio, null, -1, null).writeTag(sos2); + if (showControls) { + updateProgressBar(rxmin, rymin, swf, sos2, width, height, progressBarHeight, ratio, 65536); + } + lastRatio = ratio; } new ShowFrameTag(swf).writeTag(sos2); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ShapeExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ShapeExporter.java index a6d804827..1a5c6f57f 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ShapeExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/ShapeExporter.java @@ -178,6 +178,7 @@ public class ShapeExporter { needed.add(st.getCharacterId()); st.getNeededCharactersDeep(needed, neededClasses); ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(Utf8Helper.getBytes("var scalingGrids = {};\r\nvar boundRects = {};\r\n")); //FIXME!!! Handle Library Classes SWF.libraryToHtmlCanvas(st.getSwf(), needed, baos); fos.write(Utf8Helper.getBytes(cse.getHtml(new String(baos.toByteArray(), Utf8Helper.charset), SWF.getTypePrefix(st) + st.getCharacterId(), st.getRect()))); @@ -186,7 +187,7 @@ public class ShapeExporter { case SWF: try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(file))) { try { - new PreviewExporter().exportSwf(fos, st, null, 0, false); + new PreviewExporter().exportSwf(fos, st, null, 0, false, null); } catch (ActionParseException ex) { Logger.getLogger(MorphShapeExporter.class.getName()).log(Level.SEVERE, null, ex); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/FrameExportMode.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/FrameExportMode.java index c67ff9391..4f4dc8c5a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/FrameExportMode.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/FrameExportMode.java @@ -56,13 +56,17 @@ public enum FrameExportMode { * WEBP */ WEBP, + /** + * WEBP - animated + */ + WEBP_ANIMATED, /** * SWF - Shockwave Flash */ SWF; public boolean available() { - if (this == WEBP) { + if (this == WEBP || this == WEBP_ANIMATED) { return ImageFormat.WEBP.available(); } return true; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/MorphShapeExportMode.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/MorphShapeExportMode.java index a608c50ac..4e54afbac 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/MorphShapeExportMode.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/MorphShapeExportMode.java @@ -24,41 +24,100 @@ import com.jpexs.decompiler.flash.tags.enums.ImageFormat; * @author JPEXS */ public enum MorphShapeExportMode { - //TODO: implement other morphshape export modes - /** * SVG animation - Scalable Vector Graphics */ - SVG, + SVG(true, false), /** * SVG start and end frames - Scalable Vector Graphics */ - SVG_START_END, + SVG_START_END(false, false), + /** + * SVG individual frames - Scalable Vector Graphics + */ + SVG_FRAMES(false, true), /** * HTML5 canvas animation */ - CANVAS, + CANVAS(true, false), /** * PNG start and end frames - Portable Network Graphics */ - PNG_START_END, + PNG_START_END(false, false), + /** + * PNG individual frames - Portable Network Graphics + */ + PNG_FRAMES(false, true), /** * BMP start and end frames - Windows Bitmap */ - BMP_START_END, + BMP_START_END(false, false), + /** + * BMP individual frames - Windows Bitmap + */ + BMP_FRAMES(false, true), /** * WEBP start and end frames */ - WEBP_START_END, - //GIF, - //AVI, + WEBP_START_END(false, false), + /** + * WEBP individual frames + */ + WEBP_FRAMES(false, true), + /** + * GIF - Graphics Interchange Format + */ + GIF(true, false), + /** + * AVI - Audio Video Interleave + */ + AVI(true, false), /** * SWF - Shockwave Flash */ - SWF; + SWF(true, false), + + /** + * WEBP + */ + WEBP(true, false); + /** + * Whether this mode requires total duration in seconds + */ + private final boolean duration; + /** + * Whether this mode requires number of frames + */ + private final boolean frames; + + private MorphShapeExportMode(boolean duration, boolean frames) { + this.duration = duration; + this.frames = frames; + } + + /** + * Checks whether this mode requires total time (length) in seconds. + * @return True on required + */ + public boolean hasDuration() { + return duration; + } + + /** + * Checks whether this mode requires number of frames. + * @return True on required + */ + public boolean hasFrames() { + return frames; + } + + /** + * Checks whether this mode is available on current platform. + * @return True on available. + */ public boolean available() { - if (this == WEBP_START_END) { + if (this == WEBP_START_END || this == WEBP_FRAMES || this == WEBP) { return ImageFormat.WEBP.available(); } return true; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/SpriteExportMode.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/SpriteExportMode.java index 96c4603c3..7f98526b7 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/SpriteExportMode.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/modes/SpriteExportMode.java @@ -56,13 +56,17 @@ public enum SpriteExportMode { * WEBP */ WEBP, + /** + * WEBP - animated + */ + WEBP_ANIMATED, /** * SWF - Shockwave Flash */ SWF; public boolean available() { - if (this == WEBP) { + if (this == WEBP || this == WEBP_ANIMATED) { return ImageFormat.WEBP.available(); } return true; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/CanvasMorphShapeExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/CanvasMorphShapeExporter.java index 715c11668..bead5108b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/CanvasMorphShapeExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/CanvasMorphShapeExporter.java @@ -160,7 +160,12 @@ public class CanvasMorphShapeExporter extends MorphShapeExporterBase { * Fill height */ protected int fillHeight; - + + /** + * Duration + */ + private final double duration; + /** * Constructor. * @@ -172,13 +177,25 @@ public class CanvasMorphShapeExporter extends MorphShapeExporterBase { * @param unitDivisor Unit divisor * @param deltaX Delta X * @param deltaY Delta Y + * @param duration Duration */ - public CanvasMorphShapeExporter(int morphShapeNum, SWF swf, SHAPE shape, SHAPE endShape, ColorTransform colorTransform, double unitDivisor, int deltaX, int deltaY) { + public CanvasMorphShapeExporter( + int morphShapeNum, + SWF swf, + SHAPE shape, + SHAPE endShape, + ColorTransform colorTransform, + double unitDivisor, + int deltaX, + int deltaY, + double duration + ) { super(morphShapeNum, shape, endShape, colorTransform); this.deltaX = deltaX; this.deltaY = deltaY; this.unitDivisor = unitDivisor; this.swf = swf; + this.duration = duration; } private String getDrawJs(int width, int height, String id, RECT rect) { @@ -199,22 +216,21 @@ public class CanvasMorphShapeExporter extends MorphShapeExporterBase { public String getHtml(String needed, String id, RECT rect) { int width = (int) (rect.getWidth() / unitDivisor); int height = (int) (rect.getHeight() / unitDivisor); - - return CanvasShapeExporter.getHtmlPrefix(width, height) + getJsPrefix() + needed + getDrawJs(width, height, id, rect) + getJsSuffix(width, height) + CanvasShapeExporter.getHtmlSuffix(); + int delayMs = 10; + int stepRatio = (int) Math.round(delayMs * 65535 / (duration * 1000)); + return CanvasShapeExporter.getHtmlPrefix(width, height) + getJsPrefix() + needed + getDrawJs(width, height, id, rect) + getJsSuffix(width, height, delayMs, stepRatio) + CanvasShapeExporter.getHtmlSuffix(); } - private static String getJsSuffix(int width, int height) { + private String getJsSuffix(int width, int height, int delayMs, int stepRatio) { StringBuilder ret = new StringBuilder(); - int step = Math.round(65535 / 100); - int rate = 10; - ret.append("var step = ").append(step).append(";\r\n"); + ret.append("var step = ").append(stepRatio).append(";\r\n"); ret.append("var ratio = -1;\r\n"); ret.append("function nextFrame(ctx){\r\n"); - ret.append("\tctx.clearRect(0,0,").append(width).append(",").append(height).append(");\r\n"); - ret.append("\tratio = (ratio+step)%65535;\r\n"); + ret.append("\tctx.clearRect(0,0,canvas.width,canvas.height);"); + ret.append("\tratio = (ratio+step)%65536;\r\n"); ret.append("\tdrawFrame(ctx,ratio);\r\n"); ret.append("}\r\n"); - ret.append("window.setInterval(function(){nextFrame(ctx)},").append(rate).append(");\r\n"); + ret.append("window.setInterval(function(){nextFrame(ctx)},").append(delayMs).append(");\r\n"); ret.append(CanvasShapeExporter.getJsSuffix()); return ret.toString(); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java index ad980e220..3f26d1909 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/morphshape/SVGMorphShapeExporter.java @@ -61,6 +61,11 @@ public class SVGMorphShapeExporter extends DefaultSVGMorphShapeExporter { private final SWF swf; private final SVGExporter exporter; + + /** + * Duration in seconds + */ + private final double duration; /** * Constructor. @@ -73,13 +78,15 @@ public class SVGMorphShapeExporter extends DefaultSVGMorphShapeExporter { * @param defaultColor Default color * @param colorTransform Color transform * @param zoom Zoom + * @param duration Duration in seconds */ - public SVGMorphShapeExporter(int morphShapeNum, SWF swf, SHAPE shape, SHAPE endShape, int id, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom) { + public SVGMorphShapeExporter(int morphShapeNum, SWF swf, SHAPE shape, SHAPE endShape, int id, SVGExporter exporter, Color defaultColor, ColorTransform colorTransform, double zoom, double duration) { super(morphShapeNum, shape, endShape, colorTransform, zoom); this.swf = swf; this.id = id; this.defaultColor = defaultColor; this.exporter = exporter; + this.duration = duration; } @Override @@ -255,7 +262,7 @@ public class SVGMorphShapeExporter extends DefaultSVGMorphShapeExporter { private Element createAnimateElement(String attributeName, Object startValue, Object endValue) { Element animate = exporter.createElement("animate"); - animate.setAttribute("dur", "2s"); // todo + animate.setAttribute("dur", "" + duration + "s"); animate.setAttribute("repeatCount", "indefinite"); animate.setAttribute("attributeName", attributeName); animate.setAttribute("values", startValue + ";" + endValue); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/MorphShapeExportSettings.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/MorphShapeExportSettings.java index 98fc07896..147ea15ae 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/MorphShapeExportSettings.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/settings/MorphShapeExportSettings.java @@ -44,7 +44,39 @@ public class MorphShapeExportSettings { * Antialias conflation reducing scale coefficient */ public int aaScale; + + /** + * Duration in seconds + */ + public Double duration; + + /** + * Number of frames generated + */ + public Integer numberOfFrames; + /** + * Constructor. + * @param mode Mode + * @param zoom Zoom + * @param aaScale Antialias conflation reducing scale coefficient + * @param duration Duration + * @param numberOfFrames Number of frames + */ + public MorphShapeExportSettings(MorphShapeExportMode mode, double zoom, int aaScale, Double duration, Integer numberOfFrames) { + if (mode.hasFrames() && numberOfFrames == null) { + throw new IllegalArgumentException("The requested mode requires passing number of frames."); + } + if (mode.hasDuration() && duration == null) { + throw new IllegalArgumentException("The requested mode requires passing duration."); + } + this.mode = mode; + this.zoom = zoom; + this.aaScale = aaScale; + this.duration = duration; + this.numberOfFrames = numberOfFrames; + } + /** * Constructor. * @param mode Mode @@ -52,9 +84,7 @@ public class MorphShapeExportSettings { * @param aaScale Antialias conflation reducing scale coefficient */ public MorphShapeExportSettings(MorphShapeExportMode mode, double zoom, int aaScale) { - this.mode = mode; - this.zoom = zoom; - this.aaScale = aaScale; + this(mode, zoom, aaScale, null, null); } /** @@ -64,18 +94,27 @@ public class MorphShapeExportSettings { public String getFileExtension() { switch (mode) { case PNG_START_END: + case PNG_FRAMES: return ".png"; case BMP_START_END: + case BMP_FRAMES: return ".bmp"; case WEBP_START_END: + case WEBP_FRAMES: + case WEBP: return ".webp"; case SVG: case SVG_START_END: + case SVG_FRAMES: return ".svg"; case CANVAS: return ".html"; case SWF: return ".swf"; + case GIF: + return ".gif"; + case AVI: + return ".avi"; default: throw new Error("Unsupported morphshape export mode: " + mode); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java index df2084a22..d7e490628 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/MorphShapeTag.java @@ -420,13 +420,20 @@ public abstract class MorphShapeTag extends DrawableTag { BitmapExporter.export(ShapeTag.WIND_EVEN_ODD /*??? FIXME*/, getShapeNum() == 2 ? 4 : 1, swf, shape, null, image, unzoom, transformation, strokeTransformation, colorTransform, scaleStrokes, canUseSmoothing, aaScale); } + public void toSVG(SVGExporter exporter, ColorTransform colorTransform, double duration) { + SHAPEWITHSTYLE beginShapes = getShapeAtRatio(0); + SHAPEWITHSTYLE endShapes = getShapeAtRatio(65535); + SVGMorphShapeExporter shapeExporter = new SVGMorphShapeExporter(getShapeNum(), swf, beginShapes, endShapes, getCharacterId(), exporter, null, colorTransform, exporter.getZoom(), duration); + shapeExporter.export(); + } + @Override public void toSVG(int frame, int time, SVGExporter exporter, int ratio, ColorTransform colorTransform, int level, Matrix transformation, Matrix strokeTransformation) { if (ratio == -2) { SHAPEWITHSTYLE beginShapes = getShapeAtRatio(0); SHAPEWITHSTYLE endShapes = getShapeAtRatio(65535); - SVGMorphShapeExporter shapeExporter = new SVGMorphShapeExporter(getShapeNum(), swf, beginShapes, endShapes, getCharacterId(), exporter, null, colorTransform, exporter.getZoom()); + SVGMorphShapeExporter shapeExporter = new SVGMorphShapeExporter(getShapeNum(), swf, beginShapes, endShapes, getCharacterId(), exporter, null, colorTransform, exporter.getZoom(), 2); shapeExporter.export(); } else { SHAPEWITHSTYLE shapes = getShapeAtRatio(ratio); @@ -453,7 +460,7 @@ public abstract class MorphShapeTag extends DrawableTag { @Override public void toHtmlCanvas(StringBuilder result, double unitDivisor) { - CanvasMorphShapeExporter cmse = new CanvasMorphShapeExporter(getShapeNum(), swf, getShapeAtRatio(0), getShapeAtRatio(MAX_RATIO), null, unitDivisor, 0, 0); + CanvasMorphShapeExporter cmse = new CanvasMorphShapeExporter(getShapeNum(), swf, getShapeAtRatio(0), getShapeAtRatio(MAX_RATIO), null, unitDivisor, 0, 0, 2); cmse.export(); result.append(cmse.getShapeData()); } diff --git a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index a3b67c2b7..2fc2c88f8 100644 --- a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -65,6 +65,7 @@ import com.jpexs.decompiler.flash.exporters.FrameExporter; import com.jpexs.decompiler.flash.exporters.ImageExporter; import com.jpexs.decompiler.flash.exporters.MorphShapeExporter; import com.jpexs.decompiler.flash.exporters.MovieExporter; +import com.jpexs.decompiler.flash.exporters.PreviewExporter; import com.jpexs.decompiler.flash.exporters.ShapeExporter; import com.jpexs.decompiler.flash.exporters.SoundExporter; import com.jpexs.decompiler.flash.exporters.SymbolClassExporter; @@ -506,6 +507,8 @@ public class CommandLineArgumentParser { Map format = new HashMap<>(); Map changedImports = new LinkedHashMap<>(); double zoom = 1; + Double morphDuration = PreviewExporter.MORPH_SHAPE_DEFAULT_DURATION; + int morphNumFrames = 10; String charset = Charset.defaultCharset().name(); boolean cliMode = false; boolean air = false; @@ -554,6 +557,12 @@ public class CommandLineArgumentParser { case "-zoom": zoom = parseZoom(args); break; + case "-morphduration": + morphDuration = parseMorphDuration(args); + break; + case "-morphnumframes": + morphNumFrames = parseMorphNumFrames(args); + break; case "-format": format = parseFormat(args); break; @@ -677,7 +686,7 @@ public class CommandLineArgumentParser { } else if (command.equals("proxy")) { parseProxy(args); } else if (command.equals("export")) { - parseExport(selectionClasses, selection, selectionIds, args, handler, traceLevel, format, zoom, charset, exportEmbed, transparentBackground, urlResolver, subLength); + parseExport(selectionClasses, selection, selectionIds, args, handler, traceLevel, format, zoom, charset, exportEmbed, transparentBackground, urlResolver, subLength, morphDuration, morphNumFrames); System.exit(0); } else if (command.equals("compress")) { parseCompress(args); @@ -1757,6 +1766,42 @@ public class CommandLineArgumentParser { } return 1; } + + private static double parseMorphDuration(Stack args) { + if (args.isEmpty()) { + System.err.println("duration parameter expected"); + badArguments("morphduration"); + } + try { + double val = Double.parseDouble(args.pop()); + if (val <= 0) { + throw new NumberFormatException(); + } + return val; + } catch (NumberFormatException nfe) { + System.err.println("invalid duration"); + badArguments("morphduration"); + } + return 2; + } + + private static int parseMorphNumFrames(Stack args) { + if (args.isEmpty()) { + System.err.println("number of frames parameter expected"); + badArguments("morphnumframes"); + } + try { + int val = Integer.parseInt(args.pop()); + if (val < 2) { + throw new NumberFormatException(); + } + return val; + } catch (NumberFormatException nfe) { + System.err.println("invalid number of frames"); + badArguments("morphnumframes"); + } + return 50; + } private static AbortRetryIgnoreHandler parseOnError(Stack args) { int errorMode = AbortRetryIgnoreHandler.UNDEFINED; @@ -2119,7 +2164,10 @@ public class CommandLineArgumentParser { boolean exportEmbed, boolean transparentBackground, UrlResolver urlResolver, - int subFrameLength) { + int subFrameLength, + double morphDuration, + int morphNumFrames + ) { if (args.size() < 3) { badArguments("export"); } @@ -2298,7 +2346,7 @@ public class CommandLineArgumentParser { if (exportAll || exportFormats.contains("morphshape")) { System.out.println("Exporting morphshapes..."); - new MorphShapeExporter().exportMorphShapes(handler, outDir + (multipleExportTypes ? File.separator + MorphShapeExportSettings.EXPORT_FOLDER_NAME : ""), new ReadOnlyTagList(extags), new MorphShapeExportSettings(enumFromStr(formats.get("morphshape"), MorphShapeExportMode.class), zoom, aaScale), evl); + new MorphShapeExporter().exportMorphShapes(handler, outDir + (multipleExportTypes ? File.separator + MorphShapeExportSettings.EXPORT_FOLDER_NAME : ""), new ReadOnlyTagList(extags), new MorphShapeExportSettings(enumFromStr(formats.get("morphshape"), MorphShapeExportMode.class), zoom, aaScale, morphDuration, morphNumFrames), evl); } if (exportAll || exportFormats.contains("movie")) { diff --git a/src/com/jpexs/decompiler/flash/console/help.txt b/src/com/jpexs/decompiler/flash/console/help.txt index 2725f8aff..8a9b97dfd 100644 --- a/src/com/jpexs/decompiler/flash/console/help.txt +++ b/src/com/jpexs/decompiler/flash/console/help.txt @@ -329,6 +329,10 @@ Pre-options: morphshape:png_start_end - PNG start-end format for MorphShapes morphshape:bmp_start_end - BMP start-end format for MorphShapes morphshape:webp_start_end - WEBP start-end format for MorphShapes + morphshape:png_frames - PNG frames format for MorphShapes + morphshape:bmp_frames - BMP frames format for MorphShapes + morphshape:webp_frames - WEBP frames format for MorphShapes + morphshape:webp - WEBP format for MorphShapes frame:png - PNG format for Frames frame:gif - GIF format for Frames frame:avi - AVI format for Frames @@ -337,6 +341,7 @@ Pre-options: frame:pdf - PDF format for Frames frame:bmp - BMP format for Frames frame:webp - WEBP format for Frames + frame:webp_animated - WEBP animated format for Frames sprite:png - PNG format for Sprites sprite:gif - GIF format for Sprites sprite:avi - AVI format for Sprites @@ -345,6 +350,7 @@ Pre-options: sprite:pdf - PDF format for Sprites sprite:bmp - BMP format for Sprites sprite:webp - WEBP format for Sprites + sprite:webp_animated - WEBP animated format for Sprites button:png - PNG format for Buttons button:svg - SVG format for Buttons button:bmp - BMP format for Buttons @@ -500,3 +506,11 @@ Pre-options: Replaces URL in importAssets tag with for purposes of export. Can be used multiple times. +-morphDuration + Applies to: -export + Specifies duration in seconds (float value) for morphshape exporting. + +-morphNumFrames + Applies to: -export + Specifies number of frames for morphshape exporting. + diff --git a/src/com/jpexs/decompiler/flash/gui/ExportDialog.java b/src/com/jpexs/decompiler/flash/gui/ExportDialog.java index 8a5c91c77..66a0e17fd 100644 --- a/src/com/jpexs/decompiler/flash/gui/ExportDialog.java +++ b/src/com/jpexs/decompiler/flash/gui/ExportDialog.java @@ -59,14 +59,13 @@ import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Window; import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -156,7 +155,22 @@ public class ExportDialog extends AppDialog { private JCheckBox embedCheckBox; private JCheckBox transparentFrameBackgroundCheckBox; + + private JTextField durationTextField = new JTextField(4); + + private JTextField numberOfFramesTextField = new JTextField(4); + private JLabel durationLabel = new JLabel(translateTitle("morph.duration")); + + private JLabel secondsLabel = new JLabel(translate("morph.duration.seconds")); + + private JLabel numberOfFramesLabel = new JLabel(translateTitle("morph.numberOfFrames")); + + private JLabel zoomLabel = new JLabel(translateTitle("zoom")); + + private JLabel percentLabel = new JLabel(translate("zoom.percent")); + + @SuppressWarnings("unchecked") public E getValue(Class option) { for (int i = 0; i < optionClasses.length; i++) { @@ -171,6 +185,9 @@ public class ExportDialog extends AppDialog { public boolean isOptionEnabled(Class option) { for (int i = 0; i < optionClasses.length; i++) { if (option == optionClasses[i]) { + if (!checkBoxes[i].isVisible()) { + return false; + } return checkBoxes[i].isSelected(); } } @@ -185,6 +202,30 @@ public class ExportDialog extends AppDialog { public boolean isTransparentFrameBackgroundEnabled() { return transparentFrameBackgroundCheckBox.isSelected(); } + + public Double getMorphDuration() { + try { + Double val = Double.valueOf(durationTextField.getText()); + if (val <= 0) { + return null; + } + return val; + } catch (NumberFormatException nfe) { + return null; + } + } + + public Integer getMorphNumberOfFrames() { + try { + int val = Integer.valueOf(numberOfFramesTextField.getText()); + if (val < 2) { + return null; + } + return val; + } catch (NumberFormatException nfe) { + return null; + } + } public double getZoom() { try { @@ -194,7 +235,31 @@ public class ExportDialog extends AppDialog { } } - private void saveConfig() { + private boolean saveConfig() { + + if (isOptionEnabled(MorphShapeExportMode.class)) { + MorphShapeExportMode morphMode = getValue(MorphShapeExportMode.class); + if (morphMode.hasDuration() && getMorphDuration() == null) { + JOptionPane.showMessageDialog(ExportDialog.this, translate("morph.duration.invalid"), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + durationTextField.requestFocusInWindow(); + return false; + } + if (morphMode.hasFrames() && getMorphNumberOfFrames() == null) { + JOptionPane.showMessageDialog(ExportDialog.this, translate("morph.numberOfFrames.invalid"), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + numberOfFramesTextField.requestFocusInWindow(); + return false; + } + + try { + Double.parseDouble(zoomTextField.getText()); + } catch (NumberFormatException nfe) { + JOptionPane.showMessageDialog(ExportDialog.this, translate("zoom.invalid"), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); + zoomTextField.requestFocusInWindow(); + return false; + } + } + + StringBuilder cfg = new StringBuilder(); for (int i = 0; i < optionNames.length; i++) { Object val = ((ComboValue) combos[i].getSelectedItem()).value; @@ -206,6 +271,17 @@ public class ExportDialog extends AppDialog { cfg.append(key); } + + Double morphDuration = getMorphDuration(); + if (morphDuration != null) { + Configuration.lastExportMorphDuration.set(morphDuration); + } + + Integer morphNumberOfFrames = getMorphNumberOfFrames(); + if (morphNumberOfFrames != null) { + Configuration.lastExportMorphNumberOfFrames.set(morphNumberOfFrames); + } + Configuration.lastSelectedExportZoom.set(Double.parseDouble(zoomTextField.getText()) / 100); Configuration.lastSelectedExportFormats.set(cfg.toString()); if (embedCheckBox.isVisible()) { @@ -214,6 +290,7 @@ public class ExportDialog extends AppDialog { if (transparentFrameBackgroundCheckBox.isVisible()) { Configuration.lastExportTransparentBackground.set(transparentFrameBackgroundCheckBox.isSelected()); } + return true; } private boolean optionCanHandle(int optionIndex, Object e) { @@ -238,6 +315,36 @@ public class ExportDialog extends AppDialog { private String translateTitle(String title) { return translate("titleFormat").replace("%title%", translate(title)); } + + private void onChange() { + if (!isOptionEnabled(MorphShapeExportMode.class)) { + durationLabel.setVisible(false); + durationTextField.setVisible(false); + secondsLabel.setVisible(false); + numberOfFramesLabel.setVisible(false); + numberOfFramesTextField.setVisible(false); + } else { + MorphShapeExportMode mode = getValue(MorphShapeExportMode.class); + durationLabel.setVisible(mode.hasDuration()); + durationTextField.setVisible(mode.hasDuration()); + secondsLabel.setVisible(mode.hasDuration()); + numberOfFramesLabel.setVisible(mode.hasFrames()); + numberOfFramesTextField.setVisible(mode.hasFrames()); + } + + transparentFrameBackgroundCheckBox.setVisible(isOptionEnabled(FrameExportMode.class)); + + boolean hasZoom = false; + for (Class c : zoomClasses) { + if (isOptionEnabled(c)) { + hasZoom = true; + break; + } + } + zoomTextField.setVisible(hasZoom); + zoomLabel.setVisible(hasZoom); + percentLabel.setVisible(hasZoom); + } @SuppressWarnings("unchecked") public ExportDialog(Window owner, List exportables) { @@ -305,6 +412,7 @@ public class ExportDialog extends AppDialog { checkBox.setSelected(selected); } } + onChange(); }); gbc.gridy = 0; gbc.gridx = 3; @@ -345,15 +453,24 @@ public class ExportDialog extends AppDialog { checkBoxes[i] = new JCheckBox(); checkBoxes[i].setSelected(true); + + checkBoxes[i].addActionListener((ActionEvent e) -> { + onChange(); + }); if (!exportableExistsArray[i]) { + checkBoxes[i].setVisible(false); continue; } if (Arrays.asList(zoomClasses).contains(c)) { zoomable = true; } - + + combos[i].addItemListener((ItemEvent e) -> { + onChange(); + }); + visibleOptionClasses.add(c); JLabel lab = new JLabel(translateTitle(optionNames[i])); @@ -413,24 +530,67 @@ public class ExportDialog extends AppDialog { transparentFrameBackgroundCheckBox.setSelected(true); } } - - if (zoomable) { - JLabel zlab = new JLabel(translateTitle("zoom")); - JLabel pctLabel = new JLabel(translate("zoom.percent")); - zlab.setLabelFor(zoomTextField); + + durationTextField.setVisible(false); + numberOfFramesTextField.setVisible(false); + if (visibleOptionClasses.contains(MorphShapeExportMode.class)) { + gbc.gridy++; + durationTextField.setVisible(true); + String durationString = "" + Configuration.lastExportMorphDuration.get(); + if (durationString.endsWith(".0")) { + durationString = durationString.substring(0, durationString.length() - 2); + } + durationTextField.setText(durationString); + numberOfFramesTextField.setVisible(true); + numberOfFramesTextField.setText("" + Configuration.lastExportMorphNumberOfFrames.get()); + + durationLabel.setLabelFor(durationTextField); + numberOfFramesLabel.setLabelFor(numberOfFramesTextField); + + gbc.gridx = 0; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.LINE_END; + comboPanel.add(durationLabel, gbc); + gbc.gridx++; + gbc.anchor = GridBagConstraints.LINE_START; + comboPanel.add(durationTextField, gbc); + gbc.gridx++; + gbc.anchor = GridBagConstraints.LINE_START; + comboPanel.add(secondsLabel, gbc); + + gbc.gridy++; gbc.gridx = 0; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.LINE_END; - comboPanel.add(zlab, gbc); + comboPanel.add(numberOfFramesLabel, gbc); + gbc.gridx++; + gbc.anchor = GridBagConstraints.LINE_START; + comboPanel.add(numberOfFramesTextField, gbc); + } + + if (zoomable) { + zoomLabel.setLabelFor(zoomTextField); + gbc.gridy++; + gbc.gridx = 0; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.LINE_END; + comboPanel.add(zoomLabel, gbc); gbc.gridx++; gbc.anchor = GridBagConstraints.LINE_START; comboPanel.add(zoomTextField, gbc); gbc.gridx++; gbc.anchor = GridBagConstraints.LINE_START; - comboPanel.add(pctLabel, gbc); + comboPanel.add(percentLabel, gbc); } + gbc.gridy++; + gbc.gridx = 0; + gbc.gridwidth = 3; + gbc.fill = GridBagConstraints.BOTH; + gbc.weighty = 1; + comboPanel.add(new JPanel(), gbc); + cnt.add(comboPanel, BorderLayout.CENTER); JPanel buttonsPanel = new JPanel(new FlowLayout()); @@ -455,6 +615,7 @@ public class ExportDialog extends AppDialog { } zoomTextField.setText(pct); + onChange(); } @Override @@ -466,15 +627,11 @@ public class ExportDialog extends AppDialog { } private void okButtonActionPerformed(ActionEvent evt) { - result = OK_OPTION; - try { - saveConfig(); - } catch (NumberFormatException nfe) { - JOptionPane.showMessageDialog(ExportDialog.this, translate("zoom.invalid"), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE); - zoomTextField.requestFocusInWindow(); + if (!saveConfig()) { return; } - + + result = OK_OPTION; setVisible(false); } diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 13120dda5..bed6daeb7 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -2289,7 +2289,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se if (export.isOptionEnabled(MorphShapeExportMode.class)) { ret.addAll(new MorphShapeExporter().exportMorphShapes(handler, selFile2 + File.separator + MorphShapeExportSettings.EXPORT_FOLDER_NAME, new ReadOnlyTagList(morphshapes), - new MorphShapeExportSettings(export.getValue(MorphShapeExportMode.class), export.getZoom(), aaScale), evl)); + new MorphShapeExportSettings(export.getValue(MorphShapeExportMode.class), export.getZoom(), aaScale, export.getMorphDuration(), export.getMorphNumberOfFrames()), evl)); } if (export.isOptionEnabled(TextExportMode.class)) { @@ -2409,7 +2409,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se if (export.isOptionEnabled(MorphShapeExportMode.class)) { new MorphShapeExporter().exportMorphShapes(handler, Path.combine(selFile, MorphShapeExportSettings.EXPORT_FOLDER_NAME), swf.getTags(), - new MorphShapeExportSettings(export.getValue(MorphShapeExportMode.class), export.getZoom(), aaScale), evl); + new MorphShapeExportSettings(export.getValue(MorphShapeExportMode.class), export.getZoom(), aaScale, export.getMorphDuration(), export.getMorphNumberOfFrames()), evl); } if (export.isOptionEnabled(TextExportMode.class)) { @@ -2508,7 +2508,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se if (export.isOptionEnabled(MorphShapeExportMode.class)) { for (MorphShapeExportMode exportMode : MorphShapeExportMode.values()) { new MorphShapeExporter().exportMorphShapes(handler, Path.combine(selFile, MorphShapeExportSettings.EXPORT_FOLDER_NAME, exportMode.name()), swf.getTags(), - new MorphShapeExportSettings(exportMode, export.getZoom(), aaScale), evl); + new MorphShapeExportSettings(exportMode, export.getZoom(), aaScale, export.getMorphDuration(), export.getMorphNumberOfFrames()), evl); } } diff --git a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java index 3d1bf504e..a548fdbdd 100644 --- a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java @@ -2144,7 +2144,7 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel SWFHeader header; try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(extTempFile))) { - header = new PreviewExporter().exportSwf(fos, treeItem, backgroundColor, fontPageNum, true); + header = new PreviewExporter().exportSwf(fos, treeItem, backgroundColor, fontPageNum, true, PreviewExporter.MORPH_SHAPE_DEFAULT_DURATION); } } Main.runAsync(extTempFile); @@ -2182,7 +2182,7 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel SWFHeader header; try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile))) { - header = new PreviewExporter().exportSwf(fos, treeItem, backgroundColor, fontPageNum, false); + header = new PreviewExporter().exportSwf(fos, treeItem, backgroundColor, fontPageNum, false, PreviewExporter.MORPH_SHAPE_DEFAULT_DURATION); } this.currentItem = treeItem; diff --git a/src/com/jpexs/decompiler/flash/gui/TimelinedMaker.java b/src/com/jpexs/decompiler/flash/gui/TimelinedMaker.java index 5b26d1c21..632332dfe 100644 --- a/src/com/jpexs/decompiler/flash/gui/TimelinedMaker.java +++ b/src/com/jpexs/decompiler/flash/gui/TimelinedMaker.java @@ -233,7 +233,7 @@ public class TimelinedMaker { private void initTimeline(Timeline timeline) { if (tag instanceof MorphShapeTag) { timeline.frameRate = PreviewExporter.MORPH_SHAPE_ANIMATION_FRAME_RATE; - int framesCnt = (int) (timeline.frameRate * PreviewExporter.MORPH_SHAPE_ANIMATION_LENGTH); + int framesCnt = (int) (timeline.frameRate * PreviewExporter.MORPH_SHAPE_DEFAULT_DURATION); for (int i = 0; i < framesCnt; i++) { Frame f = new Frame(timeline, i); DepthState ds = new DepthState(tag.getSwf(), f, f); diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties index 41c5fc2ab..142ec9de5 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties @@ -654,3 +654,10 @@ config.description.reduceAntialiasConflationByScalingForExport = Reduce conflati config.name.reduceAntialiasConflationByScalingValueForExport = Max scale value for reducing antialias conflation (Export) config.description.reduceAntialiasConflationByScalingValueForExport = Maximal scale coefficient for reducing antialias conflation. Used for exporting. Larger value = slower export, but more accurate result, also uses more memory. 1 = none, 10 = optimal, 20 = perfect. It's real value might be lower depending on dimension of images. + +#after 24.1.2 +config.name.lastExportMorphDuration = Last setting of export morph duration +config.description.lastExportMorphDuration = Last setting of export morphshape duration in seconds (float). + +config.name.lastExportMorphNumberOfFrames = Last setting of export morph number of frames +config.description.lastExportMorphNumberOfFrames = Last setting of export morphshape number of frames. 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 6bacedc3d..d26892706 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties @@ -654,3 +654,10 @@ config.description.reduceAntialiasConflationByScalingForExport = Redukuje konfla config.name.reduceAntialiasConflationByScalingValueForExport = Maxim\u00e1ln\u00ed hodnota \u0161k\u00e1lov\u00e1n\u00ed pro redukci antialiasov\u00e9 konflace (Export) config.description.reduceAntialiasConflationByScalingValueForExport = Maxim\u00e1ln\u00ed \u0161k\u00e1lovac\u00ed koeficient pro redukci antialiasingov\u00e9 konflace. Pou\u017e\u00edvan\u00e9 pro export. V\u011bt\u0161\u00ed hodnota = pomalej\u0161\u00ed export, ale p\u0159esn\u011bj\u0161\u00ed v\u00fdsledek a tak\u00e9 v\u00edc pou\u017eit\u00e9 pam\u011bti. 1 = nic, 10 = optim\u00e1ln\u00ed, 20 = perfektn\u00ed. Jeho re\u00e1ln\u00ed hodnota m\u016f\u017ee b\u00fdt ni\u017e\u0161\u00ed v z\u00e1vislosti na rozm\u011brech obr\u00e1zk\u016f. + +#after 24.1.2 +config.name.lastExportMorphDuration = Posledn\u00ed nastaven\u00ed d\u00e9lky prom\u011bny p\u0159i exportu +config.description.lastExportMorphDuration = Posledn\u00ed nastaven\u00ed exportu morphshape - d\u00e9lka prom\u011bny ve vte\u0159in\u00e1ch (desetinn\u00e9 \u010d\u00edslo). + +config.name.lastExportMorphNumberOfFrames = Posledn\u00ed nastaven\u00ed po\u010dtu sn\u00edmk\u016f prom\u011bny p\u0159i exportu +config.description.lastExportMorphNumberOfFrames = Posledn\u00ed nastaven\u00ed exportu morphshape - po\u010det sn\u00edmk\u016f. diff --git a/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties index 9b4e19ab6..4f67c04de 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog.properties @@ -101,4 +101,19 @@ fonts4.cff=CFF embed = Export embedded assets via [Embed] #after 20.1.0 resampleWav = Resample Wav to 44kHz -transparentFrameBackground = Ignore background color (make transparent) \ No newline at end of file +transparentFrameBackground = Ignore background color (make transparent) +#after 21.1.2 +morphshapes.bmp_frames = BMP (frames) +morphshapes.png_frames = PNG (frames) +morphshapes.svg_frames = SVG (frames) +morphshapes.webp_frames = WEBP (frames) +morphshapes.gif = GIF +morphshapes.avi = AVI +morphshapes.webp = WEBP +frames.webp_animated = WEBP (animated) +sprites.webp_animated = WEBP (animated) +morph.duration = Morph duration +morph.duration.seconds = s +morph.numberOfFrames = Number of morph frames +morph.duration.invalid = Invalid morph duration. Enter positive float value in seconds. +morph.numberOfFrames.invalid = Invalid number of frames. Enter positive integer value >= 2. diff --git a/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog_cs.properties index 885986b5a..eee23300f 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/ExportDialog_cs.properties @@ -94,4 +94,19 @@ fonts4.cff=CFF embed = Exportovat vlo\u017een\u00e9 zdroje skrze [Embed] #after 20.1.0 resampleWav = P\u0159evzorkovat Wav na 44kHz -transparentFrameBackground = Ignorovat barvu pozad\u00ed (pou\u017e\u00edt pr\u016fhlednou) \ No newline at end of file +transparentFrameBackground = Ignorovat barvu pozad\u00ed (pou\u017e\u00edt pr\u016fhlednou) +#after 21.1.2 +morphshapes.bmp_frames = BMP (sn\u00edmky) +morphshapes.png_frames = PNG (sn\u00edmky) +morphshapes.svg_frames = SVG (sn\u00edmky) +morphshapes.webp_frames = WEBP (sn\u00edmky) +morphshapes.gif = GIF +morphshapes.avi = AVI +morphshapes.webp = WEBP +frames.webp_animated = WEBP (animovan\u00e9) +sprites.webp_animated = WEBP (animovan\u00e9) +morph.duration = D\u00e9lka prom\u011bny +morph.duration.seconds = s +morph.numberOfFrames = Po\u010det sn\u00edmk\u016f prom\u011bny +morph.duration.invalid = Neplatn\u00e1 d\u00e9lka prom\u011bny. Zadejte kladn\u00e9 desetinn\u00e9 \u010d\u00edslo ve vte\u0159in\u00e1ch. +morph.numberOfFrames.invalid = Neplatn\u00fd po\u010det sn\u00edmk\u016f. Zadejte kladn\u00e9 cel\u00e9 \u010d\u00edslo >= 2. \ 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 d88609fe6..721019413 100644 --- a/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java +++ b/src/com/jpexs/decompiler/flash/gui/tagtree/TagTreeContextMenu.java @@ -4772,7 +4772,7 @@ public class TagTreeContextMenu extends JPopupMenu { } try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile))) { - new PreviewExporter().exportSwf(fos, treeItem, backgroundColor, 1/*fontPageNum ???*/, false); + new PreviewExporter().exportSwf(fos, treeItem, backgroundColor, 1/*fontPageNum ???*/, false, PreviewExporter.MORPH_SHAPE_DEFAULT_DURATION); } catch (ActionParseException ex) { Logger.getLogger(TagTreeContextMenu.class.getName()).log(Level.SEVERE, null, ex); }