Added: FLA export - generating bin/*.dat files for movies and images

This commit is contained in:
Jindra Petřík
2024-09-07 19:51:41 +02:00
parent df9f2e3a10
commit 77f96d5ecc
5 changed files with 416 additions and 17 deletions

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Boolean> audioPresent = new Reference<>(false);
Reference<Boolean> videoPresent = new Reference<>(false);
flvIs.readHeader(audioPresent, videoPresent);
List<FLVTAG> 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<Object> 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);
}
}

View File

@@ -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();