// 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.BorderLayout; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Transparency; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Vector; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ProgressMonitor; import javax.swing.RootPaneContainer; import javax.swing.SwingConstants; import javax.swing.SwingWorker; import org.infinity.NearInfinity; import org.infinity.gui.ButtonPanel; import org.infinity.gui.ButtonPopupMenu; import org.infinity.gui.RenderCanvas; import org.infinity.gui.WindowBlocker; 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.DynamicArray; import org.infinity.util.IntegerHashMap; import org.infinity.util.io.StreamUtils; public class MosResource implements Resource, ActionListener, PropertyChangeListener { private static boolean enableTransparency = true; private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); private MosDecoder.Type mosType; private JMenuItem miExport, miExportMOSV1, miExportMOSC, miExportPNG; private JCheckBox cbTransparency; private RenderCanvas rcImage; private JPanel panel; private RootPaneContainer rpc; private SwingWorker<List<byte[]>, Void> workerConvert; private boolean exportCompressed; private WindowBlocker blocker; public MosResource(ResourceEntry entry) throws Exception { this.entry = entry; } //--------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == cbTransparency) { enableTransparency = cbTransparency.isSelected(); if (mosType == MosDecoder.Type.MOSV1 || mosType == MosDecoder.Type.MOSC) { WindowBlocker.blockWindow(true); try { rcImage.setImage(loadImage()); WindowBlocker.blockWindow(false); } catch (Exception e) { } WindowBlocker.blockWindow(false); } } else if (buttonPanel.getControlByType(ButtonPanel.Control.FIND_REFERENCES) == event.getSource()) { new ReferenceSearcher(entry, panel.getTopLevelAncestor()); } else if (event.getSource() == miExport) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); } else if (event.getSource() == miExportMOSV1) { if (mosType == MosDecoder.Type.MOSV2) { // create new MOS V1 from scratch blocker = new WindowBlocker(rpc); blocker.setBlocked(true); startConversion(false); } else { if (mosType == MosDecoder.Type.MOSC) { // decompress existing MOSC V1 and save as MOS V1 try { ByteBuffer buffer = entry.getResourceBuffer(); buffer = Compressor.decompress(buffer); ResourceFactory.exportResource(entry, buffer, entry.toString(), panel.getTopLevelAncestor()); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Error while exporting " + entry, "Error", JOptionPane.ERROR_MESSAGE); } } } } else if (event.getSource() == miExportMOSC) { if (mosType == MosDecoder.Type.MOSV2) { // create new MOSC V1 from scratch blocker = new WindowBlocker(rpc); blocker.setBlocked(true); startConversion(true); } else if (mosType == MosDecoder.Type.MOSV1) { // compress existing MOS V1 and save as MOSC V1 try { ByteBuffer buffer = entry.getResourceBuffer(); buffer = Compressor.compress(buffer, "MOSC", "V1 "); ResourceFactory.exportResource(entry, buffer, entry.toString(), panel.getTopLevelAncestor()); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Error while exporting " + entry, "Error", JOptionPane.ERROR_MESSAGE); } } } else if (event.getSource() == miExportPNG) { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); String fileName = entry.toString().replace(".MOS", ".PNG"); boolean bRet = false; WindowBlocker.blockWindow(true); try { BufferedImage image = getImage(); bRet = ImageIO.write(image, "png", os); image = null; } finally { WindowBlocker.blockWindow(false); } if (bRet) { ResourceFactory.exportResource(entry, StreamUtils.getByteBuffer(os.toByteArray()), fileName, panel.getTopLevelAncestor()); } else { JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Error while exporting " + entry, "Error", JOptionPane.ERROR_MESSAGE); } os.close(); os = null; } catch (Exception e) { e.printStackTrace(); } } } //--------------------- 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; } byte[] mosData = null; try { List<byte[]> l = workerConvert.get(); if (l != null && !l.isEmpty()) { mosData = l.get(0); l.clear(); l = null; } } catch (Exception e) { e.printStackTrace(); } if (mosData != null) { if (mosData.length > 0) { ResourceFactory.exportResource(entry, StreamUtils.getByteBuffer(mosData), entry.toString(), panel.getTopLevelAncestor()); } else { JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Export has been cancelled." + entry, "Information", JOptionPane.INFORMATION_MESSAGE); } mosData = null; } else { JOptionPane.showMessageDialog(panel.getTopLevelAncestor(), "Error while exporting " + entry, "Error", JOptionPane.ERROR_MESSAGE); } } } } //--------------------- End Interface PropertyChangeListener --------------------- //--------------------- 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(); } mosType = MosDecoder.getType(entry); ((JButton)buttonPanel.addControl(ButtonPanel.Control.FIND_REFERENCES)).addActionListener(this); miExport = new JMenuItem("original"); miExport.addActionListener(this); miExportPNG = new JMenuItem("as PNG"); miExportPNG.addActionListener(this); if (mosType == MosDecoder.Type.MOSV2) { miExportMOSV1 = new JMenuItem("as MOS V1 (uncompressed)"); miExportMOSV1.addActionListener(this); miExportMOSC = new JMenuItem("as MOS V1 (compressed)"); miExportMOSC.addActionListener(this); } else { if (mosType == MosDecoder.Type.MOSC) { miExportMOSV1 = new JMenuItem("decompressed"); miExportMOSV1.addActionListener(this); } else { if (Profile.getEngine() == Profile.Engine.BG2 || Profile.isEnhancedEdition()) { miExportMOSC = new JMenuItem("compressed"); miExportMOSC.addActionListener(this); } } } List<JMenuItem> list = new ArrayList<JMenuItem>(); if (miExport != null) list.add(miExport); if (miExportMOSV1 != null) list.add(miExportMOSV1); if (miExportMOSC != null) list.add(miExportMOSC); 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); } ButtonPopupMenu bpmExport = (ButtonPopupMenu)buttonPanel.addControl(ButtonPanel.Control.EXPORT_MENU); bpmExport.setMenuItems(mi); rcImage = new RenderCanvas(); rcImage.setHorizontalAlignment(SwingConstants.CENTER); rcImage.setVerticalAlignment(SwingConstants.CENTER); WindowBlocker.blockWindow(true); try { rcImage.setImage(loadImage()); WindowBlocker.blockWindow(false); } catch (Exception e) { WindowBlocker.blockWindow(false); } JScrollPane scroll = new JScrollPane(rcImage); scroll.getVerticalScrollBar().setUnitIncrement(16); scroll.getHorizontalScrollBar().setUnitIncrement(16); cbTransparency = new JCheckBox("Enable transparency", enableTransparency); cbTransparency.setEnabled(mosType == MosDecoder.Type.MOSV1 || mosType == MosDecoder.Type.MOSC); cbTransparency.setToolTipText("Affects only legacy MOS resources (MOS v1)"); cbTransparency.addActionListener(this); JPanel optionsPanel = new JPanel(); BoxLayout bl = new BoxLayout(optionsPanel, BoxLayout.Y_AXIS); optionsPanel.setLayout(bl); optionsPanel.add(cbTransparency); buttonPanel.addControl(optionsPanel); panel = new JPanel(new BorderLayout()); panel.add(scroll, BorderLayout.CENTER); panel.add(buttonPanel, BorderLayout.SOUTH); scroll.setBorder(BorderFactory.createLoweredBevelBorder()); return panel; } //--------------------- End Interface Viewable --------------------- public BufferedImage getImage() { if (rcImage != null) { return ColorConvert.toBufferedImage(rcImage.getImage(), true); } else if (entry != null) { return loadImage(); } return null; } private BufferedImage loadImage() { BufferedImage image = null; mosType = MosDecoder.getType(entry); if (mosType != MosDecoder.Type.INVALID) { MosDecoder decoder = null; if (entry != null) { try { decoder = MosDecoder.loadMos(entry); if (decoder instanceof MosV1Decoder) { ((MosV1Decoder)decoder).setTransparencyEnabled(enableTransparency); } mosType = decoder.getType(); image = ColorConvert.toBufferedImage(decoder.getImage(), true); decoder.close(); decoder = null; } catch (Exception e) { e.printStackTrace(); if (decoder != null) { decoder.close(); } image = null; } } } else { image = ColorConvert.createCompatibleImage(1, 1, true); } return image; } // Creates a new MOS V1 or MOSC V1 resource from scratch. DO NOT call directly! private byte[] convertToMosV1(boolean compressed) throws Exception { byte[] buf = null; if (rcImage != null && rcImage.getImage() != null) { // preparing source image Image img = rcImage.getImage(); BufferedImage srcImage = ColorConvert.createCompatibleImage(img.getWidth(null), img.getHeight(null), Transparency.BITMASK); Graphics2D g = srcImage.createGraphics(); g.drawImage(getImage(), 0, 0, null); g.dispose(); g = null; // preparing MOS V1 header int width = srcImage.getWidth(); int height = srcImage.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; buf = new byte[dataOfs + width*height]; System.arraycopy("MOS V1 ".getBytes(), 0, buf, 0, 8); DynamicArray.putShort(buf, 8, (short)width); DynamicArray.putShort(buf, 10, (short)height); DynamicArray.putShort(buf, 12, (short)cols); DynamicArray.putShort(buf, 14, (short)rows); DynamicArray.putInt(buf, 16, 64); DynamicArray.putInt(buf, 20, palOfs); String note = "Converting tile %1$d / %2$d"; int progressIndex = 0, progressMax = tileCount; ProgressMonitor progress = new ProgressMonitor(panel.getTopLevelAncestor(), "Converting MOS...", String.format(note, progressIndex, progressMax), 0, progressMax); progress.setMillisToDecideToPopup(500); progress.setMillisToPopup(2000); // 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]; srcImage.getRGB(tileX, tileY, tileW, tileH, rgbArray, 0, tileW); tileList.add(rgbArray); } } srcImage.flush(); srcImage = null; // 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 (progress.isCanceled()) { buf = new byte[0]; break; } 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 { buf = null; break; } System.arraycopy(tilePalette, 0, buf, curPalOfs, 1024); curPalOfs += 1024; DynamicArray.putInt(buf, curTableOfs, curDataOfs - dataOfs); curTableOfs += 4; System.arraycopy(tileData, 0, buf, 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) { if (buf != null) { buf = Compressor.compress(buf, "MOSC", "V1 "); } } progress.close(); } return buf; } // Starts the worker thread for MOS conversion private void startConversion(boolean compressed) { exportCompressed = compressed; workerConvert = new SwingWorker<List<byte[]>, Void>() { @Override public List<byte[]> doInBackground() { List<byte[]> list = new Vector<byte[]>(1); try { byte[] buf = convertToMosV1(exportCompressed); if (buf != null) { list.add(buf); } } catch (Exception e) { e.printStackTrace(); } return list; } }; workerConvert.addPropertyChangeListener(this); workerConvert.execute(); } /** Returns whether the specified PVRZ index can be found in the current MOS resource. */ public boolean containsPvrzReference(int index) { boolean retVal = false; if (index >= 0 && index <= 99999) { try { InputStream is = entry.getResourceDataAsStream(); if (is != null) { try { // parsing resource header byte[] sig = new byte[8]; byte[] buf = new byte[16]; long len; long curOfs = 0; if ((len = is.read(sig)) != sig.length) throw new Exception(); if (!"MOS V2 ".equals(DynamicArray.getString(sig, 0, 8))) throw new Exception(); curOfs += len; if ((len = is.read(buf)) != buf.length) throw new Exception(); curOfs += len; int numBlocks = DynamicArray.getInt(buf, 8); int ofsBlocks = DynamicArray.getInt(buf, 12); curOfs = ofsBlocks - curOfs; if (curOfs > 0) { do { len = is.skip(curOfs); if (len <= 0) throw new Exception(); curOfs -= len; } while (curOfs > 0); } // parsing blocks buf = new byte[28]; for (int i = 0; i < numBlocks && !retVal; i++) { if (is.read(buf) != buf.length) throw new Exception(); int curIndex = DynamicArray.getInt(buf, 0); retVal = (curIndex == index); } } finally { is.close(); is = null; } } } catch (Exception e) { } } return retVal; } }