// 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.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.ProgressMonitor;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingWorker;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import org.infinity.gui.ChildFrame;
import org.infinity.gui.ViewerUtil;
import org.infinity.gui.WindowBlocker;
import org.infinity.icon.Icons;
import org.infinity.resource.Profile;
import org.infinity.resource.graphics.ColorConvert;
import org.infinity.resource.graphics.Compressor;
import org.infinity.resource.graphics.DxtEncoder;
import org.infinity.util.BinPack2D;
import org.infinity.util.DynamicArray;
import org.infinity.util.IntegerHashMap;
import org.infinity.util.io.FileManager;
import org.infinity.util.io.StreamUtils;
public class ConvertToMos extends ChildFrame
implements ActionListener, PropertyChangeListener, ChangeListener, FocusListener
{
private static String currentDir = Profile.getGameRoot().toString();
private JTabbedPane tabPane;
private JTextField tfInputV1, tfOutputV1, tfInputV2, tfOutputV2;
private JButton bInputV1, bOutputV1, bInputV2, bOutputV2, bCompressionHelp;
private JButton bConvert, bCancel;
private JSpinner sPvrzIndex;
private JLabel lPvrzInfo;
private JComboBox<String> cbCompression;
private JCheckBox cbCompress, cbCloseOnExit;
private SwingWorker<List<String>, Void> workerConvert;
private WindowBlocker blocker;
/**
* Converts an image into a MOS V1 resource.
* @param parent This parameter is needed for the progress monitor only.
* @param img The source image to convert into a MOS resource.
* @param mosFileName The name of the resulting MOS file.
* @param compressed If {@code true}, converts into a compressed BAMC file.
* @param result Returns more specific information about the conversion process. Data placed in the
* first item indicates success, data in the second item indicates failure.
* @param showProgress Specify whether to show a progress monitor (needs a valid 'parent' parameter).
* @return {@code true} if the conversion finished successfully, {@code false} otherwise.
*/
public static boolean convertV1(Component parent, BufferedImage img, String mosFileName,
boolean compressed, List<String> result, boolean showProgress)
{
// checking parameters
if (result == null) {
return false;
}
if (img == null) {
result.add(null);
result.add("No source image specified.");
return false;
}
if (mosFileName == null || mosFileName.isEmpty()) {
result.add(null);
result.add("No output filename specified.");
return false;
}
// preparing MOS V1 header
int width = img.getWidth();
int height = img.getHeight();
int cols = (width + 63) / 64;
int rows = (height + 63) / 64;
int tileCount = cols * rows;
int palOfs = 24;
int tableOfs = palOfs + tileCount*1024;
int dataOfs = tableOfs + tileCount*4;
byte[] dst = new byte[dataOfs + width*height];
System.arraycopy("MOS V1 ".getBytes(), 0, dst, 0, 8);
DynamicArray.putShort(dst, 8, (short)width);
DynamicArray.putShort(dst, 10, (short)height);
DynamicArray.putShort(dst, 12, (short)cols);
DynamicArray.putShort(dst, 14, (short)rows);
DynamicArray.putInt(dst, 16, 64);
DynamicArray.putInt(dst, 20, palOfs);
ProgressMonitor progress = null;
try {
String note = "Converting tile %1$d / %2$d";
int progressIndex = 0, progressMax = tileCount;
if (showProgress) {
progress = new ProgressMonitor(parent, "Converting MOS...",
String.format(note, progressIndex, progressMax), 0, progressMax);
progress.setMillisToDecideToPopup(250);
progress.setMillisToPopup(500);
}
// creating list of tiles as int[] arrays
List<int[]> tileList = new ArrayList<int[]>(cols*rows);
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
int tileX = x * 64;
int tileY = y * 64;
int tileW = (tileX + 64 < width) ? 64 : (width - tileX);
int tileH = (tileY + 64 < height) ? 64 : (height - tileY);
int[] rgbArray = new int[tileW*tileH];
img.getRGB(tileX, tileY, tileW, tileH, rgbArray, 0, tileW);
tileList.add(rgbArray);
}
}
// applying color reduction to each tile
int[] palette = new int[255];
int[] hclPalette = new int[255];
byte[] tilePalette = new byte[1024];
byte[] tileData = new byte[64*64];
int curPalOfs = palOfs, curTableOfs = tableOfs, curDataOfs = dataOfs;
IntegerHashMap<Byte> colorCache = new IntegerHashMap<Byte>(1536); // caching RGBColor -> index
for (int tileIdx = 0; tileIdx < tileList.size(); tileIdx++) {
colorCache.clear();
if (showProgress) {
if (progress.isCanceled()) {
dst = null;
result.add(null);
result.add("Conversion has been cancelled.");
return false;
}
progressIndex++;
if ((progressIndex % 10) == 0) {
progress.setProgress(progressIndex);
progress.setNote(String.format(note, progressIndex, progressMax));
}
}
int[] pixels = tileList.get(tileIdx);
if (ColorConvert.medianCut(pixels, 255, palette, false)) {
ColorConvert.toHclPalette(palette, hclPalette);
// filling palette
// first palette entry denotes transparency
tilePalette[0] = tilePalette[2] = tilePalette[3] = 0; tilePalette[1] = (byte)255;
for (int i = 1; i < 256; i++) {
tilePalette[(i << 2) + 0] = (byte)(palette[i - 1] & 0xff);
tilePalette[(i << 2) + 1] = (byte)((palette[i - 1] >>> 8) & 0xff);
tilePalette[(i << 2) + 2] = (byte)((palette[i - 1] >>> 16) & 0xff);
tilePalette[(i << 2) + 3] = 0;
colorCache.put(palette[i - 1], (byte)(i - 1));
}
// filling pixel data
for (int i = 0; i < pixels.length; i++) {
if ((pixels[i] & 0xff000000) == 0) {
tileData[i] = 0;
} else {
Byte palIndex = colorCache.get(pixels[i]);
if (palIndex != null) {
tileData[i] = (byte)(palIndex + 1);
} else {
byte color = (byte)ColorConvert.nearestColor(pixels[i], hclPalette);
tileData[i] = (byte)(color + 1);
colorCache.put(pixels[i], color);
}
}
}
} else {
// error handling
dst = null;
result.add(null);
result.add(String.format("Error processing tile #%1$d. Conversion cancelled.", tileIdx));
return false;
}
System.arraycopy(tilePalette, 0, dst, curPalOfs, 1024);
curPalOfs += 1024;
DynamicArray.putInt(dst, curTableOfs, curDataOfs - dataOfs);
curTableOfs += 4;
System.arraycopy(tileData, 0, dst, curDataOfs, pixels.length);
curDataOfs += pixels.length;
}
tileList.clear(); tileList = null;
tileData = null; tilePalette = null; hclPalette = null; palette = null;
// optionally compressing to MOSC V1
if (compressed) {
dst = Compressor.compress(dst, "MOSC", "V1 ");
}
// writing MOS file to disk
Path mosFile = FileManager.resolve(mosFileName);
try (OutputStream os = StreamUtils.getOutputStream(mosFile, true)) {
os.write(dst);
} catch (Exception e) {
e.printStackTrace();
result.add(null);
result.add("Error writing TIS file to disk.");
return false;
}
} finally {
// some cleaning up
if (showProgress) {
progress.close();
progress = null;
}
}
// generating conversion summary
result.add("Conversion finished successfully.");
return true;
}
/**
* Converts an image into a MOS V2 resource.
* @param parent This parameter is needed for the progress monitor only.
* @param img The source image to convert into a MOS resource.
* @param mosFileName The name of the resulting MOS file.
* @param dxtType The desired compression type.
* @param pvrzIndex The starting index for PVRZ files.
* @param result Returns more specific information about the conversion process. Data placed in the
* first item indicates success, data in the second item indicates failure.
* @param showProgress Specify whether to show a progress monitor (needs a valid 'parent' parameter).
* @return {@code true} if the conversion finished successfully, {@code false} otherwise.
*/
public static boolean convertV2(Component parent, BufferedImage img, String mosFileName,
DxtEncoder.DxtType dxtType, int pvrzIndex,
List<String> result, boolean showProgress)
{
// checking parameters
if (result == null) {
return false;
}
if (img == null) {
result.add(null);
result.add("No source image specified.");
return false;
}
if (mosFileName == null || mosFileName.isEmpty()) {
result.add(null);
result.add("No output filename specified.");
return false;
}
if (pvrzIndex < 0 || pvrzIndex > 99999) {
result.add(null);
result.add("PVRZ index is out of range [0..99999].");
return false;
}
// preparing variables
ProgressMonitor progress = null;
int width = img.getWidth();
int height = img.getHeight();
List<BinPack2D> pageList = new ArrayList<BinPack2D>();
List<MosEntry> entryList = new ArrayList<MosEntry>();
try {
if (showProgress) {
// preparing progress meter
progress = new ProgressMonitor(parent, "Converting MOS...", "Preparing data", 0, 5);
progress.setMillisToDecideToPopup(0);
progress.setMillisToPopup(0);
progress.setProgress(0);
}
// processing tiles
final int pageDim = 1024;
final BinPack2D.HeuristicRules binPackRule = BinPack2D.HeuristicRules.BOTTOM_LEFT_RULE;
int x = 0, y = 0, pOfs = 0;
while (pOfs < width*height) {
int w = Math.min(pageDim, width - x);
int h = Math.min(pageDim, height - y);
Dimension space = new Dimension((w+3) & ~3, (h+3) & ~3);
int pageIdx = -1;
Rectangle rectMatch = null;
for (int i = 0; i < pageList.size(); i++) {
BinPack2D packer = pageList.get(i);
rectMatch = packer.insert(space.width, space.height, binPackRule);
if (rectMatch.height > 0) {
pageIdx = i;
break;
}
}
// create new page?
if (pageIdx < 0) {
BinPack2D packer = new BinPack2D(pageDim, pageDim);
pageList.add(packer);
pageIdx = pageList.size() - 1;
rectMatch = packer.insert(space.width, space.height, binPackRule);
}
// register page entry
MosEntry entry = new MosEntry(pvrzIndex + pageIdx,
new Point(rectMatch.x, rectMatch.y),
w, h, new Point(x, y));
entryList.add(entry);
// advance scanning
if (x + pageDim >= width) {
x = 0;
y += pageDim;
} else {
x += pageDim;
}
pOfs = y*width + x;
}
// check PVRZ index again
if (pvrzIndex + pageList.size() > 100000) {
result.add(null);
result.add(String.format("One or more PVRZ indices exceed the max. possible value of 99999.\n" +
"Please choose a start index smaller than or equal to %1$d.",
100000 - pageList.size()));
return false;
}
byte[] dst = new byte[24 + entryList.size()*28]; // header + tiles
int dstOfs = 0;
// writing MOS header and data
System.arraycopy("MOS V2 ".getBytes(), 0, dst, 0, 8);
DynamicArray.putInt(dst, 8, width);
DynamicArray.putInt(dst, 12, height);
DynamicArray.putInt(dst, 16, entryList.size());
DynamicArray.putInt(dst, 20, 24);
dstOfs += 24;
for (int i = 0; i < entryList.size(); i++, dstOfs += 28) {
MosEntry entry = entryList.get(i);
DynamicArray.putInt(dst, dstOfs, entry.page);
DynamicArray.putInt(dst, dstOfs + 4, entry.srcLocation.x);
DynamicArray.putInt(dst, dstOfs + 8, entry.srcLocation.y);
DynamicArray.putInt(dst, dstOfs + 12, entry.width);
DynamicArray.putInt(dst, dstOfs + 16, entry.height);
DynamicArray.putInt(dst, dstOfs + 20, entry.dstLocation.x);
DynamicArray.putInt(dst, dstOfs + 24, entry.dstLocation.y);
}
// writing MOS file to disk
Path mosFile = FileManager.resolve(mosFileName);
try (OutputStream os = StreamUtils.getOutputStream(mosFile, true)) {
os.write(dst);
} catch (Exception e) {
e.printStackTrace();
result.add(null);
result.add("Error writing MOS file to disk.");
return false;
}
dst = null;
// generating PVRZ files
if (!createPvrzPages(mosFile.getParent(), img, dxtType, pageList, entryList,
result, progress)) {
return false;
}
} finally {
// some cleaning up
img.flush();
if (progress != null) {
progress.close();
progress = null;
}
}
// generating conversion summary
result.add("Conversion finished successfully.");
return true;
}
// Returns a list of supported graphics file formats
private static FileNameExtensionFilter[] getInputFilters()
{
FileNameExtensionFilter[] filters = new FileNameExtensionFilter[] {
new FileNameExtensionFilter("Graphics files (*.bmp, *.png, *,jpg, *.jpeg)",
"bam", "bmp", "png", "jpg", "jpeg"),
new FileNameExtensionFilter("BMP files (*.bmp)", "bmp"),
new FileNameExtensionFilter("PNG files (*.png)", "png"),
new FileNameExtensionFilter("JPEG files (*.jpg, *.jpeg)", "jpg", "jpeg")
};
return filters;
}
// generates PVRZ textures
private static boolean createPvrzPages(Path path, BufferedImage img, DxtEncoder.DxtType dxtType,
List<BinPack2D> gridList, List<MosEntry> entryList,
List<String> result, ProgressMonitor progress)
{
// preparing variables
if (path == null) {
path = FileManager.resolve("").toAbsolutePath();
}
int dxtCode = (dxtType == DxtEncoder.DxtType.DXT5) ? 11 : 7;
byte[] output = new byte[DxtEncoder.calcImageSize(1024, 1024, dxtType)];
int pageMin = Integer.MAX_VALUE;
int pageMax = -1;
for (final MosEntry e: entryList) {
pageMin = Math.min(pageMin, e.page);
pageMax = Math.max(pageMax, e.page);
}
String note = "Generating PVRZ file %1$s / %2$s";
int curProgress = 1;
if (progress != null) {
progress.setMinimum(0);
progress.setMaximum(pageMax - pageMin + 2);
progress.setProgress(curProgress);
}
// processing each PVRZ page
for (int i = pageMin; i <= pageMax; i++) {
if (progress != null) {
if (progress.isCanceled()) {
result.add(null);
result.add("Conversion has been cancelled.");
return false;
}
progress.setProgress(curProgress);
progress.setNote(String.format(note, curProgress, pageMax - pageMin + 1));
curProgress++;
}
Path pvrzFile = path.resolve(String.format("MOS%04d.PVRZ", i));
BinPack2D packer = gridList.get(i - pageMin);
packer.shrinkBin(true);
// generating texture image
int tw = packer.getBinWidth();
int th = packer.getBinHeight();
BufferedImage texture = ColorConvert.createCompatibleImage(tw, th, true);
Graphics2D g = texture.createGraphics();
g.setBackground(new Color(0, true));
g.setColor(Color.BLACK);
g.fillRect(0, 0, texture.getWidth(), texture.getHeight());
for (final MosEntry entry: entryList) {
if (entry.page == i) {
int sx = entry.dstLocation.x, sy = entry.dstLocation.y;
int dx = entry.srcLocation.x, dy = entry.srcLocation.y;
int w = entry.width, h = entry.height;
g.clearRect(dx, dy, w, h);
g.drawImage(img, dx, dy, dx+w, dy+h, sx, sy, sx+w, sy+h, null);
}
}
g.dispose();
// compressing PVRZ
int[] textureData = ((DataBufferInt)texture.getRaster().getDataBuffer()).getData();
try {
int outSize = DxtEncoder.calcImageSize(texture.getWidth(), texture.getHeight(), dxtType);
DxtEncoder.encodeImage(textureData, texture.getWidth(), texture.getHeight(), output, dxtType);
byte[] header = ConvertToPvrz.createPVRHeader(texture.getWidth(), texture.getHeight(), dxtCode);
byte[] pvrz = new byte[header.length + outSize];
System.arraycopy(header, 0, pvrz, 0, header.length);
System.arraycopy(output, 0, pvrz, header.length, outSize);
header = null;
pvrz = Compressor.compress(pvrz, 0, pvrz.length, true);
// writing PVRZ to disk
try (OutputStream os = StreamUtils.getOutputStream(pvrzFile, true)) {
os.write(pvrz);
} catch (Exception e) {
e.printStackTrace();
result.add(null);
result.add(String.format("Error writing PVRZ file \"%s\" to disk.", pvrzFile));
return false;
}
pvrz = null;
} catch (Exception e) {
e.printStackTrace();
result.add(null);
result.add(String.format("Error while generating PVRZ files:\n%s", e.getMessage()));
return false;
}
}
output = null;
return true;
}
public ConvertToMos()
{
super("Convert to MOS", true);
init();
}
//--------------------- Begin Class ChildFrame ---------------------
@Override
protected boolean windowClosing(boolean forced) throws Exception
{
clear();
return super.windowClosing(forced);
}
//--------------------- End Class ChildFrame ---------------------
//--------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
if (event.getSource() == bConvert) {
if (workerConvert == null) {
final String msg = "MOS output file already exists. Overwrite?";
Path file = null;
do {
if (tabPane.getSelectedIndex() == 0 && !tfOutputV1.getText().isEmpty()) {
file = FileManager.resolve(tfOutputV1.getText());
} else if (tabPane.getSelectedIndex() == 1 & !tfOutputV2.getText().isEmpty()) {
file = FileManager.resolve(tfOutputV2.getText());
}
if (file != null) {
if (!Files.exists(file) ||
JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(this, msg, "Question",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE)) {
file = null;
workerConvert = new SwingWorker<List<String>, Void>() {
@Override
public List<String> doInBackground()
{
return convert();
}
};
workerConvert.addPropertyChangeListener(this);
blocker = new WindowBlocker(this);
blocker.setBlocked(true);
workerConvert.execute();
}
file = null;
}
} while (file != null);
}
} else if (event.getSource() == bCancel) {
hideWindow();
} else if (event.getSource() == bInputV1 || event.getSource() == bInputV2) {
String fileName = tfInputV1.getText().isEmpty() ? currentDir : tfInputV1.getText();
Path file = FileManager.resolve(fileName).toAbsolutePath();
if ((fileName = getImageFileName(file)) != null) {
file = FileManager.resolve(fileName).toAbsolutePath();
currentDir = file.getParent().toString();
tfInputV1.setText(fileName);
tfInputV2.setText(fileName);
if (tfOutputV1.getText().isEmpty()) {
fileName = StreamUtils.replaceFileExtension(fileName, "MOS");
tfOutputV1.setText(fileName);
tfOutputV2.setText(fileName);
}
bConvert.setEnabled(isReady());
}
} else if (event.getSource() == bOutputV1 || event.getSource() == bOutputV2) {
String fileName = tfOutputV1.getText().isEmpty() ? currentDir : tfOutputV1.getText();
Path file = FileManager.resolve(fileName).toAbsolutePath();
if ((fileName = getMosFileName(file)) != null) {
file = FileManager.resolve(fileName).toAbsolutePath();
currentDir = file.getParent().toString();
tfOutputV1.setText(fileName);
tfOutputV2.setText(fileName);
}
bConvert.setEnabled(isReady());
} else if (event.getSource() == bCompressionHelp) {
final String helpMsg =
"\"DXT1\" provides the highest compression ratio. It supports only 1 bit alpha\n" +
"(i.e. either no or full transparency) and is the preferred type for TIS or MOS resources.\n\n" +
"\"DXT5\" provides an average compression ratio. It features interpolated\n" +
"alpha transitions and is the preferred type for BAM resources.\n\n" +
"\"Auto\" selects the most appropriate compression type based on the input data.";
JOptionPane.showMessageDialog(this, helpMsg, "About Compression Types", JOptionPane.INFORMATION_MESSAGE);
}
}
//--------------------- End Interface ActionListener ---------------------
//--------------------- Begin Interface PropertyChangeListener ---------------------
@Override
public void propertyChange(PropertyChangeEvent event)
{
if (event.getSource() == workerConvert) {
if ("state".equals(event.getPropertyName()) &&
SwingWorker.StateValue.DONE == event.getNewValue()) {
if (blocker != null) {
blocker.setBlocked(false);
blocker = null;
}
List<String> sl = null;
try {
sl = workerConvert.get();
} catch (Exception e) {
e.printStackTrace();
}
workerConvert = null;
boolean isError = false;
String s = null;
if (sl != null && !sl.isEmpty()) {
if (sl.get(0) != null) {
s = sl.get(0);
} else if (sl.size() > 1 && sl.get(1) != null) {
s = sl.get(1);
isError = true;
}
}
if (s != null) {
if (isError) {
JOptionPane.showMessageDialog(this, s, "Error", JOptionPane.ERROR_MESSAGE);
} else {
JOptionPane.showMessageDialog(this, s, "Information", JOptionPane.INFORMATION_MESSAGE);
if (cbCloseOnExit.isSelected()) {
hideWindow();
} else {
clear();
}
}
} else {
JOptionPane.showMessageDialog(this, "Unknown error!", "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
}
//--------------------- End Interface PropertyChangeListener ---------------------
//--------------------- Begin Interface ChangeListener ---------------------
@Override
public void stateChanged(ChangeEvent event)
{
if (event.getSource() == sPvrzIndex) {
lPvrzInfo.setText(pvrzInfoString(sPvrzIndex.getValue()));
}
}
//--------------------- End Interface ChangeListener ---------------------
//--------------------- Begin Interface FocusListener ---------------------
@Override
public void focusGained(FocusEvent event)
{
// nothing to do
}
@Override
public void focusLost(FocusEvent event)
{
if (event.getSource() == tfInputV1) {
tfInputV2.setText(tfInputV1.getText());
bConvert.setEnabled(isReady());
} else if (event.getSource() == tfInputV2) {
tfInputV1.setText(tfInputV2.getText());
bConvert.setEnabled(isReady());
} else if (event.getSource() == tfOutputV1) {
tfOutputV2.setText(tfOutputV1.getText());
bConvert.setEnabled(isReady());
} else if (event.getSource() == tfOutputV2) {
tfOutputV1.setText(tfOutputV2.getText());
bConvert.setEnabled(isReady());
}
}
//--------------------- End Interface FocusListener ---------------------
private void init()
{
setIconImage(Icons.getImage(Icons.ICON_APPLICATION_16));
GridBagConstraints c = new GridBagConstraints();
// setting up input/output section (Legacy V1)
JPanel pFilesV1 = new JPanel(new GridBagLayout());
pFilesV1.setBorder(BorderFactory.createTitledBorder("Input & Output "));
JLabel lInputV1 = new JLabel("Input file:");
JLabel lOutputV1 = new JLabel("Output file:");
tfInputV1 = new JTextField();
tfInputV1.addFocusListener(this);
tfOutputV1 = new JTextField();
tfOutputV1.addFocusListener(this);
bInputV1 = new JButton("...");
bInputV1.addActionListener(this);
bOutputV1 = new JButton("...");
bOutputV1.addActionListener(this);
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0);
pFilesV1.add(lInputV1, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 0, 0), 0, 0);
pFilesV1.add(tfInputV1, c);
c = ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 4), 0, 0);
pFilesV1.add(bInputV1, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 4, 0), 0, 0);
pFilesV1.add(lOutputV1, c);
c = ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 4, 0), 0, 0);
pFilesV1.add(tfOutputV1, c);
c = ViewerUtil.setGBC(c, 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 4, 4), 0, 0);
pFilesV1.add(bOutputV1, c);
// setting up input/output section (PVRZ-based V2)
JPanel pFilesV2 = new JPanel(new GridBagLayout());
pFilesV2.setBorder(BorderFactory.createTitledBorder("Input & Output "));
JLabel lInputV2 = new JLabel("Input file:");
JLabel lOutputV2 = new JLabel("Output file:");
tfInputV2 = new JTextField();
tfInputV2.addFocusListener(this);
tfOutputV2 = new JTextField();
tfOutputV2.addFocusListener(this);
bInputV2 = new JButton("...");
bInputV2.addActionListener(this);
bOutputV2 = new JButton("...");
bOutputV2.addActionListener(this);
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
pFilesV2.add(lInputV2, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 0, 0), 0, 0);
pFilesV2.add(tfInputV2, c);
c = ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 4), 0, 0);
pFilesV2.add(bInputV2, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 4, 0), 0, 0);
pFilesV2.add(lOutputV2, c);
c = ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 4, 0), 0, 0);
pFilesV2.add(tfOutputV2, c);
c = ViewerUtil.setGBC(c, 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 4, 4), 0, 0);
pFilesV2.add(bOutputV2, c);
// setting up options section (legacy V1)
JPanel pOptionsV1 = new JPanel(new GridBagLayout());
pOptionsV1.setBorder(BorderFactory.createTitledBorder("Options "));
cbCompress = new JCheckBox("Compressed (MOSC)", false);
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 0, 0, 0), 0, 0);
pOptionsV1.add(cbCompress, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 0, 0, 0), 0, 0);
pOptionsV1.add(new JPanel(), c);
c = ViewerUtil.setGBC(c, 0, 1, 2, 1, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0);
pOptionsV1.add(new JPanel(), c);
// setting up options section (PVRZ-based V2)
JPanel pOptionsV2 = new JPanel(new GridBagLayout());
pOptionsV2.setBorder(BorderFactory.createTitledBorder("Options "));
JLabel lPvrzIndex = new JLabel("PVRZ index starts at:");
JLabel lCompression = new JLabel("Compression type:");
sPvrzIndex = new JSpinner(new SpinnerNumberModel(0, 0, 99999, 1));
sPvrzIndex.setToolTipText("Enter a number from 0 to 99999");
sPvrzIndex.addChangeListener(this);
cbCompression = new JComboBox<>(new String[]{"Auto", "DXT1", "DXT5"});
cbCompression.setSelectedIndex(0);
bCompressionHelp = new JButton("?");
bCompressionHelp.setToolTipText("About compression types");
bCompressionHelp.addActionListener(this);
bCompressionHelp.setMargin(new Insets(bCompressionHelp.getInsets().top, 4,
bCompressionHelp.getInsets().bottom, 4));
lPvrzInfo = new JLabel(pvrzInfoString(sPvrzIndex.getValue()));
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
pOptionsV2.add(lPvrzIndex, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 8, 0, 0), 0, 0);
pOptionsV2.add(sPvrzIndex, c);
c = ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 16, 0, 0), 0, 0);
pOptionsV2.add(lCompression, c);
c = ViewerUtil.setGBC(c, 3, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 8, 0, 0), 0, 0);
pOptionsV2.add(cbCompression, c);
c = ViewerUtil.setGBC(c, 4, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 4), 0, 0);
pOptionsV2.add(bCompressionHelp, c);
c = ViewerUtil.setGBC(c, 0, 1, 5, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(4, 4, 4, 4), 0, 0);
pOptionsV2.add(lPvrzInfo, c);
// setting up tabbed pane
tabPane = new JTabbedPane(JTabbedPane.TOP);
JPanel pTabV1 = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 4, 2, 4), 0, 0);
pTabV1.add(pFilesV1, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.BOTH, new Insets(2, 4, 4, 4), 0, 0);
pTabV1.add(pOptionsV1, c);
tabPane.addTab("Legacy (V1)", pTabV1);
tabPane.setMnemonicAt(0, KeyEvent.VK_1);
JPanel pTabV2 = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 4, 2, 4), 0, 0);
pTabV2.add(pFilesV2, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(2, 4, 4, 4), 0, 0);
pTabV2.add(pOptionsV2, c);
tabPane.addTab("PVRZ-based (V2)", pTabV2);
tabPane.setMnemonicAt(1, KeyEvent.VK_2);
tabPane.setSelectedIndex(0);
// setting up bottom button bar
cbCloseOnExit = new JCheckBox("Close dialog after conversion", true);
bConvert = new JButton("Start Conversion");
bConvert.addActionListener(this);
bConvert.setEnabled(isReady());
Insets i = bConvert.getInsets();
bConvert.setMargin(new Insets(i.top + 2, i.left, i.bottom + 2, i.right));
bCancel = new JButton("Cancel");
bCancel.addActionListener(this);
i = bCancel.getInsets();
bCancel.setMargin(new Insets(i.top + 2, i.left, i.bottom + 2, i.right));
JPanel pButtons = new JPanel(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pButtons.add(cbCloseOnExit, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pButtons.add(new JPanel(), c);
c = ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_END,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pButtons.add(bConvert, c);
c = ViewerUtil.setGBC(c, 3, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_END,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
pButtons.add(bCancel, c);
// putting all together
setLayout(new GridBagLayout());
c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 0, 8), 0, 0);
add(tabPane, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(8, 8, 0, 8), 0, 0);
add(pButtons, c);
c = ViewerUtil.setGBC(c, 0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0);
add(new JPanel(), c);
// finalizing dialog initialization
pack();
setMinimumSize(getPreferredSize());
setLocationRelativeTo(getParent());
setVisible(true);
}
private void hideWindow()
{
clear();
setVisible(false);
}
// resetting dialog state
private void clear()
{
tfInputV1.setText("");
tfInputV2.setText("");
tfOutputV1.setText("");
tfOutputV2.setText("");
bConvert.setEnabled(isReady());
}
// got enough data to start conversion?
private boolean isReady()
{
boolean ret = false;
if (!tfInputV1.getText().isEmpty() && !tfOutputV1.getText().isEmpty()) {
Path file = FileManager.resolve(tfInputV1.getText());
ret = Files.isRegularFile(file);
}
return ret;
}
private int getPvrzIndex(Object o)
{
int index = 0;
if (o != null) {
try {
if (o instanceof Integer) {
index = ((Integer)o).intValue();
} else {
index = Integer.parseInt(o.toString());
}
} catch (Exception e) {
}
}
return index;
}
private String pvrzInfoString(Object o)
{
int index = getPvrzIndex(o);
return String.format("Resulting in MOS%1$04d.PVRZ, MOS%2$04d.PVRZ, ...", index, index+1);
}
private String getImageFileName(Path path)
{
JFileChooser fc = new JFileChooser(path.toFile());
fc.setDialogTitle("Select input graphics file");
fc.setDialogType(JFileChooser.OPEN_DIALOG);
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
FileNameExtensionFilter[] filters = getInputFilters();
for (final FileNameExtensionFilter filter: filters) {
fc.addChoosableFileFilter(filter);
}
fc.setFileFilter(filters[0]);
int ret = fc.showOpenDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
return fc.getSelectedFile().toString();
} else {
return null;
}
}
private String getMosFileName(Path path)
{
JFileChooser fc = new JFileChooser(path.toFile());
fc.setDialogTitle("Specify output filename");
fc.setDialogType(JFileChooser.SAVE_DIALOG);
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
FileNameExtensionFilter filter = new FileNameExtensionFilter("MOS files (*.mos)", "mos");
fc.addChoosableFileFilter(filter);
fc.setFileFilter(filter);
int ret = fc.showSaveDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
return fc.getSelectedFile().toString();
} else {
return null;
}
}
private List<String> convert()
{
List<String> result = new Vector<String>(2);
// validating input file
Path inFile = FileManager.resolve(tfInputV1.getText());
if (!Files.isRegularFile(inFile)) {
result.add(null);
result.add(String.format("Input file \"%1$s\" does not exist.", tfInputV1.getText()));
return result;
}
// loading source image
BufferedImage srcImage = null;
try {
srcImage = ColorConvert.toBufferedImage(ImageIO.read(inFile.toFile()), true);
} catch (Exception e) {
}
if (srcImage == null) {
result.add(null);
result.add("Unable to load source image.");
return result;
}
// handling "auto" compression
DxtEncoder.DxtType dxtType = DxtEncoder.DxtType.DXT1;
if (tabPane.getSelectedIndex() == 1 && cbCompression.getSelectedIndex() == 0) {
int[] pixels = ((DataBufferInt)srcImage.getRaster().getDataBuffer()).getData();
for (int i = 0; i < pixels.length; i++) {
int alpha = pixels[i] >>> 24;
if (alpha > 0x20 && alpha < 0xe0) {
dxtType = DxtEncoder.DxtType.DXT5;
break;
}
}
}
// fetching remaining settings
int pvrzIndex = getPvrzIndex(sPvrzIndex.getValue());
boolean isMOSC = cbCompress.isSelected();
// converting
if (tabPane.getSelectedIndex() == 0) {
convertV1(this, srcImage, tfOutputV1.getText(), isMOSC, result, true);
} else if (tabPane.getSelectedIndex() == 1) {
convertV2(this, srcImage, tfOutputV2.getText(), dxtType, pvrzIndex, result, true);
} else {
result.add(null);
result.add("No MOS type specified!");
}
return result;
}
//-------------------------- INNER CLASSES --------------------------
private static class MosEntry
{
public int page;
public int width, height;
public Point srcLocation;
public Point dstLocation;
public MosEntry(int page, Point srcLocation, int width, int height, Point dstLocation)
{
this.page = page;
this.srcLocation = srcLocation;
this.width = width;
this.height = height;
this.dstLocation = dstLocation;
}
}
}