// 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.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.event.KeyListener;
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.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;
import java.util.regex.Pattern;
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.JSlider;
import javax.swing.JTextField;
import javax.swing.ProgressMonitor;
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 ConvertToTis extends ChildFrame
implements ActionListener, PropertyChangeListener, ChangeListener, FocusListener, KeyListener
{
private static String currentDir = Profile.getGameRoot().toString();
private String inFileName;
private JSlider sTileNum;
private JTextField tfInput, tfOutput, tfTileNum;
private JButton bConvert, bCancel;
private JButton bInput, bOutput, bVersionHelp;
private JComboBox<String> cbVersion;
private JCheckBox cbCloseOnExit;
private SwingWorker<List<String>, Void> workerConvert;
private WindowBlocker blocker;
/**
* Converts an image into a TIS V1 resource.
* @param parent This parameter is needed for the progress monitor only.
* @param img The source image to convert into a TIS resource.
* @param tisFileName The name of the resulting TIS file.
* @param tileCount The number of tiles to convert.
* @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 tisFileName, int tileCount,
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 (img.getWidth() <= 0 || ((img.getWidth() % 64) != 0) ||
img.getHeight() <= 0 || ((img.getHeight() % 64) != 0)) {
result.add(null);
result.add("The dimensions of the source image have to be a multiple of 64 pixels.\n" +
String.format("Current dimensions are %1$dx%2$d", img.getWidth(), img.getHeight()));
return false;
}
if (tisFileName == null || tisFileName.isEmpty()) {
result.add(null);
result.add("No output filename specified.");
return false;
}
if (tileCount < 1 || tileCount > (img.getWidth()*img.getHeight()/4096)) {
result.add(null);
result.add("Invalid number of tiles specified.");
return false;
}
ProgressMonitor progress = null;
int[] src = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
byte[] dst = new byte[24 + tileCount*5120]; // header + tiles
int dstOfs = 0; // current start offset for write operations
// writing header data
System.arraycopy("TIS V1 ".getBytes(), 0, dst, 0, 8);
DynamicArray.putInt(dst, 8, tileCount);
DynamicArray.putInt(dst, 12, 0x1400);
DynamicArray.putInt(dst, 16, 0x18);
DynamicArray.putInt(dst, 20, 0x40);
dstOfs += 24;
int[] srcBlock = new int[64*64]; // temp. storage for a single tile
int[] palette = new int[255]; // temp. storage for generated palette
int[] hclPalette = new int[255]; // needed for finding nearest color
byte[] tilePalette = new byte[1024]; // final palette for output
byte[] tileData = new byte[64*64]; // final tile data for output
int tw = img.getWidth() / 64; // tiles per row
try {
String note = "Converting tile %1$d / %2$d";
int progressIndex = 0, progressMax = tileCount;
if (showProgress) {
progress = new ProgressMonitor(parent, "Converting TIS...", String.format(note, 0, tileCount),
0, progressMax);
progress.setMillisToDecideToPopup(0);
progress.setMillisToPopup(0);
}
IntegerHashMap<Byte> colorCache = new IntegerHashMap<Byte>(2048); // caching RGBColor -> index
for (int tileIdx = 0; tileIdx < tileCount; tileIdx++) {
if (showProgress) {
if (progress.isCanceled()) {
result.add(null);
result.add("Conversion has been cancelled.");
return false;
}
progressIndex++;
if ((progressIndex % 100) == 0) {
progress.setProgress(progressIndex);
progress.setNote(String.format(note, progressIndex, progressMax));
}
}
int tx = tileIdx % tw;
int ty = tileIdx / tw;
// resetting color cache
colorCache.clear();
// initializing source tile
int inOfs = ty*64*img.getWidth() + tx*64;
for (int i = 0, outOfs = 0; i < 64; i++, inOfs += img.getWidth(), outOfs += 64) {
System.arraycopy(src, inOfs, srcBlock, outOfs, 64);
}
// reducing colors
if (ColorConvert.medianCut(srcBlock, 255, palette, false)) {
ColorConvert.toHclPalette(palette, hclPalette);
// filling palette and color cache, index 0 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));
}
// processing pixel data
for (int i = 0; i < tileData.length; i++) {
if ((srcBlock[i] & 0xff000000) == 0) {
tileData[i] = 0;
} else {
Byte palIndex = colorCache.get(srcBlock[i]);
if (palIndex != null) {
tileData[i] = (byte)(palIndex + 1);
} else {
byte color = (byte)ColorConvert.nearestColor(srcBlock[i], hclPalette);
tileData[i] = (byte)(color + 1);
colorCache.put(srcBlock[i], color);
}
}
}
} else {
// error handling
result.add(null);
result.add(String.format("Error processing tile #%1$d. Conversion cancelled.", tileIdx));
return false;
}
// writing final palette and pixel data to output
System.arraycopy(tilePalette, 0, dst, dstOfs, 1024);
dstOfs += 1024;
System.arraycopy(tileData, 0, dst, dstOfs, 4096);
dstOfs += 4096;
}
// writing TIS file to disk
Path tisFilePath = FileManager.resolve(tisFileName);
try (OutputStream os = StreamUtils.getOutputStream(tisFilePath, true)) {
os.write(dst);
} catch (Exception e) {
// error handling
e.printStackTrace();
result.add(null);
result.add("Error writing TIS file to disk.");
return false;
}
} finally {
// some cleaning up
src = null; dst = null;
img.flush();
if (progress != null) {
progress.close();
progress = null;
}
}
// generating conversion summary
result.add("Conversion finished successfully.");
return true;
}
/**
* Converts an image into a TIS V2 resource.
* @param parent This parameter is needed for the progress monitor only.
* @param img The source image to convert into a TIS resource.
* @param tisFileName The name of the resulting TIS file.
* @param tileCount The number of tiles to convert.
* @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 tisFileName,
int tileCount, 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 (img.getWidth() <= 0 || ((img.getWidth() % 64) != 0) ||
img.getHeight() <= 0 || ((img.getHeight() % 64) != 0)) {
result.add(null);
result.add("The dimensions of the source image have to be a multiple of 64 pixels.\n" +
String.format("Current dimensions are %1$dx%2$d", img.getWidth(), img.getHeight()));
return false;
}
if (tisFileName == null || tisFileName.isEmpty()) {
result.add(null);
result.add("No output filename specified.");
return false;
}
if (!tisFileName.equalsIgnoreCase(createValidTisName(tisFileName, 2))) {
result.add(null);
result.add("PVRZ-based TIS filenames have to be 2 up to 7 characters long.");
return false;
}
if (tileCount < 1 || tileCount > (img.getWidth()*img.getHeight()/4096)) {
result.add(null);
result.add("Invalid number of tiles specified.");
return false;
}
// preparing variables
ProgressMonitor progress = null;
List<BinPack2D> pageList = new ArrayList<BinPack2D>();
List<TileEntry> entryList = new ArrayList<TileEntry>(tileCount);
byte[] dst = new byte[24 + tileCount*12]; // header + tiles
int dstOfs = 0; // current start offset for write operations
try {
if (showProgress) {
// preparing progress meter
progress = new ProgressMonitor(parent, "Converting TIS...", "Preparing TIS", 0, 5);
progress.setMillisToDecideToPopup(0);
progress.setMillisToPopup(0);
progress.setProgress(0);
}
// writing header data
System.arraycopy("TIS V1 ".getBytes(), 0, dst, 0, 8);
DynamicArray.putInt(dst, 8, tileCount);
DynamicArray.putInt(dst, 12, 0x0c);
DynamicArray.putInt(dst, 16, 0x18);
DynamicArray.putInt(dst, 20, 0x40);
dstOfs += 24;
// processing tiles
final BinPack2D.HeuristicRules binPackRule = BinPack2D.HeuristicRules.BOTTOM_LEFT_RULE;
final int pageDim = 1024;
final int tileDim = 64;
final int tilesPerDim = pageDim / tileDim;
int pw = img.getWidth() / pageDim + (((img.getWidth() % pageDim) != 0) ? 1 : 0);
int ph = img.getHeight() / pageDim + (((img.getHeight() % pageDim) != 0) ? 1 : 0);
for (int py = 0; py < ph; py++) {
for (int px = 0; px < pw; px++) {
int x = px * pageDim, y = py * pageDim;
int w = Math.min(pageDim, img.getWidth() - x);
int h = Math.min(pageDim, img.getHeight() - y);
Dimension space = new Dimension(w / tileDim, h / tileDim);
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(tilesPerDim, tilesPerDim);
pageList.add(packer);
pageIdx = pageList.size() - 1;
rectMatch = packer.insert(space.width, space.height, binPackRule);
}
// registering tile entries
int tileIdx = (y*img.getWidth())/(tileDim*tileDim) + x/tileDim;
for (int ty = 0; ty < space.height; ty++, tileIdx += img.getWidth()/tileDim) {
for (int tx = 0; tx < space.width; tx++) {
// marking page index as incomplete
if (tileIdx + tx < tileCount) {
TileEntry entry = new TileEntry(tileIdx + tx, pageIdx,
(rectMatch.x + tx)*tileDim, (rectMatch.y + ty)*tileDim);
entryList.add(entry);
}
}
}
}
}
// writing TIS entries
Collections.sort(entryList, TileEntry.CompareByIndex);
for (int i = 0; i < entryList.size(); i++, dstOfs += 12) {
TileEntry entry = entryList.get(i);
DynamicArray.putInt(dst, dstOfs, entry.page);
DynamicArray.putInt(dst, dstOfs + 4, entry.x);
DynamicArray.putInt(dst, dstOfs + 8, entry.y);
}
// writing TIS file to disk
Path tisFilePath = FileManager.resolve(tisFileName);
try (OutputStream os = StreamUtils.getOutputStream(tisFilePath, true)) {
os.write(dst);
} catch (Exception e) {
// error handling
e.printStackTrace();
result.add(null);
result.add("Error writing TIS file to disk.");
return false;
}
// generating PVRZ files
if (!createPvrzPages(tisFileName, img, pageList, DxtEncoder.DxtType.DXT1, entryList,
result, progress)) {
return false;
}
} finally {
// some cleaning up
dst = null; img.flush();
if (progress != null) {
progress.close();
progress = null;
}
}
// generating conversion summary
result.add("Conversion finished successfully.");
return true;
}
/**
* Returns a valid TIS filename based on the parameters.
* @param tisFilename The TIS filename the return value is based on.
* @param tisVersion The TIS version to consider (1=V1, 2=V2).
* @return A valid TIS filename.
*/
public static String createValidTisName(String tisFilename, int tisVersion)
{
// extracting file path and filename without extension
Path outFile = FileManager.resolve(tisFilename).toAbsolutePath();
Path outPath = outFile.getParent();
String outNameBase = outFile.getFileName().toString();
if (outNameBase == null || outNameBase.isEmpty() || outNameBase.charAt(0) == '.') {
outNameBase = "OUTPUT";
}
if (outNameBase.lastIndexOf('.') > 0) {
outNameBase = outNameBase.substring(0, outNameBase.lastIndexOf('.'));
}
// limit output filename to [2,7] or 8 characters (based on the TIS version)
if (tisVersion == 2) {
if (!Pattern.matches(".{2,7}", outNameBase)) {
if (outNameBase.length() > 7) {
outNameBase = outNameBase.substring(0, 7);
} else {
final String fill = "00";
outNameBase = outNameBase + fill.substring(0, 2 - outNameBase.length());
}
}
} else {
if (outNameBase.length() > 8) {
outNameBase = outNameBase.substring(0, 8);
}
}
return outPath.resolve(outNameBase + ".TIS").toString();
}
// 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 a PVRZ filename based on the specified parameters
private static String generatePvrzName(String tisFileName, int page)
{
Path tisFile = FileManager.resolve(tisFileName);
Path tisPath = tisFile.getParent();
String tisNameBase = tisFile.getFileName().toString();
if (tisNameBase.lastIndexOf('.') > 0) {
tisNameBase = tisNameBase.substring(0, tisNameBase.lastIndexOf('.'));
}
if (Pattern.matches(".{2,7}", tisNameBase)) {
String pvrzName = String.format("%1$s%2$s%3$02d.PVRZ", tisNameBase.substring(0, 1),
tisNameBase.substring(2, tisNameBase.length()), page);
if (tisPath != null) {
return tisPath.resolve(pvrzName).toString();
} else {
return pvrzName;
}
}
return "";
}
// generates PVRZ textures
public static boolean createPvrzPages(String tisFileName, BufferedImage srcImg,
List<BinPack2D> pages, DxtEncoder.DxtType dxtType,
List<TileEntry> entryList, List<String> result,
ProgressMonitor progress)
{
int dxtCode = (dxtType == DxtEncoder.DxtType.DXT5) ? 11 : 7;
byte[] output = new byte[DxtEncoder.calcImageSize(1024, 1024, dxtType)];
String note = "Generating PVRZ file %1$s / %2$s";
if (progress != null) {
progress.setMinimum(0);
progress.setMaximum(pages.size() + 1);
progress.setProgress(1);
}
for (int pageIdx = 0; pageIdx < pages.size(); pageIdx++) {
if (progress != null) {
if (progress.isCanceled()) {
result.add(null);
result.add("Conversion has been cancelled.");
return false;
}
progress.setProgress(pageIdx + 1);
progress.setNote(String.format(note, pageIdx+1, pages.size()));
}
String pvrzName = generatePvrzName(tisFileName, pageIdx);
BinPack2D packer = pages.get(pageIdx);
packer.shrinkBin(true);
// generating texture image
int w = packer.getBinWidth() * 64;
int h = packer.getBinHeight() * 64;
BufferedImage texture = ColorConvert.createCompatibleImage(w, h, true);
Graphics2D g = texture.createGraphics();
g.setBackground(new Color(0, true));
g.setColor(Color.BLACK);
g.fillRect(0, 0, texture.getWidth(), texture.getHeight());
int tw = srcImg.getWidth() / 64;
for (final TileEntry entry: entryList) {
if (entry.page == pageIdx) {
int sx = (entry.tileIndex % tw) * 64, sy = (entry.tileIndex / tw) * 64;
int dx = entry.x, dy = entry.y;
g.clearRect(dx, dy, 64, 64);
g.drawImage(srcImg, dx, dy, dx+64, dy+64, sx, sy, sx+64, sy+64, null);
}
}
g.dispose();
int[] textureData = ((DataBufferInt)texture.getRaster().getDataBuffer()).getData();
try {
// compressing PVRZ
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
Path pvrzPath = FileManager.resolve(pvrzName);
try (OutputStream os = StreamUtils.getOutputStream(pvrzPath, true)) {
os.write(pvrz);
} catch (Exception e) {
// critical error
e.printStackTrace();
result.add(null);
result.add(String.format("Error writing PVRZ file \"%1$s\" to disk.", pvrzName));
return false;
}
pvrz = null;
} catch (Exception e) {
e.printStackTrace();
result.add(null);
result.add(String.format("Error while generating PVRZ files:\n%1$s", e.getMessage()));
return false;
}
}
output = null;
return true;
}
public ConvertToTis()
{
super("Convert to TIS", 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 = "TIS output file already exists. Overwrite?";
Path file = null;
do {
if (!tfOutput.getText().isEmpty()) {
file = FileManager.resolve(tfOutput.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() == bInput) {
JFileChooser fc = new JFileChooser(currentDir);
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]);
if (!tfInput.getText().isEmpty()) {
fc.setSelectedFile(FileManager.resolve(tfInput.getText()).toFile());
}
int ret = fc.showOpenDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
Path file = fc.getSelectedFile().toPath();
currentDir = file.getParent().toString();
inFileName = file.toString();
tfInput.setText(inFileName);
validateInput(tfInput.getText());
if (tfOutput.getText().isEmpty()) {
tfOutput.setText(createValidTisName(tfInput.getText(), getTisVersion()));
}
}
} else if (event.getSource() == bOutput) {
JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile());
fc.setDialogTitle("Specify output filename");
fc.setDialogType(JFileChooser.SAVE_DIALOG);
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
FileNameExtensionFilter filter = new FileNameExtensionFilter("TIS files (*.tis)", "tis");
fc.addChoosableFileFilter(filter);
fc.setFileFilter(filter);
String fileName = tfOutput.getText();
if (fileName.isEmpty() && !tfInput.getText().isEmpty()) {
Path f = FileManager.resolve(tfInput.getText());
if (Files.isRegularFile(f)) {
fileName = createValidTisName(tfInput.getText(), getTisVersion());
}
}
fc.setSelectedFile(FileManager.resolve(fileName).toFile());
int ret = fc.showSaveDialog(this);
while (ret == JFileChooser.APPROVE_OPTION) {
currentDir = fc.getSelectedFile().getParent();
String orig = StreamUtils.replaceFileExtension(fc.getSelectedFile().toString(), "TIS");
String fixed = createValidTisName(orig, getTisVersion());
if (!orig.equalsIgnoreCase(fixed)) {
ret = JOptionPane.showConfirmDialog(this,
"The chosen output file is not compatible with the current TIS settings.\n" +
"Do you want me to fix it?\n",
"Invalid output filename", JOptionPane.YES_NO_CANCEL_OPTION);
if (ret == JOptionPane.YES_OPTION) {
tfOutput.setText(fixed);
break;
} else if (ret == JOptionPane.NO_OPTION) {
ret = fc.showSaveDialog(this);
} else {
break;
}
} else {
tfOutput.setText(orig);
break;
}
}
} else if (event.getSource() == bVersionHelp) {
final String helpMsg =
"\"Legacy\" is the old and proven TIS format supported by all available\n" +
"Infinity Engine games. Graphics data is stored in the TIS file directly.\n" +
"Each tile (64x64 pixel block) is limited to a 256 color table.\n\n" +
"\"PVRZ-based\" uses a new TIS format introduced by BG:EE. Graphics data\n" +
"is stored separately in PVRZ files and is not limited to a 256 color table.\n" +
"It is only supported by the Enhanced Editions of the Baldur's Gate games.";
JOptionPane.showMessageDialog(this, helpMsg, "About TIS versions",
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() == sTileNum) {
tileNumSliderUpdated();
}
}
//--------------------- End Interface ChangeListener ---------------------
//--------------------- Begin Interface FocusListener ---------------------
@Override
public void focusGained(FocusEvent event)
{
// nothing to do
}
@Override
public void focusLost(FocusEvent event)
{
if (event.getSource() == tfInput) {
// validating input file (if it has changed)
if (inFileName == null || !inFileName.equals(tfInput.getText())) {
try {
WindowBlocker.blockWindow(this, true);
validateInput(tfInput.getText());
} finally {
WindowBlocker.blockWindow(this, false);
}
}
} else if (event.getSource() == tfTileNum) {
tileNumEditUpdated(true);
}
}
//--------------------- End Interface FocusListener ---------------------
//--------------------- Begin Interface KeyListener ---------------------
@Override
public void keyTyped(KeyEvent event)
{
// nothing to do
}
@Override
public void keyPressed(KeyEvent event)
{
if (event.getSource() == tfTileNum) {
if (event.getKeyCode() == KeyEvent.VK_ENTER) {
tileNumEditUpdated(true);
sTileNum.requestFocus();
}
}
}
@Override
public void keyReleased(KeyEvent event)
{
// nothing to do
}
//--------------------- End Interface KeyListener ---------------------
private void init()
{
setIconImage(Icons.getImage(Icons.ICON_APPLICATION_16));
// setting up files section
JPanel pFiles = new JPanel(new GridBagLayout());
pFiles.setBorder(BorderFactory.createTitledBorder("Input & Output "));
GridBagConstraints c = new GridBagConstraints();
JLabel lInput = new JLabel("Input file:");
JLabel lOutput = new JLabel("Output file:");
tfInput = new JTextField();
tfInput.addFocusListener(this);
tfOutput = new JTextField();
tfOutput.addFocusListener(this);
bInput = new JButton("...");
bInput.addActionListener(this);
bOutput = new JButton("...");
bOutput.addActionListener(this);
JLabel lInputNote =
new JLabel("Note: Width and height of the source image have to be a multiple of 64 pixels.");
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);
pFiles.add(lInput, 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);
pFiles.add(tfInput, 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);
pFiles.add(bInput, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 0), 0, 0);
pFiles.add(lOutput, c);
c = ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 8, 0, 0), 0, 0);
pFiles.add(tfOutput, c);
c = ViewerUtil.setGBC(c, 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 4, 0, 4), 0, 0);
pFiles.add(bOutput, c);
c = ViewerUtil.setGBC(c, 0, 2, 3, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(8, 4, 4, 0), 0, 0);
pFiles.add(lInputNote, c);
// setting up options section
JPanel pSubOptions = new JPanel(new GridBagLayout());
JLabel lVersion = new JLabel("TIS version:");
cbVersion = new JComboBox<>(new String[]{"Legacy", "PVRZ-based"});
cbVersion.setSelectedIndex(0);
bVersionHelp = new JButton("?");
bVersionHelp.setToolTipText("About TIS versions");
bVersionHelp.addActionListener(this);
bVersionHelp.setMargin(new Insets(bVersionHelp.getInsets().top, 4,
bVersionHelp.getInsets().bottom, 4));
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);
pSubOptions.add(lVersion, c);
c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 8, 0, 0), 8, 0);
pSubOptions.add(cbVersion, c);
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);
pSubOptions.add(bVersionHelp, c);
c = ViewerUtil.setGBC(c, 3, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0);
pSubOptions.add(new JPanel(), c);
JPanel pOptions = new JPanel(new GridBagLayout());
pOptions.setBorder(BorderFactory.createTitledBorder("Options "));
JLabel lTileNum = new JLabel("Number of tiles to convert:");
lTileNum.setToolTipText("Counting from left to right, top to bottom.");
sTileNum = new JSlider(JSlider.HORIZONTAL);
sTileNum.addChangeListener(this);
tfTileNum = new JTextField(6);
tfTileNum.addKeyListener(this);
tfTileNum.addFocusListener(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);
pOptions.add(lTileNum, 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);
pOptions.add(sTileNum, c);
c = ViewerUtil.setGBC(c, 2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 8, 0, 4), 0, 0);
pOptions.add(tfTileNum, c);
c = ViewerUtil.setGBC(c, 0, 1, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);
pOptions.add(pSubOptions, c);
// 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(pFiles, c);
c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 8, 8, 8), 0, 0);
add(pOptions, c);
c = ViewerUtil.setGBC(c, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(4, 8, 0, 8), 0, 0);
add(pButtons, c);
c = ViewerUtil.setGBC(c, 0, 3, 1, 1, 1.0, 1.0, GridBagConstraints.LINE_START,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0);
add(new JPanel(), c);
// finalizing dialog initialization
validateInput(inFileName);
pack();
setMinimumSize(getPreferredSize());
setLocationRelativeTo(getParent());
bInput.requestFocus();
setVisible(true);
}
private void hideWindow()
{
clear();
setVisible(false);
}
// resetting dialog state
private void clear()
{
tfInput.setText("");
tfOutput.setText("");
sTileNum.setMinimum(0);
sTileNum.setMaximum(0);
sTileNum.setValue(0);
tfTileNum.setText("0");
bConvert.setEnabled(isReady());
}
// got enough data to start conversion?
private boolean isReady()
{
boolean ret = false;
if (!getInputFile().isEmpty()) {
Path f = FileManager.resolve(getInputFile());
ret = Files.isRegularFile(f);
}
return ret;
}
private void tileNumEditUpdated(boolean updateText)
{
try {
int tiles = Integer.parseInt(tfTileNum.getText());
if (tiles < sTileNum.getMinimum())
tiles = sTileNum.getMinimum();
if (tiles > sTileNum.getMaximum())
tiles = sTileNum.getMaximum();
if (sTileNum.getValue() != tiles) {
sTileNum.setValue(tiles);
}
tileNumSliderUpdated();
} catch (NumberFormatException e) {
if (updateText)
tfTileNum.setText(Integer.toString(sTileNum.getValue()));
}
}
private void tileNumSliderUpdated()
{
String s = Integer.toString(sTileNum.getValue());
if (!tfTileNum.getText().equals(s)) {
tfTileNum.setText(s);
}
}
// returns 1=TIS V1 or 2=TIS V2
private int getTisVersion()
{
return (cbVersion.getSelectedIndex() == 1) ? 2 : 1;
}
// returns number of tiles to convert
private int getTileCount()
{
return (sTileNum.getValue() > 0) ? sTileNum.getValue() : 0;
}
private String getInputFile()
{
return (inFileName != null) ? inFileName : "";
}
// checking image dimensions and calculating max. possible number of tiles
private boolean validateInput(String inputFile)
{
int tileCount = 0;
boolean isValid = false;
inFileName = inputFile;
if (inFileName != null && !inFileName.isEmpty()) {
Path f = FileManager.resolve(inFileName);
if (Files.isRegularFile(f)) {
Dimension dimImage = ColorConvert.getImageDimension(f);
if (dimImage.width >= 0 && (dimImage.width % 64) == 0 &&
dimImage.height >= 0 && (dimImage.height % 64) == 0) {
tileCount = (dimImage.width * dimImage.height) / 4096;
isValid = true;
}
}
}
if (isValid) {
// enable and initialize relevant components
sTileNum.setMinimum(1);
sTileNum.setMaximum(tileCount);
sTileNum.setValue(sTileNum.getMaximum());
sTileNum.setEnabled(true);
tfTileNum.setEnabled(true);
tileNumSliderUpdated();
bConvert.setEnabled(isReady());
} else {
// disable relevant components
sTileNum.setMinimum(0);
sTileNum.setMaximum(0);
sTileNum.setValue(0);
sTileNum.setEnabled(false);
tfTileNum.setText("0");
tfTileNum.setEnabled(false);
bConvert.setEnabled(false);
}
return isValid;
}
// Converts source image into a TIS file and optional PVRZ file(s). Returns a summary of the result.
// Return value: First list element is used for success message, second element for error message.
private List<String> convert()
{
List<String> ret = new Vector<String>(2);
// validating input file
Path inFile = FileManager.resolve(inFileName);
if (!Files.isRegularFile(inFile)) {
ret.add(null);
ret.add(String.format("Input file \"%1$s\" does not exist.", inFileName));
return ret;
}
// loading source image
BufferedImage srcImage = null;
try {
srcImage = ColorConvert.toBufferedImage(ImageIO.read(inFile.toFile()), true);
} catch (Exception e) {
}
if (srcImage == null) {
ret.add(null);
ret.add("Unable to load source image.");
return ret;
}
// fetching remaining settings
String outFileName = StreamUtils.replaceFileExtension(tfOutput.getText(), "TIS");
int maxTileCount = (srcImage.getWidth()*srcImage.getHeight()) / 4096;
int tileCount = Math.max(1, Math.min(getTileCount(), maxTileCount));
int tisVersion = getTisVersion();
if (tisVersion == 2) {
// TIS V2 conversion
convertV2(this, srcImage, outFileName, tileCount, ret, true);
} else {
// TIS V1 conversion
convertV1(this, srcImage, outFileName, tileCount, ret, true);
}
return ret;
}
// -------------------------- INNER CLASSES --------------------------
public static class TileEntry
{
public int tileIndex;
public int page;
public int x, y;
public static Comparator<TileEntry> CompareByIndex = new Comparator<TileEntry>() {
@Override
public int compare(TileEntry te1, TileEntry te2)
{
return te1.tileIndex - te2.tileIndex;
}
};
public TileEntry(int index, int page, int x, int y)
{
this.tileIndex = index;
this.page = page;
this.x = x;
this.y = y;
}
}
}