// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.graphics;
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
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.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.ProgressMonitor;
import javax.swing.RootPaneContainer;
import javax.swing.SwingWorker;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import org.infinity.NearInfinity;
import org.infinity.gui.ButtonPanel;
import org.infinity.gui.ButtonPopupMenu;
import org.infinity.gui.TileGrid;
import org.infinity.gui.WindowBlocker;
import org.infinity.gui.converter.ConvertToPvrz;
import org.infinity.gui.converter.ConvertToTis;
import org.infinity.resource.Closeable;
import org.infinity.resource.Profile;
import org.infinity.resource.Resource;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.ViewableContainer;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.search.ReferenceSearcher;
import org.infinity.util.BinPack2D;
import org.infinity.util.DynamicArray;
import org.infinity.util.IntegerHashMap;
import org.infinity.util.io.StreamUtils;
public class TisResource implements Resource, Closeable, ActionListener, ChangeListener,
ItemListener, KeyListener, PropertyChangeListener
{
private enum Status { SUCCESS, CANCELLED, ERROR, UNSUPPORTED }
private static final Color TransparentColor = new Color(0, true);
private static final int DEFAULT_COLUMNS = 5;
private static boolean showGrid = false;
private final ResourceEntry entry;
private final ButtonPanel buttonPanel = new ButtonPanel();
private TisDecoder decoder;
private List<Image> tileImages; // stores one tile per image
private TileGrid tileGrid; // the main component for displaying the tileset
private JSlider slCols; // changes the tiles per row
private JTextField tfCols; // input/output tiles per row
private JCheckBox cbGrid; // show/hide frame around each tile
private JMenuItem miExport, miExportPaletteTis, miExportPvrzTis, miExportPNG;
private JPanel panel; // top-level panel of the viewer
private RootPaneContainer rpc;
private SwingWorker<Status, Void> workerToPalettedTis, workerToPvrzTis, workerExport;
private WindowBlocker blocker;
private int defaultWidth;
public TisResource(ResourceEntry entry) throws Exception
{
this.entry = entry;
initTileset();
}
//--------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
if (event.getSource() == buttonPanel.getControlByType(ButtonPanel.Control.FIND_REFERENCES)) {
new ReferenceSearcher(entry, panel.getTopLevelAncestor());
} else if (event.getSource() == miExport) {
ResourceFactory.exportResource(entry, panel.getTopLevelAncestor());
} else if (event.getSource() == miExportPaletteTis) {
final Path tisFile = getTisFileName(panel.getTopLevelAncestor(), false);
if (tisFile != null) {
blocker = new WindowBlocker(rpc);
blocker.setBlocked(true);
workerToPalettedTis = new SwingWorker<Status, Void>() {
@Override
public Status doInBackground()
{
Status retVal = Status.ERROR;
try {
retVal = convertToPaletteTis(tisFile, true);
} catch (Exception e) {
e.printStackTrace();
}
return retVal;
}
};
workerToPalettedTis.addPropertyChangeListener(this);
workerToPalettedTis.execute();
}
} else if (event.getSource() == miExportPvrzTis) {
final Path tisFile = getTisFileName(panel.getTopLevelAncestor(), true);
if (tisFile != null) {
blocker = new WindowBlocker(rpc);
blocker.setBlocked(true);
workerToPvrzTis = new SwingWorker<Status, Void>() {
@Override
public Status doInBackground()
{
Status retVal = Status.ERROR;
try {
retVal = convertToPvrzTis(tisFile, true);
} catch (Exception e) {
e.printStackTrace();
}
return retVal;
}
};
workerToPvrzTis.addPropertyChangeListener(this);
workerToPvrzTis.execute();
}
} else if (event.getSource() == miExportPNG) {
final Path pngFile = getPngFileName(panel.getTopLevelAncestor());
if (pngFile != null) {
blocker = new WindowBlocker(rpc);
blocker.setBlocked(true);
workerExport = new SwingWorker<Status, Void>() {
@Override
public Status doInBackground()
{
Status retVal = Status.ERROR;
try {
retVal = exportPNG(pngFile, true);
} catch (Exception e) {
e.printStackTrace();
}
return retVal;
}
};
workerExport.addPropertyChangeListener(this);
workerExport.execute();
}
}
}
//--------------------- End Interface ActionListener ---------------------
//--------------------- Begin Interface ChangeListener ---------------------
@Override
public void stateChanged(ChangeEvent event)
{
if (event.getSource() == slCols) {
int cols = slCols.getValue();
tfCols.setText(Integer.toString(cols));
tileGrid.setGridSize(calcGridSize(tileGrid.getImageCount(), cols));
}
}
//--------------------- End Interface ChangeListener ---------------------
//--------------------- Begin Interface ItemListener ---------------------
@Override
public void itemStateChanged(ItemEvent event)
{
if (event.getSource() == cbGrid) {
showGrid = cbGrid.isSelected();
tileGrid.setShowGrid(showGrid);
}
}
//--------------------- End Interface ChangeListener ---------------------
//--------------------- Begin Interface KeyListener ---------------------
@Override
public void keyPressed(KeyEvent event)
{
if (event.getSource() == tfCols) {
if (event.getKeyCode() == KeyEvent.VK_ENTER) {
int cols;
try {
cols = Integer.parseInt(tfCols.getText());
} catch (NumberFormatException e) {
cols = slCols.getValue();
tfCols.setText(Integer.toString(slCols.getValue()));
}
if (cols != slCols.getValue()) {
if (cols <= 0)
cols = 1;
if (cols >= decoder.getTileCount())
cols = decoder.getTileCount();
slCols.setValue(cols);
tfCols.setText(Integer.toString(slCols.getValue()));
tileGrid.setGridSize(calcGridSize(tileGrid.getImageCount(), cols));
}
slCols.requestFocus(); // remove focus from textfield
}
}
}
@Override
public void keyReleased(KeyEvent event)
{
// nothing to do
}
@Override
public void keyTyped(KeyEvent event)
{
// nothing to do
}
//--------------------- End Interface KeyListener ---------------------
//--------------------- Begin Interface PropertyChangeListener ---------------------
@Override
public void propertyChange(PropertyChangeEvent event)
{
if (event.getSource() instanceof SwingWorker<?, ?>) {
@SuppressWarnings("unchecked")
SwingWorker<Status, Void> worker = (SwingWorker<Status, Void>)event.getSource();
if ("state".equals(event.getPropertyName()) &&
SwingWorker.StateValue.DONE == event.getNewValue()) {
if (blocker != null) {
blocker.setBlocked(false);
blocker = null;
}
Status retVal = Status.ERROR;
try {
retVal = worker.get();
if (retVal == null) {
retVal = Status.ERROR;
}
} catch (Exception e) {
e.printStackTrace();
}
if (retVal == Status.SUCCESS) {
JOptionPane.showMessageDialog(panel.getTopLevelAncestor(),
"File exported successfully.", "Export complete",
JOptionPane.INFORMATION_MESSAGE);
} else if (retVal == Status.CANCELLED) {
JOptionPane.showMessageDialog(panel.getTopLevelAncestor(),
"Export has been cancelled.", "Information",
JOptionPane.INFORMATION_MESSAGE);
} else if (retVal == Status.UNSUPPORTED) {
JOptionPane.showMessageDialog(panel.getTopLevelAncestor(),
"Operation not (yet) supported.", "Information",
JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(panel.getTopLevelAncestor(),
"Error while exporting " + entry, "Error",
JOptionPane.ERROR_MESSAGE);
}
}
}
}
//--------------------- End Interface PropertyChangeListener ---------------------
//--------------------- Begin Interface Closeable ---------------------
@Override
public void close() throws Exception
{
if (workerToPalettedTis != null) {
if (!workerToPalettedTis.isDone()) {
workerToPalettedTis.cancel(true);
}
workerToPalettedTis = null;
}
if (workerToPvrzTis != null) {
if (!workerToPvrzTis.isDone()) {
workerToPvrzTis.cancel(true);
}
workerToPvrzTis = null;
}
if (workerExport != null) {
if (!workerExport.isDone()) {
workerExport.cancel(true);
}
workerExport = null;
}
if (tileImages != null) {
tileImages.clear();
tileImages = null;
}
if (tileGrid != null) {
tileGrid.clearImages();
tileGrid = null;
}
if (decoder != null) {
decoder.close();
decoder = null;
}
System.gc();
}
//--------------------- End Interface Closeable ---------------------
//--------------------- Begin Interface Resource ---------------------
@Override
public ResourceEntry getResourceEntry()
{
return entry;
}
//--------------------- End Interface Resource ---------------------
//--------------------- Begin Interface Viewable ---------------------
@Override
public JComponent makeViewer(ViewableContainer container)
{
if (container instanceof RootPaneContainer) {
rpc = (RootPaneContainer)container;
} else {
rpc = NearInfinity.getInstance();
}
if (decoder == null) {
return new JPanel(new BorderLayout());
}
int tileCount = decoder.getTileCount();
int defaultColumns = Math.min(tileCount, DEFAULT_COLUMNS);
// 1. creating top panel
// 1.1. creating label with text field
JLabel lblTPR = new JLabel("Tiles per row:");
tfCols = new JTextField(Integer.toString(defaultColumns), 5);
tfCols.addKeyListener(this);
JPanel tPanel1 = new JPanel(new FlowLayout(FlowLayout.CENTER));
tPanel1.add(lblTPR);
tPanel1.add(tfCols);
// 1.2. creating slider
slCols = new JSlider(JSlider.HORIZONTAL, 1, tileCount, defaultColumns);
if (tileCount > 1000) {
slCols.setMinorTickSpacing(100);
slCols.setMajorTickSpacing(1000);
} else if (tileCount > 100) {
slCols.setMinorTickSpacing(10);
slCols.setMajorTickSpacing(100);
} else {
slCols.setMinorTickSpacing(1);
slCols.setMajorTickSpacing(10);
}
slCols.setPaintTicks(true);
slCols.addChangeListener(this);
// 1.3. adding left side of the top panel together
JPanel tlPanel = new JPanel(new GridLayout(2, 1));
tlPanel.add(tPanel1);
tlPanel.add(slCols);
// 1.4. configuring checkbox
cbGrid = new JCheckBox("Show Grid", showGrid);
cbGrid.addItemListener(this);
JPanel trPanel = new JPanel(new GridLayout());
trPanel.add(cbGrid);
// 1.5. putting top panel together
BorderLayout bl = new BorderLayout();
JPanel topPanel = new JPanel(bl);
topPanel.add(tlPanel, BorderLayout.CENTER);
topPanel.add(trPanel, BorderLayout.LINE_END);
// 2. creating main panel
// 2.1. creating tiles table and scroll pane
tileGrid = new TileGrid(1, defaultColumns, decoder.getTileWidth(), decoder.getTileHeight());
tileGrid.addImage(tileImages);
tileGrid.setGridSize(calcGridSize(tileGrid.getImageCount(), getDefaultTilesPerRow()));
tileGrid.setShowGrid(showGrid);
slCols.setValue(tileGrid.getTileColumns());
tfCols.setText(Integer.toString(tileGrid.getTileColumns()));
JScrollPane scroll = new JScrollPane(tileGrid);
scroll.getVerticalScrollBar().setUnitIncrement(16);
scroll.getHorizontalScrollBar().setUnitIncrement(16);
// 2.2. putting main panel together
JPanel centerPanel = new JPanel(new BorderLayout());
centerPanel.add(scroll, BorderLayout.CENTER);
// 3. creating bottom panel
// 3.1. creating export button
miExport = new JMenuItem("original");
miExport.addActionListener(this);
if (decoder.getType() == TisDecoder.Type.PVRZ) {
miExportPaletteTis = new JMenuItem("as palette-based TIS");
miExportPaletteTis.addActionListener(this);
} else if (decoder.getType() == TisDecoder.Type.PALETTE) {
miExportPvrzTis = new JMenuItem("as PVRZ-based TIS");
miExportPvrzTis.addActionListener(this);
}
miExportPNG = new JMenuItem("as PNG");
miExportPNG.addActionListener(this);
List<JMenuItem> list = new ArrayList<JMenuItem>();
if (miExport != null)
list.add(miExport);
if (miExportPaletteTis != null) {
list.add(miExportPaletteTis);
}
if (miExportPvrzTis != null) {
list.add(miExportPvrzTis);
}
if (miExportPNG != null)
list.add(miExportPNG);
JMenuItem[] mi = new JMenuItem[list.size()];
for (int i = 0; i < mi.length; i++) {
mi[i] = list.get(i);
}
((JButton)buttonPanel.addControl(ButtonPanel.Control.FIND_REFERENCES)).addActionListener(this);
ButtonPopupMenu bpmExport = (ButtonPopupMenu)buttonPanel.addControl(ButtonPanel.Control.EXPORT_MENU);
bpmExport.setMenuItems(mi);
// 4. packing all together
panel = new JPanel(new BorderLayout());
panel.add(topPanel, BorderLayout.NORTH);
panel.add(centerPanel, BorderLayout.CENTER);
panel.add(buttonPanel, BorderLayout.SOUTH);
centerPanel.setBorder(BorderFactory.createLoweredBevelBorder());
return panel;
}
//--------------------- End Interface Viewable ---------------------
// Returns detected or guessed number of tiles per row of the current TIS
private int getDefaultTilesPerRow()
{
return defaultWidth;
}
// Returns an output filename for a TIS file
private Path getTisFileName(Component parent, boolean enforceValidName)
{
Path retVal = null;
JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile());
fc.setDialogTitle("Export resource");
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
FileNameExtensionFilter filter = new FileNameExtensionFilter("TIS files (*.tis)", "tis");
fc.addChoosableFileFilter(filter);
fc.setFileFilter(filter);
fc.setSelectedFile(new File(fc.getCurrentDirectory(), getResourceEntry().getResourceName()));
boolean repeat = enforceValidName;
do {
retVal = null;
if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
retVal = fc.getSelectedFile().toPath();
if (enforceValidName && !isTisFileNameValid(retVal)) {
JOptionPane.showMessageDialog(parent,
"PVRZ-based TIS filenames have to be 2 up to 7 characters long.",
"Error", JOptionPane.ERROR_MESSAGE);
} else {
repeat = false;
}
if (Files.exists(retVal)) {
final String options[] = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(parent, retVal + " exists. Overwrite?", "Export resource",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
null, options, options[0]) != 0) {
retVal = null;
repeat = false;
}
}
} else {
repeat = false;
}
} while (repeat);
return retVal;
}
// Returns output filename for a PNG file
private Path getPngFileName(Component parent)
{
Path retVal = null;
JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile());
fc.setDialogTitle("Export resource");
fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
FileNameExtensionFilter filter = new FileNameExtensionFilter("PNG files (*.png)", "png");
fc.addChoosableFileFilter(filter);
fc.setFileFilter(filter);
fc.setSelectedFile(new File(fc.getCurrentDirectory(), getResourceEntry().getResourceName().toUpperCase(Locale.ENGLISH).replace(".TIS", ".PNG")));
if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
retVal = fc.getSelectedFile().toPath();
if (!Files.exists(retVal)) {
final String options[] = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(parent, retVal + " exists. Overwrite?", "Export resource",
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE,
null, options, options[0]) != 0) {
retVal = null;
}
}
}
return retVal;
}
private void initTileset()
{
try {
WindowBlocker.blockWindow(true);
decoder = TisDecoder.loadTis(entry);
if (decoder != null) {
int tileCount = decoder.getTileCount();
defaultWidth = calcTileWidth(entry, tileCount);
tileImages = new ArrayList<Image>(tileCount);
for (int tileIdx = 0; tileIdx < tileCount; tileIdx++) {
BufferedImage image = ColorConvert.createCompatibleImage(64, 64, Transparency.BITMASK);
decoder.getTile(tileIdx, image);
tileImages.add(image);
}
} else {
throw new Exception("No TIS resource loaded");
}
WindowBlocker.blockWindow(false);
} catch (Exception e) {
e.printStackTrace();
WindowBlocker.blockWindow(false);
if (tileImages == null)
tileImages = new ArrayList<Image>();
if (tileImages.isEmpty())
tileImages.add(ColorConvert.createCompatibleImage(1, 1, Transparency.BITMASK));
JOptionPane.showMessageDialog(NearInfinity.getInstance(),
"Error while loading TIS resource: " + entry.getResourceName(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
// Converts the current PVRZ-based tileset into the old tileset variant.
public Status convertToPaletteTis(Path output, boolean showProgress)
{
Status retVal = Status.ERROR;
if (output != null) {
if (tileImages != null && !tileImages.isEmpty()) {
String note = "Converting tile %1$d / %2$d";
int progressIndex = 0, progressMax = decoder.getTileCount();
ProgressMonitor progress = null;
if (showProgress) {
progress = new ProgressMonitor(panel.getTopLevelAncestor(), "Converting TIS...",
String.format(note, progressIndex, progressMax), 0, progressMax);
progress.setMillisToDecideToPopup(500);
progress.setMillisToPopup(2000);
}
try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(output))) {
retVal = Status.SUCCESS;
// writing header data
byte[] header = new byte[24];
System.arraycopy("TIS V1 ".getBytes(), 0, header, 0, 8);
DynamicArray.putInt(header, 8, decoder.getTileCount());
DynamicArray.putInt(header, 12, 0x1400);
DynamicArray.putInt(header, 16, 0x18);
DynamicArray.putInt(header, 20, 0x40);
bos.write(header);
// writing tile data
int[] palette = new int[255];
int[] hclPalette = new int[255];
byte[] tilePalette = new byte[1024];
byte[] tileData = new byte[64*64];
BufferedImage image =
ColorConvert.createCompatibleImage(decoder.getTileWidth(), decoder.getTileHeight(),
Transparency.BITMASK);
IntegerHashMap<Byte> colorCache = new IntegerHashMap<Byte>(1800); // caching RGBColor -> index
for (int tileIdx = 0; tileIdx < decoder.getTileCount(); tileIdx++) {
colorCache.clear();
if (progress != null && progress.isCanceled()) {
retVal = Status.CANCELLED;
break;
}
progressIndex++;
if (progress != null && (progressIndex % 100) == 0) {
progress.setProgress(progressIndex);
progress.setNote(String.format(note, progressIndex, progressMax));
}
Graphics2D g = image.createGraphics();
try {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
g.setColor(TransparentColor);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
g.drawImage(tileImages.get(tileIdx), 0, 0, null);
} finally {
g.dispose();
g = null;
}
int[] pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
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 < tileData.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 {
retVal = Status.ERROR;
break;
}
bos.write(tilePalette);
bos.write(tileData);
}
image.flush(); image = null;
tileData = null; tilePalette = null; hclPalette = null; palette = null;
} catch (Exception e) {
retVal = Status.ERROR;
e.printStackTrace();
} finally {
if (progress != null) {
progress.close();
progress = null;
}
}
if (retVal != Status.SUCCESS && Files.isRegularFile(output)) {
try {
Files.delete(output);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return retVal;
}
// Converts the current palette-based tileset into the new PVRZ-based variant.
public Status convertToPvrzTis(Path output, boolean showProgress)
{
Status retVal = Status.ERROR;
if (output != null) {
try {
ProgressMonitor progress = null;
if (showProgress) {
progress = new ProgressMonitor(panel.getTopLevelAncestor(),
"Converting TIS...", "Preparing TIS", 0, 5);
progress.setMillisToDecideToPopup(0);
progress.setMillisToPopup(0);
}
int numTiles = decoder.getTileCount();
int tilesPerRow = getDefaultTilesPerRow();
List<ConvertToTis.TileEntry> entryList = new ArrayList<ConvertToTis.TileEntry>(numTiles);
List<BinPack2D> pageList = new ArrayList<BinPack2D>();
try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(output))) {
// writing header data
byte[] header = new byte[24];
System.arraycopy("TIS V1 ".getBytes(), 0, header, 0, 8);
DynamicArray.putInt(header, 8, numTiles);
DynamicArray.putInt(header, 12, 0x0c);
DynamicArray.putInt(header, 16, 0x18);
DynamicArray.putInt(header, 20, 0x40);
bos.write(header);
// 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 tisWidth = tilesPerRow*tileDim;
int tisHeight = ((numTiles+tilesPerRow-1) / tilesPerRow) * tileDim;
int pw = tisWidth / pageDim + (((tisWidth % pageDim) != 0) ? 1 : 0);
int ph = tisHeight / pageDim + (((tisHeight % 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, tisWidth - x);
int h = Math.min(pageDim, tisHeight - 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*tisWidth)/(tileDim*tileDim) + x/tileDim;
for (int ty = 0; ty < space.height; ty++, tileIdx += tisWidth/tileDim) {
for (int tx = 0; tx < space.width; tx++) {
// marking page index as incomplete
if (tileIdx + tx < numTiles) {
ConvertToTis.TileEntry entry =
new ConvertToTis.TileEntry(tileIdx + tx, pageIdx,
(rectMatch.x + tx)*tileDim,
(rectMatch.y + ty)*tileDim);
entryList.add(entry);
}
}
}
}
}
// writing TIS entries
Collections.sort(entryList, ConvertToTis.TileEntry.CompareByIndex);
for (int i = 0; i < entryList.size(); i++) {
ConvertToTis.TileEntry entry = entryList.get(i);
bos.write(DynamicArray.convertInt(entry.page));
bos.write(DynamicArray.convertInt(entry.x));
bos.write(DynamicArray.convertInt(entry.y));
}
// generating PVRZ files
retVal = writePvrzPages(output, pageList, entryList, progress);
} finally {
if (progress != null) {
progress.close();
progress = null;
}
}
} catch (Exception e) {
retVal = Status.ERROR;
e.printStackTrace();
}
if (retVal != Status.SUCCESS && Files.isRegularFile(output)) {
try {
Files.delete(output);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return retVal;
}
// Converts the tileset into the PNG format.
public Status exportPNG(Path output, boolean showProgress)
{
Status retVal = Status.ERROR;
if (output != null) {
if (tileImages != null && !tileImages.isEmpty()) {
int tilesX = tileGrid.getTileColumns();
int tilesY = tileGrid.getTileRows();
if (tilesX > 0 && tilesY > 0) {
BufferedImage image = null;
ProgressMonitor progress = null;
if (showProgress) {
progress = new ProgressMonitor(panel.getTopLevelAncestor(), "Exporting TIS to PNG...", "", 0, 2);
progress.setMillisToDecideToPopup(0);
progress.setMillisToPopup(0);
progress.setProgress(0);
}
image = ColorConvert.createCompatibleImage(tilesX*64, tilesY*64, Transparency.BITMASK);
Graphics2D g = image.createGraphics();
for (int idx = 0; idx < tileImages.size(); idx++) {
if (tileImages.get(idx) != null) {
int tx = idx % tilesX;
int ty = idx / tilesX;
g.drawImage(tileImages.get(idx), tx*64, ty*64, null);
}
}
g.dispose();
if (progress != null) {
progress.setProgress(1);
}
try (OutputStream os = StreamUtils.getOutputStream(output, true)) {
if (ImageIO.write(image, "png", os)) {
retVal = Status.SUCCESS;
}
} catch (IOException e) {
retVal = Status.ERROR;
e.printStackTrace();
}
if (progress != null && progress.isCanceled()) {
retVal = Status.CANCELLED;
}
if (progress != null) {
progress.close();
progress = null;
}
}
if (retVal != Status.SUCCESS && Files.isRegularFile(output)) {
try {
Files.delete(output);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return retVal;
}
// Generates PVRZ files based on the current TIS resource and the specified parameters
private Status writePvrzPages(Path tisFile, List<BinPack2D> pageList,
List<ConvertToTis.TileEntry> entryList, ProgressMonitor progress)
{
Status retVal = Status.SUCCESS;
DxtEncoder.DxtType dxtType = DxtEncoder.DxtType.DXT1;
int dxtCode = 7; // PVR code for DXT1
byte[] output = new byte[DxtEncoder.calcImageSize(1024, 1024, dxtType)];
String note = "Generating PVRZ file %1$s / %2$s";
if (progress != null) {
progress.setMaximum(pageList.size() + 1);
progress.setProgress(1);
}
try {
for (int pageIdx = 0; pageIdx < pageList.size(); pageIdx++) {
if (progress != null) {
if (progress.isCanceled()) {
retVal = Status.CANCELLED;
return retVal;
}
progress.setProgress(pageIdx + 1);
progress.setNote(String.format(note, pageIdx+1, pageList.size()));
}
Path pvrzFile = generatePvrzFileName(tisFile, pageIdx);
BinPack2D packer = pageList.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.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
try {
g.setBackground(new Color(0, true));
g.setColor(Color.BLACK);
g.fillRect(0, 0, texture.getWidth(), texture.getHeight());
for (final ConvertToTis.TileEntry entry: entryList) {
if (entry.page == pageIdx) {
Image tileImg = decoder.getTile(entry.tileIndex);
int dx = entry.x, dy = entry.y;
g.drawImage(tileImg, dx, dy, dx+64, dy+64, 0, 0, 64, 64, null);
}
}
} finally {
g.dispose();
g = null;
}
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
try (BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(pvrzFile))) {
bos.write(pvrz);
} catch (IOException e) {
retVal = Status.ERROR;
e.printStackTrace();
return retVal;
}
pvrz = null;
} catch (Exception e) {
retVal = Status.ERROR;
e.printStackTrace();
return retVal;
}
}
} finally {
// cleaning up
if (retVal != Status.SUCCESS) {
for (int i = 0; i < pageList.size(); i++) {
Path pvrzFile = generatePvrzFileName(tisFile, i);
if (pvrzFile != null && Files.isRegularFile(pvrzFile)) {
try {
Files.delete(pvrzFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
return retVal;
}
// Generates PVRZ filename with full path from the given parameters
private Path generatePvrzFileName(Path tisFile, int page)
{
if (tisFile != null) {
Path path = tisFile.getParent();
String tisName = tisFile.getFileName().toString();
int extOfs = tisName.lastIndexOf('.');
if (extOfs > 0) {
tisName = tisName.substring(0, extOfs);
}
if (Pattern.matches(".{2,7}", tisName)) {
String pvrzName = String.format("%1$s%2$s%3$02d.PVRZ", tisName.substring(0, 1),
tisName.substring(2, tisName.length()), page);
return path.resolve(pvrzName);
}
}
return null;
}
// Returns true only if TIS filename can be used to generate PVRZ filenames from
public static boolean isTisFileNameValid(Path fileName)
{
if (fileName != null) {
String name = fileName.getFileName().toString();
int extOfs = name.lastIndexOf('.');
if (extOfs >= 0) {
name = name.substring(0, extOfs);
}
return Pattern.matches(".{2,7}", name);
}
return false;
}
// Attempts to fix the specified filename to make it compatible with the naming scheme of TIS V2 files
public static Path makeTisFileNameValid(Path fileName)
{
if (fileName != null && !isTisFileNameValid(fileName)) {
Path path = fileName.getParent();
String name = fileName.getFileName().toString();
String ext = "";
int extOfs = name.lastIndexOf('.');
if (extOfs >= 0) {
ext = name.substring(extOfs);
name = name.substring(0, extOfs);
}
boolean isNight = (Character.toUpperCase(name.charAt(name.length() - 1)) == 'N');
if (name.length() > 7) {
int numDelete = name.length() - 7;
int ofsDelete = name.length() - numDelete - (isNight ? 1 : 0);
name = name.substring(ofsDelete, numDelete);
return path.resolve(name);
} else if (name.length() < 2) {
String fmt, newName = null;
int maxNum;
switch (name.length()) {
case 0: fmt = name + "%1$s02d"; maxNum = 99; break;
default: fmt = name + "%1$s01d"; maxNum = 9; break;
}
for (int i = 0; i < maxNum; i++) {
String s = String.format(fmt, i) + (isNight ? "N" : "") + ext;
if (!ResourceFactory.resourceExists(s)) {
newName = s;
break;
}
}
if (newName != null) {
return path.resolve(newName);
}
}
}
return fileName;
}
/**
* Attempts to calculate the TIS width from an associated WED file.
* @param entry The TIS resource entry.
* @param tileCount An optional tile count that will be used to "guess" the correct number of tiles
* per row if no associated WED resource has been found.
* @return The number of tiles per row for the specified TIS resource.
*/
public static int calcTileWidth(ResourceEntry entry, int tileCount)
{
// Try to fetch the correct width from an associated WED if available
if (entry != null) {
try {
String tisNameBase = entry.getResourceName();
if (tisNameBase.lastIndexOf('.') > 0) {
tisNameBase = tisNameBase.substring(0, tisNameBase.lastIndexOf('.'));
}
ResourceEntry wedEntry = null;
while (tisNameBase.length() >= 6) {
String wedFileName = tisNameBase + ".WED";
wedEntry = ResourceFactory.getResourceEntry(wedFileName);
if (wedEntry != null) {
break;
} else {
tisNameBase = tisNameBase.substring(0, tisNameBase.length() - 1);
}
}
if (wedEntry != null) {
ByteBuffer wed = wedEntry.getResourceBuffer();
if (wed != null) {
String sig = StreamUtils.readString(wed, 0, 8);
if (sig.equals("WED V1.3")) {
final int sizeOvl = 0x18;
int numOvl = wed.getInt(8);
int ofsOvl = wed.getInt(16);
for (int i = 0; i < numOvl; i++) {
int ofs = ofsOvl + i*sizeOvl;
String tisName = StreamUtils.readString(wed, ofs + 4, 8);
if (tisName.equalsIgnoreCase(tisNameBase)) {
int width = wed.getShort(ofs);
if (width > 0) {
return width;
}
}
}
}
}
}
} catch (Exception e) {
}
}
// If WED is not available: approximate the most commonly used aspect ratio found in TIS files
// Disadvantage: does not take extra tiles into account
return (tileCount < 9) ? tileCount : (int)(Math.sqrt(tileCount)*1.18);
}
// Calculates a Dimension structure with the correct number of columns and rows from the specified arguments
private static Dimension calcGridSize(int imageCount, int colSize)
{
if (imageCount >= 0 && colSize > 0) {
int rowSize = imageCount / colSize;
if (imageCount % colSize > 0)
rowSize++;
return new Dimension(colSize, Math.max(1, rowSize));
}
return null;
}
/** Returns whether the specified PVRZ index can be found in the current TIS resource. */
public boolean containsPvrzReference(int index)
{
boolean retVal = false;
if (index >= 0 && index <= 99) {
if (decoder instanceof TisV2Decoder) {
TisV2Decoder tisv2 = (TisV2Decoder)decoder;
for (int i = 0, count = tisv2.getTileCount(); i < count && !retVal; i++) {
retVal = (tisv2.getPvrzPage(i) == index);
}
}
}
return retVal;
}
}