// 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 tv.porst.jhexview.DataChangedEvent; import tv.porst.jhexview.IDataChangedListener; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferInt; import java.awt.image.IndexColorModel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Locale; 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.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTabbedPane; import javax.swing.JToggleButton; import javax.swing.RootPaneContainer; import javax.swing.SwingConstants; import javax.swing.SwingWorker; import javax.swing.Timer; 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.ChildFrame; import org.infinity.gui.RenderCanvas; import org.infinity.gui.WindowBlocker; import org.infinity.gui.converter.ConvertToBam; import org.infinity.gui.hexview.GenericHexViewer; import org.infinity.icon.Icons; 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.Writeable; import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.FileResourceEntry; 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.FileManager; import org.infinity.util.io.StreamUtils; public class BamResource implements Resource, Closeable, Writeable, ActionListener, PropertyChangeListener, ChangeListener, IDataChangedListener { private static final Color TransparentColor = new Color(0, true); private static final int ANIM_DELAY = 1000 / 15; // 15 fps in milliseconds private static final ButtonPanel.Control CtrlNextCycle = ButtonPanel.Control.CUSTOM_1; private static final ButtonPanel.Control CtrlPrevCycle = ButtonPanel.Control.CUSTOM_2; private static final ButtonPanel.Control CtrlNextFrame = ButtonPanel.Control.CUSTOM_3; private static final ButtonPanel.Control CtrlPrevFrame = ButtonPanel.Control.CUSTOM_4; private static final ButtonPanel.Control CtrlPlay = ButtonPanel.Control.CUSTOM_5; private static final ButtonPanel.Control CtrlCycleLabel = ButtonPanel.Control.CUSTOM_6; private static final ButtonPanel.Control CtrlFrameLabel = ButtonPanel.Control.CUSTOM_7; private static final ButtonPanel.Control BamEdit = ButtonPanel.Control.CUSTOM_8; private static boolean transparencyEnabled = true; private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); private final ButtonPanel buttonControlPanel = new ButtonPanel(); private BamDecoder decoder; private BamDecoder.BamControl bamControl; private JTabbedPane tabbedPane; private GenericHexViewer hexViewer; private JMenuItem miExport, miExportBAM, miExportBAMC, miExportFramesPNG; private RenderCanvas rcDisplay; private JCheckBox cbTransparency; private JPanel panelMain, panelRaw; private int curCycle, curFrame; private Timer timer; private RootPaneContainer rpc; private SwingWorker<List<byte[]>, Void> workerConvert; private boolean exportCompressed; private WindowBlocker blocker; public BamResource(ResourceEntry entry) { this.entry = entry; WindowBlocker.blockWindow(true); try { decoder = BamDecoder.loadBam(entry); bamControl = decoder.createControl(); bamControl.setMode(BamDecoder.BamControl.Mode.SHARED); if (bamControl instanceof BamV1Decoder.BamV1Control) { ((BamV1Decoder.BamV1Control)bamControl).setTransparencyEnabled(transparencyEnabled); ((BamV1Decoder.BamV1Control)bamControl).setTransparencyMode(BamV1Decoder.TransparencyMode.NORMAL); } } catch (Throwable t) { t.printStackTrace(); decoder = null; } WindowBlocker.blockWindow(false); } //--------------------- Begin Interface Closeable --------------------- @Override public void close() throws Exception { if (isRawModified()) { Path output = null; if (entry instanceof BIFFResourceEntry) { output = FileManager.query(Profile.getRootFolders(), Profile.getOverrideFolderName(), entry.toString()); } else if (entry instanceof FileResourceEntry) { output = entry.getActualPath(); } if (output != null) { final String options[] = {"Save changes", "Discard changes", "Cancel"}; int result = JOptionPane.showOptionDialog(panelMain, "Save changes to " + output.toString(), "Resource changed", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (result == 0) { ResourceFactory.saveResource(this, panelMain.getTopLevelAncestor()); } else if (result != 1) { throw new Exception("Save aborted"); } } } } //--------------------- End Interface Closeable --------------------- //--------------------- Begin Interface Writeable --------------------- @Override public void write(OutputStream os) throws IOException { StreamUtils.writeBytes(os, hexViewer.getData()); } //--------------------- End Interface Writeable --------------------- //--------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (buttonControlPanel.getControlByType(CtrlPrevCycle) == event.getSource()) { curCycle--; bamControl.setSharedPerCycle(curCycle >= 0); bamControl.cycleSet(curCycle); updateCanvasSize(); if (timer != null && timer.isRunning() && bamControl.cycleFrameCount() == 0) { timer.stop(); ((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).setSelected(false); } curFrame = 0; showFrame(); } else if (buttonControlPanel.getControlByType(CtrlNextCycle) == event.getSource()) { curCycle++; bamControl.setSharedPerCycle(curCycle >= 0); bamControl.cycleSet(curCycle); updateCanvasSize(); if (timer != null && timer.isRunning() && bamControl.cycleFrameCount() == 0) { timer.stop(); ((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).setSelected(false); } curFrame = 0; showFrame(); } else if (buttonControlPanel.getControlByType(CtrlPrevFrame) == event.getSource()) { curFrame--; showFrame(); } else if (buttonControlPanel.getControlByType(CtrlNextFrame) == event.getSource()) { curFrame++; showFrame(); } else if (buttonControlPanel.getControlByType(CtrlPlay) == event.getSource()) { if (((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).isSelected()) { if (timer == null) { timer = new Timer(ANIM_DELAY, this); } timer.restart(); } else { if (timer != null) { timer.stop(); } } } else if (buttonPanel.getControlByType(ButtonPanel.Control.FIND_REFERENCES) == event.getSource()) { new ReferenceSearcher(entry, panelMain.getTopLevelAncestor()); } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE) == event.getSource()) { if (ResourceFactory.saveResource(this, panelMain.getTopLevelAncestor())) { setRawModified(false); } } else if (event.getSource() == timer) { if (curCycle >= 0) { curFrame = (curFrame + 1) % bamControl.cycleFrameCount(); } else { curFrame = (curFrame + 1) % decoder.frameCount(); } showFrame(); } else if (event.getSource() == cbTransparency) { setTransparencyEnabled(cbTransparency.isSelected()); } else if (event.getSource() == miExport) { ResourceFactory.exportResource(entry, panelMain.getTopLevelAncestor()); } else if (event.getSource() == miExportBAM) { if (decoder != null) { if (decoder.getType() == BamDecoder.Type.BAMV2) { // create new BAM V1 from scratch if (checkCompatibility(panelMain.getTopLevelAncestor())) { blocker = new WindowBlocker(rpc); blocker.setBlocked(true); startConversion(false); } } else { // decompress existing BAMC V1 and save as BAM V1 try { ByteBuffer buffer = Compressor.decompress(entry.getResourceBuffer()); ResourceFactory.exportResource(entry, buffer, entry.toString(), panelMain.getTopLevelAncestor()); } catch (Exception e) { e.printStackTrace(); } } } } else if (event.getSource() == miExportBAMC) { if (decoder != null) { if (decoder.getType() == BamDecoder.Type.BAMV2) { // create new BAMC V1 from scratch if (checkCompatibility(panelMain.getTopLevelAncestor())) { blocker = new WindowBlocker(rpc); blocker.setBlocked(true); startConversion(true); } } else { // compress existing BAM V1 and save as BAMC V1 try { ByteBuffer buffer = Compressor.compress(entry.getResourceBuffer(), "BAMC", "V1 "); ResourceFactory.exportResource(entry, buffer, entry.toString(), panelMain.getTopLevelAncestor()); } catch (Exception e) { e.printStackTrace(); } } } } else if (event.getSource() == miExportFramesPNG) { JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile()); fc.setDialogTitle("Export BAM frames"); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.setSelectedFile(new File(fc.getCurrentDirectory(), entry.toString().replace(".BAM", ""))); // Output graphics format depends on BAM type while (fc.getChoosableFileFilters().length > 0) { // removing default filter entries fc.removeChoosableFileFilter(fc.getChoosableFileFilters()[0]); } fc.addChoosableFileFilter(new FileNameExtensionFilter("PNG files (*.png)", "PNG")); if (decoder.getType() == BamDecoder.Type.BAMC || decoder.getType() == BamDecoder.Type.BAMV1) { fc.addChoosableFileFilter(new FileNameExtensionFilter("BMP files (*.bmp)", "BMP")); } fc.setFileFilter(fc.getChoosableFileFilters()[0]); if (fc.showSaveDialog(panelMain.getTopLevelAncestor()) == JFileChooser.APPROVE_OPTION) { Path filePath = fc.getSelectedFile().toPath().getParent(); String fileName = fc.getSelectedFile().getName(); String fileExt = null; String format = ((FileNameExtensionFilter)fc.getFileFilter()).getExtensions()[0].toLowerCase(Locale.ENGLISH); int extIdx = fileName.lastIndexOf('.'); if (extIdx > 0) { fileExt = fileName.substring(extIdx); fileName = fileName.substring(0, extIdx); } else { fileExt = "." + ((FileNameExtensionFilter)fc.getFileFilter()).getExtensions()[0]; } exportFrames(filePath, fileName, fileExt, format); } } else if (buttonPanel.getControlByType(BamEdit) == event.getSource()) { ConvertToBam dlg = (ConvertToBam)ChildFrame.getFirstFrame(ConvertToBam.class); if (dlg == null) { dlg = new ConvertToBam(entry); } else { dlg.setVisible(true); dlg.framesImportBam(entry); } } } //--------------------- 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[] bamData = null; try { List<byte[]> l = workerConvert.get(); if (l != null && !l.isEmpty()) { bamData = l.get(0); l.clear(); l = null; } } catch (Exception e) { e.printStackTrace(); } if (bamData != null) { if (bamData.length > 0) { ResourceFactory.exportResource(entry, StreamUtils.getByteBuffer(bamData), entry.toString(), panelMain.getTopLevelAncestor()); } else { JOptionPane.showMessageDialog(panelMain.getTopLevelAncestor(), "Export has been cancelled." + entry, "Information", JOptionPane.INFORMATION_MESSAGE); } bamData = null; } else { JOptionPane.showMessageDialog(panelMain.getTopLevelAncestor(), "Error while exporting " + entry, "Error", JOptionPane.ERROR_MESSAGE); } } } } //--------------------- End Interface PropertyChangeListener --------------------- //--------------------- Begin Interface ChangeListener --------------------- @Override public void stateChanged(ChangeEvent event) { if (event.getSource() == tabbedPane) { if (tabbedPane.getSelectedComponent() == panelRaw) { // lazy initialization of hex viewer if (hexViewer == null) { // confirm action when opening first time int ret = JOptionPane.showConfirmDialog(panelMain, "Editing BAM resources directly may result in corrupt data. " + "Open hex editor?", "Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (ret == JOptionPane.YES_OPTION) { try { WindowBlocker.blockWindow(true); hexViewer = new GenericHexViewer(entry); hexViewer.addDataChangedListener(this); hexViewer.setCurrentOffset(0L); panelRaw.add(hexViewer, BorderLayout.CENTER); } catch (Exception e) { e.printStackTrace(); } finally { WindowBlocker.blockWindow(false); } } else { tabbedPane.setSelectedIndex(0); return; } } // stop playback if (((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).isSelected()) { if (timer != null) { timer.stop(); } ((JToggleButton)buttonControlPanel.getControlByType(CtrlPlay)).setSelected(false); } hexViewer.requestFocusInWindow(); } } } //--------------------- End Interface ChangeListener --------------------- //--------------------- Begin Interface IDataChangedListener --------------------- @Override public void dataChanged(DataChangedEvent event) { setRawModified(true); } //--------------------- End Interface IDataChangedListener --------------------- //--------------------- 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(); } // creating "View" tab Dimension dim = (decoder != null) ? bamControl.getSharedDimension() : new Dimension(1, 1); rcDisplay = new RenderCanvas(new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB)); rcDisplay.setHorizontalAlignment(SwingConstants.CENTER); rcDisplay.setVerticalAlignment(SwingConstants.CENTER); JButton bFind = (JButton)ButtonPanel.createControl(ButtonPanel.Control.FIND_REFERENCES); bFind.addActionListener(this); JToggleButton bPlay = new JToggleButton("Play", Icons.getIcon(Icons.ICON_PLAY_16)); bPlay.addActionListener(this); JLabel lCycle = new JLabel("", JLabel.CENTER); JButton bPrevCycle = new JButton(Icons.getIcon(Icons.ICON_BACK_16)); bPrevCycle.setMargin(new Insets(bPrevCycle.getMargin().top, 2, bPrevCycle.getMargin().bottom, 2)); bPrevCycle.addActionListener(this); JButton bNextCycle = new JButton(Icons.getIcon(Icons.ICON_FORWARD_16)); bNextCycle.setMargin(bPrevCycle.getMargin()); bNextCycle.addActionListener(this); JLabel lFrame = new JLabel("", JLabel.CENTER); JButton bPrevFrame = new JButton(Icons.getIcon(Icons.ICON_BACK_16)); bPrevFrame.setMargin(bPrevCycle.getMargin()); bPrevFrame.addActionListener(this); JButton bNextFrame = new JButton(Icons.getIcon(Icons.ICON_FORWARD_16)); bNextFrame.setMargin(bPrevCycle.getMargin()); bNextFrame.addActionListener(this); cbTransparency = new JCheckBox("Enable transparency", transparencyEnabled); if (decoder != null) { cbTransparency.setEnabled(decoder.getType() != BamDecoder.Type.BAMV2); } cbTransparency.setToolTipText("Affects only legacy BAM resources (BAM v1)"); cbTransparency.addActionListener(this); JPanel optionsPanel = new JPanel(); BoxLayout bl = new BoxLayout(optionsPanel, BoxLayout.Y_AXIS); optionsPanel.setLayout(bl); optionsPanel.add(cbTransparency); buttonControlPanel.addControl(lCycle, CtrlCycleLabel); buttonControlPanel.addControl(bPrevCycle, CtrlPrevCycle); buttonControlPanel.addControl(bNextCycle, CtrlNextCycle); buttonControlPanel.addControl(lFrame, CtrlFrameLabel); buttonControlPanel.addControl(bPrevFrame, CtrlPrevFrame); buttonControlPanel.addControl(bNextFrame, CtrlNextFrame); buttonControlPanel.addControl(bPlay, CtrlPlay); buttonControlPanel.add(optionsPanel); buttonControlPanel.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); JPanel pView = new JPanel(new BorderLayout()); pView.add(rcDisplay, BorderLayout.CENTER); pView.add(buttonControlPanel, BorderLayout.SOUTH); // creating "Raw" tab (stub) panelRaw = new JPanel(new BorderLayout()); hexViewer = null; // creating main panel miExport = new JMenuItem("original"); miExport.addActionListener(this); if (decoder != null) { if (decoder.getType() == BamDecoder.Type.BAMC) { miExportBAM = new JMenuItem("decompressed"); miExportBAM.addActionListener(this); } else if (decoder.getType() == BamDecoder.Type.BAMV1 && Profile.getEngine() == Profile.Engine.PST) { miExportBAMC = new JMenuItem("compressed"); miExportBAMC.addActionListener(this); } else if (decoder.getType() == BamDecoder.Type.BAMV2) { miExportBAM = new JMenuItem("as BAM V1 (uncompressed)"); miExportBAM.addActionListener(this); miExportBAM.setEnabled(decoder.frameCount() < 65536 && bamControl.cycleCount() < 256); miExportBAMC = new JMenuItem("as BAM V1 (compressed)"); miExportBAMC.addActionListener(this); miExportBAMC.setEnabled(decoder.frameCount() < 65536 && bamControl.cycleCount() < 256); } miExportFramesPNG = new JMenuItem("all frames as graphics"); miExportFramesPNG.addActionListener(this); } List<JMenuItem> list = new ArrayList<JMenuItem>(); if (miExport != null) { list.add(miExport); } if (miExportBAM != null) { list.add(miExportBAM); } if (miExportBAMC != null) { list.add(miExportBAMC); } if (miExportFramesPNG != null) { list.add(miExportFramesPNG); } JMenuItem[] mi = new JMenuItem[list.size()]; for (int i = 0; i < mi.length; i++) { mi[i] = list.get(i); } ButtonPopupMenu bpmExport = (ButtonPopupMenu)ButtonPanel.createControl(ButtonPanel.Control.EXPORT_MENU); bpmExport.setMenuItems(mi); JButton bEdit = new JButton("Edit BAM", Icons.getIcon(Icons.ICON_APPLICATION_16)); bEdit.setToolTipText("Opens resource in BAM Converter."); bEdit.addActionListener(this); buttonPanel.addControl(bFind, ButtonPanel.Control.FIND_REFERENCES); buttonPanel.addControl(bpmExport, ButtonPanel.Control.EXPORT_MENU); ((JButton)buttonPanel.addControl(ButtonPanel.Control.SAVE)).addActionListener(this); buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(false); buttonPanel.addControl(bEdit, BamEdit); buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); tabbedPane = new JTabbedPane(JTabbedPane.TOP); tabbedPane.setBorder(BorderFactory.createEmptyBorder()); tabbedPane.addTab("View", pView); tabbedPane.addTab("Raw", panelRaw); tabbedPane.setSelectedIndex(0); tabbedPane.addChangeListener(this); panelMain = new JPanel(new BorderLayout()); panelMain.add(tabbedPane, BorderLayout.CENTER); panelMain.add(buttonPanel, BorderLayout.SOUTH); showFrame(); return panelMain; } //--------------------- End Interface Viewable --------------------- private boolean viewerInitialized() { return (panelMain != null && rcDisplay != null); } public boolean isTransparencyEnabled() { return transparencyEnabled; } public void setTransparencyEnabled(boolean enable) { transparencyEnabled = enable; if (bamControl != null && bamControl instanceof BamV1Decoder.BamV1Control) { ((BamV1Decoder.BamV1Control)bamControl).setTransparencyEnabled(transparencyEnabled); showFrame(); } } public int getFrameCount() { int retVal = 0; if (decoder != null ) { retVal = decoder.frameCount(); } return retVal; } public int getFrameCount(int cycleIdx) { int retVal = 0; if (decoder != null) { if (cycleIdx >= 0 && cycleIdx < bamControl.cycleCount()) { int cycle = bamControl.cycleGet(); int frame = bamControl.cycleGetFrameIndex(); bamControl.cycleSet(cycleIdx); retVal = bamControl.cycleFrameCount(); bamControl.cycleSet(cycle); bamControl.cycleSetFrameIndex(frame); } } return retVal; } public int getCycleCount() { int retVal = 0; if (decoder != null) { retVal = bamControl.cycleCount(); } return retVal; } public Image getFrame(int frameIdx) { Image image = null; if (decoder != null) { BamDecoder.BamControl.Mode oldMode = bamControl.getMode(); bamControl.setMode(BamDecoder.BamControl.Mode.INDIVIDUAL); image = decoder.frameGet(bamControl, frameIdx); bamControl.setMode(oldMode); } // must always return a valid image! if (image == null) { image = ColorConvert.createCompatibleImage(1, 1, true); } return image; } public int getFrameIndex(int cycleIdx, int frameIdx) { if (decoder != null) { return bamControl.cycleGetFrameIndexAbsolute(cycleIdx, frameIdx); } else { return 0; } } public Point getFrameCenter(int frameIdx) { Point p = new Point(); if (decoder != null) { p.x = decoder.getFrameInfo(frameIdx).getCenterX(); p.y = decoder.getFrameInfo(frameIdx).getCenterY(); } return p; } public void updateCanvasSize() { if (decoder != null && viewerInitialized()) { Dimension dim = bamControl.getSharedDimension(); rcDisplay.setImage(new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB)); updateCanvas(); } } public void updateCanvas() { if (viewerInitialized()) { BufferedImage image = (BufferedImage)rcDisplay.getImage(); Graphics2D g = image.createGraphics(); try { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); g.setColor(TransparentColor); g.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { g.dispose(); g = null; } // rendering new frame if (curCycle >= 0) { bamControl.cycleGetFrame(image); } else { if (decoder instanceof BamV1Decoder && bamControl instanceof BamV1Decoder.BamV1Control) { ((BamV1Decoder)decoder).frameGet(bamControl, curFrame, image); } else { decoder.frameGet(null, curFrame, image); } } rcDisplay.repaint(); } } private void showFrame() { if (viewerInitialized()) { if (decoder != null) { if (curCycle >= 0) { if (!bamControl.cycleSetFrameIndex(curFrame)) { bamControl.cycleReset(); curFrame = 0; } } updateCanvas(); if (curCycle >= 0) { ((JLabel)buttonControlPanel.getControlByType(CtrlCycleLabel)) .setText("Cycle: " + (curCycle + 1) + "/" + bamControl.cycleCount()); ((JLabel)buttonControlPanel.getControlByType(CtrlFrameLabel)) .setText("Frame: " + (curFrame + 1) + "/" + bamControl.cycleFrameCount()); } else { ((JLabel)buttonControlPanel.getControlByType(CtrlCycleLabel)).setText("All frames"); ((JLabel)buttonControlPanel.getControlByType(CtrlFrameLabel)) .setText("Frame: " + (curFrame + 1) + "/" + decoder.frameCount()); } buttonControlPanel.getControlByType(CtrlPrevCycle).setEnabled(curCycle > -1); buttonControlPanel.getControlByType(CtrlNextCycle).setEnabled(curCycle + 1 < bamControl.cycleCount()); buttonControlPanel.getControlByType(CtrlPrevFrame).setEnabled(curFrame > 0); if (curCycle >= 0) { buttonControlPanel.getControlByType(CtrlNextFrame).setEnabled(curFrame + 1 < bamControl.cycleFrameCount()); buttonControlPanel.getControlByType(CtrlPlay).setEnabled(bamControl.cycleFrameCount() > 1); } else { buttonControlPanel.getControlByType(CtrlNextFrame).setEnabled(curFrame + 1 < decoder.frameCount()); buttonControlPanel.getControlByType(CtrlPlay).setEnabled(decoder.frameCount() > 1); } } else { buttonControlPanel.getControlByType(CtrlPlay).setEnabled(false); buttonControlPanel.getControlByType(CtrlPrevCycle).setEnabled(false); buttonControlPanel.getControlByType(CtrlNextCycle).setEnabled(false); buttonControlPanel.getControlByType(CtrlPrevFrame).setEnabled(false); buttonControlPanel.getControlByType(CtrlNextFrame).setEnabled(false); } } } /** * Exports each frame of the BamDecoder data. * @param decoder Contains the BAM graphics data to export. * @param filePath The target path without filename. * @param fileBase The filename without path and extension. * @param fileExt The file extension * @param format The format (currently supported: BMP and PNG). * @param enableTransparency Specifies whether to consider transparent pixels. * @return A status message describing the result of the operation (can be null). */ public static String exportFrames(BamDecoder decoder, Path filePath, String fileBase, String fileExt, String format, boolean enableTransparency) { if (decoder == null) { return null; } if (filePath == null) { filePath = FileManager.resolve("").toAbsolutePath(); } if (format == null || format.isEmpty() || !("png".equalsIgnoreCase(format) || "bmp".equalsIgnoreCase(format))) { format = "png"; } int max = 0, counter = 0, failCounter = 0; try { if (decoder != null) { BamDecoder.BamControl control = decoder.createControl(); control.setMode(BamDecoder.BamControl.Mode.INDIVIDUAL); // using selected transparency mode for BAM v1 frames if (control instanceof BamV1Decoder.BamV1Control) { ((BamV1Decoder.BamV1Control)control).setTransparencyEnabled(enableTransparency); } max = decoder.frameCount(); for (int i = 0; i < decoder.frameCount(); i++) { String fileIndex = String.format("%1$05d", i); BufferedImage image = null; try { image = prepareFrameImage(decoder, i); } catch (Exception e) { } if (image != null) { decoder.frameGet(control, i, image); try { Path file = filePath.resolve(fileBase + fileIndex + fileExt); ImageIO.write(image, format, file.toFile()); counter++; } catch (IOException e) { failCounter++; System.err.println("Error writing frame #" + i); } image.flush(); image = null; } else { failCounter++; System.err.println("Skipping frame #" + i); } } } } catch (Throwable t) { } // displaying results String msg = null; if (failCounter == 0 && counter == max) { msg = String.format("All %1$d frames exported successfully.", max); } else { msg = String.format("%2$d/%1$d frame(s) exported.\n%3$d/%1$d frame(s) skipped.", max, counter, failCounter); } return msg; } // Returns a BufferedImage object in the most appropriate format for the current BAM resource private static BufferedImage prepareFrameImage(BamDecoder decoder, int frameIdx) { BufferedImage image = null; if (decoder != null && frameIdx >= 0 && frameIdx < decoder.frameCount()) { if (decoder instanceof BamV1Decoder) { // preparing palette BamV1Decoder decoderV1 = (BamV1Decoder)decoder; BamV1Decoder.BamV1Control control = decoderV1.createControl(); int[] palette = control.getPalette(); int transIndex = control.getTransparencyIndex(); boolean hasAlpha = control.isAlphaEnabled(); IndexColorModel cm = new IndexColorModel(8, 256, palette, 0, hasAlpha, transIndex, DataBuffer.TYPE_BYTE); image = new BufferedImage(decoder.getFrameInfo(frameIdx).getWidth(), decoder.getFrameInfo(frameIdx).getHeight(), BufferedImage.TYPE_BYTE_INDEXED, cm); } else { image = new BufferedImage(decoder.getFrameInfo(frameIdx).getWidth(), decoder.getFrameInfo(frameIdx).getHeight(), BufferedImage.TYPE_INT_ARGB); } } return image; } // Exports frames as graphics, specified by "format" private void exportFrames(Path filePath, String fileBase, String fileExt, String format) { String msg = null; WindowBlocker blocker = new WindowBlocker(NearInfinity.getInstance()); try { blocker.setBlocked(true); msg = exportFrames(decoder, filePath, fileBase, fileExt, format, cbTransparency.isSelected()); } finally { blocker.setBlocked(false); blocker = null; } if (msg != null) { JOptionPane.showMessageDialog(panelMain.getTopLevelAncestor(), msg, "Information", JOptionPane.INFORMATION_MESSAGE); } } // Checks current BAM (V2 only) for compatibility and shows an appropriate warning or error message private boolean checkCompatibility(Component parent) { if (parent == null) parent = NearInfinity.getInstance(); if (decoder != null && decoder.getType() == BamDecoder.Type.BAMV2) { // Compatibility: 0=OK, 1=issues, but conversion is possible, 2=conversion is not possible int compatibility = 0; int numFrames = decoder.frameCount(); int numCycles = bamControl.cycleCount(); boolean hasSemiTrans = false; int maxWidth = 0, maxHeight = 0; List<String> issues = new ArrayList<String>(10); // checking for issues BamDecoder.BamControl control = decoder.createControl(); control.setMode(BamDecoder.BamControl.Mode.INDIVIDUAL); for (int i = 0; i < numFrames; i++) { BufferedImage img = ColorConvert.toBufferedImage(decoder.frameGet(control, i), true); if (img != null) { maxWidth = Math.max(maxWidth, img.getWidth()); maxHeight = Math.max(maxHeight, img.getHeight()); if (hasSemiTrans == false) { int[] data = ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); for (int j = 0; j < data.length; j++) { int a = (data[j] >>> 24) & 0xff; if (a >= 0x20 && a <= 0xA0) { hasSemiTrans = true; break; } } } } } // number of frames > 65535 if (numFrames > 65535) { issues.add("- [critical] Number of frames exceeds the supported maximum of 65535 frames."); compatibility = Math.max(compatibility, 2); } // number of cycles > 255 if (numCycles > 255) { issues.add("- [severe] Number of cycles exceeds the supported maximum of 255 cycles. Excess cycles will be cut off."); compatibility = Math.max(compatibility, 1); } // frame dimensions > 256 pixels if (maxWidth > 256 || maxHeight > 256) { issues.add("- [severe] One or more frames are greater than 256x256 pixels. Those frames may not be visible in certain games."); compatibility = Math.max(compatibility, 1); } // semi-transparent pixels if (hasSemiTrans) { issues.add("- [medium] One or more frames contain semi-transparent pixels. The transparency information will be lost."); compatibility = Math.max(compatibility, 1); } if (compatibility == 0) { return true; } else if (compatibility == 1) { StringBuilder sb = new StringBuilder(); sb.append("The BAM resource is not fully compatible with the legacy BAM V1 format:\n"); for (final String s: issues) { sb.append(s); sb.append("\n"); } sb.append("\n"); sb.append("Do you still want to continue?"); int retVal = JOptionPane.showConfirmDialog(parent, sb.toString(), "Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); return (retVal == JOptionPane.YES_OPTION); } else { StringBuilder sb = new StringBuilder(); sb.append("The BAM resource is not compatible with the legacy BAM V1 format because of the following issues:\n"); for (final String s: issues) { sb.append(s); sb.append("\n"); } JOptionPane.showMessageDialog(parent, sb.toString(), "Error", JOptionPane.ERROR_MESSAGE); return false; } } return false; } // Creates a new BAM V1 or BAMC V1 resource from scratch. DO NOT call directly! private byte[] convertToBamV1(boolean compressed) throws Exception { if (decoder != null) { BamDecoder.BamControl control = decoder.createControl(); control.setMode(BamDecoder.BamControl.Mode.INDIVIDUAL); // max. supported number of frames and cycles int frameCount = Math.min(decoder.frameCount(), 65535); int cycleCount = Math.min(control.cycleCount(), 255); // 1. calculating global palette for all frames final int transThreshold = 0x20; boolean[] frameTransparency = new boolean[frameCount]; boolean hasTransparency = false; int totalWidth = 0, totalHeight = 0; for (int i = 0; i < frameCount; i++) { BufferedImage img = ColorConvert.toBufferedImage(decoder.frameGet(control, i), true); // getting max. dimensions if (img != null) { if (img.getHeight() > totalHeight) totalHeight = img.getHeight(); totalWidth += img.getWidth(); // frame uses transparent pixels? int[] data = ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); frameTransparency[i] = false; for (int j = 0; j < data.length; j++) { if (((data[j] >>> 24) & 0xff) < transThreshold) { frameTransparency[i] = true; hasTransparency = true; break; } } } } // creating global palette for all available frames BufferedImage composedImage = ColorConvert.createCompatibleImage(totalWidth, totalHeight, true); Graphics2D g = composedImage.createGraphics(); for (int i = 0, w = 0; i < frameCount; i++) { BufferedImage img = ColorConvert.toBufferedImage(decoder.frameGet(control, i), true); if (img != null) { g.drawImage(img, w, 0, null); w += img.getWidth(); } } g.dispose(); int[] chainedImageData = ((DataBufferInt)composedImage.getRaster().getDataBuffer()).getData(); int[] palette = ColorConvert.medianCut(chainedImageData, hasTransparency ? 255 : 256, false); int[] hclPalette = new int[palette.length]; ColorConvert.toHclPalette(palette, hclPalette); // initializing color cache IntegerHashMap<Byte> colorCache = new IntegerHashMap<Byte>(1536); for (int i = 0; i < palette.length; i++) { colorCache.put(palette[i] & 0x00ffffff, (byte)i); } // adding transparent color index to the palette if available if (hasTransparency) { int[] tmp = palette; palette = new int[tmp.length + 1]; palette[0] = 0x00ff00; // it's usually defined as RGB(0, 255, 0) System.arraycopy(tmp, 0, palette, 1, tmp.length); tmp = null; } // 2. encoding frames List<byte[]> frameList = new ArrayList<byte[]>(frameCount); int colorShift = hasTransparency ? 1 : 0; // considers transparent color index for (int i = 0; i < frameCount; i++) { if (decoder.frameGet(control, i) != null) { BufferedImage img = ColorConvert.toBufferedImage(decoder.frameGet(control, i), true); int[] srcData = ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); if (frameTransparency[i] == true) { // do RLE encoding (on transparent pixels only) byte[] dstData = new byte[img.getWidth()*img.getHeight() + (img.getWidth()*img.getHeight() + 1)/2]; int srcIdx = 0, dstIdx = 0, srcMax = img.getWidth()*img.getHeight(); while (srcIdx < srcMax) { if (((srcData[srcIdx] >>> 24) & 0xff) < transThreshold) { // transparent pixel int cnt = 0; srcIdx++; while (srcIdx < srcMax && cnt < 255 && ((srcData[srcIdx] >>> 24) & 0xff) < transThreshold) { cnt++; srcIdx++; } dstData[dstIdx++] = 0; dstData[dstIdx++] = (byte)cnt; } else { // visible pixel Byte colIdx = colorCache.get(srcData[srcIdx] & 0x00ffffff); if (colIdx != null) { dstData[dstIdx++] = (byte)(colIdx + colorShift); } else { int color = ColorConvert.nearestColor(srcData[srcIdx], hclPalette); dstData[dstIdx++] = (byte)(color + colorShift); colorCache.put(srcData[srcIdx] & 0x00ffffff, (byte)color); } srcIdx++; } } // truncating byte array byte[] tmp = dstData; dstData = new byte[dstIdx]; System.arraycopy(tmp, 0, dstData, 0, dstData.length); tmp = null; frameList.add(dstData); } else { // storing uncompressed pixel data byte[] dstData = new byte[img.getWidth()*img.getHeight()]; int idx = 0, max = dstData.length; while (idx < max) { Byte colIdx = colorCache.get(srcData[idx] & 0x00ffffff); if (colIdx != null) { dstData[idx] = (byte)(colIdx + colorShift); } else { int color = ColorConvert.nearestColor(srcData[idx], hclPalette); dstData[idx] = (byte)(color + colorShift); colorCache.put(srcData[idx] & 0x00ffffff, (byte)color); } idx++; } frameList.add(dstData); } } else { frameList.add(new byte[]{}); } } // 3. creating header structures final int frameEntrySize = 0x0c; final int cycleEntrySize = 0x04; final int paletteSize = 0x400; final int lookupTableEntrySize = 0x02; // main header byte[] header = new byte[0x18]; DynamicArray buf = DynamicArray.wrap(header, DynamicArray.ElementType.BYTE); buf.put(0x00, "BAM V1 ".getBytes()); buf.putShort(0x08, (short)frameCount); buf.putByte(0x0a, (byte)cycleCount); buf.putByte(0x0b, (byte)0); // compressed color index int frameEntryOfs = 0x18; int paletteOfs = frameEntryOfs + frameCount*frameEntrySize + cycleCount*cycleEntrySize; int lookupTableOfs = paletteOfs + paletteSize; buf.putInt(0x0c, frameEntryOfs); buf.putInt(0x10, paletteOfs); buf.putInt(0x14, lookupTableOfs); // cycle entries byte[] cycleEntryHeader = new byte[cycleCount*cycleEntrySize]; buf = DynamicArray.wrap(cycleEntryHeader, DynamicArray.ElementType.BYTE); for (int i = 0; i < cycleCount; i++) { control.cycleSet(i); buf.putShort(0x00, (short)control.cycleFrameCount()); buf.putShort(0x02, (short)control.cycleGetFrameIndexAbsolute(0)); buf.addToBaseOffset(cycleEntrySize); } // frame entries and frame lookup table byte[] frameEntryHeader = new byte[frameCount*frameEntrySize]; buf = DynamicArray.wrap(frameEntryHeader, DynamicArray.ElementType.BYTE); byte[] lookupTableHeader = new byte[frameCount*lookupTableEntrySize]; DynamicArray buf2 = DynamicArray.wrap(lookupTableHeader, DynamicArray.ElementType.BYTE); int dataOfs = lookupTableOfs + frameCount*lookupTableEntrySize; for (int i = 0; i < frameCount; i++) { buf.putShort(0x00, (short)decoder.getFrameInfo(i).getWidth()); buf.putShort(0x02, (short)decoder.getFrameInfo(i).getHeight()); buf.putShort(0x04, (short)decoder.getFrameInfo(i).getCenterX()); buf.putShort(0x06, (short)decoder.getFrameInfo(i).getCenterY()); int ofs = dataOfs & 0x7fffffff; if (frameTransparency[i] == false) ofs |= 0x80000000; buf.putInt(0x08, ofs); dataOfs += frameList.get(i).length; buf.addToBaseOffset(frameEntrySize); buf2.putShort(0x00, (short)i); buf2.addToBaseOffset(lookupTableEntrySize); } // 4. putting it all together int bamSize = dataOfs; // dataOfs should now point to the end of the data int bamOfs = 0; byte[] bamArray = new byte[bamSize]; System.arraycopy(header, 0, bamArray, bamOfs, header.length); bamOfs += header.length; System.arraycopy(frameEntryHeader, 0, bamArray, bamOfs, frameEntryHeader.length); bamOfs += frameEntryHeader.length; System.arraycopy(cycleEntryHeader, 0, bamArray, bamOfs, cycleEntryHeader.length); bamOfs += cycleEntryHeader.length; for (int i = 0; i < palette.length; i++) { System.arraycopy(DynamicArray.convertInt(palette[i]), 0, bamArray, bamOfs, 4); bamOfs += 4; } System.arraycopy(lookupTableHeader, 0, bamArray, bamOfs, lookupTableHeader.length); bamOfs += lookupTableHeader.length; for (int i = 0; i < frameList.size(); i++) { byte[] frame = frameList.get(i); if (frame != null) { System.arraycopy(frame, 0, bamArray, bamOfs, frame.length); bamOfs += frame.length; } } frameList.clear(); frameList = null; colorCache.clear(); colorCache = null; palette = null; hclPalette = null; // optionally compressing to MOSC V1 if (compressed) { if (bamArray != null) { bamArray = Compressor.compress(bamArray, "BAMC", "V1 "); } } return bamArray; } return null; } // Starts the worker thread for BAM 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 = convertToBamV1(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 BAM 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[24]; long len; long curOfs = 0; if ((len = is.read(sig)) != sig.length) throw new Exception(); if (!"BAM 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, 20); 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; } private boolean isRawModified() { if (hexViewer != null) { return hexViewer.isModified(); } return false; } private void setRawModified(boolean modified) { if (hexViewer != null) { if (!modified) { hexViewer.clearModified(); } buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(modified); } } }