From 77f96d5ecc94fda1babaab50daae3df50ac404ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Sat, 7 Sep 2024 19:51:41 +0200 Subject: [PATCH] Added: FLA export - generating bin/*.dat files for movies and images --- CHANGELOG.md | 2 + .../flash/xfl/BinDataOutputStream.java | 85 ++++++++ .../flash/xfl/ImageBinDataGenerator.java | 98 +++++++++ .../flash/xfl/MovieBinDataGenerator.java | 204 ++++++++++++++++++ .../decompiler/flash/xfl/XFLConverter.java | 44 ++-- 5 files changed, 416 insertions(+), 17 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/BinDataOutputStream.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/ImageBinDataGenerator.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/MovieBinDataGenerator.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc7489da..e87a1965f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- FLA export - generating bin/*.dat files for movies and images ## [21.0.5] - 2024-09-05 ### Fixed diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/BinDataOutputStream.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/BinDataOutputStream.java new file mode 100644 index 000000000..c6d336700 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/BinDataOutputStream.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.xfl; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * + * @author JPEXS + */ +public class BinDataOutputStream extends OutputStream { + + private final OutputStream os; + + public BinDataOutputStream(OutputStream os) { + this.os = os; + } + + @Override + public void write(int b) throws IOException { + os.write(b); + } + + public void write(int... values) throws IOException { + for (int i : values) { + os.write(i); + } + } + + public void writeUI16(int value) throws IOException { + write(value & 0xFF); + write((value >> 8) & 0xFF); + } + + @Override + public void write(byte[] b) throws IOException { + os.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + os.write(b, off, len); + } + + public void writeUI32(long value) throws IOException { + write((int) (value & 0xFF)); + write((int) ((value >> 8) & 0xFF)); + write((int) ((value >> 16) & 0xFF)); + write((int) ((value >> 24) & 0xFF)); + } + + public void writeUI64(long value) throws IOException { + write((int) (value & 0xFF)); + write((int) ((value >> 8) & 0xFF)); + write((int) ((value >> 16) & 0xFF)); + write((int) ((value >> 24) & 0xFF)); + write((int) ((value >> 32) & 0xFF)); + write((int) ((value >> 40) & 0xFF)); + write((int) ((value >> 48) & 0xFF)); + write((int) ((value >> 56) & 0xFF)); + } + + public void writeFloat(float val) throws IOException { + writeUI32(Float.floatToIntBits(val)); + } + + public void writeDouble(double val) throws IOException { + writeUI64(Double.doubleToLongBits(val)); + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/ImageBinDataGenerator.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/ImageBinDataGenerator.java new file mode 100644 index 000000000..565a903b5 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/ImageBinDataGenerator.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.xfl; + +import com.jpexs.decompiler.flash.tags.enums.ImageFormat; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import javax.imageio.ImageIO; + +/** + * + * @author JPEXS + */ +public class ImageBinDataGenerator { + + public void generateBinData(InputStream is, OutputStream os, ImageFormat format) throws IOException { + BufferedImage bimg = ImageIO.read(is); + if (format == ImageFormat.JPEG) { + byte[] buf = new byte[4096]; + int cnt; + while ((cnt = is.read(buf)) > 0) { + os.write(buf, 0, cnt); + } + BinDataOutputStream dw2 = new BinDataOutputStream(os); + dw2.writeUI32(0); + dw2.writeUI32(0); + dw2.writeUI32(20 * bimg.getWidth()); + dw2.writeUI32(20 * bimg.getHeight()); + } else { + //https://stackoverflow.com/questions/4082812/xfl-what-are-the-bin-dat-files/4082907#4082907 + os.write(0x03); + os.write(0x05); + BinDataOutputStream w = new BinDataOutputStream(os); + + int decRowLen = 4 * bimg.getWidth(); + w.writeUI16(decRowLen); + + w.writeUI16(bimg.getWidth()); + w.writeUI16(bimg.getHeight()); + + w.writeUI32(0); + w.writeUI32(0); + w.writeUI32(20 * bimg.getWidth()); + w.writeUI32(20 * bimg.getHeight()); + + w.write(0x01); //has transparency + w.write(0x01); //compressed variant + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DeflaterOutputStream def = new DeflaterOutputStream(baos, new Deflater(1)); + + for (int y = 0; y < bimg.getHeight(); y++) { + for (int x = 0; x < bimg.getWidth(); x++) { + int rgba = bimg.getRGB(x, y); + def.write((rgba >> 24) & 0xFF); //a + def.write((rgba >> 16) & 0xFF); //b + def.write((rgba >> 8) & 0xFF); //g + def.write(rgba & 0xFF); //r + } + } + def.flush(); + def.finish(); + byte[] data = baos.toByteArray(); + int pos = 0; + while (pos < data.length) { + int cnt = 2048; //it seems that using large chunk sizes like 0xFFFF crashes flash. 2024 is used in CS5. + if (pos + cnt > data.length) { + cnt = data.length - pos; + } + w.writeUI16(cnt); + os.write(data, pos, cnt); + pos += cnt; + } + w.writeUI16(0); + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/MovieBinDataGenerator.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/MovieBinDataGenerator.java new file mode 100644 index 000000000..334adf316 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/MovieBinDataGenerator.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2010-2024 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.xfl; + +import com.jpexs.decompiler.flash.SWFInputStream; +import com.jpexs.decompiler.flash.flv.AUDIODATA; +import com.jpexs.decompiler.flash.flv.FLVInputStream; +import com.jpexs.decompiler.flash.flv.FLVTAG; +import com.jpexs.decompiler.flash.flv.SCRIPTDATA; +import com.jpexs.decompiler.flash.flv.SCRIPTDATAVARIABLE; +import com.jpexs.decompiler.flash.flv.VIDEODATA; +import com.jpexs.decompiler.flash.types.sound.SoundFormat; +import com.jpexs.helpers.Reference; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * + * @author JPEXS + */ +public class MovieBinDataGenerator { + + public byte[] generateEmptyBinData() { + return new byte[]{ + (byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xA0, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x78, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x59, (byte) 0x40, (byte) 0x18, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFE, (byte) 0xFF, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 + }; + } + + public void generateBinData(InputStream is, OutputStream os, float fps) throws IOException { + BinDataOutputStream df = new BinDataOutputStream(os); + FLVInputStream flvIs = new FLVInputStream(is); + int width = 0; + int height = 0; + Reference audioPresent = new Reference<>(false); + Reference videoPresent = new Reference<>(false); + flvIs.readHeader(audioPresent, videoPresent); + List flvTags = flvIs.readTags(); + long lastOffset = 0L; + long videoFrameCount = 0; + + int soundFormat = 0; + int samplingRate = 0; + boolean stereo = false; + int videoCodec = 0; + boolean hasAudio = false; + long maxTimestamp = 0L; + for (FLVTAG tag : flvTags) { + if (tag.timeStamp > maxTimestamp) { + maxTimestamp = tag.timeStamp; + } + if (tag.data instanceof VIDEODATA) { + videoFrameCount++; + VIDEODATA vd = (VIDEODATA) tag.data; + videoCodec = vd.codecId; + } + if (tag.data instanceof AUDIODATA) { + AUDIODATA ad = (AUDIODATA) tag.data; + soundFormat = ad.soundFormat; + samplingRate = ad.soundRate; + stereo = ad.soundType; + hasAudio = true; + } + } + + long videoDataIndex = 0; + for (FLVTAG tag : flvTags) { + if (tag.data instanceof SCRIPTDATA) { + SCRIPTDATA sd = (SCRIPTDATA) tag.data; + if (sd.name.type != 2) { + continue; + } + if (!"onMetaData".equals(sd.name.value)) { + continue; + } + if (sd.value.type != 8) { + continue; + } + @SuppressWarnings("unchecked") + List subVals = (List) sd.value.value; + for (Object o : subVals) { + if (o instanceof SCRIPTDATAVARIABLE) { + SCRIPTDATAVARIABLE v = (SCRIPTDATAVARIABLE) o; + if ("width".equals(v.variableName)) { + width = (int) (double) v.variableData.value; + } + if ("height".equals(v.variableName)) { + height = (int) (double) v.variableData.value; + } + } + } + df.write(0x03, videoCodec, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x28, 0x40, + 0x00, 0x00, 0x00, 0x00); + if (hasAudio) { + df.write(0x80, 0x88, 0xE5, 0x40, 0x10); + } else { + df.write(0x00, 0x00, 0x00, 0x00, 0x00); + } + df.write(0x00, 0x00, 0x00); + if (hasAudio) { + if (stereo) { + df.write(2); + } else { + df.write(1); + } + } else { + df.write(0); + } + df.write(0x00, 0x00, 0x00); + + df.writeUI32(width); + df.writeUI32(height); + df.writeDouble(videoFrameCount / fps); + + df.write( + 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x59, 0x40, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + ); + + } + if (tag.data instanceof VIDEODATA) { + VIDEODATA vd = (VIDEODATA) tag.data; + long startOffset = lastOffset; + df.writeUI32(startOffset); + df.writeUI32(vd.videoData.length); + lastOffset = startOffset + vd.videoData.length; + df.write(0x01, 0x00, 0x00, 0x00, 0x04 + (vd.frameType == 1 ? 1 : 0), 0x00, 0x00, 0x00); + if (videoDataIndex < videoFrameCount - 1) { + df.write(0x01); + } else { + df.write(0x00); + } + videoDataIndex++; + } + } + df.writeUI32(lastOffset); + for (FLVTAG tag : flvTags) { + if (tag.data instanceof VIDEODATA) { + VIDEODATA vd = (VIDEODATA) tag.data; + df.write(vd.videoData); + } + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (FLVTAG tag : flvTags) { + if (tag.data instanceof AUDIODATA) { + AUDIODATA ad = (AUDIODATA) tag.data; + baos.write(ad.soundData); + } + } + + df.writeUI32(1); + if (hasAudio) { + SoundFormat sf = new SoundFormat(soundFormat, samplingRate, stereo); + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + sf.decode(new SWFInputStream(new ByteArrayInputStream(baos.toByteArray())), baos2); + df.writeUI32(1); + df.write(0); + df.writeUI32(baos2.size()); + df.writeUI32(1); + df.writeUI32(0); + df.write(0); + df.writeUI32(baos2.size()); + df.write(baos2.toByteArray()); + } else { + df.writeUI32(0); + df.write(0); + } + df.write(0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFE, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00); + + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java index 2b11c164d..6b3954b29 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java @@ -161,6 +161,8 @@ import com.jpexs.helpers.utf8.Utf8Helper; import java.awt.Font; import java.awt.Point; import java.awt.geom.Point2D; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -2063,7 +2065,15 @@ public class XFLConverter { writer.writeAttribute("frameRight", image.getWidth()); writer.writeAttribute("frameBottom", image.getHeight()); writer.writeEndElement(); - datfiles.put(datFileName, new byte[0]); //empty byte arrays are not written + + ImageBinDataGenerator ibg = new ImageBinDataGenerator(); + ByteArrayOutputStream iba = new ByteArrayOutputStream(); + try { + ibg.generateBinData(new ByteArrayInputStream(imageBytes), iba, format); + } catch (IOException ex) { + Logger.getLogger(XFLConverter.class.getName()).log(Level.SEVERE, "Error during bin/dat file generation for image", ex); + } + datfiles.put(datFileName, iba.toByteArray()); statusStack.popStatus(); } else if (symbol instanceof DefineSoundTag) { statusStack.pushStatus(symbol.toString()); @@ -2134,22 +2144,9 @@ public class XFLConverter { writer.writeAttribute("linkageURL", characterImportLinkageURL.get(symbol)); } writer.writeEndElement(); - //Use the dat file, otherwise it does not work - datfiles.put(datFileName, new byte[]{ //Magic numbers, if anybody knows why, please tell me - (byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xA0, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x78, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x59, (byte) 0x40, (byte) 0x18, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFE, (byte) 0xFF, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 - }); + + MovieBinDataGenerator mbd = new MovieBinDataGenerator(); + datfiles.put(datFileName, mbd.generateEmptyBinData()); } else { files.put(symbolFile, data); writer.writeStartElement("DOMVideoItem", new String[]{ @@ -2184,6 +2181,19 @@ public class XFLConverter { writer.writeAttribute("linkageExportForAS", true); } + + long ts = getTimestamp(swf); + String datFileName = "M " + (datfiles.size() + 1) + " " + ts + ".dat"; + writer.writeAttribute("videoDataHRef", datFileName); + MovieBinDataGenerator mbg = new MovieBinDataGenerator(); + ByteArrayOutputStream bba = new ByteArrayOutputStream(); + try { + mbg.generateBinData(new ByteArrayInputStream(data), bba, swf.frameRate); + } catch (IOException ex) { + Logger.getLogger(XFLConverter.class.getName()).log(Level.SEVERE, "Error during bin/dat file generation for movie", ex); + } + datfiles.put(datFileName, bba.toByteArray()); + writer.writeEndElement(); } statusStack.popStatus();