// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.gui.converter;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.IndexColorModel;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.infinity.gui.ViewerUtil;
import org.infinity.resource.graphics.DxtEncoder;
import org.infinity.resource.graphics.PseudoBamDecoder;
import org.infinity.resource.graphics.PseudoBamDecoder.PseudoBamFrameEntry;
import org.infinity.util.Misc;
import org.infinity.util.io.FileManager;
/**
* Output filter: split BAM and output each part into a separate file.
*/
public class BamFilterOutputSplitted extends BamFilterBaseOutput
implements ActionListener, ChangeListener
{
private static final String FilterName = "Splitted BAM output";
private static final String FilterDesc = "This filter allows you to split the BAM into multiple " +
"parts and output each one into a separate BAM file.\n" +
"Note: Output filters will always be processed last.";
private static final int MaxSplits = 7; // max. supported number of splits
private JSpinner spinnerSplitX, spinnerSplitY, spinnerSuffixStart, spinnerSuffixStep;
private JCheckBox cbSplitAuto;
private JComboBox<String> cbSuffixDigits;
public static String getFilterName() { return FilterName; }
public static String getFilterDesc() { return FilterDesc; }
public BamFilterOutputSplitted(ConvertToBam parent)
{
super(parent, FilterName, FilterDesc);
}
@Override
public boolean process(PseudoBamDecoder decoder) throws Exception
{
return applyEffect(decoder);
}
@Override
public PseudoBamFrameEntry updatePreview(PseudoBamFrameEntry entry)
{
// does not modify the source image
return entry;
}
@Override
public String getConfiguration()
{
StringBuilder sb = new StringBuilder();
sb.append(spinnerSplitX.getValue()).append(';');
sb.append(spinnerSplitY.getValue()).append(';');
sb.append(cbSplitAuto.isSelected()).append(';');
sb.append(cbSuffixDigits.getSelectedIndex()).append(';');
sb.append(spinnerSuffixStart.getValue()).append(';');
sb.append(spinnerSuffixStep.getValue());
return sb.toString();
}
@Override
public boolean setConfiguration(String config)
{
if (config != null) {
config = config.trim();
if (!config.isEmpty()) {
String[] params = config.split(";");
Integer splitX = Integer.MIN_VALUE;
Integer splitY = Integer.MIN_VALUE;
boolean auto = true;
int digits = -1;
Integer start = Integer.MIN_VALUE;
Integer step = Integer.MIN_VALUE;
if (params.length > 0) {
int min = ((Number)((SpinnerNumberModel)spinnerSplitX.getModel()).getMinimum()).intValue();
int max = ((Number)((SpinnerNumberModel)spinnerSplitX.getModel()).getMaximum()).intValue();
splitX = decodeNumber(params[0], min, max, Integer.MIN_VALUE);
if (splitX == Integer.MIN_VALUE) {
return false;
}
}
if (params.length > 1) {
int min = ((Number)((SpinnerNumberModel)spinnerSplitY.getModel()).getMinimum()).intValue();
int max = ((Number)((SpinnerNumberModel)spinnerSplitY.getModel()).getMaximum()).intValue();
splitY = decodeNumber(params[1], min, max, Integer.MIN_VALUE);
if (splitY == Integer.MIN_VALUE) {
return false;
}
}
if (params.length > 2) {
if (params[2].equalsIgnoreCase("true")) {
auto = true;
} else if (params[2].equalsIgnoreCase("false")) {
auto = false;
} else {
return false;
}
}
if (params.length > 3) {
digits = Misc.toNumber(params[3], -1);
if (digits < 0 || digits >= cbSuffixDigits.getModel().getSize()) {
return false;
}
}
if (params.length > 4) {
int min = ((Number)((SpinnerNumberModel)spinnerSuffixStart.getModel()).getMinimum()).intValue();
int max = ((Number)((SpinnerNumberModel)spinnerSuffixStart.getModel()).getMaximum()).intValue();
start = decodeNumber(params[4], min, max, Integer.MIN_VALUE);
if (start == Integer.MIN_VALUE) {
return false;
}
}
if (params.length > 5) {
int min = ((Number)((SpinnerNumberModel)spinnerSuffixStep.getModel()).getMinimum()).intValue();
int max = ((Number)((SpinnerNumberModel)spinnerSuffixStep.getModel()).getMaximum()).intValue();
step = decodeNumber(params[5], min, max, Integer.MIN_VALUE);
if (step == Integer.MIN_VALUE) {
return false;
}
}
if (splitX != Integer.MIN_VALUE) {
spinnerSplitX.setValue(splitX);
}
if (splitY != Integer.MIN_VALUE) {
spinnerSplitY.setValue(splitY);
}
cbSplitAuto.setSelected(auto);
if (digits >= 0) {
cbSuffixDigits.setSelectedIndex(digits);
}
if (start != Integer.MIN_VALUE) {
spinnerSuffixStart.setValue(start);
}
if (step != Integer.MIN_VALUE) {
spinnerSuffixStep.setValue(step);
}
}
return true;
}
return false;
}
@Override
protected JPanel loadControls()
{
GridBagConstraints c = new GridBagConstraints();
JLabel l1 = new JLabel("Split");
JLabel l2 = new JLabel("x horizontally and");
JLabel l3 = new JLabel("x vertically.");
JLabel l4 = new JLabel("Output filename suffix:");
JLabel l5 = new JLabel("Digits:");
JLabel l6 = new JLabel("Start at:");
JLabel l7 = new JLabel("Step by:");
spinnerSplitX = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1));
((SpinnerNumberModel)spinnerSplitX.getModel()).setMaximum(Integer.valueOf(MaxSplits));
spinnerSplitX.addChangeListener(this);
spinnerSplitY = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1));
((SpinnerNumberModel)spinnerSplitY.getModel()).setMaximum(Integer.valueOf(MaxSplits));
spinnerSplitY.addChangeListener(this);
cbSplitAuto = new JCheckBox("Split automatically", true);
cbSplitAuto.addActionListener(this);
String[] items = new String[7];
for (int i = 0; i < items.length; i++) {
items[i] = String.format("%1$d", i+1);
}
cbSuffixDigits = new JComboBox<>(items);
cbSuffixDigits.setSelectedIndex(1);
cbSuffixDigits.addActionListener(this);
spinnerSuffixStart = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
((SpinnerNumberModel)spinnerSuffixStart.getModel()).setMaximum(Integer.valueOf(100000));
spinnerSuffixStart.addChangeListener(this);
spinnerSuffixStep = new JSpinner(new SpinnerNumberModel(1, 1, 1000, 1));
((SpinnerNumberModel)spinnerSuffixStep.getModel()).setMaximum(Integer.valueOf(10000));
spinnerSuffixStep.addChangeListener(this);
JPanel p1 = new JPanel(new GridBagLayout());
ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
p1.add(l1, c);
ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0);
p1.add(spinnerSplitX, c);
ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
p1.add(l2, c);
ViewerUtil.setGBC(c, 3, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0);
p1.add(spinnerSplitY, c);
ViewerUtil.setGBC(c, 4, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
p1.add(l3, c);
ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 0, 0, 0), 0, 0);
p1.add(new JPanel(), c);
ViewerUtil.setGBC(c, 1, 1, 4, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 0, 0, 0), 0, 0);
p1.add(cbSplitAuto, c);
JPanel p2 = new JPanel(new GridBagLayout());
ViewerUtil.setGBC(c, 0, 0, 6, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
p2.add(l4, c);
ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 0, 0, 0), 0, 0);
p2.add(l5, c);
ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 4, 0, 0), 8, 0);
p2.add(cbSuffixDigits, c);
ViewerUtil.setGBC(c, 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 12, 0, 0), 0, 0);
p2.add(l6, c);
ViewerUtil.setGBC(c, 3, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 4, 0, 0), 0, 0);
p2.add(spinnerSuffixStart, c);
ViewerUtil.setGBC(c, 4, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 12, 0, 0), 0, 0);
p2.add(l7, c);
ViewerUtil.setGBC(c, 5, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 4, 0, 0), 0, 0);
p2.add(spinnerSuffixStep, c);
JPanel pMain = new JPanel(new GridBagLayout());
ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pMain.add(p1, c);
ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(8, 0, 0, 0), 0, 0);
pMain.add(p2, c);
JPanel panel = new JPanel(new GridBagLayout());
ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
panel.add(pMain, c);
updateAutoSplit();
return panel;
}
//--------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
if (event.getSource() == cbSplitAuto) {
updateAutoSplit();
fireChangeListener();
} else if (event.getSource() == cbSuffixDigits) {
fireChangeListener();
}
}
//--------------------- End Interface ActionListener ---------------------
//--------------------- Begin Interface ChangeListener ---------------------
@Override
public void stateChanged(ChangeEvent event)
{
if (event.getSource() == spinnerSplitX || event.getSource() == spinnerSplitY ||
event.getSource() == spinnerSuffixStart || event.getSource() == spinnerSuffixStep) {
fireChangeListener();
}
}
//--------------------- End Interface ChangeListener ---------------------
private void updateAutoSplit()
{
boolean b = cbSplitAuto.isSelected();
spinnerSplitX.setEnabled(!b);
spinnerSplitY.setEnabled(!b);
}
private boolean applyEffect(PseudoBamDecoder decoder) throws Exception
{
if (getConverter() != null && decoder != null) {
// finding largest dimension
Dimension maxDim = new Dimension(0, 0);
for (int i = 0; i < decoder.frameCount(); i++) {
int w = decoder.getFrameInfo(i).getWidth();
int h = decoder.getFrameInfo(i).getHeight();
if (w > maxDim.width) maxDim.width = w;
if (h > maxDim.height) maxDim.height = h;
}
// getting number of segments per dimension
int segmentsX, segmentsY;
if (cbSplitAuto.isSelected()) {
segmentsX = (maxDim.width < 256) ? 1 : 0;
segmentsY = (maxDim.height < 256) ? 1 : 0;
for (int i = 2; i <= MaxSplits; i++) {
if (segmentsX == 0 && maxDim.width <= 255*i) {
segmentsX = i;
}
if (segmentsY == 0 && maxDim.height <= 255*i) {
segmentsY = i;
}
if(segmentsX > 0 && segmentsY > 0) {
break;
}
}
} else {
segmentsX = ((Integer)spinnerSplitX.getValue()).intValue() + 1;
segmentsY = ((Integer)spinnerSplitY.getValue()).intValue() + 1;
}
// calculating individual splits for each frame
List<List<Rectangle>> listSegments = new ArrayList<List<Rectangle>>(decoder.frameCount());
int segmentCount = segmentsX*segmentsY;
for (int frameIdx = 0; frameIdx < decoder.frameCount(); frameIdx++) {
listSegments.add(new ArrayList<Rectangle>(segmentCount));
final double fract = 0.499999; // fractions of .5 or less will be rounded down!
int curHeight = decoder.getFrameInfo(frameIdx).getHeight(), y = 0;
for (int curSegY = segmentsY; curSegY > 0; curSegY--) {
double dh = (double)curHeight / (double)curSegY;
int h = (int)(dh + fract);
curHeight -= h;
int curWidth = decoder.getFrameInfo(frameIdx).getWidth(), x = 0;
for (int curSegX = segmentsX; curSegX > 0; curSegX--) {
double dw = (double)curWidth / (double)curSegX;
int w = (int)(dw + fract);
curWidth -= w;
// store current segment as Rectangle structure into list
listSegments.get(frameIdx).add(new Rectangle(x, y, w, h));
x += w;
}
y += h;
}
}
// creating a format string for BAM output filenames
String bamFileName = getConverter().getBamOutput().toString();
String ext = "BAM";
int idx = bamFileName.lastIndexOf('.');
if (idx >= 0) {
ext = bamFileName.substring(idx+1);
bamFileName = bamFileName.substring(0, idx);
}
String fmtBamFileName = String.format("%1$s%%1$0%2$dd.%3$s", bamFileName,
cbSuffixDigits.getSelectedIndex() + 1, ext);
// creating BamDecoder instances for each individual segment
PseudoBamDecoder segmentDecoder = new PseudoBamDecoder();
// adding global custom options
String[] options = decoder.getOptionNames();
for (int i = 0; i < options.length; i++) {
segmentDecoder.setOption(options[i], decoder.getOption(options[i]));
}
// for each segment...
for (int segIdx = 0; segIdx < segmentCount; segIdx++) {
// creating segmented frames list
List<PseudoBamFrameEntry> framesList = new ArrayList<PseudoBamFrameEntry>(decoder.getFramesList().size());
for (int i = 0; i < listSegments.size(); i++) {
framesList.add(createFrameSegment(decoder.getFramesList().get(i), listSegments.get(i).get(segIdx)));
}
segmentDecoder.setFramesList(framesList);
// attaching cycles list
segmentDecoder.setCyclesList(decoder.getCyclesList());
// converting segmented BAM structure
if (!convertBam(FileManager.resolve(String.format(fmtBamFileName, segIdx)), segmentDecoder)) {
throw new Exception(String.format("Error converting segment %1$d/%2$d", segIdx + 1, segmentCount));
}
// resetting decoder
segmentDecoder.setCyclesList(null);
segmentDecoder.setFramesList(null);
}
return true;
}
return false;
}
// Creates a new FrameEntry based on the specified original entry and the segment rectangle
private PseudoBamFrameEntry createFrameSegment(PseudoBamFrameEntry entry, Rectangle rect)
{
PseudoBamFrameEntry retVal = null;
if (entry != null && rect != null) {
// preparations
BufferedImage srcImage = entry.getFrame();
BufferedImage dstImage = null;
byte[] srcB = null, dstB = null;
int[] srcI = null, dstI = null;
Dimension dstDim = new Dimension(rect.width, rect.height);
if (dstDim.width == 0 || dstDim.height == 0) {
dstDim.width = dstDim.height = 1;
}
if (srcImage.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
srcB = ((DataBufferByte)srcImage.getRaster().getDataBuffer()).getData();
IndexColorModel cm1 = (IndexColorModel)srcImage.getColorModel();
int[] colors = new int[1 << cm1.getPixelSize()];
cm1.getRGBs(colors);
IndexColorModel cm2 = new IndexColorModel(cm1.getPixelSize(), colors.length, colors, 0,
cm1.hasAlpha(), cm1.getTransparentPixel(),
DataBuffer.TYPE_BYTE);
dstImage = new BufferedImage(dstDim.width, dstDim.height, BufferedImage.TYPE_BYTE_INDEXED, cm2);
dstB = ((DataBufferByte)dstImage.getRaster().getDataBuffer()).getData();
} else {
srcI = ((DataBufferInt)srcImage.getRaster().getDataBuffer()).getData();
dstImage = new BufferedImage(dstDim.width, dstDim.height, srcImage.getType());
dstI = ((DataBufferInt)dstImage.getRaster().getDataBuffer()).getData();
}
// copying segment
if (rect.width > 0 && rect.height > 0) {
int srcOfs = rect.y*srcImage.getWidth() + rect.x;
int dstOfs = 0;
for (int y = 0; y < rect.height; y++, srcOfs += srcImage.getWidth(), dstOfs += dstImage.getWidth()) {
if (srcB != null) {
System.arraycopy(srcB, srcOfs, dstB, dstOfs, rect.width);
}
if (srcI != null) {
System.arraycopy(srcI, srcOfs, dstI, dstOfs, rect.width);
}
}
} else {
dstB[0] = 0;
}
// creating new FrameEntry structure
retVal = new PseudoBamFrameEntry(dstImage, entry.getCenterX() - rect.x, entry.getCenterY() - rect.y);
}
return retVal;
}
// Exports the BAM specified by "decoder" into the filename "outFileName" using global settings
private boolean convertBam(Path outFileName, PseudoBamDecoder decoder) throws Exception
{
if (getConverter() != null && outFileName != null && decoder != null) {
if (getConverter().isBamV1Selected()) {
// convert to BAM v1
decoder.setOption(PseudoBamDecoder.OPTION_INT_RLEINDEX,
Integer.valueOf(getConverter().getPaletteDialog().getRleIndex()));
decoder.setOption(PseudoBamDecoder.OPTION_BOOL_COMPRESSED,
Boolean.valueOf(getConverter().isBamV1Compressed()));
try {
return decoder.exportBamV1(outFileName, getConverter().getProgressMonitor(),
getConverter().getProgressMonitorStage());
} catch (Exception e) {
e.printStackTrace();
throw e;
}
} else {
// convert to BAM v2
DxtEncoder.DxtType dxtType = getConverter().getDxtType();
int pvrzIndex = getConverter().getPvrzIndex();
try {
return decoder.exportBamV2(outFileName, dxtType, pvrzIndex, getConverter().getProgressMonitor(),
getConverter().getProgressMonitorStage());
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
return false;
}
}