// 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.BorderLayout; import java.awt.Color; import java.awt.Dialog; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.IndexColorModel; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.infinity.gui.ColorGrid; import org.infinity.gui.ViewerUtil; import org.infinity.gui.ColorGrid.MouseOverEvent; import org.infinity.resource.graphics.ColorConvert; import org.infinity.resource.graphics.PseudoBamDecoder.PseudoBamFrameEntry; import org.infinity.util.Misc; import org.infinity.util.io.StreamUtils; // TODO: change filter to display the palette in the state after applying previous filters (if any) /** * Color filter: adjust palette entries (for BAM v1 only). */ public class BamFilterColorReplace extends BamFilterBaseColor implements ActionListener { private static final String FilterName = "Replace colors (BAM v1 only)"; private static final String FilterDesc = "This filter allows you to manipulate individual palette " + "entries or replace the palette as a whole.\n" + "Note: Supports legacy BAM (v1) only."; private final PaletteDialog paletteDialog; private JButton bPalette; public static String getFilterName() { return FilterName; } public static String getFilterDesc() { return FilterDesc; } public BamFilterColorReplace(ConvertToBam parent) { super(parent, FilterName, FilterDesc); paletteDialog = new PaletteDialog(parent); } @Override public BufferedImage process(BufferedImage frame) throws Exception { return applyEffect(frame); } @Override public PseudoBamFrameEntry updatePreview(PseudoBamFrameEntry entry) { if (entry != null) { entry.setFrame(applyEffect(entry.getFrame())); } return entry; } @Override protected JPanel loadControls() { GridBagConstraints c = new GridBagConstraints(); bPalette = new JButton("Palette..."); bPalette.addActionListener(this); JPanel panel = new JPanel(new GridBagLayout()); ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); panel.add(bPalette, c); updateControls(); return panel; } @Override public void updateControls() { // supportys BAM v1 only bPalette.setEnabled(getConverter().isBamV1Selected()); } @Override public String getConfiguration() { StringBuilder sb = new StringBuilder(); sb.append(encodeColorList(paletteDialog.getPalette())); return sb.toString(); } @Override public boolean setConfiguration(String config) { if (config != null) { config = config.trim(); if (!config.isEmpty()) { String[] params = config.trim().split(";"); int[] colors = null; if (params.length > 0) { colors = decodePalette(params[0]); if (colors == null) { return false; } } if (colors != null) { paletteDialog.loadPalette(colors); } } return true; } return false; } //--------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bPalette) { paletteDialog.open(); fireChangeListener(); } } //--------------------- End Interface ActionListener --------------------- private BufferedImage applyEffect(BufferedImage srcImage) { BufferedImage dstImage = srcImage; if (srcImage != null && paletteDialog.isModified()) { if (srcImage.getType() == BufferedImage.TYPE_BYTE_INDEXED) { int width = srcImage.getWidth(); int height = srcImage.getHeight(); IndexColorModel cm = (IndexColorModel)srcImage.getColorModel(); int[] newPalette = paletteDialog.getPalette(); for (int i = 0; i < newPalette.length; i++) { newPalette[i] |= 0xff000000; } IndexColorModel cm2 = new IndexColorModel(8, 256, newPalette, 0, cm.hasAlpha(), cm.getTransparentPixel(), DataBuffer.TYPE_BYTE); dstImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, cm2); byte[] srcBuffer = ((DataBufferByte)srcImage.getRaster().getDataBuffer()).getData(); byte[] dstBuffer = ((DataBufferByte)dstImage.getRaster().getDataBuffer()).getData(); System.arraycopy(srcBuffer, 0, dstBuffer, 0, dstBuffer.length); srcBuffer = null; dstBuffer = null; } } return dstImage; } /** Parses a list of color values from a parameter string of the format "[rgb1,rgb2,...]". */ private int[] decodePalette(String param) { int[] palette = null; if (param != null && param.matches("\\[.*\\]")) { String colorString = param.substring(1, param.length() - 1).trim(); if (!colorString.isEmpty()) { String[] colors = colorString.split(","); palette = new int[colors.length]; for (int i = 0; i < colors.length; i++) { palette[i] = Misc.toNumber(colors[i], 0); } } else { palette = new int[0]; } } return palette; } //-------------------------- INNER CLASSES -------------------------- // Provides controls for modifying individual colors or the whole palette private static class PaletteDialog extends JDialog implements ActionListener, ChangeListener, FocusListener, ColorGrid.MouseOverListener { private static final String TitleClean = "Replacement palette"; private static final String TitleModified = "Replacement palette [enabled]"; private static final String FmtInfoRGB = "%1$d %2$d %3$d"; private static final String FmtInfoHexRGB = "#%1$02X%2$02X%3$02X"; private final ConvertToBam parent; private ColorGrid cgPalette; private JLabel lInfoIndex, lInfoRGB, lInfoHexRGB, lColorIndex; private JTextField tfColorRed, tfColorGreen, tfColorBlue; private JButton bClose, bLoadPalette, bResetPalette; private int currentRed, currentGreen, currentBlue; private boolean isModified; public PaletteDialog(ConvertToBam parent) { super(parent, TitleClean, Dialog.ModalityType.DOCUMENT_MODAL); this.parent = parent; init(); } /** Shows the dialog. */ public void open() { if (!isVisible()) { if (!isModified()) { resetPalette(); } setLocationRelativeTo(getOwner()); setVisible(true); toFront(); requestFocusInWindow(); } } /** Hides the dialog. */ public void close() { if (isVisible()) { if (tfColorRed.isFocusOwner()) { registerColorValue(tfColorRed); } else if (tfColorGreen.isFocusOwner()) { registerColorValue(tfColorGreen); } else if (tfColorBlue.isFocusOwner()) { registerColorValue(tfColorBlue); } setVisible(false); } } /** * Returns whether the palette contains any modified data. * (Either because an external palette has been loaded or color entries have been modified.) */ public boolean isModified() { return isModified; } /** Returns the currently defined palette as int array. */ public int[] getPalette() { int[] retVal = new int[256]; int max = Math.min(retVal.length, cgPalette.getColorCount()); for (int i = 0; i < max; i++) { retVal[i] = cgPalette.getColor(i).getRGB() & 0xffffff; } return retVal; } /** Loads the palette from the specified file resource into the color grid component. */ public void loadPalette(Path paletteFile) throws Exception { if (paletteFile != null && Files.isRegularFile(paletteFile)) { byte[] signature = new byte[8]; try (InputStream is = StreamUtils.getInputStream(paletteFile)) { is.read(signature); } catch (IOException e) { throw new Exception("Error reading from file " + paletteFile.getFileName()); } // fetching palette data int[] palette = null; if ("BM".equals(new String(signature, 0, 2))) { palette = ColorConvert.loadPaletteBMP(paletteFile); } else if ("RIFF".equals(new String(signature, 0, 4))) { palette = ColorConvert.loadPalettePAL(paletteFile); } else { String sig = new String(signature, 0, 4); String ver = new String(signature, 4, 4); if ("BAM ".equals(sig) || "BAMC".equals(sig)) { if ("V1 ".equals(ver)) { palette = ColorConvert.loadPaletteBAM(paletteFile); } else { throw new Exception(String.format("BAM file \"%1$s\" does not contain palette data.", paletteFile.getFileName())); } } else { // Photoshop ACT files don't have a header palette = ColorConvert.loadPaletteACT(paletteFile); } } // applying palette if (palette != null && palette.length > 0) { loadPalette(palette); } else { throw new Exception("No palette found in file " + paletteFile.getFileName()); } } else { throw new Exception("File does not exist."); } } /** Loads the palette from the specified color array into the color grid component. */ public void loadPalette(int[] colors) { if (colors != null && colors.length > 0) { int max = Math.min(cgPalette.getColorCount(), colors.length); for (int i = 0; i < max; i++) { cgPalette.setColor(i, new Color(colors[i], false)); } for (int i = max; i < cgPalette.getColorCount(); i++) { cgPalette.setColor(i, Color.BLACK); } setModified(); } } /** Discards the current palette and loads the default palette as defined in the ConvertToBam instance. */ public void resetPalette() { parent.getPaletteDialog().updateGeneratedPalette(); int[] colors = parent.getPaletteDialog().getPalette(parent.getPaletteDialog().getPaletteType()); if (colors != null) { int max = Math.min(cgPalette.getColorCount(), colors.length); for (int i = 0; i < max; i++) { cgPalette.setColor(i, new Color(colors[i], false)); } for (int i = max; i < cgPalette.getColorCount(); i++) { cgPalette.setColor(i, Color.BLACK); } } resetModified(); } //--------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == cgPalette) { updateColorBox(cgPalette.getSelectedIndex(), cgPalette.getSelectedColor()); } else if (event.getSource() == bClose) { close(); } else if (event.getSource() == bLoadPalette) { Path[] files = ConvertToBam.getOpenFileName(this, "Load palette from", null, false, ConvertToBam.getPaletteFilters(), 0); if (files != null && files.length > 0) { try { loadPalette(files[0]); } catch (Exception e) { JOptionPane.showMessageDialog(this, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } } else if (event.getSource() == bResetPalette) { resetPalette(); } } //--------------------- End Interface ActionListener --------------------- //--------------------- Begin Interface ChangeListener --------------------- @Override public void stateChanged(ChangeEvent event) { } //--------------------- End Interface ChangeListener --------------------- //--------------------- Begin Interface FocusListener --------------------- @Override public void focusGained(FocusEvent event) { if (event.getSource() instanceof JTextField) { ((JTextField)event.getSource()).selectAll(); } } @Override public void focusLost(FocusEvent event) { if (event.getSource() instanceof JTextField) { registerColorValue((JTextField)event.getSource()); } } //--------------------- End Interface FocusListener --------------------- //--------------------- Begin Interface MouseOverListener --------------------- @Override public void mouseOver(MouseOverEvent event) { if (event.getSource() == cgPalette) { updateInfoBox(event.getColorIndex()); } } //--------------------- End Interface MouseOverListener --------------------- private void init() { // first-time initializations currentRed = currentGreen = currentBlue = 0; isModified = false; GridBagConstraints c = new GridBagConstraints(); // creating palette section JPanel pPalette = new JPanel(new GridBagLayout()); pPalette.setBorder(BorderFactory.createTitledBorder("Palette ")); cgPalette = new ColorGrid(256); cgPalette.setSelectionFrame(ColorGrid.Frame.DOUBLE_LINE); cgPalette.addActionListener(this); cgPalette.addMouseOverListener(this); cgPalette.addChangeListener(this); c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, new Insets(0, 4, 2, 4), 0, 0); pPalette.add(cgPalette, c); // creating information panel JPanel pInfo = new JPanel(new GridBagLayout()); pInfo.setBorder(BorderFactory.createTitledBorder("Information ")); JLabel lInfoIndexTitle = new JLabel("Index:"); JLabel lInfoRGBTitle = new JLabel("RGB:"); JLabel lInfoHexRGBTitle = new JLabel("Hex:"); lInfoIndex = new JLabel("255", SwingConstants.LEFT); lInfoRGB = new JLabel(String.format(FmtInfoRGB, 255, 255, 255)); lInfoHexRGB = new JLabel(String.format(FmtInfoHexRGB, 255, 255, 255)); lInfoHexRGB.setMinimumSize(lInfoHexRGB.getPreferredSize()); c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 4, 0, 4), 0, 0); pInfo.add(lInfoIndexTitle, c); c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 8, 0, 4), 0, 0); pInfo.add(lInfoIndex, c); c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 4, 0, 4), 0, 0); pInfo.add(lInfoRGBTitle, c); c = ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 8, 0, 4), 0, 0); pInfo.add(lInfoRGB, c); c = ViewerUtil.setGBC(c, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 4, 4, 4), 0, 0); pInfo.add(lInfoHexRGBTitle, c); c = ViewerUtil.setGBC(c, 1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 8, 4, 4), 0, 0); pInfo.add(lInfoHexRGB, c); // creating color edit panel JPanel pColor = new JPanel(new GridBagLayout()); pColor.setBorder(BorderFactory.createTitledBorder("Color ")); JLabel lColorIndexTitle = new JLabel("Index:"); JLabel lColorRedTitle = new JLabel("Red:"); JLabel lColorGreenTitle = new JLabel("Green:"); JLabel lColorBlueTitle = new JLabel("Blue:"); lColorIndex = new JLabel("255"); tfColorRed = new JTextField(4); tfColorRed.addFocusListener(this); tfColorGreen = new JTextField(4); tfColorGreen.addFocusListener(this); tfColorBlue = new JTextField(4); tfColorBlue.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); pColor.add(lColorIndexTitle, c); c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 10, 0, 4), 0, 0); pColor.add(lColorIndex, c); c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 4, 0, 0), 0, 0); pColor.add(lColorRedTitle, c); c = ViewerUtil.setGBC(c, 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 8, 0, 4), 0, 0); pColor.add(tfColorRed, c); c = ViewerUtil.setGBC(c, 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 4, 0, 0), 0, 0); pColor.add(lColorGreenTitle, c); c = ViewerUtil.setGBC(c, 1, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 8, 0, 4), 0, 0); pColor.add(tfColorGreen, c); c = ViewerUtil.setGBC(c, 0, 3, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 4, 4, 0), 0, 0); pColor.add(lColorBlueTitle, c); c = ViewerUtil.setGBC(c, 1, 3, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(4, 8, 4, 4), 0, 0); pColor.add(tfColorBlue, c); pColor.setMinimumSize(pColor.getPreferredSize()); // creating "Options" panel bLoadPalette = new JButton("Load palette..."); bLoadPalette.addActionListener(this); bResetPalette = new JButton("Reset palette"); bResetPalette.addActionListener(this); JPanel pOptions = new JPanel(new GridBagLayout()); pOptions.setBorder(BorderFactory.createTitledBorder("Options ")); c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 4), 0, 0); pOptions.add(bLoadPalette, c); c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(8, 4, 4, 4), 0, 0); pOptions.add(bResetPalette, c); // putting right sidebar together JPanel pSideBar = new JPanel(new GridBagLayout()); c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0); pSideBar.add(pInfo, c); c = ViewerUtil.setGBC(c, 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(4, 0, 0, 0), 0, 0); pSideBar.add(pColor, c); c = ViewerUtil.setGBC(c, 0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(4, 0, 0, 0), 0, 0); pSideBar.add(pOptions, c); // creating bottom bar JPanel pBottom = new JPanel(new GridBagLayout()); bClose = new JButton("Close"); bClose.addActionListener(this); bClose.setMargin(new Insets(4, bClose.getInsets().left, 4, bClose.getInsets().right)); c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); pBottom.add(bClose, c); // putting all together JPanel pMain = new JPanel(new GridBagLayout()); c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, new Insets(4, 4, 4, 0), 0, 0); pMain.add(pPalette, c); c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(4, 8, 4, 4), 0, 0); pMain.add(pSideBar, c); c = ViewerUtil.setGBC(c, 0, 1, 2, 1, 1.0, 0.0, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL, new Insets(4, 0, 8, 0), 0, 0); pMain.add(pBottom, c); setLayout(new BorderLayout()); add(pMain, BorderLayout.CENTER); pack(); setResizable(false); // "Closing" the dialog only makes it invisible setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); getRootPane().setDefaultButton(bClose); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), getRootPane()); getRootPane().getActionMap().put(getRootPane(), new AbstractAction() { @Override public void actionPerformed(ActionEvent event) { close(); } }); resetPalette(); updateInfoBox(cgPalette.getSelectedIndex()); updateColorBox(cgPalette.getSelectedIndex(), cgPalette.getSelectedColor()); } // marks palette as modified private void setModified() { isModified = true; updateState(); } // resets modified flag private void resetModified() { isModified = false; updateState(); } // Updates dialog components private void updateState() { setTitle(isModified() ? TitleModified : TitleClean); bResetPalette.setEnabled(isModified()); } // Updates the information panel private void updateInfoBox(int index) { if (index >= 0 && index < cgPalette.getColorCount()) { Color c = cgPalette.getColor(index); lInfoIndex.setText(Integer.toString(index)); lInfoRGB.setText(String.format(FmtInfoRGB, c.getRed(), c.getGreen(), c.getBlue())); lInfoHexRGB.setText(String.format(FmtInfoHexRGB, c.getRed(), c.getGreen(), c.getBlue())); } else { lInfoIndex.setText(""); lInfoRGB.setText(""); lInfoHexRGB.setText(""); } } // Updates the color edit panel with the selected color data private void updateColorBox(int index, Color color) { boolean isValid = (index >= 0 && index < cgPalette.getColorCount()); if (isValid) { lColorIndex.setText(Integer.toString(index)); } else { lColorIndex.setText(""); } if (color != null) { currentRed = color.getRed(); currentGreen = color.getGreen(); currentBlue = color.getBlue(); } else { currentRed = currentGreen = currentBlue = 0; } tfColorRed.setText(Integer.toString(currentRed)); tfColorGreen.setText(Integer.toString(currentGreen)); tfColorBlue.setText(Integer.toString(currentBlue)); tfColorRed.setEnabled(isValid); tfColorGreen.setEnabled(isValid); tfColorBlue.setEnabled(isValid); } // Applies the color values specified in the color edit controls to the currently selected color private void updateCurrentColor() { if (cgPalette.getSelectedIndex() >= 0) { Color c = new Color(currentRed, currentGreen, currentBlue); if (!c.equals(cgPalette.getSelectedColor())) { cgPalette.setColor(cgPalette.getSelectedIndex(), c); setModified(); } } } // Parses color component value from specified text field private void registerColorValue(JTextField tf) { if (tf == tfColorRed) { currentRed = ConvertToBam.numberValidator(tfColorRed.getText(), 0, 255, currentRed); tfColorRed.setText(Integer.toString(currentRed)); updateCurrentColor(); } else if (tf == tfColorGreen) { currentGreen = ConvertToBam.numberValidator(tfColorGreen.getText(), 0, 255, currentGreen); tfColorGreen.setText(Integer.toString(currentGreen)); updateCurrentColor(); } else if (tf == tfColorBlue) { currentBlue = ConvertToBam.numberValidator(tfColorBlue.getText(), 0, 255, currentBlue); tfColorBlue.setText(Integer.toString(currentBlue)); updateCurrentColor(); } } } }