feat: combined button SVG export using all states

This commit is contained in:
Jindra Petřík
2026-03-24 23:04:48 +01:00
parent 2ef5d26b0e
commit 4e12a7590b
10 changed files with 167 additions and 36 deletions

View File

@@ -27,6 +27,7 @@ import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.exporters.commonshape.ExportRectangle;
import com.jpexs.decompiler.flash.exporters.commonshape.Matrix;
import com.jpexs.decompiler.flash.exporters.commonshape.SVGExporter;
import com.jpexs.decompiler.flash.exporters.modes.ButtonExportMode;
import com.jpexs.decompiler.flash.exporters.modes.FontExportMode;
import com.jpexs.decompiler.flash.exporters.modes.FrameExportMode;
import com.jpexs.decompiler.flash.exporters.settings.ButtonExportSettings;
@@ -40,6 +41,7 @@ import com.jpexs.decompiler.flash.tags.SetBackgroundColorTag;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.base.ButtonTag;
import com.jpexs.decompiler.flash.tags.base.CharacterTag;
import com.jpexs.decompiler.flash.tags.base.DrawableTag;
import com.jpexs.decompiler.flash.tags.base.FontTag;
import com.jpexs.decompiler.flash.tags.base.RenderContext;
import com.jpexs.decompiler.flash.tags.base.TextTag;
@@ -107,6 +109,7 @@ import net.kroo.elliot.GifSequenceWriter;
import net.weiner.kevin.AnimatedGifEncoder;
import org.monte.media.VideoFormatKeys;
import org.monte.media.avi.AVIWriter;
import org.w3c.dom.Element;
/**
* Frame exporter.
@@ -129,6 +132,9 @@ public class FrameExporter {
case SVG:
fem = FrameExportMode.SVG;
break;
case SVG_COMBINED:
fem = FrameExportMode.SVG;
break;
case WEBP:
fem = FrameExportMode.WEBP;
break;
@@ -139,16 +145,16 @@ public class FrameExporter {
throw new Error("Unsupported button export mode: " + settings.mode);
}
if (frames == null) {
/*if (frames == null) {
frames = new ArrayList<>();
frames.add(ButtonTag.FRAME_UP);
frames.add(ButtonTag.FRAME_OVER);
frames.add(ButtonTag.FRAME_DOWN);
frames.add(ButtonTag.FRAME_HITTEST);
}
}*/
FrameExportSettings fes = new FrameExportSettings(fem, settings.zoom, true, settings.aaScale);
return exportFrames(handler, outdir, swf, containerId, frames, 1, fes, evl, true);
return exportFrames(handler, outdir, swf, containerId, frames, 1, fes, evl, true, settings.mode == ButtonExportMode.SVG_COMBINED);
}
public List<File> exportSpriteFrames(AbortRetryIgnoreHandler handler, String outdir, SWF swf, int containerId, List<Integer> frames, int subframesLength, SpriteExportSettings settings, EventListener evl) throws IOException, InterruptedException {
@@ -275,10 +281,6 @@ public class FrameExporter {
}
}
public List<File> exportFrames(AbortRetryIgnoreHandler handler, String outdir, final SWF swf, int containerId, List<Integer> frames, int subFramesLength, final FrameExportSettings settings, final EventListener evl) throws IOException, InterruptedException {
return exportFrames(handler, outdir, swf, containerId, frames, subFramesLength, settings, evl, false);
}
private String getFrameFileName(int frame, boolean button) {
String buttonSuffix = "";
if (button) {
@@ -300,7 +302,11 @@ public class FrameExporter {
return "" + frame + buttonSuffix;
}
public List<File> exportFrames(AbortRetryIgnoreHandler handler, String outdir, final SWF swf, int containerId, List<Integer> frames, int subFramesLength, final FrameExportSettings settings, final EventListener evl, boolean button) throws IOException, InterruptedException {
public List<File> exportFrames(AbortRetryIgnoreHandler handler, String outdir, final SWF swf, int containerId, List<Integer> frames, int subFramesLength, final FrameExportSettings settings, final EventListener evl) throws IOException, InterruptedException {
return exportFrames(handler, outdir, swf, containerId, frames, subFramesLength, settings, evl, false, false);
}
private List<File> exportFrames(AbortRetryIgnoreHandler handler, String outdir, final SWF swf, int containerId, List<Integer> frames, int subFramesLength, final FrameExportSettings settings, final EventListener evl, boolean button, boolean combined) throws IOException, InterruptedException {
final List<File> ret = new ArrayList<>();
if (CancellableWorker.isInterrupted()) {
return ret;
@@ -309,6 +315,7 @@ public class FrameExporter {
if (swf.getTags().isEmpty()) {
return ret;
}
Timelined timelined;
Timeline tim0;
List<String> paths = new ArrayList<>();
String subPath = "";
@@ -317,10 +324,12 @@ public class FrameExporter {
}
if (containerId == 0) {
timelined = swf;
tim0 = swf.getTimeline();
paths.add(subPath);
} else {
tim0 = ((Timelined) swf.getCharacter(containerId)).getTimeline();
timelined = (Timelined) swf.getCharacter(containerId);
tim0 = timelined.getTimeline();
Set<String> classNames = swf.getCharacter(containerId).getClassNames();
if (Configuration.as3ExportNamesUseClassNamesOnly.get() && !classNames.isEmpty()) {
@@ -367,23 +376,12 @@ public class FrameExporter {
Map<Integer, FontTag> normalizedFonts = new LinkedHashMap<>();
Map<Integer, TextTag> normalizedTexts = new LinkedHashMap<>();
normalizer.normalizeFonts(tim.timelined.getSwf(), normalizedFonts, normalizedTexts);
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, max, parentTag == null ? "" : parentTag.getName());
}
final int fi = i;
if (combined) {
final Color fbackgroundColor = backgroundColor;
for (File foutdir : foutdirs) {
new RetryTask(() -> {
int frame = subFramesLength > 1 ? fframes.get(0) : fframes.get(fi);
File f = new File(foutdir + File.separator + getFrameFileName(((subFramesLength > 1 ? fi : fframes.get(fi)) + 1), button) + ".svg");
File f = new File(foutdir + File.separator + "combined.svg");
try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(f))) {
ExportRectangle rect = new ExportRectangle(tim.displayRect);
rect.xMax *= settings.zoom;
@@ -391,21 +389,56 @@ public class FrameExporter {
rect.xMin *= settings.zoom;
rect.yMin *= settings.zoom;
SVGExporter exporter = new SVGExporter(rect, settings.zoom, "frame", fbackgroundColor);
exporter.setNormalizedFonts(normalizedFonts, normalizedTexts);
tim.toSVG(frame, subFramesLength > 1 ? fi : 0, null, 0, exporter, null, 0, new Matrix(), new Matrix());
exporter.setNormalizedFonts(normalizedFonts, normalizedTexts);
//Here is not timelined.toSVG, but drawableTag.toSVG, which draws combined button
((DrawableTag) timelined).toSVG(0, 0, exporter, 0, null, 0, new Matrix(), new Matrix());
fos.write(Utf8Helper.getBytes(exporter.getSVG()));
}
ret.add(f);
}, handler).run();
}
if (CancellableWorker.isInterrupted()) {
break;
} else {
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, max, parentTag == null ? "" : parentTag.getName());
}
if (evl != null) {
Tag parentTag = tim.getParentTag();
evl.handleExportedEvent("frame", i + 1, max, parentTag == null ? "" : parentTag.getName());
final int fi = i;
final Color fbackgroundColor = backgroundColor;
for (File foutdir : foutdirs) {
new RetryTask(() -> {
int frame = subFramesLength > 1 ? fframes.get(0) : fframes.get(fi);
File f = new File(foutdir + File.separator + getFrameFileName(((subFramesLength > 1 ? fi : fframes.get(fi)) + 1), button) + ".svg");
try (OutputStream fos = new BufferedOutputStream(new FileOutputStream(f))) {
ExportRectangle rect = new ExportRectangle(tim.displayRect);
rect.xMax *= settings.zoom;
rect.yMax *= settings.zoom;
rect.xMin *= settings.zoom;
rect.yMin *= settings.zoom;
SVGExporter exporter = new SVGExporter(rect, settings.zoom, "frame", fbackgroundColor);
exporter.setNormalizedFonts(normalizedFonts, normalizedTexts);
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);
}, handler).run();
}
if (CancellableWorker.isInterrupted()) {
break;
}
if (evl != null) {
Tag parentTag = tim.getParentTag();
evl.handleExportedEvent("frame", i + 1, max, parentTag == null ? "" : parentTag.getName());
}
}
}

View File

@@ -100,6 +100,8 @@ public class SVGExporter implements RequiresNormalizedFonts {
private Map<Integer, FontTag> normalizedFonts = new LinkedHashMap<>();
private Map<Integer, TextTag> normalizedTexts = new LinkedHashMap<>();
private boolean buttonStyleAdded = false;
@Override
public void setNormalizedFonts(Map<Integer, FontTag> normalizedFonts, Map<Integer, TextTag> normalizedTexts) {
@@ -656,7 +658,55 @@ public class SVGExporter implements RequiresNormalizedFonts {
}
}
public void addStyle(String fontFace, byte[] data, FontExportMode mode) {
public void addStyle(String css) {
String value = getStyle().getTextContent();
value += Helper.newLine;
value += css;
getStyle().setTextContent(value);
}
public void requireButtonStyle() {
if (buttonStyleAdded) {
return;
}
buttonStyleAdded = true;
addStyle(" .button-frame {\n"
+ " pointer-events: none;\n"
+ " }\n"
+ "\n"
+ " .button-frame-up,\n"
+ " .button-frame-over,\n"
+ " .button-frame-down {\n"
+ " opacity: 0;\n"
+ " }\n"
+ "\n"
+ " .button .button-frame-up {\n"
+ " opacity: 1;\n"
+ " }\n"
+ "\n"
+ " .button:hover .button-frame-up {\n"
+ " opacity: 0;\n"
+ " }\n"
+ " .button:hover .button-frame-over {\n"
+ " opacity: 1;\n"
+ " }\n"
+ "\n"
+ " .button:active .button-frame-over {\n"
+ " opacity: 0;\n"
+ " }\n"
+ " .button:active .button-frame-down {\n"
+ " opacity: 1;\n"
+ " }\n"
+ "\n"
+ " .button-frame-hittest {\n"
+ " opacity: 0;\n"
+ " pointer-events: all;\n"
+ " cursor: pointer;\n"
+ " }\n");
}
public void addFontFace(String fontFace, byte[] data, FontExportMode mode) {
if (!fontFaces.contains(fontFace)) {
fontFaces.add(fontFace);
String base64Data = Helper.byteArrayToBase64String(data);

View File

@@ -32,6 +32,10 @@ public enum ButtonExportMode {
* SVG - Scalable Vector Graphics
*/
SVG,
/**
* SVG - Scalable Vector Graphics - Combined button
*/
SVG_COMBINED,
/**
* BMP - Windows Bitmap
*/

View File

@@ -40,6 +40,7 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.w3c.dom.Element;
/**
* Base class for button tags.
@@ -134,7 +135,36 @@ public abstract class ButtonTag extends DrawableTag implements Timelined {
@Override
public void toSVG(int frame, int time, SVGExporter exporter, int ratio, ColorTransform colorTransform, int level, Matrix transformation, Matrix strokeTransformation) throws IOException {
getTimeline().toSVG(0, 0, null, 0, exporter, colorTransform, level + 1, transformation, strokeTransformation);
//getTimeline().toSVG(0, 0, null, 0, exporter, colorTransform, level + 1, transformation, strokeTransformation);
exporter.requireButtonStyle();
Timeline tim = getTimeline();
Element buttonGroup = exporter.createSubGroup(new Matrix(), null);
buttonGroup.setAttribute("class", "button");
Element upGroup = exporter.createSubGroup(new Matrix(), null);
upGroup.setAttribute("class", "button-frame button-frame-up");
tim.toSVG(ButtonTag.FRAME_UP, 0, null, 0, exporter, colorTransform, level + 1, transformation, strokeTransformation);
exporter.endGroup();
Element overGroup = exporter.createSubGroup(new Matrix(), null);
overGroup.setAttribute("class", "button-frame button-frame-over");
tim.toSVG(ButtonTag.FRAME_OVER, 0, null, 0, exporter, colorTransform, level + 1, transformation, strokeTransformation);
exporter.endGroup();
Element downGroup = exporter.createSubGroup(new Matrix(), null);
downGroup.setAttribute("class", "button-frame button-frame-down");
tim.toSVG(ButtonTag.FRAME_DOWN, 0, null, 0, exporter, colorTransform, level + 1, transformation, strokeTransformation);
exporter.endGroup();
Element hitTestGroup = exporter.createSubGroup(new Matrix(), null);
hitTestGroup.setAttribute("class", "button-frame button-frame-hittest");
tim.toSVG(ButtonTag.FRAME_HITTEST, 0, null, 0, exporter, colorTransform, level + 1, transformation, strokeTransformation);
exporter.endGroup();
exporter.endGroup(); //buttonGroup
}
/**

View File

@@ -1114,7 +1114,7 @@ public abstract class TextTag extends DrawableTag {
exporter.addToGroup(textElement);
FontExportMode fontExportMode = FontExportMode.WOFF;
exporter.addStyle(fontFamily, new FontExporter().exportFont(font, fontExportMode), fontExportMode);
exporter.addFontFace(fontFamily, new FontExporter().exportFont(font, fontExportMode), fontExportMode);
if (hasOffset) {
exporter.endGroup();