diff --git a/.classpath b/.classpath index 3e5f133fc..c729bcf8c 100644 --- a/.classpath +++ b/.classpath @@ -2,7 +2,7 @@ - + diff --git a/lib/rsyntaxtextarea/RSyntaxTextArea.License.txt b/lib/rsyntaxtextarea/RSyntaxTextArea.License.txt index df1db1b71..29c40309d 100644 --- a/lib/rsyntaxtextarea/RSyntaxTextArea.License.txt +++ b/lib/rsyntaxtextarea/RSyntaxTextArea.License.txt @@ -1,4 +1,4 @@ -Copyright (c) 2022, Robert Futrell +Copyright (c) 2023, Robert Futrell All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/lib/rsyntaxtextarea/rsyntaxtextarea-3.3.0-sources.jar b/lib/rsyntaxtextarea/rsyntaxtextarea-3.3.2-sources.jar similarity index 62% rename from lib/rsyntaxtextarea/rsyntaxtextarea-3.3.0-sources.jar rename to lib/rsyntaxtextarea/rsyntaxtextarea-3.3.2-sources.jar index 2f659c5b9..cba94e097 100644 Binary files a/lib/rsyntaxtextarea/rsyntaxtextarea-3.3.0-sources.jar and b/lib/rsyntaxtextarea/rsyntaxtextarea-3.3.2-sources.jar differ diff --git a/lib/rsyntaxtextarea/rsyntaxtextarea.jar b/lib/rsyntaxtextarea/rsyntaxtextarea.jar index fb90944b9..9c64ebfee 100644 Binary files a/lib/rsyntaxtextarea/rsyntaxtextarea.jar and b/lib/rsyntaxtextarea/rsyntaxtextarea.jar differ diff --git a/src/org/infinity/RSyntaxTextArea.License.txt b/src/org/infinity/RSyntaxTextArea.License.txt index 2b634080c..29c40309d 100644 --- a/src/org/infinity/RSyntaxTextArea.License.txt +++ b/src/org/infinity/RSyntaxTextArea.License.txt @@ -1,4 +1,4 @@ -Copyright (c) 2019, Robert Futrell +Copyright (c) 2023, Robert Futrell All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/src/org/infinity/datatype/AreResourceRef.java b/src/org/infinity/datatype/AreResourceRef.java index 79689e4c4..fc9d803fc 100644 --- a/src/org/infinity/datatype/AreResourceRef.java +++ b/src/org/infinity/datatype/AreResourceRef.java @@ -9,6 +9,7 @@ import org.infinity.resource.Profile; import org.infinity.resource.ResourceFactory; import org.infinity.resource.are.AreResource; +import org.infinity.resource.key.BIFFEntry; import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.ResourceEntry; @@ -26,7 +27,10 @@ public AreResourceRef(ByteBuffer buffer, int offset, String name, AreResource ar .getResourceEntry(((ResourceRef) are.getAttribute(AreResource.ARE_WED_RESOURCE)).getResourceName()); String wedBIFF = "_dummy"; if (res instanceof BIFFResourceEntry) { - wedBIFF = ((BIFFResourceEntry) res).getBIFFEntry().getFileName(); + BIFFEntry biffEntry = ((BIFFResourceEntry) res).getBIFFEntry(); + if (biffEntry != null) { + wedBIFF = biffEntry.getFileName(); + } } if (Profile.getEngine() == Profile.Engine.BG1) { legalBIFs = new String[] { wedBIFF, "data/sfxsound.bif", "data/cresound.bif" }; diff --git a/src/org/infinity/datatype/TextEdit.java b/src/org/infinity/datatype/TextEdit.java index 70b6ab2f2..c52f02592 100644 --- a/src/org/infinity/datatype/TextEdit.java +++ b/src/org/infinity/datatype/TextEdit.java @@ -211,6 +211,7 @@ public ByteBuffer toBuffer() { buffer.put((byte) 0); } } + buffer.position(0); } return buffer; } diff --git a/src/org/infinity/gui/BrowserMenuBar.java b/src/org/infinity/gui/BrowserMenuBar.java index bc7a68bf7..0dd367803 100644 --- a/src/org/infinity/gui/BrowserMenuBar.java +++ b/src/org/infinity/gui/BrowserMenuBar.java @@ -117,7 +117,7 @@ import org.infinity.util.tuples.Couple; public final class BrowserMenuBar extends JMenuBar implements KeyEventDispatcher { - public static final String VERSION = "v2.3-20221108"; + public static final String VERSION = "v2.3-20230303"; public static final LookAndFeelInfo DEFAULT_LOOKFEEL = new LookAndFeelInfo("Metal", "javax.swing.plaf.metal.MetalLookAndFeel"); @@ -292,6 +292,20 @@ public boolean showSysInfo() { return optionsMenu.optionShowSystemInfo.isSelected(); } + /** Returns whether to show a dialog prompt whenever a bookmarked game is opened. */ + public boolean showOpenBookmarksPrompt() { + return optionsMenu.optionOpenBookmarksPrompt.isSelected(); + } + + /** + * Controls whether to show a dialog prompt whenever a bookmarked game is opened. + * + * @param show {@code true} to show a dialog prompt, {@code false} to open bookmarked games without confirmation. + */ + public void setShowOpenBookmarksPrompt(boolean show) { + optionsMenu.optionOpenBookmarksPrompt.setSelected(show); + } + /** Returns whether scripts are automatically scanned for compile errors. */ public boolean autocheckBCS() { return optionsMenu.optionAutocheckBCS.isSelected(); @@ -1087,9 +1101,18 @@ public void actionPerformed(ActionEvent event) { e.printStackTrace(); } if (!isEqual) { - String message = String.format("Open bookmarked game \"%s\"?", bookmark.getName()); - int confirm = JOptionPane.showConfirmDialog(NearInfinity.getInstance(), message, "Open game", - JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + int confirm = JOptionPane.YES_OPTION; + if (BrowserMenuBar.getInstance().showOpenBookmarksPrompt()) { + String message = String.format("Open bookmarked game \"%s\"?", bookmark.getName()); + Couple result = StandardDialogs.showConfirmDialogExtra(NearInfinity.getInstance(), + message, "Open game", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, + StandardDialogs.Extra.with(StandardDialogs.Extra.MESSAGE_DO_NOT_SHOW_PROMPT, + "Confirmation prompt can be enabled or disabled in the options menu.")); + if (result.getValue1()) { + BrowserMenuBar.getInstance().setShowOpenBookmarksPrompt(false); + } + confirm = result.getValue0(); + } if (confirm == JOptionPane.YES_OPTION) { NearInfinity.getInstance().openGame(keyFile); } @@ -1850,6 +1873,7 @@ private static final class OptionsMenu extends JMenu implements ActionListener, private static final String OPTION_SHOWCOLOREDSTRUCTURES = "ShowColoredStructures"; private static final String OPTION_SHOWHEXCOLORED = "ShowHexColored"; private static final String OPTION_SHOWSYSINFO = "ShowSysInfo"; + private static final String OPTION_OPENBOOKMARKSPROMPT = "OpenBookmarksPrompt"; private static final String OPTION_KEEPVIEWONCOPY = "UpdateTreeOnCopy"; private static final String OPTION_SHOWTREESEARCHNAMES = "ShowTreeSearchNames"; private static final String OPTION_HIGHLIGHT_OVERRIDDEN = "HighlightOverridden"; @@ -1945,6 +1969,7 @@ private static final class OptionsMenu extends JMenu implements ActionListener, private JCheckBoxMenuItem optionShowColoredStructures; private JCheckBoxMenuItem optionShowHexColored; private JCheckBoxMenuItem optionShowSystemInfo; + private JCheckBoxMenuItem optionOpenBookmarksPrompt; private JCheckBoxMenuItem optionShowUnknownResources; private JCheckBoxMenuItem optionKeepViewOnCopy; private JCheckBoxMenuItem optionTreeSearchNames; @@ -2058,8 +2083,12 @@ private OptionsMenu() { optionShowHexColored = new JCheckBoxMenuItem("Show colored blocks in Raw tabs", getPrefs().getBoolean(OPTION_SHOWHEXCOLORED, true)); add(optionShowHexColored); - optionShowSystemInfo = new JCheckBoxMenuItem("Display system information at startup", getPrefs().getBoolean(OPTION_SHOWSYSINFO, true)); + optionShowSystemInfo = new JCheckBoxMenuItem("Display system information at startup", + getPrefs().getBoolean(OPTION_SHOWSYSINFO, true)); add(optionShowSystemInfo); + optionOpenBookmarksPrompt = new JCheckBoxMenuItem("Confirm opening bookmarked games", + getPrefs().getBoolean(OPTION_OPENBOOKMARKSPROMPT, true)); + add(optionOpenBookmarksPrompt); addSeparator(); @@ -2714,6 +2743,7 @@ private void storePreferences() { getPrefs().putBoolean(OPTION_SHOWCOLOREDSTRUCTURES, optionShowColoredStructures.isSelected()); getPrefs().putBoolean(OPTION_SHOWHEXCOLORED, optionShowHexColored.isSelected()); getPrefs().putBoolean(OPTION_SHOWSYSINFO, optionShowSystemInfo.isSelected()); + getPrefs().putBoolean(OPTION_OPENBOOKMARKSPROMPT, optionOpenBookmarksPrompt.isSelected()); getPrefs().putBoolean(OPTION_KEEPVIEWONCOPY, optionKeepViewOnCopy.isSelected()); getPrefs().putBoolean(OPTION_SHOWTREESEARCHNAMES, optionTreeSearchNames.isSelected()); getPrefs().putBoolean(OPTION_HIGHLIGHT_OVERRIDDEN, optionHighlightOverridden.isSelected()); diff --git a/src/org/infinity/gui/InfinityAmp.java b/src/org/infinity/gui/InfinityAmp.java index 32a0160e5..0a3c30dc4 100644 --- a/src/org/infinity/gui/InfinityAmp.java +++ b/src/org/infinity/gui/InfinityAmp.java @@ -272,7 +272,7 @@ private void playMus(ResourceEntry musEntry) { int nextnr = 0; while (keepPlaying) { AudioBuffer audio = entryList.get(nextnr).getAudioBuffer(); - player.play(audio); + player.playContinuous(audio); if (entryList.get(nextnr).getNextNr() <= nextnr || entryList.get(nextnr).getNextNr() >= entryList.size()) { break; } diff --git a/src/org/infinity/gui/RenderCanvas.java b/src/org/infinity/gui/RenderCanvas.java index 4e8100600..6ce567e53 100644 --- a/src/org/infinity/gui/RenderCanvas.java +++ b/src/org/infinity/gui/RenderCanvas.java @@ -309,14 +309,12 @@ public void setComposite(Composite c) { composite = (c != null) ? c : AlphaComposite.SrcOver; } - protected void update() { - invalidate(); - if (getParent() != null) { - getParent().repaint(); - } - } - - protected Rectangle getCanvasSize() { + /** + * Returns the actual placement of the image canvas within the {@code RenderCanvas} component. + * + * @return {@code Rectangle} of the canvas bounds relative to the {@code RenderCanvas} component. + */ + public Rectangle getCanvasBounds() { Rectangle rect = new Rectangle(); if (currentImage != null) { rect.width = currentImage.getWidth(null); @@ -349,6 +347,13 @@ protected Rectangle getCanvasSize() { return rect; } + protected void update() { + invalidate(); + if (getParent() != null) { + getParent().repaint(); + } + } + /** * Renders the image to the canvas. * @@ -359,7 +364,7 @@ protected void paintCanvas(Graphics g) { Graphics2D g2 = (Graphics2D) g; Composite oldComposite = g2.getComposite(); g2.setComposite(getComposite()); - Rectangle rect = getCanvasSize(); + Rectangle rect = getCanvasBounds(); if (isScaling) { g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationType); g2.drawImage(currentImage, rect.x, rect.y, rect.width, rect.height, null); diff --git a/src/org/infinity/gui/StandardDialogs.java b/src/org/infinity/gui/StandardDialogs.java new file mode 100644 index 000000000..bb2eb772c --- /dev/null +++ b/src/org/infinity/gui/StandardDialogs.java @@ -0,0 +1,450 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2022 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.gui; + +import java.awt.Component; +import java.awt.HeadlessException; +import java.awt.Insets; +import java.util.Arrays; + +import javax.swing.Icon; +import javax.swing.JCheckBox; +import javax.swing.JOptionPane; + +import org.infinity.util.tuples.Couple; + +/** + * Expands several standard dialog methods of the {@link JOptionPane} class by optional checkboxes. + * They are primarily intended for "Do not show this message again."-kind of options. + */ +public class StandardDialogs extends JOptionPane { + /** + * Brings up an information-message dialog titled "Message". An additional {@code JCheckBox} element is shown below + * the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A boolean that indicates the selection state of the checkbox. + */ + public static boolean showMessageDialogExtra(Component parentComponent, + Object message, Extra extra) throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createMessageCheckBox() : null; + showMessageDialog(parentComponent, createMessageObject(message, cbOption)); + return cbOption != null && cbOption.isSelected(); + } + + /** + * Brings up a dialog that displays a message using a default icon determined by the {@code messageType} parameter. An + * additional {@code JCheckBox} element is shown below the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param title the title string for the dialog. + * @param messageType the type of message to be displayed: {@code ERROR_MESSAGE}, {@code INFORMATION_MESSAGE}, + * {@code WARNING_MESSAGE}, {@code QUESTION_MESSAGE}, or {@code PLAIN_MESSAGE}. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A boolean that indicates the selection state of the checkbox. + */ + public static boolean showMessageDialogExtra(Component parentComponent, + Object message, String title, int messageType, Extra extra) + throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createMessageCheckBox() : null; + showMessageDialog(parentComponent, createMessageObject(message, cbOption), title, messageType); + return cbOption != null && cbOption.isSelected(); + } + + /** + * Brings up a dialog displaying a message, specifying all parameters. An additional {@code JCheckBox} element is + * shown below the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param title the title string for the dialog. + * @param messageType the type of message to be displayed: {@code ERROR_MESSAGE}, {@code INFORMATION_MESSAGE}, + * {@code WARNING_MESSAGE}, {@code QUESTION_MESSAGE}, or {@code PLAIN_MESSAGE}. + * @param icon an icon to display in the dialog that helps the user identify the kind of message that is + * being displayed. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A boolean that indicates the selection state of the checkbox. + */ + public static boolean showMessageDialogExtra(Component parentComponent, + Object message, String title, int messageType, Icon icon, Extra extra) + throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createMessageCheckBox() : null; + showMessageDialog(parentComponent, createMessageObject(message, cbOption), title, messageType, icon); + return cbOption != null && cbOption.isSelected(); + } + + /** + * Brings up a dialog with the options Yes, No and Cancel; with the title, Select an + * Option. An additional {@code JCheckBox} element is shown below the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A tuple consisting of an {@code Integer} indicating the option selected by the user and the selection state + * of the checkbox, represented by a {@code Boolean}. + */ + public static Couple showConfirmDialogExtra(Component parentComponent, Object message, Extra extra) + throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createConfirmCheckBox() : null; + int result = showConfirmDialog(parentComponent, createMessageObject(message, cbOption)); + return getDialogResult(result, cbOption); + } + + /** + * Brings up a dialog where the number of choices is determined by the {@code optionType} parameter. An additional + * {@code JCheckBox} element is shown below the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param title the title string for the dialog. + * @param optionType an int designating the options available on the dialog: {@code YES_NO_OPTION}, + * {@code YES_NO_CANCEL_OPTION}, or {@code OK_CANCEL_OPTION}. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A tuple consisting of an {@code Integer} indicating the option selected by the user and the selection state + * of the checkbox, represented by a {@code Boolean}. + */ + public static Couple showConfirmDialogExtra(Component parentComponent, Object message, String title, + int optionType, Extra extra) throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createConfirmCheckBox() : null; + int result = showConfirmDialog(parentComponent, createMessageObject(message, cbOption), title, optionType); + return getDialogResult(result, cbOption); + } + + /** + * Brings up a dialog where the number of choices is determined by the {@code optionType} parameter, where the + * {@code messageType} parameter determines the icon to display. The {@code messageType} parameter is primarily used + * to supply a default icon from the Look and Feel. An additional {@code JCheckBox} element is shown below the dialog + * message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param title the title string for the dialog. + * @param optionType an int designating the options available on the dialog: {@code YES_NO_OPTION}, + * {@code YES_NO_CANCEL_OPTION}, or {@code OK_CANCEL_OPTION}. + * @param messageType an integer designating the kind of message this is; primarily used to determine the icon + * from the pluggable Look and Feel: {@code ERROR_MESSAGE}, {@code INFORMATION_MESSAGE}, + * {@code WARNING_MESSAGE}, {@code QUESTION_MESSAGE}, or {@code PLAIN_MESSAGE}. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A tuple consisting of an {@code Integer} indicating the option selected by the user and the selection state + * of the checkbox, represented by a {@code Boolean}. + */ + public static Couple showConfirmDialogExtra(Component parentComponent, Object message, String title, + int optionType, int messageType, Extra extra) throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createConfirmCheckBox() : null; + int result = showConfirmDialog(parentComponent, createMessageObject(message, cbOption), title, optionType, + messageType); + return getDialogResult(result, cbOption); + } + + /** + * Brings up a dialog with a specified icon, where the number of choices is determined by the {@code optionType} + * parameter. The {@code messageType} parameter is primarily used to supply a default icon from the look and feel. An + * additional {@code JCheckBox} element is shown below the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param title the title string for the dialog. + * @param optionType an int designating the options available on the dialog: {@code YES_NO_OPTION}, + * {@code YES_NO_CANCEL_OPTION}, or {@code OK_CANCEL_OPTION}. + * @param messageType an integer designating the kind of message this is; primarily used to determine the icon + * from the pluggable Look and Feel: {@code ERROR_MESSAGE}, {@code INFORMATION_MESSAGE}, + * {@code WARNING_MESSAGE}, {@code QUESTION_MESSAGE}, or {@code PLAIN_MESSAGE}. + * @param icon the icon to display in the dialog. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A tuple consisting of an {@code Integer} indicating the option selected by the user and the selection state + * of the checkbox, represented by a {@code Boolean}. + */ + public static Couple showConfirmDialogExtra(Component parentComponent, Object message, String title, + int optionType, int messageType, Icon icon, Extra extra) throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createConfirmCheckBox() : null; + int result = showConfirmDialog(parentComponent, createMessageObject(message, cbOption), title, optionType, + messageType, icon); + return getDialogResult(result, cbOption); + } + + /** + * Brings up a dialog with a specified icon, where the initial choice is determined by the {@code initialValue} + * parameter and the number of choices is determined by the {@code optionType} parameter. + *

+ * If {@code optionType} is {@code YES_NO_OPTION}, or {@code YES_NO_CANCEL_OPTION} and the {@code options} parameter + * is {@code null}, then the options are supplied by the look and feel. + *

+ * The {@code messageType} parameter is primarily used to supply a default icon from the look and feel. + *

+ * An additional {@code JCheckBox} element is shown below the dialog message. + * + * @param parentComponent determines the {@code Frame} in which the dialog is displayed; if {@code null}, or if the + * {@code parentComponent} has no {@code Frame}, a default {@code Frame} is used. + * @param message the {@code Object} to display. + * @param title the title string for the dialog. + * @param optionType an int designating the options available on the dialog: {@code YES_NO_OPTION}, + * {@code YES_NO_CANCEL_OPTION}, or {@code OK_CANCEL_OPTION}. + * @param messageType an integer designating the kind of message this is; primarily used to determine the icon + * from the pluggable Look and Feel: {@code ERROR_MESSAGE}, {@code INFORMATION_MESSAGE}, + * {@code WARNING_MESSAGE}, {@code QUESTION_MESSAGE}, or {@code PLAIN_MESSAGE}. + * @param icon the icon to display in the dialog. + * @param options an array of objects indicating the possible choices the user can make; if the objects are + * components, they are rendered properly; non-{@code String} objects are rendered using their + * {@code toString} methods; if this parameter is {@code null}, the options are determined by + * the Look and Feel. + * @param initialValue the object that represents the default selection for the dialog; only meaningful if + * {@code options} is used; can be {@code null}. + * @param extra An optional {@link Extra} object containing definitions for the extra checkbox. + * @return A tuple consisting of an {@code Integer} indicating the option selected by the user and the selection state + * of the checkbox, represented by a {@code Boolean}. + */ + public static Couple showOptionDialogExtra(Component parentComponent, Object message, String title, + int optionType, int messageType, Icon icon, Object[] options, Object initialValue, Extra extra) + throws HeadlessException { + final JCheckBox cbOption = extra != null ? extra.createOptionCheckBox() : null; + int result = showOptionDialog(parentComponent, createMessageObject(message, cbOption), title, optionType, + messageType, icon, options, initialValue); + return getDialogResult(result, cbOption); + } + + /** Returns the appropriate message object for the dialog methods, depending on the given parameters. */ + private static Object createMessageObject(Object message, Object newItem) { + if (newItem == null) { + return message; + } + + if (message == null) { + return newItem; + } else if (message.getClass().isArray()) { + Object[] array = Arrays.copyOf((Object[]) message, ((Object[]) message).length + 1); + array[array.length - 1] = newItem; + return array; + } else { + return new Object[] { message, newItem }; + } + } + + /** Ensures a valid result object for confirmation dialogs. */ + private static Couple getDialogResult(int result, JCheckBox cbOption) { + return Couple.with(result, cbOption != null ? cbOption.isSelected() : false); + } + + // -------------------------- INNER CLASSES -------------------------- + + /** + * This class handles configuration and creation of the extra checkbox that can be added to the standard dialogs by + * the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static class Extra { + /** A standard message for use in {@link StandardDialogs#showMessageDialogExtra}. */ + public static final String MESSAGE_DO_NOT_SHOW_MESSAGE = "Do not show this message again."; + + /** A standard message for use in {@link StandardDialogs#showConfirmDialogExtra}. */ + public static final String MESSAGE_DO_NOT_SHOW_PROMPT = "Do not show this prompt again."; + + /** A standard message for use in {@link StandardDialogs#showOptionDialogExtra}. */ + public static final String MESSAGE_DO_NOT_SHOW_DIALOG = "Do not show this dialog again."; + + private String text; + private String tooltip; + private boolean selected; + private boolean small; + private boolean padded; + + /** + * Prepares a {@link JCheckBox} instance with a standard text suitable for the dialog type. The checkbox will use + * smaller text and is added with extra padding on the top border. It is initially unselected. + *

+ * The following standard messages are considered: + *

    + *
  • for message dialogs: {@link #MESSAGE_DO_NOT_SHOW_MESSAGE}
  • + *
  • for confirmation dialogs: {@link #MESSAGE_DO_NOT_SHOW_PROMPT}
  • + *
  • for option dialogs: {@link #MESSAGE_DO_NOT_SHOW_DIALOG}
  • + *
+ * + * @return {@link StandardDialogs.Extra} object for use with the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static Extra withDefaults() { + return new Extra(null, null, false, true, true); + } + + /** + * Prepares a {@link JCheckBox} instance with the specified {@code text}. The checkbox will use smaller text and is + * added with extra padding on the top border. It is initially unselected. + * + * @param text the text of the checkbox. + * @return {@link StandardDialogs.Extra} object for use with the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static Extra with(String text) { + return new Extra(text, null, false, true, true); + } + + /** + * Prepares a {@link JCheckBox} instance with the specified {@code text} and {@code tooltip}. The checkbox will use + * smaller text and is added with extra padding on the top border. It is initially unselected. + * + * @param text the text of the checkbox. + * @param tooltip the tooltip for the checkbox. + * @return {@link StandardDialogs.Extra} object for use with the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static Extra with(String text, String tooltip) { + return new Extra(text, tooltip, false, true, true); + } + + /** + * Prepares a {@link JCheckBox} instance with the specified {@code text} and {@code tooltip}. The checkbox will use + * smaller text and is added with extra padding on the top border. + * + * @param text the text of the checkbox. + * @param tooltip the tooltip for the checkbox. + * @param selected whether the checkbox is initially selected. + * @return {@link StandardDialogs.Extra} object for use with the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static Extra with(String text, String tooltip, boolean selected) { + return new Extra(text, tooltip, selected, true, true); + } + + /** + * Prepares a {@link JCheckBox} instance with the specified {@code text} and {@code tooltip}. The checkbox is added + * with extra padding on the top border. + * + * @param text the text of the checkbox. + * @param tooltip the tooltip for the checkbox. + * @param selected whether the checkbox is initially selected. + * @param small whether the checkbox text is produced in a slightly smaller font size. + * @return {@link StandardDialogs.Extra} object for use with the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static Extra with(String text, String tooltip, boolean selected, boolean small) { + return new Extra(text, tooltip, selected, small, true); + } + + /** + * Prepares a {@link JCheckBox} instance with the specified {@code text} and {@code tooltip}. + * + * @param text the text of the checkbox. + * @param tooltip the tooltip for the checkbox. + * @param selected whether the checkbox is initially selected. + * @param small whether the checkbox text is produced in a slightly smaller font size. + * @param padded whether the checkbox adds extra padding to the top border. + * @return {@link StandardDialogs.Extra} object for use with the {@code StandardDialogs.show*DialogExtra()} methods. + */ + public static Extra with(String text, String tooltip, boolean selected, boolean small, boolean padded) { + return new Extra(text, tooltip, selected, small, padded); + } + + /** Returns the checkbox text. */ + public String getText() { + return text; + } + + /** + * Sets the checkbox text. Specify {@code null} to use a standard text, depending on use with a message or + * confirmation dialog. + */ + public void setText(String text) { + this.text = text; + } + + /** Returns the checkbox tooltip. */ + public String getTooltip() { + return tooltip; + } + + /** Sets the checkbox tooltip. Specify {@code null} to disable the tooltip. */ + public void setTooltip(String tooltip) { + this.tooltip = tooltip; + } + + /** Returns whether the checkbox is initially selected. */ + public boolean isSelected() { + return selected; + } + + /** Sets whether the checkbox is initially selected. */ + public void setSelected(boolean selected) { + this.selected = selected; + } + + /** Returns whether checkbox text is produced in a smaller font size. */ + public boolean isSmall() { + return small; + } + + /** Sets whether checkbox text is produced in a smaller font size. */ + public void setSmall(boolean small) { + this.small = small; + } + + /** Returns whether the checkbox adds extra padding to the top border. */ + public boolean isPadded() { + return padded; + } + + /** Sets whether the checkbox should add extra padding to the top border. */ + public void setPadded(boolean padded) { + this.padded = padded; + } + + private Extra(String text, String tooltip, boolean selected, boolean small, boolean padded) { + this.text = text; + this.tooltip = tooltip; + this.selected = selected; + this.small = small; + this.padded = padded; + } + + /** Returns a checkbox intended for a message dialog. */ + private JCheckBox createMessageCheckBox() { + return createCheckBox(MESSAGE_DO_NOT_SHOW_MESSAGE); + } + + /** Returns a checkbox intended for a confirmation dialog. */ + private JCheckBox createConfirmCheckBox() { + return createCheckBox(MESSAGE_DO_NOT_SHOW_PROMPT); + } + + /** Returns a checkbox intended for an option dialog. */ + private JCheckBox createOptionCheckBox() { + return createCheckBox(MESSAGE_DO_NOT_SHOW_DIALOG); + } + + /** + * Creates a {@link JCheckBox} object, based on the current settings. + * + * @param textOverride Checkbox text to use if {@code text} property is {@code null}. + * @return A {@code JCheckBox} object. + */ + private JCheckBox createCheckBox(String textOverride) { + if (textOverride == null) { + textOverride = MESSAGE_DO_NOT_SHOW_DIALOG; + } + + final JCheckBox cbOption = new JCheckBox(text != null ? text : textOverride, selected); + + if (tooltip != null) { + cbOption.setToolTipText(tooltip); + } + + if (small) { + cbOption.setFont(cbOption.getFont().deriveFont(cbOption.getFont().getSize2D() * 0.85f)); + } + + if (padded) { + Insets insets = cbOption.getMargin(); + if (insets == null) { + insets = new Insets(0, 0, 0, 0); + } + insets.top = cbOption.getFont().getSize() * 2 / 3; + cbOption.setMargin(insets); + } + + return cbOption; + } + } +} diff --git a/src/org/infinity/gui/TileGrid.java b/src/org/infinity/gui/TileGrid.java index c673c910e..9e264c6f2 100644 --- a/src/org/infinity/gui/TileGrid.java +++ b/src/org/infinity/gui/TileGrid.java @@ -615,6 +615,29 @@ public void setShowIcons(boolean showIcons) { } } + /** + * Calculates the tile index from the specified pixel position. + * + * @param location Pixel position within the tile grid. + * @return Index of the tile at the specified location. Returns -1 if no tile could be determined. + */ + public int getTileIndexAt(Point location) { + int retVal = -1; + + int width = getTileWidth() * getTileColumns(); + int height = getTileHeight() * getTileRows(); + if (location.x >= 0 && location.x < width && location.y >= 0 && location.y < height) { + int x = location.x / getTileWidth(); + int y = location.y / getTileHeight(); + int idx = y * getTileColumns() + x; + if (idx >= 0 && idx < getTileCount()) { + retVal = idx; + } + } + + return retVal; + } + // -------------------------- PRIVATE METHODS -------------------------- private void init(int rows, int cols, int tw, int th) { diff --git a/src/org/infinity/gui/hexview/StructHexViewer.java b/src/org/infinity/gui/hexview/StructHexViewer.java index f00e79a25..384292ba0 100644 --- a/src/org/infinity/gui/hexview/StructHexViewer.java +++ b/src/org/infinity/gui/hexview/StructHexViewer.java @@ -112,6 +112,7 @@ public class StructHexViewer extends JPanel private FindDataDialog findData; private JScrollPane spInfo; + private JSplitPane splitv; private boolean tabSelected; private int cachedSize; @@ -317,6 +318,14 @@ public void stateChanged(ChangeEvent e) { tabSelected = true; getHexView().requestFocusInWindow(); updateStatusBar((int) getHexView().getCurrentOffset()); + + // FIXME: Workaround to enforce correct JHexView scrollbar calculation + if (splitv != null) { + final int loc = splitv.getDividerLocation(); + splitv.setDividerLocation(loc - 1); + // "invokeLater()" needed for the UI to register the change + SwingUtilities.invokeLater(() -> splitv.setDividerLocation(loc)); + } } else if (tabSelected) { // actions when leaving Raw tab tabSelected = false; @@ -407,7 +416,7 @@ private void initGui() { spInfo = new JScrollPane(pInfo); spInfo.getVerticalScrollBar().setUnitIncrement(16); - JSplitPane splitv = new JSplitPane(JSplitPane.VERTICAL_SPLIT, hexView, spInfo); + splitv = new JSplitPane(JSplitPane.VERTICAL_SPLIT, hexView, spInfo); splitv.setDividerLocation(2 * NearInfinity.getInstance().getContentPane().getHeight() / 3); add(splitv, BorderLayout.CENTER); diff --git a/src/org/infinity/resource/are/viewer/AreaViewer.java b/src/org/infinity/resource/are/viewer/AreaViewer.java index adaa469d6..c6ab8facd 100644 --- a/src/org/infinity/resource/are/viewer/AreaViewer.java +++ b/src/org/infinity/resource/are/viewer/AreaViewer.java @@ -347,9 +347,12 @@ private void init() { cbDrawGrid.addActionListener(getListeners()); cbDrawOverlays = new JCheckBox(LABEL_DRAW_OVERLAYS); + cbDrawOverlays.setToolTipText("Shows overlay tilesets.
" + + "(Note: This checkbox is also available for primary tilesets with animated tiles.)"); cbDrawOverlays.addActionListener(getListeners()); cbAnimateOverlays = new JCheckBox(LABEL_ANIMATE_OVERLAYS); + cbAnimateOverlays.setToolTipText("Plays back overlays and primary tiles with multiple frames."); cbAnimateOverlays.addActionListener(getListeners()); JLabel lZoomLevel = new JLabel("Zoom map:"); diff --git a/src/org/infinity/resource/are/viewer/TilesetRenderer.java b/src/org/infinity/resource/are/viewer/TilesetRenderer.java index 355ddf75d..94c5706e6 100644 --- a/src/org/infinity/resource/are/viewer/TilesetRenderer.java +++ b/src/org/infinity/resource/are/viewer/TilesetRenderer.java @@ -228,8 +228,9 @@ public void setOverlaysEnabled(boolean enable) { * Returns whether the current map contains overlays */ public boolean hasOverlays() { - if (isInitialized()) { - return !listTilesets.get(0).listOverlayTiles.isEmpty(); + if (isInitialized() && !listTilesets.isEmpty()) { + final Tileset ts = listTilesets.get(0); + return ts.hasOverlays() || ts.hasAnimatedTiles(); } return false; } @@ -397,10 +398,7 @@ public int getMapHeight(boolean scaled) { * Advances the frame index by one for animated overlays. */ public void advanceTileFrame() { - for (int i = 1, size = listTilesets.size(); i < size; i++) { - listTilesets.get(i).advanceTileFrame(); - hasChangedOverlays = true; - } + listTilesets.forEach(ts -> { ts.advanceTileFrame(); hasChangedOverlays = true; }); if (hasChangedOverlays) { updateDisplay(); } @@ -412,10 +410,7 @@ public void advanceTileFrame() { * @param index The frame index to set. */ public void setTileFrame(int index) { - for (int i = 1, size = listTilesets.size(); i < size; i++) { - listTilesets.get(i).setTileFrame(index); - hasChangedOverlays = true; - } + listTilesets.forEach(ts -> { ts.setTileFrame(index); hasChangedOverlays = true; }); if (hasChangedOverlays) { updateDisplay(); } @@ -759,25 +754,29 @@ private boolean hasOverlay(int ovlIdx) { // draws all tiles of the map private void drawAllTiles() { - Tileset ts = listTilesets.get(0); - for (Tile tile : ts.listTiles) { - drawTile(tile, isDoorTile(tile)); - } + final Tileset ts = listTilesets.get(0); + ts.listTiles.forEach(tile -> drawTile(tile, isDoorTile(tile))); } - // draws overlayed tiles only + // draws overlayed and animated tiles only private void drawOverlayTiles() { - Tileset ts = listTilesets.get(0); - for (Tile tile : ts.listOverlayTiles) { - drawTile(tile, isDoorTile(tile)); + final Tileset ts = listTilesets.get(0); + + if (ts.hasAnimatedTiles) { + ts.listTiles.stream().filter(tile -> tile.tileCount > 1).forEach(tile -> drawTile(tile, isDoorTile(tile))); + } + + if (ts.hasOverlays) { + ts.listOverlayTiles.forEach(tile -> drawTile(tile, isDoorTile(tile))); } } // draws door tiles only private void drawDoorTiles() { + final List tileList = listTilesets.get(0).listTiles; for (DoorInfo di : listDoorTileIndices) { for (int j = 0, iCount = di.getIndicesCount(); j < iCount; j++) { - Tile tile = listTilesets.get(0).listTiles.get(di.getIndex(j)); + final Tile tile = tileList.get(di.getIndex(j)); drawTile(tile, isDoorTile(tile)); } } @@ -1077,22 +1076,33 @@ private static class Tileset { public int tilesY; // stores number of tiles per row/column public boolean isTisPalette; // whether tileset is palette-based + private boolean hasOverlays; + private boolean hasAnimatedTiles; + public Tileset(WedResource wed, Overlay ovl) { init(wed, ovl); } public void advanceTileFrame() { - for (Tile listTile : listTiles) { - listTile.advancePrimaryIndex(); + if (hasAnimatedTiles) { + listTiles.forEach(tile -> tile.advancePrimaryIndex()); } } public void setTileFrame(int index) { - for (Tile listTile : listTiles) { - listTile.setCurrentPrimaryIndex(index); + if (hasAnimatedTiles) { + listTiles.forEach(tile -> tile.setCurrentPrimaryIndex(index)); } } + public boolean hasOverlays() { + return hasOverlays; + } + + public boolean hasAnimatedTiles() { + return hasAnimatedTiles; + } + private void init(WedResource wed, Overlay ovl) { if (wed != null && ovl != null) { // storing tile data @@ -1176,8 +1186,11 @@ private void init(WedResource wed, Overlay ovl) { } } + hasOverlays = !listOverlayTiles.isEmpty(); + hasAnimatedTiles = listTiles.stream().anyMatch(tile -> tile.tileCount > 1); } else { tilesX = tilesY = 0; + hasOverlays = hasAnimatedTiles = false; } } @@ -1255,15 +1268,14 @@ public int getPrimaryIndex() { } } - // // Returns the primary tile index of the specified frame (useful for animated tiles) - // public int getPrimaryIndex(int frame) - // { - // if (tileCount > 0) { - // return tileIdx[frame % tileCount]; - // } else { - // return -1; - // } - // } +// // Returns the primary tile index of the specified frame (useful for animated tiles) +// public int getPrimaryIndex(int frame) { +// if (tileCount > 0) { +// return tileIdx[frame % tileCount]; +// } else { +// return -1; +// } +// } // Sets a new selected primary tile index public void setCurrentPrimaryIndex(int frame) { @@ -1277,11 +1289,10 @@ public void setCurrentPrimaryIndex(int frame) { } } - // // Returns the primary tile count - // public int getPrimaryIndexCount() - // { - // return tileCount; - // } +// // Returns the primary tile count +// public int getPrimaryIndexCount() { +// return tileCount; +// } // Advances the primary tile index by 1 for animated tiles, wraps around automatically public void advancePrimaryIndex() { @@ -1335,22 +1346,20 @@ private static class DoorInfo { private int[] indices; // list of tilemap indices used for the door public DoorInfo(String name, boolean isClosed, int[] indices) { - // this.name = (name != null) ? name : ""; - // this.isClosed = isClosed; +// this.name = (name != null) ? name : ""; +// this.isClosed = isClosed; this.indices = (indices != null) ? indices : new int[0]; } - // // Returns the name of the door structure - // public String getName() - // { - // return name; - // } - - // // Returns whether the tile indices are used for the closed state of the door - // public boolean isClosed() - // { - // return isClosed; - // } +// // Returns the name of the door structure +// public String getName() { +// return name; +// } + +// // Returns whether the tile indices are used for the closed state of the door +// public boolean isClosed() { +// return isClosed; +// } // Returns number of tiles used in this door structure public int getIndicesCount() { diff --git a/src/org/infinity/resource/cre/decoder/SpriteDecoder.java b/src/org/infinity/resource/cre/decoder/SpriteDecoder.java index ed0bb4c30..95d6e0fbf 100644 --- a/src/org/infinity/resource/cre/decoder/SpriteDecoder.java +++ b/src/org/infinity/resource/cre/decoder/SpriteDecoder.java @@ -1122,6 +1122,9 @@ protected void createAnimation(SeqDef definition, List directions, Be // Ensure that BeforeSourceBam function is applied only once per source BAM HashSet bamControlSet = new HashSet<>(); + // Ensures that global effects are only applied once per resource entry + final HashSet entrySet = new HashSet<>(); + for (final DirDef dd : definition.getDirections()) { if (!directions.contains(dd.getDirection())) { continue; @@ -1158,7 +1161,11 @@ protected void createAnimation(SeqDef definition, List directions, Be if (sd.getCurrentFrame() >= 0) { if (beforeSrcBam != null && !bamControlSet.contains(srcCtrl)) { bamControlSet.add(srcCtrl); - beforeSrcBam.accept(srcCtrl, sd); + if (!entrySet.contains(entry)) { + // apply only once per resource + beforeSrcBam.accept(srcCtrl, sd); + entrySet.add(entry); + } } frameInfo.add(new FrameInfo(srcCtrl, sd, centerShift)); } diff --git a/src/org/infinity/resource/graphics/MosResource.java b/src/org/infinity/resource/graphics/MosResource.java index 8a006ceb0..5dcd641b4 100644 --- a/src/org/infinity/resource/graphics/MosResource.java +++ b/src/org/infinity/resource/graphics/MosResource.java @@ -8,9 +8,13 @@ import java.awt.Component; import java.awt.Graphics2D; import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; import java.awt.Transparency; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -32,6 +36,7 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.ProgressMonitor; import javax.swing.RootPaneContainer; @@ -42,8 +47,10 @@ import org.infinity.gui.ButtonPanel; import org.infinity.gui.ButtonPopupMenu; import org.infinity.gui.RenderCanvas; +import org.infinity.gui.ViewFrame; import org.infinity.gui.WindowBlocker; import org.infinity.icon.Icons; +import org.infinity.resource.Closeable; import org.infinity.resource.Profile; import org.infinity.resource.Referenceable; import org.infinity.resource.Resource; @@ -62,15 +69,22 @@ * @see * https://gibberlings3.github.io/iesdp/file_formats/ie_formats/mos_v1.htm */ -public class MosResource implements Resource, Referenceable, ActionListener, PropertyChangeListener { +public class MosResource implements Resource, Closeable, Referenceable, ActionListener, PropertyChangeListener { private static final ButtonPanel.Control PROPERTIES = ButtonPanel.Control.CUSTOM_1; + private static final String FMT_PVRZ_SHOW = "Block %d: Show MOS block information..."; + private static final String FMT_PVRZ_OPEN = "Block %d: Open referenced PVRZ resource..."; + private static boolean enableTransparency = true; private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); + private final JPopupMenu menuPvrz = new JPopupMenu(); + private final JMenuItem miPvrzShow = new JMenuItem(); + private final JMenuItem miPvrzOpen = new JMenuItem(); private MosDecoder.Type mosType; + private MosDecoder decoder; private JMenuItem miExport; private JMenuItem miExportMOSV1; private JMenuItem miExportMOSC; @@ -82,6 +96,7 @@ public class MosResource implements Resource, Referenceable, ActionListener, Pro private SwingWorker, Void> workerConvert; private boolean exportCompressed; private WindowBlocker blocker; + private int lastBlockIndex = -1; public MosResource(ResourceEntry entry) throws Exception { this.entry = entry; @@ -168,6 +183,22 @@ public void actionPerformed(ActionEvent event) { } catch (Exception e) { e.printStackTrace(); } + } else if (event.getSource() == miPvrzShow) { + if (lastBlockIndex >= 0) { + if (!showPvrzInfo(lastBlockIndex)) { + JOptionPane.showMessageDialog(panel, + String.format("Could not retrieve PVRZ information for MOS block %d.", lastBlockIndex), "Error", + JOptionPane.ERROR_MESSAGE); + } + } + } else if (event.getSource() == miPvrzOpen) { + if (lastBlockIndex >= 0) { + if (!openPvrzResource(lastBlockIndex)) { + JOptionPane.showMessageDialog(panel, + String.format("Could not open PVRZ resource for MOS block %d.", lastBlockIndex), "Error", + JOptionPane.ERROR_MESSAGE); + } + } } } @@ -308,6 +339,12 @@ public JComponent makeViewer(ViewableContainer container) { scroll.getVerticalScrollBar().setUnitIncrement(16); scroll.getHorizontalScrollBar().setUnitIncrement(16); + menuPvrz.add(miPvrzShow); + menuPvrz.add(miPvrzOpen); + miPvrzShow.addActionListener(this); + miPvrzOpen.addActionListener(this); + rcImage.addMouseListener(new PopupListener()); + 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)"); @@ -337,6 +374,21 @@ public BufferedImage getImage() { return null; } + // --------------------- Begin Interface Closeable --------------------- + + @Override + public void close() throws Exception { + if (decoder != null) { + decoder.close(); + } + + if (rcImage != null) { + rcImage = null; + } + } + + // --------------------- End Interface Closeable --------------------- + // Shows message box about basic resource properties private void showProperties() { MosDecoder decoder = null; @@ -395,33 +447,70 @@ private void showProperties() { } } - 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); + /** Opens a message dialog with PVRZ-related information about the MOS block at the specified index. */ + private boolean showPvrzInfo(int blockIndex) { + MosV2Decoder d = (decoder instanceof MosV2Decoder) ? (MosV2Decoder) decoder : null; + if (d != null) { + final MosV2Decoder.MosBlock block = d.getBlockInfo(blockIndex); + if (block != null) { + final String info = String.format( + "PVRZ resource: %s
" + + "Block dimension: w=%d, h=%d
" + + "PVRZ coordinates: x=%d, y=%d
" + + "MOS coordinates: x=%d, y=%d", + MosV2Decoder.getPvrzFileName(block.getPage()), block.getWidth(), block.getHeight(), + block.getPvrzRect().x, block.getPvrzRect().y, block.getMosRect().x, block.getMosRect().y); + JOptionPane.showMessageDialog(panel, + String.format("
%s
 
", info), + "MOS block information", JOptionPane.INFORMATION_MESSAGE); + return true; + } + } + return false; + } + + /** Opens the PVRZ resource referenced by the MOS block at the specified index. */ + private boolean openPvrzResource(int blockIndex) { + MosV2Decoder d = (decoder instanceof MosV2Decoder) ? (MosV2Decoder) decoder : null; + if (d != null) { + final MosV2Decoder.MosBlock block = d.getBlockInfo(blockIndex); + if (block != null && block.getPage() >= 0 && block.getPage() < 100000) { + final ResourceEntry resEntry = ResourceFactory.getResourceEntry(MosV2Decoder.getPvrzFileName(block.getPage())); + if (resEntry != null) { + final Resource res = ResourceFactory.getResource(resEntry); + if (res != null) { + new ViewFrame(panel, res); + return true; } - mosType = decoder.getType(); - image = ColorConvert.toBufferedImage(decoder.getImage(), true); - decoder.close(); - decoder = null; - } catch (Exception e) { - e.printStackTrace(); - if (decoder != null) { - decoder.close(); + } + } + } + return false; + } + + private BufferedImage loadImage() { + if (decoder == null) { + mosType = MosDecoder.getType(entry); + if (mosType != MosDecoder.Type.INVALID) { + if (entry != null) { + try { + decoder = MosDecoder.loadMos(entry); + mosType = decoder.getType(); + } catch (Exception e) { + e.printStackTrace(); } - image = null; } } + } + + if (decoder != null) { + if (decoder instanceof MosV1Decoder) { + ((MosV1Decoder) decoder).setTransparencyEnabled(enableTransparency); + } + return ColorConvert.toBufferedImage(decoder.getImage(), true); } else { - image = ColorConvert.createCompatibleImage(1, 1, true); + return ColorConvert.createCompatibleImage(1, 1, true); } - return image; } // Creates a new MOS V1 or MOSC V1 resource from scratch. DO NOT call directly! @@ -620,4 +709,38 @@ public boolean containsPvrzReference(int index) { } return retVal; } + + // -------------------------- INNER CLASSES -------------------------- + + private final class PopupListener extends MouseAdapter { + @Override + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + if (decoder instanceof MosV2Decoder) { + final MosV2Decoder d = (MosV2Decoder) decoder; + + // Adjust location to canvas alignment within the RenderCanvas component + final Rectangle bounds = rcImage.getCanvasBounds(); + final int x = e.getX() - bounds.x; + final int y = e.getY() - bounds.y; + lastBlockIndex = d.getBlockIndexAt(new Point(x, y)); + + if (lastBlockIndex >= 0) { + miPvrzShow.setText(String.format(FMT_PVRZ_SHOW, lastBlockIndex)); + miPvrzOpen.setText(String.format(FMT_PVRZ_OPEN, lastBlockIndex)); + menuPvrz.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + } + } } diff --git a/src/org/infinity/resource/graphics/MosV2Decoder.java b/src/org/infinity/resource/graphics/MosV2Decoder.java index aa4531186..1603fd3d7 100644 --- a/src/org/infinity/resource/graphics/MosV2Decoder.java +++ b/src/org/infinity/resource/graphics/MosV2Decoder.java @@ -7,12 +7,15 @@ import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.nio.ByteBuffer; -import java.util.Collections; +import java.util.ArrayList; +import java.util.List; import java.util.Set; -import java.util.TreeSet; +import java.util.stream.Collectors; import org.infinity.resource.ResourceFactory; import org.infinity.resource.key.ResourceEntry; @@ -20,9 +23,8 @@ public class MosV2Decoder extends MosDecoder { private static final int HEADER_SIZE = 16; // size of the MOS header - private static final int BLOCK_SIZE = 28; // size of a single data block - private final TreeSet pvrIndices = new TreeSet<>(); + private final List dataBlocks = new ArrayList<>(); private ByteBuffer mosBuffer; private int width; @@ -40,21 +42,13 @@ public MosV2Decoder(ResourceEntry mosEntry) { * tiles are decoded at constant speed. */ public void preloadPvrz() { - for (int i = 0; i < getBlockCount(); i++) { - int ofs = getBlockOffset(i); - if (ofs > 0) { - int page = mosBuffer.getInt(ofs); - if (page >= 0) { - getPVR(page); - } - } - } + dataBlocks.stream().filter((e) -> e.page >= 0).forEach((e) -> getPVR(e.page)); } @Override public void close() { PvrDecoder.flushCache(); - pvrIndices.clear(); + dataBlocks.clear(); mosBuffer = null; width = height = blockCount = 0; ofsData = 0; @@ -96,16 +90,7 @@ public Image getImage() { @Override public boolean getImage(Image canvas) { if (isInitialized() && canvas != null) { - boolean bRet = false; - for (int i = 0; i < getBlockCount(); i++) { - int ofs = getBlockOffset(i); - if (ofs > 0) { - int dx = mosBuffer.getInt(ofs + 0x14); - int dy = mosBuffer.getInt(ofs + 0x18); - bRet |= renderBlock(i, canvas, dx, dy); - } - } - return bRet; + return dataBlocks.stream().allMatch(e -> renderBlock(e, canvas)); } return false; } @@ -126,16 +111,7 @@ public int[] getImageData() { @Override public boolean getImageData(int[] buffer) { if (isInitialized() && buffer != null) { - boolean bRet = false; - for (int i = 0; i < getBlockCount(); i++) { - int ofs = getBlockOffset(i); - if (ofs > 0) { - int dx = mosBuffer.getInt(ofs + 0x14); - int dy = mosBuffer.getInt(ofs + 0x18); - bRet |= renderBlock(i, buffer, getWidth(), getHeight(), dx, dy); - } - } - return bRet; + return dataBlocks.stream().allMatch(e -> renderBlock(e, buffer, getWidth(), getHeight())); } return false; } @@ -147,18 +123,16 @@ public int getBlockCount() { @Override public int getBlockWidth(int blockIdx) { - int ofs = getBlockOffset(blockIdx); - if (ofs > 0) { - return mosBuffer.getInt(ofs + 0x0c); + if (blockIdx >= 0 && blockIdx < dataBlocks.size()) { + return dataBlocks.get(blockIdx).pvrzRect.width; } return 0; } @Override public int getBlockHeight(int blockIdx) { - int ofs = getBlockOffset(blockIdx); - if (ofs > 0) { - return mosBuffer.getInt(ofs + 0x10); + if (blockIdx >= 0 && blockIdx < dataBlocks.size()) { + return dataBlocks.get(blockIdx).pvrzRect.height; } return 0; } @@ -167,7 +141,7 @@ public int getBlockHeight(int blockIdx) { public Image getBlock(int blockIdx) { if (isValidBlock(blockIdx)) { Image image = ColorConvert.createCompatibleImage(getBlockWidth(blockIdx), getBlockHeight(blockIdx), true); - if (renderBlock(blockIdx, image, 0, 0)) { + if (renderBlock(dataBlocks.get(blockIdx), image, 0, 0)) { return image; } else { image = null; @@ -179,7 +153,7 @@ public Image getBlock(int blockIdx) { @Override public boolean getBlock(int blockIdx, Image canvas) { if (isValidBlock(blockIdx) && canvas != null) { - return renderBlock(blockIdx, canvas, 0, 0); + return renderBlock(dataBlocks.get(blockIdx), canvas, 0, 0); } return false; } @@ -190,7 +164,7 @@ public int[] getBlockData(int blockIdx) { int w = getBlockWidth(blockIdx); int h = getBlockHeight(blockIdx); int[] buffer = new int[w * h]; - if (renderBlock(blockIdx, buffer, w, h, 0, 0)) { + if (renderBlock(dataBlocks.get(blockIdx), buffer, 0, 0)) { return buffer; } else { buffer = null; @@ -205,15 +179,53 @@ public boolean getBlockData(int blockIdx, int[] buffer) { int w = getBlockWidth(blockIdx); int h = getBlockHeight(blockIdx); if (buffer.length >= w * h) { - return renderBlock(blockIdx, buffer, w, h, 0, 0); + return renderBlock(dataBlocks.get(blockIdx), buffer, 0, 0); } } return false; } + /** + * Returns the MOS block index at the specified image location. + * + * @param location Pixel position within the MOS graphics. + * @return The MOS block index if available, -1 otherwise. + */ + public int getBlockIndexAt(Point location) { + for (int i = 0, count = dataBlocks.size(); i < count; i++) { + if (dataBlocks.get(i).mosRect.contains(location)) { + return i; + } + } + return -1; + } + + /** + * Returns {@code MosBlock} information for the pixel at the specified image location. + * + * @param location Pixel position within the MOS graphics. + * @return A {@code MosBlock} instance if available, {@code null} otherwise. + */ + public MosBlock getBlockInfoAt(Point location) { + return dataBlocks.stream().filter(e -> e.mosRect.contains(location)).findFirst().orElse(null); + } + + /** + * Returns {@code MosBlock} information for the specified MOS block. + * + * @param index The MOS block index. + * @return A {@code MosBlock} instance if available, {@code null} otherwise. + */ + public MosBlock getBlockInfo(int index) { + if (index >= 0 && index < dataBlocks.size()) { + return dataBlocks.get(index); + } + return null; + } + /** Returns the set of referenced PVRZ pages by this MOS. */ public Set getReferencedPVRZPages() { - return Collections.unmodifiableSet(pvrIndices); + return dataBlocks.stream().map((e) -> e.page).collect(Collectors.toSet()); } private void init() { @@ -248,12 +260,18 @@ private void init() { throw new Exception("Invalid data offset: " + ofsData); } // collecting referened pvrz pages + dataBlocks.clear(); for (int idx = 0; idx < blockCount; idx++) { int ofs = ofsData + (idx * 28); int page = mosBuffer.getInt(ofs); - if (page >= 0) { - pvrIndices.add(page); - } + int sx = mosBuffer.getInt(ofs + 0x04); + int sy = mosBuffer.getInt(ofs + 0x08); + int w = mosBuffer.getInt(ofs + 0x0c); + int h = mosBuffer.getInt(ofs + 0x10); + int dx = mosBuffer.getInt(ofs + 0x14); + int dy = mosBuffer.getInt(ofs + 0x18); + final MosBlock block = new MosBlock(page, sx, sy, w, h, dx, dy); + dataBlocks.add(block); } } catch (Exception e) { e.printStackTrace(); @@ -265,8 +283,7 @@ private void init() { // Returns and caches the PVRZ resource of the specified page private PvrDecoder getPVR(int page) { try { - String name = String.format("MOS%04d.PVRZ", page); - ResourceEntry entry = ResourceFactory.getResourceEntry(name); + ResourceEntry entry = ResourceFactory.getResourceEntry(getPvrzFileName(page)); if (entry != null) { return PvrDecoder.loadPvr(entry); } @@ -286,84 +303,155 @@ private boolean isValidBlock(int blockIdx) { return (blockIdx >= 0 && blockIdx < blockCount); } - // Returns the start offset of the specified data block - private int getBlockOffset(int blockIdx) { - if (blockIdx >= 0 && blockIdx < blockCount) { - return ofsData + blockIdx * BLOCK_SIZE; + // Renders the specified block onto the canvas at predefined position + private boolean renderBlock(MosBlock block, Image canvas) { + if (block != null) { + return renderBlock(block, canvas, block.mosRect.x, block.mosRect.y); } - return -1; + return false; } // Renders the specified block onto the canvas at position (left, top) - private boolean renderBlock(int blockIdx, Image canvas, int left, int top) { - int ofsBlock = getBlockOffset(blockIdx); - if (ofsBlock > 0 && canvas != null && left >= 0 && top >= 0) { - int page = mosBuffer.getInt(ofsBlock); - int srcX = mosBuffer.getInt(ofsBlock + 0x04); - int srcY = mosBuffer.getInt(ofsBlock + 0x08); - int blockWidth = mosBuffer.getInt(ofsBlock + 0x0c); - int blockHeight = mosBuffer.getInt(ofsBlock + 0x10); - PvrDecoder decoder = getPVR(page); - if (decoder != null) { - try { - int w = (left + blockWidth < canvas.getWidth(null)) ? canvas.getWidth(null) - left : blockWidth; - int h = (top + blockHeight < canvas.getHeight(null)) ? canvas.getHeight(null) - top : blockHeight; - if (w > 0 && h > 0) { - BufferedImage imgBlock = decoder.decode(srcX, srcY, blockWidth, blockHeight); - Graphics2D g = (Graphics2D) canvas.getGraphics(); - try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.drawImage(imgBlock, left, top, left + w, top + h, 0, 0, w, h, null); - } finally { - g.dispose(); - g = null; - } - imgBlock = null; + private boolean renderBlock(MosBlock block, Image canvas, int left, int top) { + if (block == null) { + return false; + } + + final PvrDecoder decoder = getPVR(block.page); + if (decoder != null) { + try { + final Rectangle pvrzRect = block.pvrzRect; + final Rectangle mosRect = block.mosRect; + + int w = (left + mosRect.width < canvas.getWidth(null)) ? canvas.getWidth(null) - left : mosRect.width; + int h = (top + mosRect.height < canvas.getHeight(null)) ? canvas.getHeight(null) - top : mosRect.height; + if (w > 0 && h > 0) { + BufferedImage imgBlock = decoder.decode(pvrzRect.x, pvrzRect.y, pvrzRect.width, pvrzRect.height); + Graphics2D g = (Graphics2D) canvas.getGraphics(); + try { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.drawImage(imgBlock, left, top, left + w, top + h, 0, 0, w, h, null); + } finally { + g.dispose(); + g = null; } - return true; - } catch (Exception e) { - e.printStackTrace(); + imgBlock = null; } + return true; + } catch (Exception e) { + e.printStackTrace(); } } return false; } + // Writes the specified block into the buffer of specified dimensions at predefined position + private boolean renderBlock(MosBlock block, int[] buffer, int width, int height) { + if (block != null) { + return renderBlock(block, buffer, width, height, block.mosRect.x, block.mosRect.y); + } + return false; + } + // Writes the specified block into the buffer of specified dimensions at position (left, top) - private boolean renderBlock(int blockIdx, int[] buffer, int width, int height, int left, int top) { - int ofsBlock = getBlockOffset(blockIdx); - if (ofsBlock > 0 && buffer != null && width > 0 && height > 0 && left >= 0 && top >= 0) { - int page = mosBuffer.getInt(ofsBlock); - int srcX = mosBuffer.getInt(ofsBlock + 0x04); - int srcY = mosBuffer.getInt(ofsBlock + 0x08); - int blockWidth = mosBuffer.getInt(ofsBlock + 0x0c); - int blockHeight = mosBuffer.getInt(ofsBlock + 0x10); - PvrDecoder decoder = getPVR(page); - if (decoder != null) { - try { - int w = (left + blockWidth < width) ? width - left : blockWidth; - int h = (top + blockHeight < height) ? height - top : blockHeight; - if (w > 0 && h > 0) { - BufferedImage imgBlock = decoder.decode(srcX, srcY, blockWidth, blockHeight); - int[] srcData = ((DataBufferInt) imgBlock.getRaster().getDataBuffer()).getData(); - int srcOfs = 0; - int dstOfs = top * width + left; - for (int y = 0; y < h; y++) { - System.arraycopy(srcData, srcOfs, buffer, dstOfs, w); - srcOfs += blockWidth; - dstOfs += width; - } - srcData = null; - imgBlock = null; - decoder = null; - return true; + private boolean renderBlock(MosBlock block, int[] buffer, int width, int height, int left, int top) { + if (block == null || buffer == null || width < 0 || height < 0) { + return false; + } + + final PvrDecoder decoder = getPVR(block.page); + if (decoder != null) { + try { + final Rectangle pvrzRect = block.pvrzRect; + final Rectangle mosRect = block.mosRect; + + int w = (left + mosRect.width < width) ? width - left : mosRect.width; + int h = (top + mosRect.height < height) ? height - top : mosRect.height; + if (w > 0 && h > 0) { + BufferedImage imgBlock = decoder.decode(pvrzRect.x, pvrzRect.y, pvrzRect.width, pvrzRect.height); + int[] srcData = ((DataBufferInt) imgBlock.getRaster().getDataBuffer()).getData(); + int srcOfs = 0; + int dstOfs = top * width + left; + for (int y = 0; y < h; y++) { + System.arraycopy(srcData, srcOfs, buffer, dstOfs, w); + srcOfs += pvrzRect.width; + dstOfs += width; } - } catch (Exception e) { - e.printStackTrace(); - decoder = null; + srcData = null; + imgBlock = null; + return true; } + } catch (Exception e) { + e.printStackTrace(); } } return false; } + + /** + * Returns the PVRZ resource filename for the specified pvrz page. + * + * @param page A page index between 0 and 10000. + * @return The PVRZ resource filename. Returns empty string if {@code page} is out of bounds. + */ + public static String getPvrzFileName(int page) { + if (page >= 0 && page < 100000) { + return String.format("MOS%04d.PVRZ", page); + } + return ""; + } + + // -------------------------- INNER CLASSES -------------------------- + + /** + * Provides information about a single MOS data block. + */ + public static class MosBlock { + private final int page; + private final Rectangle pvrzRect; + private final Rectangle mosRect; + + /** + * Creates a new MosBlock instance. + * + * @param page The PVRZ page. + * @param srcX Source (PVRZ) x coordinate of the graphics block. + * @param srcY Source (PVRZ) y coordinate of the graphics block. + * @param width Block width, in pixels. + * @param height Block height, in pixels. + * @param dstX Destination (MOS) x coordinate of the graphics block. + * @param dstY Destination (MOS) x coordinate of the graphics block. + */ + public MosBlock(int page, int srcX, int srcY, int width, int height, int dstX, int dstY) { + this.page = page; + this.pvrzRect = new Rectangle(srcX, srcY, width, height); + this.mosRect = new Rectangle(dstX, dstY, width, height); + } + + /** Returns the page index of the PVRZ containing this graphics block. */ + public int getPage() { + return page; + } + + /** Returns the source (PVRZ) rectangle of pixel data for this graphics block. */ + public Rectangle getPvrzRect() { + return pvrzRect; + } + + /** Returns the destination (MOS) rectangle of pixel data for this graphics block. */ + public Rectangle getMosRect() { + return mosRect; + } + + /** Returns the width of the graphics block, in pixels. */ + public int getWidth() { + return pvrzRect.width; + } + + /** Returns the height of the graphics block, in pixels. */ + public int getHeight() { + return pvrzRect.height; + } + } + } diff --git a/src/org/infinity/resource/graphics/PvrDecoder.java b/src/org/infinity/resource/graphics/PvrDecoder.java index 2322f6300..bc27eab27 100644 --- a/src/org/infinity/resource/graphics/PvrDecoder.java +++ b/src/org/infinity/resource/graphics/PvrDecoder.java @@ -1,27 +1,19 @@ // Near Infinity - An Infinity Engine Browser and Editor -// Copyright (C) 2001 - 2022 Jon Olav Hauglid +// Copyright (C) 2001 - 2023 Jon Olav Hauglid // See LICENSE.txt for license information -// ---------------------------------------------------------------- -// PVRT format specifications and reference implementation: -// Copyright (c) Imagination Technologies Ltd. All Rights Reserved package org.infinity.resource.graphics; -import java.awt.AlphaComposite; -import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; import java.io.InputStream; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.zip.InflaterInputStream; +import org.infinity.resource.graphics.decoder.PvrInfo; import org.infinity.resource.key.FileResourceEntry; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.DynamicArray; @@ -33,48 +25,12 @@ * PVRZ resources (this includes only a selected number of supported pixel formats). */ public class PvrDecoder { - /** Flags indicates special properties of the color data. */ - public enum Flags { - NONE, PRE_MULTIPLIED - } - - /** Format specifies the pixel format of the color data. */ - public enum PixelFormat { - PVRTC_2BPP_RGB, PVRTC_2BPP_RGBA, PVRTC_4BPP_RGB, PVRTC_4BPP_RGBA, - PVRTC2_2BPP, PVRTC2_4BPP, - ETC1, - DXT1, DXT2, DXT3, DXT4, DXT5, BC4, BC5, BC6, BC7, - UYVY, YUY2, - BW1BPP, R9G9B9E5, RGBG8888, GRGB8888, - ETC2_RGB, ETC2_RGBA, ETC2_RGB_A1, - EAC_R11_RGB_U, EAC_R11_RGB_S, EAC_RG11_RGB_U, EAC_RG11_RGB_S, - CUSTOM - } - - /** Color space of the color data. */ - public enum ColorSpace { - RGB, SRGB - } - - /** Datatype used to describe a color component. */ - public enum ChannelType { - UBYTE_NORM, SBYTE_NORM, UBYTE, SBYTE, - USHORT_NORM, SSHORT_NORM, USHORT, SSHORT, - UINT_NORM, SINT_NORM, UINT, SINT, - FLOAT - } - // The global cache list for PVR objects. The "key" has to be a unique String (e.g. filename or integer as string) private static final Map PVR_CACHE = new LinkedHashMap<>(); // The max. number of cache entries to hold private static int MaxCacheEntries = 32; - // Supported pixel formats - private static final EnumSet SupportedFormat = EnumSet.of(PixelFormat.DXT1, PixelFormat.DXT3, - PixelFormat.DXT5, PixelFormat.PVRTC_2BPP_RGB, PixelFormat.PVRTC_2BPP_RGBA, PixelFormat.PVRTC_4BPP_RGB, - PixelFormat.PVRTC_4BPP_RGBA); - private PvrInfo info; /** @@ -189,7 +145,7 @@ else if (maxValue > 65535) /** Clears all available caches. */ public static synchronized void flushCache() { PVR_CACHE.clear(); - DecodePVRT.flushCache(); + PvrInfo.flushCache(); } /** Returns the current cache load as percentage value. */ @@ -201,22 +157,6 @@ public static int getCacheLoad() { } } -// // Returns a PvrDecoder object only if it already exists in the cache. -// private static synchronized PvrDecoder getCachedPvrDecoder(String key) -// { -// PvrDecoder retVal = null; -// if (key != null && !key.isEmpty()) { -// key = key.toUpperCase(Locale.ENGLISH); -// if (pvrCache.containsKey(key)) { -// retVal = pvrCache.get(key); -// // re-inserting entry to prevent premature removal from cache -// pvrCache.remove(key); -// pvrCache.put(key, retVal); -// } -// } -// return retVal; -// } - // Returns a PvrDecoder object of the specified key if available, or creates and returns a new one otherwise. private static synchronized PvrDecoder createPvrDecoder(String key, InputStream input) { PvrDecoder retVal = null; @@ -243,150 +183,11 @@ private static synchronized PvrDecoder createPvrDecoder(String key, InputStream } } return retVal; -// PvrDecoder retVal = getCachedPvrDecoder(key); -// if (retVal == null && input != null) { -// try { -// retVal = new PvrDecoder(input); -// if (retVal != null) { -// pvrCache.put(key, retVal); -// // removing excess cache entries -// while (pvrCache.size() > MaxCacheEntries) { -// pvrCache.remove(pvrCache.keySet().iterator().next()); -// } -// } -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } -// return retVal; - } - - // Returns a rectangle that is aligned to the values specified as arguments 2 and 3 - private static Rectangle alignRectangle(Rectangle rect, int alignX, int alignY) { - if (rect == null) - return null; - - Rectangle retVal = new Rectangle(rect); - if (alignX < 1) - alignX = 1; - if (alignY < 1) - alignY = 1; - if (rect.x < 0) { - rect.width -= -rect.x; - rect.x = 0; - } - if (rect.y < 0) { - rect.height -= -rect.y; - rect.y = 0; - } - - int diffX = retVal.x % alignX; - if (diffX != 0) { - retVal.x -= diffX; - retVal.width += diffX; - } - int diffY = retVal.y % alignY; - if (diffY != 0) { - retVal.y -= diffY; - retVal.height += diffY; - } - - diffX = (alignX - (retVal.width % alignX)) % alignX; - retVal.width += diffX; - - diffY = (alignY - (retVal.height % alignY)) % alignY; - retVal.height += diffY; - - return retVal; - } - - /** Returns flags that indicate special properties of the color data. */ - public Flags getFlags() { - return info.flags; - } - - /** Returns the pixel format used to encode image data within the PVR file. */ - public PixelFormat getPixelFormat() { - return info.pixelFormat; - } - - /** Returns meaningful data only if pixelFormat() returns {@code PixelFormat.CUSTOM}. */ - public byte[] getPixelFormatEx() { - return info.pixelFormatEx; - } - - /** Returns the color space the image data is in. */ - public ColorSpace getColorSpace() { - return info.colorSpace; - } - - /** Returns the data type used to encode the image data within the PVR file. */ - public ChannelType getChannelType() { - return info.channelType; - } - - /** Returns the texture width in pixels. */ - public int getWidth() { - return info.width; - } - - /** Returns the texture height in pixels. */ - public int getHeight() { - return info.height; - } - - /** Returns the color depth of the pixel type used to encode the color data in bits/pixel. */ - public int getColorDepth() { - return info.colorDepth; - } - - /** Returns the average number of bits used for each input pixel. */ - public int getAverageBitsPerPixel() { - return info.bitsPerInputPixel; - } - - /** Returns the depth of the texture stored in the image data, in pixels. */ - public int getTextureDepth() { - return info.textureDepth; - } - - /** Returns the number of surfaces within the texture array. */ - public int getNumSurfaces() { - return info.numSurfaces; - } - - /** Returns the number of faces in a cube map. */ - public int getNumFaces() { - return info.numFaces; - } - - /** Returns the number of MIP-Map levels present including the top level. */ - public int getNumMipMaps() { - return info.numMipMaps; - } - - /** Returns the total size of meta data embedded in the PVR header. */ - public int getMetaSize() { - return info.metaSize; - } - - /** Provides access to the content of the meta data, embedded in the PVR header. Can be empty (size = 0). */ - public byte[] getMetaData() { - return info.metaData; } - /** Provides direct access to the content of the encoded pixel data. */ - public byte[] getData() { - return info.data; - } - - /** Returns whether the pixel format of the current texture is supported by the PvrDecoder. */ - public boolean isSupported() { - try { - return SupportedFormat.contains(getPixelFormat()); - } catch (Throwable t) { - } - return false; + /** Provides access to the PVR information data structure. */ + public PvrInfo getInfo() { + return info; } /** @@ -396,7 +197,7 @@ public boolean isSupported() { * @throws Exception on error. */ public BufferedImage decode() throws Exception { - return decode(0, 0, getWidth(), getHeight()); + return decode(0, 0, info.getWidth(), info.getHeight()); } /** @@ -407,7 +208,7 @@ public BufferedImage decode() throws Exception { * @throws Exception on error. */ public boolean decode(BufferedImage image) throws Exception { - return decode(image, 0, 0, getWidth(), getHeight()); + return decode(image, 0, 0, info.getWidth(), info.getHeight()); } /** @@ -426,7 +227,7 @@ public BufferedImage decode(int x, int y, int width, int height) throws Exceptio width = 1; if (height < 1) height = 1; - int imgType = (getFlags() == Flags.PRE_MULTIPLIED) ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_ARGB; + int imgType = (info.getFlags() == PvrInfo.Flags.PRE_MULTIPLIED) ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_ARGB; BufferedImage image = new BufferedImage(width, height, imgType); if (decode(image, x, y, width, height)) { return image; @@ -451,32 +252,17 @@ public boolean decode(BufferedImage image, int x, int y, int width, int height) if (image == null) { throw new Exception("No target image specified"); } - if (x < 0 || y < 0 || width < 1 || height < 1 || x + width > getWidth() || y + height > getHeight()) { + if (x < 0 || y < 0 || width < 1 || height < 1 || x + width > info.getWidth() || y + height > info.getHeight()) { throw new Exception("Invalid dimensions specified"); } - if (!isSupported()) { - throw new Exception(String.format("Pixel format '%s' not supported", getPixelFormat().toString())); + if (!info.isSupported()) { + throw new Exception(String.format("Pixel format '%s' not supported", info.getPixelFormat().toString())); } - if (getChannelType() != ChannelType.UBYTE_NORM) { - throw new Exception(String.format("Channel type '%s' not supported", getChannelType().toString())); + if (info.getChannelType() != PvrInfo.ChannelType.UBYTE_NORM) { + throw new Exception(String.format("Channel type '%s' not supported", info.getChannelType().toString())); } Rectangle region = new Rectangle(x, y, width, height); - switch (getPixelFormat()) { - case PVRTC_2BPP_RGB: - case PVRTC_2BPP_RGBA: - return DecodePVRT.decodePVRT2bpp(info, image, region); - case PVRTC_4BPP_RGB: - case PVRTC_4BPP_RGBA: - return DecodePVRT.decodePVRT4bpp(info, image, region); - case DXT1: - return DecodeDXT.decodeDXT1(info, image, region); - case DXT3: - return DecodeDXT.decodeDXT3(info, image, region); - case DXT5: - return DecodeDXT.decodeDXT5(info, image, region); - default: - return DecodeDummy.decode(info, image, region); - } + return info.decode(image, region); } private PvrDecoder(InputStream input) throws Exception { @@ -536,1318 +322,4 @@ private PvrDecoder(InputStream input) throws Exception { input.close(); } } - -// ----------------------------- INNER CLASSES ----------------------------- - - // Contains preprocessed data of a single PVR resource - private class PvrInfo { - public int signature; // the "PVR\u0003" signature - public Flags flags; - public PixelFormat pixelFormat; - public byte[] pixelFormatEx; - public ColorSpace colorSpace; - public ChannelType channelType; - public int height; // texture height in pixels - public int width; // texture width in pixels - public int colorDepth; // the color depth of a single decoded pixel (without decompression-specific artefacts) - public int bitsPerInputPixel; // average bits/pixel for encoded pixel data - public int textureDepth; // NOT bits per pixel! - public int numSurfaces; - public int numFaces; - public int numMipMaps; - public int metaSize; // metadata block size - public byte[] metaData; // optional metadata - public int headerSize; // size of the header incl. meta data - public byte[] data; // the encoded pixel data - - public PvrInfo(byte[] buffer, int size) throws Exception { - init(buffer, size); - } - - private void init(byte[] buffer, int size) throws Exception { - if (buffer == null || size <= 0x34) { - throw new Exception("Invalid or incomplete PVR input data"); - } - - signature = DynamicArray.getInt(buffer, 0); - if (signature != 0x03525650) { - throw new Exception("No PVR signature found"); - } - - int v = DynamicArray.getInt(buffer, 4); - switch (v) { - case 0: - flags = Flags.NONE; - break; - case 1: - flags = Flags.PRE_MULTIPLIED; - break; - default: - throw new Exception(String.format("Unsupported PVR flags: %d", v)); - } - - long l = DynamicArray.getLong(buffer, 8); - if ((l & 0xffffffff00000000L) != 0L) { - // custom pixel format - pixelFormat = PixelFormat.CUSTOM; - pixelFormatEx = new byte[8]; - System.arraycopy(buffer, 8, pixelFormatEx, 0, 8); - } else { - // predefined pixel format - switch ((int) l) { - case 0: - pixelFormat = PixelFormat.PVRTC_2BPP_RGB; - bitsPerInputPixel = 2; - break; - case 1: - pixelFormat = PixelFormat.PVRTC_2BPP_RGBA; - bitsPerInputPixel = 2; - break; - case 2: - pixelFormat = PixelFormat.PVRTC_4BPP_RGB; - bitsPerInputPixel = 4; - break; - case 3: - pixelFormat = PixelFormat.PVRTC_4BPP_RGBA; - bitsPerInputPixel = 4; - break; - case 4: - pixelFormat = PixelFormat.PVRTC2_2BPP; - bitsPerInputPixel = 2; - break; - case 5: - pixelFormat = PixelFormat.PVRTC2_4BPP; - bitsPerInputPixel = 4; - break; - case 6: - pixelFormat = PixelFormat.ETC1; - break; - case 7: - pixelFormat = PixelFormat.DXT1; - bitsPerInputPixel = 4; - break; - case 8: - pixelFormat = PixelFormat.DXT2; - bitsPerInputPixel = 8; - break; - case 9: - pixelFormat = PixelFormat.DXT3; - bitsPerInputPixel = 8; - break; - case 10: - pixelFormat = PixelFormat.DXT4; - bitsPerInputPixel = 8; - break; - case 11: - pixelFormat = PixelFormat.DXT5; - bitsPerInputPixel = 8; - break; - case 12: - pixelFormat = PixelFormat.BC4; - break; - case 13: - pixelFormat = PixelFormat.BC5; - break; - case 14: - pixelFormat = PixelFormat.BC6; - break; - case 15: - pixelFormat = PixelFormat.BC7; - break; - case 16: - pixelFormat = PixelFormat.UYVY; - break; - case 17: - pixelFormat = PixelFormat.YUY2; - break; - case 18: - pixelFormat = PixelFormat.BW1BPP; - break; - case 19: - pixelFormat = PixelFormat.R9G9B9E5; - break; - case 20: - pixelFormat = PixelFormat.RGBG8888; - break; - case 21: - pixelFormat = PixelFormat.GRGB8888; - break; - case 22: - pixelFormat = PixelFormat.ETC2_RGB; - break; - case 23: - pixelFormat = PixelFormat.ETC2_RGBA; - break; - case 24: - pixelFormat = PixelFormat.ETC2_RGB_A1; - break; - case 25: - pixelFormat = PixelFormat.EAC_R11_RGB_U; - break; - case 26: - pixelFormat = PixelFormat.EAC_R11_RGB_S; - break; - case 27: - pixelFormat = PixelFormat.EAC_RG11_RGB_U; - break; - case 28: - pixelFormat = PixelFormat.EAC_RG11_RGB_S; - break; - default: - throw new Exception(String.format("Unsupported pixel format: %s", Integer.toString((int) l))); - } - pixelFormatEx = new byte[0]; - } - - v = DynamicArray.getInt(buffer, 16); - switch (v) { - case 0: - colorSpace = ColorSpace.RGB; - break; - case 1: - colorSpace = ColorSpace.SRGB; - break; - default: - throw new Exception(String.format("Unsupported color space: %d", v)); - } - - v = DynamicArray.getInt(buffer, 20); - switch (v) { - case 0: - channelType = ChannelType.UBYTE_NORM; - break; - case 1: - channelType = ChannelType.SBYTE_NORM; - break; - case 2: - channelType = ChannelType.UBYTE; - break; - case 3: - channelType = ChannelType.SBYTE; - break; - case 4: - channelType = ChannelType.USHORT_NORM; - break; - case 5: - channelType = ChannelType.SSHORT_NORM; - break; - case 6: - channelType = ChannelType.USHORT; - break; - case 7: - channelType = ChannelType.SSHORT; - break; - case 8: - channelType = ChannelType.UINT_NORM; - break; - case 9: - channelType = ChannelType.SINT_NORM; - break; - case 10: - channelType = ChannelType.UINT; - break; - case 11: - channelType = ChannelType.SINT; - break; - case 12: - channelType = ChannelType.FLOAT; - break; - default: - throw new Exception(String.format("Unsupported channel type: %d", v)); - } - - switch (pixelFormat) { - case PVRTC_2BPP_RGB: - case PVRTC_2BPP_RGBA: - case PVRTC_4BPP_RGB: - case PVRTC_4BPP_RGBA: - case DXT1: - case DXT2: - case DXT3: - case DXT4: - case DXT5: - colorDepth = 16; - break; - default: - colorDepth = 32; // most likely wrong, but not important for us anyway - } - - height = DynamicArray.getInt(buffer, 24); - width = DynamicArray.getInt(buffer, 28); - textureDepth = DynamicArray.getInt(buffer, 32); - numSurfaces = DynamicArray.getInt(buffer, 36); - numFaces = DynamicArray.getInt(buffer, 40); - numMipMaps = DynamicArray.getInt(buffer, 44); - metaSize = DynamicArray.getInt(buffer, 48); - if (metaSize > 0) { - if (metaSize + 0x34 > size) { - throw new Exception("Input buffer too small"); - } - metaData = new byte[metaSize]; - System.arraycopy(buffer, 52, metaData, 0, metaSize); - } else { - metaData = new byte[0]; - } - headerSize = 0x34 + metaSize; - - // storing pixel data - data = new byte[size - headerSize]; - System.arraycopy(buffer, headerSize, data, 0, data.length); - } - } - - // "Decodes" unsupported pixel data. - private static class DecodeDummy { - /** - * Decodes PVR data in "unknown" format and draws the specified "region" into "image". - * - * @param pvr The PVR data - * @param image The output image - * @param region The of the PVR texture region to draw onto "image" - * @return The success state of the operation. - * @throws Exception on error. - */ - public static boolean decode(PvrInfo pvr, BufferedImage image, Rectangle region) throws Exception { - if (pvr == null || image == null || region == null) { - return false; - } - - int[] imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - int ofs = 0; - int maxX = (image.getWidth() < region.width) ? image.getWidth() : region.width; - int maxY = (image.getHeight() < region.height) ? image.getHeight() : region.height; - for (int y = 0; y < maxY; y++) { - for (int x = 0; x < maxX; x++) { - imgData[ofs + x] = 0; - } - ofs += image.getWidth(); - } - imgData = null; - - return true; - } - } - - // Decodes DXTn pixel data. - private static class DecodeDXT { - /** - * Decodes PVR data in DXT1 format and draws the specified "region" into "image". - * - * @param pvr The PVR data - * @param image The output image - * @param region The of the PVR texture region to draw onto "image" - * @return The success state of the operation. - * @throws Exception on error. - */ - public static boolean decodeDXT1(PvrInfo pvr, BufferedImage image, Rectangle region) throws Exception { - if (pvr == null || image == null || region == null) { - return false; - } - - int imgWidth = image.getWidth(); - int imgHeight = image.getHeight(); - int[] imgData = null; - - // checking region bounds and alignment - if (region.x < 0) { - region.width += -region.x; - region.x = 0; - } - if (region.y < 0) { - region.height += -region.y; - region.y = 0; - } - if (region.x + region.width > pvr.width) - region.width = pvr.width - region.x; - if (region.y + region.height > pvr.height) - region.height = pvr.height - region.y; - Rectangle rect = alignRectangle(region, 4, 4); - - // preparing aligned image buffer for faster rendering - BufferedImage alignedImage = null; - int imgWidthAligned; - if (!region.equals(rect)) { - alignedImage = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB); - imgWidthAligned = alignedImage.getWidth(); - imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); - // translating "region" to be relative to "rect" - region.x -= rect.x; - region.y -= rect.y; - if (imgWidth < region.width) { - region.width = imgWidth; - } - if (imgHeight < region.height) { - region.height = imgHeight; - } - } else { - imgWidthAligned = imgWidth; - imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - } - - final int wordSize = 8; // data size of an encoded 4x4 pixel block - int wordImageWidth = pvr.width >>> 2; // the image width in data blocks - int wordRectWidth = rect.width >>> 2; // the aligned region's width in data blocks - int wordRectHeight = rect.height >>> 2; // the aligned region's height in data blocks - int wordPosX = rect.x >>> 2; - int wordPosY = rect.y >>> 2; - - int[] colors = new int[8]; - int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; - int imgOfs = 0; - for (int y = 0; y < wordRectHeight; y++) { - for (int x = 0; x < wordRectWidth; x++) { - // decoding single DXT1 block - int c = DynamicArray.getInt(pvr.data, pvrOfs); - unpackColors565(c, colors); - int code = DynamicArray.getInt(pvr.data, pvrOfs + 4); - for (int idx = 0; idx < 16; idx++, code >>>= 2) { - int ofs = imgOfs + (idx >>> 2) * imgWidthAligned + (idx & 3); - if ((code & 3) == 0) { - // 100% c0, 0% c1 - imgData[ofs] = 0xff000000 | (colors[2] << 16) | (colors[1] << 8) | colors[0]; - } else if ((code & 3) == 1) { - // 0% c0, 100% c1 - imgData[ofs] = 0xff000000 | (colors[6] << 16) | (colors[5] << 8) | colors[4]; - } else if ((code & 3) == 2) { - if ((c & 0xffff) > ((c >>> 16) & 0xffff)) { - // 66% c0, 33% c1 - int v = 0xff000000; - v |= (((colors[2] << 1) + colors[6]) / 3) << 16; - v |= (((colors[1] << 1) + colors[5]) / 3) << 8; - v |= ((colors[0] << 1) + colors[4]) / 3; - imgData[ofs] = v; - } else { - // 50% c0, 50% c1 - int v = 0xff000000; - v |= ((colors[2] + colors[6]) >>> 1) << 16; - v |= ((colors[1] + colors[5]) >>> 1) << 8; - v |= (colors[0] + colors[4]) >>> 1; - imgData[ofs] = v; - } - } else { - if ((c & 0xffff) > ((c >>> 16) & 0xffff)) { - // 33% c0, 66% c1 - int v = 0xff000000; - v |= ((colors[2] + (colors[6] << 1)) / 3) << 16; - v |= ((colors[1] + (colors[5] << 1)) / 3) << 8; - v |= (colors[0] + (colors[4] << 1)) / 3; - imgData[ofs] = v; - } else { - // transparent - imgData[ofs] = 0; - } - } - } - - pvrOfs += wordSize; - imgOfs += 4; - } - pvrOfs += (wordImageWidth - wordRectWidth) * wordSize; - imgOfs += imgWidthAligned * 4 - rect.width; - } - imgData = null; - - // rendering aligned image to target image - if (alignedImage != null) { - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, - region.y + region.height, null); - } finally { - g.dispose(); - g = null; - } - alignedImage = null; - } - return true; - } - - /** - * Decodes PVR data in DXT3 format and draws the specified "region" into "image". - * - * @param pvr The PVR data - * @param image The output image - * @param region The of the PVR texture region to draw onto "image" - * @return The success state of the operation. - * @throws Exception on error. - */ - public static boolean decodeDXT3(PvrInfo pvr, BufferedImage image, Rectangle region) throws Exception { - if (pvr == null || image == null || region == null) { - return false; - } - - int imgWidth = image.getWidth(); - int imgHeight = image.getHeight(); - int[] imgData = null; - - // checking region bounds and alignment - if (region.x < 0) { - region.width += -region.x; - region.x = 0; - } - if (region.y < 0) { - region.height += -region.y; - region.y = 0; - } - if (region.x + region.width > pvr.width) - region.width = pvr.width - region.x; - if (region.y + region.height > pvr.height) - region.height = pvr.height - region.y; - Rectangle rect = alignRectangle(region, 4, 4); - - // preparing aligned image buffer for faster rendering - BufferedImage alignedImage = null; - int imgWidthAligned; - if (!region.equals(rect)) { - alignedImage = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB); - imgWidthAligned = alignedImage.getWidth(); - imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); - // translating "region" to be relative to "rect" - region.x -= rect.x; - region.y -= rect.y; - if (imgWidth < region.width) { - region.width = imgWidth; - } - if (imgHeight < region.height) { - region.height = imgHeight; - } - } else { - imgWidthAligned = imgWidth; - imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - } - - final int wordSize = 16; // data size of an encoded 4x4 pixel block - int wordImageWidth = pvr.width >>> 2; // the image width in data blocks - int wordRectWidth = rect.width >>> 2; // the aligned region's width in data blocks - int wordRectHeight = rect.height >>> 2; // the aligned region's height in data blocks - int wordPosX = rect.x >>> 2; - int wordPosY = rect.y >>> 2; - - int[] colors = new int[8]; - int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; - int imgOfs = 0; - for (int y = 0; y < wordRectHeight; y++) { - for (int x = 0; x < wordRectWidth; x++) { - // decoding single DXT3 block - long alpha = DynamicArray.getByte(pvr.data, pvrOfs); - int c = DynamicArray.getInt(pvr.data, pvrOfs + 8); - unpackColors565(c, colors); - int code = DynamicArray.getInt(pvr.data, pvrOfs + 12); - for (int idx = 0; idx < 16; idx++, code >>>= 2, alpha >>>= 4) { - // calculating alpha (4 bit -> 8 bit) - int ofs = imgOfs + (idx >>> 2) * imgWidthAligned + (idx & 3); - int color = (int) (alpha & 0xf) << 24; - color |= color << 4; - // decoding pixels - if ((code & 3) == 0) { - // 100% c0, 0% c1 - color |= (colors[2] << 16) | (colors[1] << 8) | colors[0]; - } else if ((code & 3) == 1) { - // 0% c0, 100% c1 - color |= (colors[6] << 16) | (colors[5] << 8) | colors[4]; - } else if ((code & 3) == 2) { - // 66% c0, 33% c1 - int v = (((colors[2] << 1) + colors[6]) / 3) << 16; - color |= v; - v = (((colors[1] << 1) + colors[5]) / 3) << 8; - color |= v; - v = ((colors[0] << 1) + colors[4]) / 3; - color |= v; - } else { - // 33% c0, 66% c1 - int v = ((colors[2] + (colors[6] << 1)) / 3) << 16; - color |= v; - v = ((colors[1] + (colors[5] << 1)) / 3) << 8; - color |= v; - v = (colors[0] + (colors[4] << 1)) / 3; - if (v > 255) - v = 255; - color |= v; - } - imgData[ofs] = color; - } - - pvrOfs += wordSize; - imgOfs += 4; - } - pvrOfs += (wordImageWidth - wordRectWidth) * wordSize; - imgOfs += (imgWidthAligned << 2) - rect.width; - } - imgData = null; - - // rendering aligned image to target image - if (alignedImage != null) { - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, - region.y + region.height, null); - } finally { - g.dispose(); - g = null; - } - alignedImage = null; - } - return true; - } - - /** - * Decodes PVR data in DXT5 format and draws the specified "region" into "image". - * - * @param pvr The PVR data - * @param image The output image - * @param region The of the PVR texture region to draw onto "image" - * @return The success state of the operation. - * @throws Exception on error. - */ - public static boolean decodeDXT5(PvrInfo pvr, BufferedImage image, Rectangle region) throws Exception { - if (pvr == null || image == null || region == null) { - return false; - } - - int imgWidth = image.getWidth(); - int imgHeight = image.getHeight(); - int[] imgData = null; - - // checking region bounds and alignment - if (region.x < 0) { - region.width += -region.x; - region.x = 0; - } - if (region.y < 0) { - region.height += -region.y; - region.y = 0; - } - if (region.x + region.width > pvr.width) - region.width = pvr.width - region.x; - if (region.y + region.height > pvr.height) - region.height = pvr.height - region.y; - Rectangle rect = alignRectangle(region, 4, 4); - - // preparing aligned image buffer for faster rendering - BufferedImage alignedImage = null; - int imgWidthAligned; - if (!region.equals(rect)) { - alignedImage = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB); - imgWidthAligned = alignedImage.getWidth(); - imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); - // translating "region" to be relative to "rect" - region.x -= rect.x; - region.y -= rect.y; - if (imgWidth < region.width) { - region.width = imgWidth; - } - if (imgHeight < region.height) { - region.height = imgHeight; - } - } else { - imgWidthAligned = imgWidth; - imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - } - - final int wordSize = 16; // data size of an encoded 4x4 pixel block - int wordImageWidth = pvr.width >>> 2; // the image width in data blocks - int wordRectWidth = rect.width >>> 2; // the aligned region's width in data blocks - int wordRectHeight = rect.height >>> 2; // the aligned region's height in data blocks - int wordPosX = rect.x >>> 2; - int wordPosY = rect.y >>> 2; - - int[] alpha = new int[8]; - int[] colors = new int[8]; - int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; - int imgOfs = 0; - for (int y = 0; y < wordRectHeight; y++) { - for (int x = 0; x < wordRectWidth; x++) { - // creating alpha table - alpha[0] = DynamicArray.getByte(pvr.data, pvrOfs) & 0xff; - alpha[1] = DynamicArray.getByte(pvr.data, pvrOfs + 1) & 0xff; - if (alpha[0] > alpha[1]) { - alpha[2] = (6 * alpha[0] + alpha[1]) / 7; - alpha[3] = (5 * alpha[0] + 2 * alpha[1]) / 7; - alpha[4] = (4 * alpha[0] + 3 * alpha[1]) / 7; - alpha[5] = (3 * alpha[0] + 4 * alpha[1]) / 7; - alpha[6] = (2 * alpha[0] + 5 * alpha[1]) / 7; - alpha[7] = (alpha[0] + 6 * alpha[1]) / 7; - } else { - alpha[2] = (4 * alpha[0] + alpha[1]) / 5; - alpha[3] = (3 * alpha[0] + 2 * alpha[1]) / 5; - alpha[4] = (2 * alpha[0] + 3 * alpha[1]) / 5; - alpha[5] = (alpha[0] + 4 * alpha[1]) / 5; - alpha[6] = 0; - alpha[7] = 255; - } - - // decoding single DXT5 block - long ctrl = DynamicArray.getLong(pvr.data, pvrOfs + 2) & 0xffffffffffffL; - int c = DynamicArray.getInt(pvr.data, pvrOfs + 8); - unpackColors565(c, colors); - int code = DynamicArray.getInt(pvr.data, pvrOfs + 12); - for (int idx = 0; idx < 16; idx++, code >>>= 2, ctrl >>>= 3) { - int ofs = imgOfs + (idx >>> 2) * imgWidthAligned + (idx & 3); - int color = alpha[(int) (ctrl & 7L)] << 24; - if ((code & 3) == 0) { - // 100% c0, 0% c1 - color |= (colors[2] << 16) | (colors[1] << 8) | colors[0]; - } else if ((code & 3) == 1) { - // 0% c0, 100% c1 - color |= (colors[6] << 16) | (colors[5] << 8) | colors[4]; - } else if ((code & 3) == 2) { - // 66% c0, 33% c1 - int v = (((colors[2] << 1) + colors[6]) / 3) << 16; - color |= v; - v = (((colors[1] << 1) + colors[5]) / 3) << 8; - color |= v; - v = ((colors[0] << 1) + colors[4]) / 3; - color |= v; - } else { - // 33% c0, 66% c1 - int v = ((colors[2] + (colors[6] << 1)) / 3) << 16; - color |= v; - v = ((colors[1] + (colors[5] << 1)) / 3) << 8; - color |= v; - v = (colors[0] + (colors[4] << 1)) / 3; - if (v > 255) - v = 255; - color |= v; - } - imgData[ofs] = color; - } - - pvrOfs += wordSize; - imgOfs += 4; - } - pvrOfs += (wordImageWidth - wordRectWidth) * wordSize; - imgOfs += (imgWidthAligned << 2) - rect.width; - } - imgData = null; - - // rendering aligned image to target image - if (alignedImage != null) { - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, - region.y + region.height, null); - } finally { - g.dispose(); - g = null; - } - alignedImage = null; - } - return true; - } - - // Converts two RGB565 words into separate components, ordered { B, G, R, A, B, G, R, A } - private static void unpackColors565(int inData, int[] outData) { - outData[0] = ((inData << 3) & 0xf8) | (inData >>> 2) & 0x07; // b1 - outData[1] = ((inData >>> 3) & 0xfc) | (inData >>> 9) & 0x03; // g1 - outData[2] = ((inData >>> 8) & 0xf8) | (inData >>> 13) & 0x07; // r1 - outData[3] = 255; // a1 - outData[4] = ((inData >>> 13) & 0xf8) | (inData >>> 18) & 0x07; // b2 - outData[5] = ((inData >>> 19) & 0xfc) | (inData >>> 25) & 0x03; // g2 - outData[6] = ((inData >>> 24) & 0xf8) | (inData >>> 29) & 0x07; // r2 - outData[7] = 255; // a2 - } - } - - // Decodes PVRTC pixel data. - private static class DecodePVRT { - // The local cache list for decoded PVR textures. The "key" has to be a unique PvrInfo structure. - private static final Map TEXTURE_CACHE = Collections - .synchronizedMap(new LinkedHashMap()); - - // The max. number of cache entries to hold - private static final int MAX_CACHE_ENTRIES = 8; - - // Datatypes as used in the reference implementation: - // Pixel32/128S: int[]{red, green, blue, alpha} - // PVRTCWord: int[]{modulation, color} - // PVRTCWordIndices: int[]{p0, p1, q0, q1, r0, r1, s0, s1} - - // color channel indices into an array of pixel values - private static final int CH_R = 0; - private static final int CH_G = 1; - private static final int CH_B = 2; - private static final int CH_A = 3; - // start indices into an array of PVRTC blocks - private static final int IDX_P = 0; - private static final int IDX_Q = 2; - private static final int IDX_R = 4; - private static final int IDX_S = 6; - // indices into a PVRTC data block - private static final int BLK_MOD = 0; - private static final int BLK_COL = 1; - - /** Removes all PvrDecoder objects from the cache. */ - public static void flushCache() { - TEXTURE_CACHE.clear(); - } - - // Returns a PvrDecoder object only if it already exists in the cache. - private static BufferedImage getCachedImage(PvrInfo pvr) { - BufferedImage retVal = null; - if (pvr != null) { - if (TEXTURE_CACHE.containsKey(pvr)) { - retVal = TEXTURE_CACHE.get(pvr); - // re-inserting entry to prevent premature removal from cache - TEXTURE_CACHE.remove(pvr); - TEXTURE_CACHE.put(pvr, retVal); - } - } - return retVal; - } - - // Returns a PvrDecoder object of the specified key if available, or creates and returns a new one otherwise. - private static void registerCachedImage(PvrInfo pvr, BufferedImage image) { - if (pvr != null && getCachedImage(pvr) == null && image != null) { - TEXTURE_CACHE.put(pvr, image); - // removing excess cache entries - while (TEXTURE_CACHE.size() > MAX_CACHE_ENTRIES) { - TEXTURE_CACHE.remove(TEXTURE_CACHE.keySet().iterator().next()); - } - } - } - - /** - * Decodes PVR data in PVRT 2bpp format and draws the specified "region" into "image". - * - * @param pvr The PVR data - * @param image The output image - * @param region The of the PVR texture region to draw onto "image" - * @return The success state of the operation. - * @throws Exception on error. - */ - public static boolean decodePVRT2bpp(PvrInfo pvr, BufferedImage image, Rectangle region) throws Exception { - return decodePVRT(pvr, image, region, true); - } - - /** - * Decodes PVR data in PVRT 4bpp format and draws the specified "region" into "image". - * - * @param pvr The PVR data - * @param image The output image - * @param region The of the PVR texture region to draw onto "image" - * @return The success state of the operation. - * @throws Exception on error. - */ - public static boolean decodePVRT4bpp(PvrInfo pvr, BufferedImage image, Rectangle region) throws Exception { - return decodePVRT(pvr, image, region, false); - } - - // Decodes both 2bpp and 4bpp versions of the PVRT format - private static boolean decodePVRT(PvrInfo pvr, BufferedImage image, Rectangle region, boolean is2bpp) - throws Exception { - if (pvr == null || image == null || region == null) { - return false; - } - - int imgWidth = image.getWidth(); - int imgHeight = image.getHeight(); - int[] imgData = null; - - // bounds checking - if (region.x < 0) { - region.width += -region.x; - region.x = 0; - } - if (region.y < 0) { - region.height += -region.y; - region.y = 0; - } - if (region.x + region.width > pvr.width) - region.width = pvr.width - region.x; - if (region.y + region.height > pvr.height) - region.height = pvr.height - region.y; - - // preparing image buffer for faster rendering - BufferedImage alignedImage = getCachedImage(pvr); - if (alignedImage == null) { - if (!region.equals(new Rectangle(0, 0, pvr.width, pvr.height))) { - alignedImage = new BufferedImage(pvr.width, pvr.height, BufferedImage.TYPE_INT_ARGB); - imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); - if (imgWidth < region.width) { - region.width = imgWidth; - } - if (imgHeight < region.height) { - region.height = imgHeight; - } - } else { - imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - } - - int wordWidth = is2bpp ? 8 : 4; - int wordHeight = 4; - int numXWords = pvr.width / wordWidth; - int numYWords = pvr.height / wordHeight; - int[] indices = new int[8]; - int[] p = new int[2], q = new int[2], r = new int[2], s = new int[2]; - int[][] pixels = new int[wordWidth * wordHeight][4]; - - for (int wordY = -1; wordY < numYWords - 1; wordY++) { - for (int wordX = -1; wordX < numXWords - 1; wordX++) { - indices[IDX_P] = wrapWordIndex(numXWords, wordX); - indices[IDX_P + 1] = wrapWordIndex(numYWords, wordY); - indices[IDX_Q] = wrapWordIndex(numXWords, wordX + 1); - indices[IDX_Q + 1] = wrapWordIndex(numYWords, wordY); - indices[IDX_R] = wrapWordIndex(numXWords, wordX); - indices[IDX_R + 1] = wrapWordIndex(numYWords, wordY + 1); - indices[IDX_S] = wrapWordIndex(numXWords, wordX + 1); - indices[IDX_S + 1] = wrapWordIndex(numYWords, wordY + 1); - - // work out the offsets into the twiddle structs, multiply by two as there are two members per word - int[] wordOffsets = new int[] { twiddleUV(numXWords, numYWords, indices[IDX_P], indices[IDX_P + 1]) << 1, - twiddleUV(numXWords, numYWords, indices[IDX_Q], indices[IDX_Q + 1]) << 1, - twiddleUV(numXWords, numYWords, indices[IDX_R], indices[IDX_R + 1]) << 1, - twiddleUV(numXWords, numYWords, indices[IDX_S], indices[IDX_S + 1]) << 1 }; - - // access individual elements to fill out input words - p[BLK_MOD] = DynamicArray.getInt(pvr.data, wordOffsets[0] << 2); - p[BLK_COL] = DynamicArray.getInt(pvr.data, (wordOffsets[0] + 1) << 2); - q[BLK_MOD] = DynamicArray.getInt(pvr.data, wordOffsets[1] << 2); - q[BLK_COL] = DynamicArray.getInt(pvr.data, (wordOffsets[1] + 1) << 2); - r[BLK_MOD] = DynamicArray.getInt(pvr.data, wordOffsets[2] << 2); - r[BLK_COL] = DynamicArray.getInt(pvr.data, (wordOffsets[2] + 1) << 2); - s[BLK_MOD] = DynamicArray.getInt(pvr.data, wordOffsets[3] << 2); - s[BLK_COL] = DynamicArray.getInt(pvr.data, (wordOffsets[3] + 1) << 2); - - // assemble four words into struct to get decompressed pixels from - getDecompressedPixels(p, q, r, s, pixels, is2bpp); - mapDecompressedData(imgData, pvr.width, pixels, indices, is2bpp); - } - } - imgData = null; - registerCachedImage(pvr, alignedImage); - } else { - if (imgWidth < region.width) { - region.width = imgWidth; - } - if (imgHeight < region.height) { - region.height = imgHeight; - } - } - - // rendering aligned image to target image - if (alignedImage != null) { - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); - g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, - region.y + region.height, null); - } finally { - g.dispose(); - g = null; - } - alignedImage = null; - } - return true; - } - - // Decodes the first color in a PVRT data word - private static int[] getColorA(int colorData) { - int[] retVal = new int[4]; - if ((colorData & 0x8000) != 0) { - // opaque color mode: RGB554 - retVal[CH_R] = (colorData & 0x7c00) >>> 10; // red: 5->5 bits - retVal[CH_G] = (colorData & 0x3e0) >>> 5; // green: 5->5 bits - retVal[CH_B] = (colorData & 0x1e) | ((colorData & 0x1e) >>> 4); // blue: 4->5 bits - retVal[CH_A] = 0x0f; // alpha: 0->4 bits - } else { - // transparent color mode: ARGB3443 - retVal[CH_R] = ((colorData & 0xf00) >>> 7) | ((colorData & 0xf00) >>> 11); // red: 4->5 bits - retVal[CH_G] = ((colorData & 0xf0) >>> 3) | ((colorData & 0xf0) >>> 7); // green: 4->5 bits - retVal[CH_B] = ((colorData & 0xe) << 1) | ((colorData & 0xe) >>> 2); // blue: 3->5 bits - retVal[CH_A] = ((colorData & 0x7000) >>> 11); // alpha: 3->4 bits - } - return retVal; - } - - // Decodes the second color in a PVRT data word - private static int[] getColorB(int colorData) { - int[] retVal = new int[4]; - if ((colorData & 0x80000000) != 0) { - // opaque color mode: RGB555 - retVal[CH_R] = (colorData & 0x7c000000) >>> 26; // red: 5->5 bits - retVal[CH_G] = (colorData & 0x3e00000) >>> 21; // green: 5->5 bits - retVal[CH_B] = (colorData & 0x1f0000) >>> 16; // blue: 5->5 bits - retVal[CH_A] = 0x0f; // alpha: 0->4 bits - } else { - // transparent color mode: ARGB3444 - retVal[CH_R] = ((colorData & 0xf000000) >>> 23) | ((colorData & 0xf000000) >>> 27); // red: 4->5 bits - retVal[CH_G] = ((colorData & 0xf00000) >>> 19) | ((colorData & 0xf00000) >>> 23); // green: 4->5 bits - retVal[CH_B] = ((colorData & 0xf0000) >>> 15) | ((colorData & 0xf0000) >>> 19); // blue: 4->5 bits - retVal[CH_A] = ((colorData & 0x70000000) >>> 27); // alpha: 3->4 bits - } - return retVal; - } - - // Bilinear upscale from 2x2 pixels to 4x4/8x4 pixels (depending on is2bpp argument) - // p, q, r, s = [channels] - // outBlock = [pixels][channels] - // is2bpp: true=2bpp mode, false=4bpp mode - private static void interpolateColors(int[] p, int[] q, int[] r, int[] s, int[][] outBlock, boolean is2bpp) { - int wordWidth = is2bpp ? 8 : 4; - int wordHeight = 4; - - // making working copy - int[] hp = Arrays.copyOf(p, p.length); - int[] hq = Arrays.copyOf(q, q.length); - int[] hr = Arrays.copyOf(r, r.length); - int[] hs = Arrays.copyOf(s, s.length); - - // get vectors - int[] qmp = new int[] { hq[CH_R] - hp[CH_R], hq[CH_G] - hp[CH_G], hq[CH_B] - hp[CH_B], hq[CH_A] - hp[CH_A] }; - int[] smr = new int[] { hs[CH_R] - hr[CH_R], hs[CH_G] - hr[CH_G], hs[CH_B] - hr[CH_B], hs[CH_A] - hr[CH_A] }; - - // multiply colors - for (int i = 0; i < 4; i++) { - hp[i] *= wordWidth; - hr[i] *= wordWidth; - } - - int[] result = new int[4], dy = new int[4]; - if (is2bpp) { - // loop through pixels to achieve results - for (int x = 0; x < wordWidth; x++) { - for (int i = 0; i < 4; i++) { - result[i] = hp[i] << 2; - dy[i] = hr[i] - hp[i]; - } - - for (int y = 0; y < wordHeight; y++) { - outBlock[y * wordWidth + x][CH_R] = (result[CH_R] >> 7) + (result[CH_R] >> 2); - outBlock[y * wordWidth + x][CH_G] = (result[CH_G] >> 7) + (result[CH_G] >> 2); - outBlock[y * wordWidth + x][CH_B] = (result[CH_B] >> 7) + (result[CH_B] >> 2); - outBlock[y * wordWidth + x][CH_A] = (result[CH_A] >> 5) + (result[CH_A] >> 1); - - result[CH_R] += dy[CH_R]; - result[CH_G] += dy[CH_G]; - result[CH_B] += dy[CH_B]; - result[CH_A] += dy[CH_A]; - } - - hp[CH_R] += qmp[CH_R]; - hp[CH_G] += qmp[CH_G]; - hp[CH_B] += qmp[CH_B]; - hp[CH_A] += qmp[CH_A]; - hr[CH_R] += smr[CH_R]; - hr[CH_G] += smr[CH_G]; - hr[CH_B] += smr[CH_B]; - hr[CH_A] += smr[CH_A]; - } - } else { - // loop through pixels to achieve results - for (int y = 0; y < wordHeight; y++) { - for (int i = 0; i < 4; i++) { - result[i] = hp[i] << 2; - dy[i] = hr[i] - hp[i]; - } - - for (int x = 0; x < wordWidth; x++) { - outBlock[y * wordWidth + x][CH_R] = (result[CH_R] >> 6) + (result[CH_R] >> 1); - outBlock[y * wordWidth + x][CH_G] = (result[CH_G] >> 6) + (result[CH_G] >> 1); - outBlock[y * wordWidth + x][CH_B] = (result[CH_B] >> 6) + (result[CH_B] >> 1); - outBlock[y * wordWidth + x][CH_A] = (result[CH_A] >> 4) + result[CH_A]; - - result[CH_R] += dy[CH_R]; - result[CH_G] += dy[CH_G]; - result[CH_B] += dy[CH_B]; - result[CH_A] += dy[CH_A]; - } - - hp[CH_R] += qmp[CH_R]; - hp[CH_G] += qmp[CH_G]; - hp[CH_B] += qmp[CH_B]; - hp[CH_A] += qmp[CH_A]; - hr[CH_R] += smr[CH_R]; - hr[CH_G] += smr[CH_G]; - hr[CH_B] += smr[CH_B]; - hr[CH_A] += smr[CH_A]; - } - } - } - - // Reads out and decodes the modulation values within the specified data word - // modValues, modModes = [x][y] - private static void unpackModulations(int[] word, int ofsX, int ofsY, int[][] modValues, int[][] modModes, - boolean is2bpp) { - int modMode = word[BLK_COL] & 1; - int modBits = word[BLK_MOD]; - - // unpack differently depending on 2bpp or 4bpp modes - if (is2bpp) { - if (modMode != 0) { - // determine which of the three modes are in use: - if ((modBits & 1) != 0) { - // look at the LSB for the center (V=2, H=4) texel. Its LSB is now actually used to - // indicate whether it's the H-only mode or the V-only - - // the center texel data is at (y=2, x=4) and so its LSB is at bit 20 - if ((modBits & (1 << 20)) != 0) { - // this is V-only mode - modMode = 3; - } else { - // this is H-only mode - modMode = 2; - } - - // create an extra bit for the center pixel so that it looks like we have 2 actual bits - // for this texel. It makes later coding much easier. - if ((modBits & (1 << 21)) != 0) { - modBits |= (1 << 20); - } else { - modBits &= ~(1 << 20); - } - } - - if ((modBits & 2) != 0) { - modBits |= 1; // set it - } else { - modBits &= ~1; // clear it - } - - // run through all the pixels in the block. Note we can now treat all the stored values as - // if they have 2 bits (even when they didn't!) - for (int y = 0; y < 4; y++) { - for (int x = 0; x < 8; x++) { - modModes[x + ofsX][y + ofsY] = modMode; - - // if this is a stored value... - if (((x ^ y) & 1) == 0) { - modValues[x + ofsX][y + ofsY] = modBits & 3; - modBits >>>= 2; - } - } - } - } else { - // if direct encoded 2bpp mode - i.e. mode bit per pixel - for (int y = 0; y < 4; y++) { - for (int x = 0; x < 8; x++) { - modModes[x + ofsX][y + ofsY] = modMode; - - // double the bits, so 0 -> 00, and 1 -> 11 - modValues[x + ofsX][y + ofsY] = ((modBits & 1) != 0) ? 3 : 0; - modBits >>>= 1; - } - } - } - } else { - // much simpler than 2bpp decompression, only two modes, so the n/8 values are set directly - // run through all the pixels in the word - if (modMode != 0) { - for (int y = 0; y < 4; y++) { - for (int x = 0; x < 4; x++) { - modValues[y + ofsY][x + ofsX] = modBits & 3; - if (modValues[y + ofsY][x + ofsX] == 1) { - modValues[y + ofsY][x + ofsX] = 4; - } else if (modValues[y + ofsY][x + ofsX] == 2) { - modValues[y + ofsY][x + ofsX] = 14; // +10 tells the decompressor to punch through alpha - } else if (modValues[y + ofsY][x + ofsX] == 3) { - modValues[y + ofsY][x + ofsX] = 8; - } - modBits >>>= 2; - } - } - } else { - for (int y = 0; y < 4; y++) { - for (int x = 0; x < 4; x++) { - modValues[y + ofsY][x + ofsX] = modBits & 3; - modValues[y + ofsY][x + ofsX] *= 3; - if (modValues[y + ofsY][x + ofsX] > 3) { - modValues[y + ofsY][x + ofsX] -= 1; - } - modBits >>>= 2; - } - } - } - } - } - - // Gets the effective modulation values for the given pixel - // modValues, modModes = [x][y] - // xPos, yPos = x, y positions within the current data word - private static int getModulationValues(int[][] modValues, int[][] modModes, int xPos, int yPos, boolean is2bpp) { - if (is2bpp) { - final int[] repVals0 = new int[] { 0, 3, 5, 8 }; - - // extract the modulation value... - if (modModes[xPos][yPos] == 0) { - // ...if a simple encoding - return repVals0[modValues[xPos][yPos]]; - } else { - // ...if this is a stored value - if (((xPos ^ yPos) & 1) == 0) { - return repVals0[modValues[xPos][yPos]]; - - // else average from the neighbors - } else if (modModes[xPos][yPos] == 1) { - // if H&V interpolation - return (repVals0[modValues[xPos][yPos - 1]] + repVals0[modValues[xPos][yPos + 1]] - + repVals0[modValues[xPos - 1][yPos]] + repVals0[modValues[xPos + 1][yPos]] + 2) >> 2; - } else if (modModes[xPos][yPos] == 2) { - // if H-only - return (repVals0[modValues[xPos - 1][yPos]] + repVals0[modValues[xPos + 1][yPos]] + 1) >> 1; - } else { - // if V-only - return (repVals0[modValues[xPos][yPos - 1]] + repVals0[modValues[xPos][yPos + 1]] + 1) >> 1; - } - } - } else { - return modValues[xPos][yPos]; - } - } - - // Gets decompressed pixels for a given decompression area - // p, q, r, s = [block word] - // outBlock = [pixels][channels] - // is2bpp: true=2bpp mode, false=4bpp mode - private static void getDecompressedPixels(int[] p, int[] q, int[] r, int[] s, int[][] outData, boolean is2bpp) { - // 4bpp only needs 8*8 values, but 2bpp needs 16*8, so rather than wasting processor time we just statically - // allocate 16*8 - int[][] modValues = new int[16][8]; - // Only 2bpp needs this - int[][] modModes = new int[16][8]; - // 4bpp only needs 16 values, but 2bpp needs 32, so rather than wasting processor time we just statically allocate - // 32. - int[][] upscaledColorA = new int[32][4]; - int[][] upscaledColorB = new int[32][4]; - - int wordWidth = is2bpp ? 8 : 4; - int wordHeight = 4; - - // get modulation from each word - unpackModulations(p, 0, 0, modValues, modModes, is2bpp); - unpackModulations(q, wordWidth, 0, modValues, modModes, is2bpp); - unpackModulations(r, 0, wordHeight, modValues, modModes, is2bpp); - unpackModulations(s, wordWidth, wordHeight, modValues, modModes, is2bpp); - - // bilinear upscale image data from 2x2 -> 4x4 - interpolateColors(getColorA(p[BLK_COL]), getColorA(q[BLK_COL]), getColorA(r[BLK_COL]), getColorA(s[BLK_COL]), - upscaledColorA, is2bpp); - interpolateColors(getColorB(p[BLK_COL]), getColorB(q[BLK_COL]), getColorB(r[BLK_COL]), getColorB(s[BLK_COL]), - upscaledColorB, is2bpp); - - int[] result = new int[4]; - for (int y = 0; y < wordHeight; y++) { - for (int x = 0; x < wordWidth; x++) { - int mod = getModulationValues(modValues, modModes, x + (wordWidth >>> 1), y + (wordHeight >>> 1), is2bpp); - boolean punchThroughAlpha = false; - if (mod > 10) { - punchThroughAlpha = true; - mod -= 10; - } - - result[CH_R] = (upscaledColorA[y * wordWidth + x][CH_R] * (8 - mod) - + upscaledColorB[y * wordWidth + x][CH_R] * mod) >> 3; - result[CH_G] = (upscaledColorA[y * wordWidth + x][CH_G] * (8 - mod) - + upscaledColorB[y * wordWidth + x][CH_G] * mod) >> 3; - result[CH_B] = (upscaledColorA[y * wordWidth + x][CH_B] * (8 - mod) - + upscaledColorB[y * wordWidth + x][CH_B] * mod) >> 3; - if (punchThroughAlpha) { - result[CH_A] = 0; - } else { - result[CH_A] = (upscaledColorA[y * wordWidth + x][CH_A] * (8 - mod) - + upscaledColorB[y * wordWidth + x][CH_A] * mod) >> 3; - } - - // convert the 32bit precision result to 8 bit per channel color - if (is2bpp) { - outData[y * wordWidth + x][CH_R] = result[CH_R]; - outData[y * wordWidth + x][CH_G] = result[CH_G]; - outData[y * wordWidth + x][CH_B] = result[CH_B]; - outData[y * wordWidth + x][CH_A] = result[CH_A]; - } else { - outData[y + x * wordHeight][CH_R] = result[CH_R]; - outData[y + x * wordHeight][CH_G] = result[CH_G]; - outData[y + x * wordHeight][CH_B] = result[CH_B]; - outData[y + x * wordHeight][CH_A] = result[CH_A]; - } - } - } - } - - // Maps decompressed data to the correct location in the output buffer - private static int wrapWordIndex(int numWords, int word) { - return ((word + numWords) % numWords); - } - - // Given the word coordinates and the dimension of the texture in words, this returns the twiddled - // offset of the word from the start of the map - private static int twiddleUV(int xSize, int ySize, int xPos, int yPos) { - // initially assume x is the larger size - int minDimension = xSize; - int maxValue = yPos; - int twiddled = 0; - int srcBitPos = 1; - int dstBitPos = 1; - int shiftCount = 0; - - // if y is the larger dimension - switch the min/max values - if (ySize < xSize) { - minDimension = ySize; - maxValue = xPos; - } - - // step through all the bits in the "minimum" dimension - while (srcBitPos < minDimension) { - if ((yPos & srcBitPos) != 0) { - twiddled |= dstBitPos; - } - - if ((xPos & srcBitPos) != 0) { - twiddled |= (dstBitPos << 1); - } - - srcBitPos <<= 1; - dstBitPos <<= 2; - shiftCount++; - } - - // prepend any unused bits - maxValue >>>= shiftCount; - twiddled |= (maxValue << (shiftCount << 1)); - - return twiddled; - } - - // Maps decompressed data to the correct location in the output buffer - // outBuffer = [pixel] - // inData = [pixel][channel] - // indices = [two per p, q, r, s] - private static void mapDecompressedData(int[] outBuffer, int width, int[][] inData, int[] indices, boolean is2bpp) { - int wordWidth = is2bpp ? 8 : 4; - int wordHeight = 4; - - for (int y = 0; y < (wordHeight >>> 1); y++) { - for (int x = 0; x < (wordWidth >>> 1); x++) { - // map p - int outOfs = (((indices[IDX_P + 1] * wordHeight) + y + (wordHeight >>> 1)) * width - + indices[IDX_P + 0] * wordWidth + x + (wordWidth >>> 1)); - int inOfs = y * wordWidth + x; - outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) - | inData[inOfs][CH_B]; - - // map q - outOfs = (((indices[IDX_Q + 1] * wordHeight) + y + (wordHeight >>> 1)) * width - + indices[IDX_Q + 0] * wordWidth + x); - inOfs = y * wordWidth + x + (wordWidth >>> 1); - outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) - | inData[inOfs][CH_B]; - - // map r - outOfs = (((indices[IDX_R + 1] * wordHeight) + y) * width + indices[IDX_R + 0] * wordWidth + x - + (wordWidth >>> 1)); - inOfs = (y + (wordHeight >>> 1)) * wordWidth + x; - outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) - | inData[inOfs][CH_B]; - - // map s - outOfs = (((indices[IDX_S + 1] * wordHeight) + y) * width + indices[IDX_S + 0] * wordWidth + x); - inOfs = (y + (wordHeight >>> 1)) * wordWidth + x + (wordWidth >>> 1); - outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) - | inData[inOfs][CH_B]; - } - } - } - } } diff --git a/src/org/infinity/resource/graphics/PvrzResource.java b/src/org/infinity/resource/graphics/PvrzResource.java index 37114ba35..cd20a3473 100644 --- a/src/org/infinity/resource/graphics/PvrzResource.java +++ b/src/org/infinity/resource/graphics/PvrzResource.java @@ -207,12 +207,12 @@ private void showProperties() { try { decoder = PvrDecoder.loadPvr(entry); String resName = entry.getResourceName().toUpperCase(Locale.ENGLISH); - int width = decoder.getWidth(); - int height = decoder.getHeight(); + int width = decoder.getInfo().getWidth(); + int height = decoder.getInfo().getHeight(); String br = "
"; String type; - type = decoder.getPixelFormat().toString(); + type = decoder.getInfo().getPixelFormat().toString(); StringBuilder sb = new StringBuilder("
"); sb.append("Type:   ").append(type).append(br); sb.append("Width:  ").append(width).append(br); @@ -232,7 +232,7 @@ private BufferedImage loadImage() { if (entry != null) { try { decoder = PvrDecoder.loadPvr(entry); - image = new BufferedImage(decoder.getWidth(), decoder.getHeight(), BufferedImage.TYPE_INT_ARGB); + image = new BufferedImage(decoder.getInfo().getWidth(), decoder.getInfo().getHeight(), BufferedImage.TYPE_INT_ARGB); if (!decoder.decode(image)) { image = null; } diff --git a/src/org/infinity/resource/graphics/TisResource.java b/src/org/infinity/resource/graphics/TisResource.java index 9fb2a439e..506bff6df 100644 --- a/src/org/infinity/resource/graphics/TisResource.java +++ b/src/org/infinity/resource/graphics/TisResource.java @@ -22,6 +22,8 @@ import java.awt.event.ItemListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.beans.PropertyChangeEvent; @@ -36,6 +38,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @@ -50,6 +53,7 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JTextField; @@ -66,19 +70,23 @@ import org.infinity.gui.ButtonPanel; import org.infinity.gui.ButtonPopupMenu; import org.infinity.gui.TileGrid; +import org.infinity.gui.ViewFrame; import org.infinity.gui.WindowBlocker; import org.infinity.gui.converter.ConvertToPvrz; import org.infinity.gui.converter.ConvertToTis; import org.infinity.gui.converter.ConvertToTis.TileEntry; +import org.infinity.resource.AbstractStruct; import org.infinity.resource.Closeable; import org.infinity.resource.Profile; import org.infinity.resource.Referenceable; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; +import org.infinity.resource.StructEntry; import org.infinity.resource.ViewableContainer; import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.wed.Door; +import org.infinity.resource.wed.IndexNumber; import org.infinity.resource.wed.Overlay; import org.infinity.resource.wed.Tilemap; import org.infinity.resource.wed.WedResource; @@ -131,11 +139,21 @@ private enum Status { private static final int DEFAULT_COLUMNS = 5; + private static final String FMT_TILEINFO_SHOW = "Tile %d: Show PVRZ information..."; + private static final String FMT_TILEINFO_PVRZ = "Tile %d: Open PVRZ resource..."; + private static final String FMT_TILEINFO_WED = "Tile %d: Open WED overlay tilemap... "; + private static boolean showGrid = false; private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); + private final JPopupMenu menuTileInfo = new JPopupMenu(); + private final JMenuItem miTileInfoShow = new JMenuItem(); + private final JMenuItem miTileInfoPvrz = new JMenuItem(); + private final JMenuItem miTileInfoWed = new JMenuItem(); + private WedResource wedResource; + private HashMap wedTileMap; private TisDecoder decoder; private List tileImages; // stores one tile per image private TileGrid tileGrid; // the main component for displaying the tileset @@ -153,6 +171,7 @@ private enum Status { private SwingWorker workerExport; private WindowBlocker blocker; private int defaultWidth; + private int lastTileInfoIndex = -1; public TisResource(ResourceEntry entry) throws Exception { this.entry = entry; @@ -227,6 +246,25 @@ public Status doInBackground() { workerExport.addPropertyChangeListener(this); workerExport.execute(); } + } else if (event.getSource() == miTileInfoShow) { + if (!showPvrzInfo(lastTileInfoIndex)) { + JOptionPane.showMessageDialog(panel, + String.format("Could not retrieve PVRZ information for tile %d.", lastTileInfoIndex), "Error", + JOptionPane.ERROR_MESSAGE); + } + } else if (event.getSource() == miTileInfoPvrz) { + if (!openPvrzResource(lastTileInfoIndex)) { + JOptionPane.showMessageDialog(panel, + String.format("Could not open PVRZ resource for tile %d.", lastTileInfoIndex), "Error", + JOptionPane.ERROR_MESSAGE); + } + } else if (event.getSource() == miTileInfoWed) { + final Tilemap tm = wedTileMap.get(lastTileInfoIndex); + if (!openStructEntry(tm)) { + JOptionPane.showMessageDialog(panel, + String.format("Could not open overlay structure for tile %d.", lastTileInfoIndex), "Error", + JOptionPane.ERROR_MESSAGE); + } } } @@ -465,6 +503,15 @@ public JComponent makeViewer(ViewableContainer container) { tileGrid.addImage(tileImages); tileGrid.setGridSize(calcGridSize(tileGrid.getImageCount(), getDefaultTilesPerRow())); tileGrid.setShowGrid(showGrid); + + menuTileInfo.add(miTileInfoShow); + menuTileInfo.add(miTileInfoPvrz); + menuTileInfo.add(miTileInfoWed); + miTileInfoShow.addActionListener(this); + miTileInfoPvrz.addActionListener(this); + miTileInfoWed.addActionListener(this); + tileGrid.addMouseListener(new PopupListener()); + slCols.setValue(tileGrid.getTileColumns()); tfCols.setText(Integer.toString(tileGrid.getTileColumns())); JScrollPane scroll = new JScrollPane(tileGrid); @@ -592,8 +639,10 @@ private void initTileset() { decoder = TisDecoder.loadTis(entry); if (decoder != null) { + wedResource = loadWedForTis(entry); + initOverlayMap(wedResource); int tileCount = decoder.getTileCount(); - defaultWidth = calcTileWidth(entry, tileCount); + defaultWidth = calcTileWidth(wedResource, tileCount); tileImages = new ArrayList<>(tileCount); for (int tileIdx = 0; tileIdx < tileCount; tileIdx++) { BufferedImage image = ColorConvert.createCompatibleImage(64, 64, Transparency.BITMASK); @@ -618,6 +667,99 @@ private void initTileset() { } } + // Initializes the WED overlay lookup table for tile information, if WED resource is available. + private void initOverlayMap(WedResource wed) { + if (wed != null) { + try { + final Overlay ovl = (Overlay) wed.getAttribute(Overlay.WED_OVERLAY + " 0"); + int width = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); + int height = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_HEIGHT)).getValue(); + wedTileMap = new HashMap<>(width * height * 4 / 3); + + // mapping primary tiles + final List tileMapList = ovl.getFields(Tilemap.class); + final List tileIndexList = ovl.getFields(IndexNumber.class); + for (final StructEntry e : tileMapList) { + final Tilemap tm = (Tilemap) e; + final int idx = ((IsNumeric) tm.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_PRI)).getValue(); + final int tileIdx = ((IsNumeric) tileIndexList.get(idx)).getValue(); + wedTileMap.put(tileIdx, tm); + } + + // mapping secondary tiles + wedTileMap.entrySet().stream().filter(e -> { + final int idx = ((IsNumeric) e.getValue().getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_SEC)).getValue(); + return (idx >= 0 && !wedTileMap.containsKey(idx)); + }).forEach((e) -> { + final Tilemap tm = e.getValue(); + final int idx = ((IsNumeric) tm.getAttribute(Tilemap.WED_TILEMAP_TILE_INDEX_SEC)).getValue(); + wedTileMap.putIfAbsent(idx, tm); + }); + } catch (Exception e) { + } + } + } + + /** Opens the specified {@code StructEntry} in a new {@code ViewFrame} window. */ + private boolean openStructEntry(StructEntry e) { + if (e != null) { + for (StructEntry se = e; se != null; se = se.getParent()) { + if (se instanceof Resource) { + final Resource viewable = (Resource) se; + new ViewFrame(panel, viewable); + ((AbstractStruct) viewable).getViewer().selectEntry(e.getOffset()); + return true; + } + } + } + return false; + } + + /** Opens the PVRZ resource containing the specified tile index. */ + private boolean openPvrzResource(int tileIndex) { + if (decoder instanceof TisV2Decoder && tileIndex >= 0 && tileIndex < decoder.getTileCount()) { + final TisV2Decoder d = (TisV2Decoder) decoder; + final ResourceEntry resEntry = ResourceFactory.getResourceEntry(d.getPvrzFileName(tileIndex)); + if (resEntry != null) { + final Resource res = ResourceFactory.getResource(resEntry); + if (res != null) { + new ViewFrame(panel, res); + return true; + } + } + } + return false; + } + + /** Opens a message dialog with PVRZ-related information about the specified tile. */ + private boolean showPvrzInfo(int tileIndex) { + if (decoder instanceof TisV2Decoder && tileIndex >= 0 && tileIndex < decoder.getTileCount()) { + final TisV2Decoder d = (TisV2Decoder) decoder; + final ByteBuffer buf = d.getResourceBuffer(); + final int tileOfs = buf.getInt(0x10) + tileIndex * 0x0c; + final int tilePage = buf.getInt(tileOfs); + + final String info; + if (tilePage < 0) { + info = "Tile type: Solid black color
No PVRZ reference available."; + } else { + final String pvrzName = d.getPvrzFileName(tileIndex); + final int tileX = buf.getInt(tileOfs + 4); + final int tileY = buf.getInt(tileOfs + 8); + info = String.format( + "PVRZ resource: %s
" + + "PVRZ page: %d
" + + "PVRZ coordinates: x=%d, y=%d", + pvrzName, tilePage, tileX, tileY); + } + JOptionPane.showMessageDialog(panel, + String.format("
%s
 
", info), + "PVRZ information: tile " + tileIndex, JOptionPane.INFORMATION_MESSAGE); + return true; + } + return false; + } + // Converts the current PVRZ-based tileset into the old tileset variant. public Status convertToPaletteTis(Path output, boolean showProgress) { Status retVal = Status.ERROR; @@ -1027,6 +1169,31 @@ public Status exportPNG(Path output, boolean showProgress) { return retVal; } + /** + * Attempts to retrieve the tileset width, in tiles, from the specified WED resource. Falls back to a value + * based on {@code defTileCount}, if WED information is not available. + * + * @param wed WED resource for this TIS. + * @param defTileCount A an optional tile count that will be used to "guess" the correct number of tiles per row + * if WED information is no available. + * @return Number of tiles per row for the current TIS resource. + */ + private int calcTileWidth(WedResource wed, int defTileCount) { + int retVal = (defTileCount < 9) ? defTileCount : (int) (Math.sqrt(defTileCount) * 1.18); + + if (wed != null) { + final Overlay ovl = (Overlay) wed.getAttribute(Overlay.WED_OVERLAY + " 0"); + if (ovl != null) { + final int width = ((IsNumeric) ovl.getAttribute(Overlay.WED_OVERLAY_WIDTH)).getValue(); + if (width > 0) { + retVal = width; + } + } + } + + return retVal; + } + // Generates PVRZ files based on the current TIS resource and the specified parameters private Status writePvrzPages(Path tisFile, List pageList, List entryList, ProgressMonitor progress) { @@ -1199,6 +1366,39 @@ public static Path makeTisFileNameValid(Path fileName) { return fileName; } + /** + * Attempts to find and load the WED resource associated with the specified TIS resource. + * + * @param tisEntry The TIS resource entry. + * @return {@code WedResource} instance if successful, {@code null} otherwise. + */ + public static WedResource loadWedForTis(ResourceEntry tisEntry) { + WedResource wed = null; + + if (tisEntry != null) { + String tisBase = tisEntry.getResourceRef(); + ResourceEntry wedEntry = null; + while (tisBase.length() >= 6) { + String wedName = tisBase + ".WED"; + wedEntry = ResourceFactory.getResourceEntry(wedName); + if (wedEntry != null) { + break; + } else { + tisBase = tisBase.substring(0, tisBase.length() - 1); + } + } + + if (wedEntry != null) { + try { + wed = new WedResource(wedEntry); + } catch (Exception e) { + } + } + } + + return wed; + } + /** * Attempts to calculate the TIS width from an associated WED file. * @@ -1279,6 +1479,40 @@ public boolean containsPvrzReference(int index) { // -------------------------- INNER CLASSES -------------------------- + private final class PopupListener extends MouseAdapter { + @Override + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + int index = tileGrid.getTileIndexAt(new Point(e.getX(), e.getY())); + if (index >= 0 && index < decoder.getTileCount()) { + lastTileInfoIndex = index; + + miTileInfoShow.setText(String.format(FMT_TILEINFO_SHOW, lastTileInfoIndex)); + miTileInfoShow.setVisible(decoder.getType() == TisDecoder.Type.PVRZ); + + miTileInfoPvrz.setText(String.format(FMT_TILEINFO_PVRZ, lastTileInfoIndex)); + miTileInfoPvrz.setVisible(decoder.getType() == TisDecoder.Type.PVRZ); + + miTileInfoWed.setText(String.format(FMT_TILEINFO_WED, lastTileInfoIndex)); + miTileInfoWed.setVisible(wedTileMap != null && wedTileMap.containsKey(lastTileInfoIndex)); + + if (miTileInfoShow.isVisible() || miTileInfoPvrz.isVisible() || miTileInfoWed.isVisible()) { + menuTileInfo.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + } + } + // Tracks regions of tiles used for the tile -> pvrz packing algorithm private static class TileRect { private Dimension bounds; diff --git a/src/org/infinity/resource/graphics/decoder/Decodable.java b/src/org/infinity/resource/graphics/decoder/Decodable.java new file mode 100644 index 000000000..fe06afabd --- /dev/null +++ b/src/org/infinity/resource/graphics/decoder/Decodable.java @@ -0,0 +1,27 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics.decoder; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; + +/** + * Common interface for texture decoder classes. + */ +public interface Decodable { + /** + * Decodes PVR data in the format as specified by the associated {@link PvrInfo}, and draws the specified + * "region" into "image". + * + * @param image The output image + * @param region The PVR texture region to draw onto "image" + * @return Success state of the operation. + * @throws Exception on error. + */ + boolean decode(BufferedImage image, Rectangle region) throws Exception; + + /** Returns the associated {@link PvrInfo} object. */ + PvrInfo getPvrInfo(); +} diff --git a/src/org/infinity/resource/graphics/decoder/DummyDecoder.java b/src/org/infinity/resource/graphics/decoder/DummyDecoder.java new file mode 100644 index 000000000..0462901a4 --- /dev/null +++ b/src/org/infinity/resource/graphics/decoder/DummyDecoder.java @@ -0,0 +1,56 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics.decoder; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +/** + * Texture decoder for unsupported pixel formats. + */ +public class DummyDecoder implements Decodable { + private final PvrInfo info; + + /** + * Initializes a new dummy decoder from with the specified {@link PvrInfo}. + *

+ * Note: The parameter is currently unused and exists only to satisfy the {@link Decodable} interface. + *

+ */ + public DummyDecoder(PvrInfo pvr) { + this.info = pvr; + } + + // --------------------- Begin Interface Decodable --------------------- + + @Override + public boolean decode(BufferedImage image, Rectangle region) throws Exception { + if (image == null || region == null) { + return false; + } + + int[] imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + int ofs = 0; + int maxX = (image.getWidth() < region.width) ? image.getWidth() : region.width; + int maxY = (image.getHeight() < region.height) ? image.getHeight() : region.height; + for (int y = 0; y < maxY; y++) { + for (int x = 0; x < maxX; x++) { + imgData[ofs + x] = 0; + } + ofs += image.getWidth(); + } + imgData = null; + + return true; + } + + @Override + public PvrInfo getPvrInfo() { + return info; + } + + // --------------------- End Interface Decodable --------------------- +} diff --git a/src/org/infinity/resource/graphics/decoder/DxtDecoder.java b/src/org/infinity/resource/graphics/decoder/DxtDecoder.java new file mode 100644 index 000000000..dc2bafb3f --- /dev/null +++ b/src/org/infinity/resource/graphics/decoder/DxtDecoder.java @@ -0,0 +1,374 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics.decoder; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.Objects; + +import org.infinity.util.DynamicArray; + +/** + * Texture decoder for DXT1, DXT3 and DXT5 pixel formats. + */ +public class DxtDecoder implements Decodable { + private final PvrInfo info; + + /** Initializes a new {@code DXT} decoder from with the specified {@link PvrInfo}. */ + public DxtDecoder(PvrInfo pvr) { + this.info = Objects.requireNonNull(pvr); + } + + // --------------------- Begin Interface Decodable --------------------- + + @Override + public boolean decode(BufferedImage image, Rectangle region) throws Exception { + return decodeDXT(image, region); + } + + @Override + public PvrInfo getPvrInfo() { + return info; + } + + // --------------------- End Interface Decodable --------------------- + + private boolean decodeDXT(BufferedImage image, Rectangle region) throws Exception { + if (image == null || region == null) { + return false; + } + + int imgWidth = image.getWidth(); + int imgHeight = image.getHeight(); + int[] imgData = null; + + // checking region bounds and alignment + if (region.x < 0) { + region.width += -region.x; + region.x = 0; + } + if (region.y < 0) { + region.height += -region.y; + region.y = 0; + } + if (region.x + region.width > info.width) + region.width = info.width - region.x; + if (region.y + region.height > info.height) + region.height = info.height - region.y; + Rectangle rect = alignRectangle(region, 4, 4); + + // preparing aligned image buffer for faster rendering + BufferedImage alignedImage = null; + int imgWidthAligned; + if (!region.equals(rect)) { + alignedImage = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB); + imgWidthAligned = alignedImage.getWidth(); + imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); + // translating "region" to be relative to "rect" + region.x -= rect.x; + region.y -= rect.y; + if (imgWidth < region.width) { + region.width = imgWidth; + } + if (imgHeight < region.height) { + region.height = imgHeight; + } + } else { + imgWidthAligned = imgWidth; + imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + + switch (info.pixelFormat) { + case DXT1: + decodeDXT1(imgData, rect, imgWidthAligned); + break; + case DXT3: + decodeDXT3(imgData, rect, imgWidthAligned); + break; + case DXT5: + decodeDXT5(imgData, rect, imgWidthAligned); + break; + default: + return false; + } + imgData = null; + + // copying aligned image back to target image + if (alignedImage != null) { + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, + region.y + region.height, null); + } finally { + g.dispose(); + g = null; + } + alignedImage = null; + } + return true; + } + + // Performs DXT1-specific decoding on {@code imgData}, within the aligned bounds + // specified by {@code rect} and {@code imgWidth}. + private void decodeDXT1(int[] imgData, Rectangle rect, int imgWidth) { + final int wordSize = 8; // data size of an encoded 4x4 pixel block + int wordImageWidth = info.width >>> 2; // the image width in data blocks + int wordRectWidth = rect.width >>> 2; // the aligned region's width in data blocks + int wordRectHeight = rect.height >>> 2; // the aligned region's height in data blocks + int wordPosX = rect.x >>> 2; + int wordPosY = rect.y >>> 2; + + int[] colors = new int[8]; + int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; + int imgOfs = 0; + for (int y = 0; y < wordRectHeight; y++) { + for (int x = 0; x < wordRectWidth; x++) { + // decoding single DXT1 block + int c = DynamicArray.getInt(info.data, pvrOfs); + unpackColors565(c, colors); + int code = DynamicArray.getInt(info.data, pvrOfs + 4); + for (int idx = 0; idx < 16; idx++, code >>>= 2) { + int ofs = imgOfs + (idx >>> 2) * imgWidth + (idx & 3); + if ((code & 3) == 0) { + // 100% c0, 0% c1 + imgData[ofs] = 0xff000000 | (colors[2] << 16) | (colors[1] << 8) | colors[0]; + } else if ((code & 3) == 1) { + // 0% c0, 100% c1 + imgData[ofs] = 0xff000000 | (colors[6] << 16) | (colors[5] << 8) | colors[4]; + } else if ((code & 3) == 2) { + if ((c & 0xffff) > ((c >>> 16) & 0xffff)) { + // 66% c0, 33% c1 + int v = 0xff000000; + v |= (((colors[2] << 1) + colors[6]) / 3) << 16; + v |= (((colors[1] << 1) + colors[5]) / 3) << 8; + v |= ((colors[0] << 1) + colors[4]) / 3; + imgData[ofs] = v; + } else { + // 50% c0, 50% c1 + int v = 0xff000000; + v |= ((colors[2] + colors[6]) >>> 1) << 16; + v |= ((colors[1] + colors[5]) >>> 1) << 8; + v |= (colors[0] + colors[4]) >>> 1; + imgData[ofs] = v; + } + } else { + if ((c & 0xffff) > ((c >>> 16) & 0xffff)) { + // 33% c0, 66% c1 + int v = 0xff000000; + v |= ((colors[2] + (colors[6] << 1)) / 3) << 16; + v |= ((colors[1] + (colors[5] << 1)) / 3) << 8; + v |= (colors[0] + (colors[4] << 1)) / 3; + imgData[ofs] = v; + } else { + // transparent + imgData[ofs] = 0; + } + } + } + + pvrOfs += wordSize; + imgOfs += 4; + } + pvrOfs += (wordImageWidth - wordRectWidth) * wordSize; + imgOfs += imgWidth * 4 - rect.width; + } + } + + // Performs DXT3-specific decoding on {@code imgData}, within the aligned bounds + // specified by {@code rect} and {@code imgWidth}. + private void decodeDXT3(int[] imgData, Rectangle rect, int imgWidth) { + final int wordSize = 16; // data size of an encoded 4x4 pixel block + int wordImageWidth = info.width >>> 2; // the image width in data blocks + int wordRectWidth = rect.width >>> 2; // the aligned region's width in data blocks + int wordRectHeight = rect.height >>> 2; // the aligned region's height in data blocks + int wordPosX = rect.x >>> 2; + int wordPosY = rect.y >>> 2; + + int[] colors = new int[8]; + int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; + int imgOfs = 0; + for (int y = 0; y < wordRectHeight; y++) { + for (int x = 0; x < wordRectWidth; x++) { + // decoding single DXT3 block + long alpha = DynamicArray.getByte(info.data, pvrOfs); + int c = DynamicArray.getInt(info.data, pvrOfs + 8); + unpackColors565(c, colors); + int code = DynamicArray.getInt(info.data, pvrOfs + 12); + for (int idx = 0; idx < 16; idx++, code >>>= 2, alpha >>>= 4) { + // calculating alpha (4 bit -> 8 bit) + int ofs = imgOfs + (idx >>> 2) * imgWidth + (idx & 3); + int color = (int) (alpha & 0xf) << 24; + color |= color << 4; + // decoding pixels + if ((code & 3) == 0) { + // 100% c0, 0% c1 + color |= (colors[2] << 16) | (colors[1] << 8) | colors[0]; + } else if ((code & 3) == 1) { + // 0% c0, 100% c1 + color |= (colors[6] << 16) | (colors[5] << 8) | colors[4]; + } else if ((code & 3) == 2) { + // 66% c0, 33% c1 + int v = (((colors[2] << 1) + colors[6]) / 3) << 16; + color |= v; + v = (((colors[1] << 1) + colors[5]) / 3) << 8; + color |= v; + v = ((colors[0] << 1) + colors[4]) / 3; + color |= v; + } else { + // 33% c0, 66% c1 + int v = ((colors[2] + (colors[6] << 1)) / 3) << 16; + color |= v; + v = ((colors[1] + (colors[5] << 1)) / 3) << 8; + color |= v; + v = (colors[0] + (colors[4] << 1)) / 3; + if (v > 255) + v = 255; + color |= v; + } + imgData[ofs] = color; + } + + pvrOfs += wordSize; + imgOfs += 4; + } + pvrOfs += (wordImageWidth - wordRectWidth) * wordSize; + imgOfs += (imgWidth << 2) - rect.width; + } + } + + // Performs DXT5-specific decoding on {@code imgData}, within the aligned bounds + // specified by {@code rect} and {@code imgWidth}. + private void decodeDXT5(int[] imgData, Rectangle rect, int imgWidth) { + final int wordSize = 16; // data size of an encoded 4x4 pixel block + int wordImageWidth = info.width >>> 2; // the image width in data blocks + int wordRectWidth = rect.width >>> 2; // the aligned region's width in data blocks + int wordRectHeight = rect.height >>> 2; // the aligned region's height in data blocks + int wordPosX = rect.x >>> 2; + int wordPosY = rect.y >>> 2; + + int[] alpha = new int[8]; + int[] colors = new int[8]; + int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; + int imgOfs = 0; + for (int y = 0; y < wordRectHeight; y++) { + for (int x = 0; x < wordRectWidth; x++) { + // creating alpha table + alpha[0] = DynamicArray.getByte(info.data, pvrOfs) & 0xff; + alpha[1] = DynamicArray.getByte(info.data, pvrOfs + 1) & 0xff; + if (alpha[0] > alpha[1]) { + alpha[2] = (6 * alpha[0] + alpha[1]) / 7; + alpha[3] = (5 * alpha[0] + 2 * alpha[1]) / 7; + alpha[4] = (4 * alpha[0] + 3 * alpha[1]) / 7; + alpha[5] = (3 * alpha[0] + 4 * alpha[1]) / 7; + alpha[6] = (2 * alpha[0] + 5 * alpha[1]) / 7; + alpha[7] = (alpha[0] + 6 * alpha[1]) / 7; + } else { + alpha[2] = (4 * alpha[0] + alpha[1]) / 5; + alpha[3] = (3 * alpha[0] + 2 * alpha[1]) / 5; + alpha[4] = (2 * alpha[0] + 3 * alpha[1]) / 5; + alpha[5] = (alpha[0] + 4 * alpha[1]) / 5; + alpha[6] = 0; + alpha[7] = 255; + } + + // decoding single DXT5 block + long ctrl = DynamicArray.getLong(info.data, pvrOfs + 2) & 0xffffffffffffL; + int c = DynamicArray.getInt(info.data, pvrOfs + 8); + unpackColors565(c, colors); + int code = DynamicArray.getInt(info.data, pvrOfs + 12); + for (int idx = 0; idx < 16; idx++, code >>>= 2, ctrl >>>= 3) { + int ofs = imgOfs + (idx >>> 2) * imgWidth + (idx & 3); + int color = alpha[(int) (ctrl & 7L)] << 24; + if ((code & 3) == 0) { + // 100% c0, 0% c1 + color |= (colors[2] << 16) | (colors[1] << 8) | colors[0]; + } else if ((code & 3) == 1) { + // 0% c0, 100% c1 + color |= (colors[6] << 16) | (colors[5] << 8) | colors[4]; + } else if ((code & 3) == 2) { + // 66% c0, 33% c1 + int v = (((colors[2] << 1) + colors[6]) / 3) << 16; + color |= v; + v = (((colors[1] << 1) + colors[5]) / 3) << 8; + color |= v; + v = ((colors[0] << 1) + colors[4]) / 3; + color |= v; + } else { + // 33% c0, 66% c1 + int v = ((colors[2] + (colors[6] << 1)) / 3) << 16; + color |= v; + v = ((colors[1] + (colors[5] << 1)) / 3) << 8; + color |= v; + v = (colors[0] + (colors[4] << 1)) / 3; + if (v > 255) + v = 255; + color |= v; + } + imgData[ofs] = color; + } + + pvrOfs += wordSize; + imgOfs += 4; + } + pvrOfs += (wordImageWidth - wordRectWidth) * wordSize; + imgOfs += (imgWidth << 2) - rect.width; + } + } + + // Returns a rectangle that is aligned to the values specified as arguments 2 and 3. + private static Rectangle alignRectangle(Rectangle rect, int alignX, int alignY) { + if (rect == null) + return null; + + Rectangle retVal = new Rectangle(rect); + if (alignX < 1) + alignX = 1; + if (alignY < 1) + alignY = 1; + if (rect.x < 0) { + rect.width -= -rect.x; + rect.x = 0; + } + if (rect.y < 0) { + rect.height -= -rect.y; + rect.y = 0; + } + + int diffX = retVal.x % alignX; + if (diffX != 0) { + retVal.x -= diffX; + retVal.width += diffX; + } + int diffY = retVal.y % alignY; + if (diffY != 0) { + retVal.y -= diffY; + retVal.height += diffY; + } + + diffX = (alignX - (retVal.width % alignX)) % alignX; + retVal.width += diffX; + + diffY = (alignY - (retVal.height % alignY)) % alignY; + retVal.height += diffY; + + return retVal; + } + + // Converts two RGB565 words into separate components, ordered { B, G, R, A, B, G, R, A } + private static void unpackColors565(int inData, int[] outData) { + outData[0] = ((inData << 3) & 0xf8) | (inData >>> 2) & 0x07; // b1 + outData[1] = ((inData >>> 3) & 0xfc) | (inData >>> 9) & 0x03; // g1 + outData[2] = ((inData >>> 8) & 0xf8) | (inData >>> 13) & 0x07; // r1 + outData[3] = 255; // a1 + outData[4] = ((inData >>> 13) & 0xf8) | (inData >>> 18) & 0x07; // b2 + outData[5] = ((inData >>> 19) & 0xfc) | (inData >>> 25) & 0x03; // g2 + outData[6] = ((inData >>> 24) & 0xf8) | (inData >>> 29) & 0x07; // r2 + outData[7] = 255; // a2 + } +} diff --git a/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java b/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java new file mode 100644 index 000000000..20752c0db --- /dev/null +++ b/src/org/infinity/resource/graphics/decoder/Etc2Decoder.java @@ -0,0 +1,763 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics.decoder; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.LongBuffer; +import java.util.Objects; + +/** + * Texture decoder for ETC1 and ETC2 pixel formats, implemented according to the + * Khronos Data Format Specification. + * + * @see Khronos Data Format Specification Registry + */ +public class Etc2Decoder implements Decodable { + // Combined modifier table for base colors in + // - "individual" and "differential" modes, used by format ETC2_RGB + // - "differential" mode, used by format ETC2_RGB_A1 + // Values have been rearranged (compared to reference tables) to allow + // indexing values without additional mapping. + // Important: + // For ETC2_RGB, bit 3 is always 1. + // For ETC2_RGB_A1, bit 3 of the table index is borrowed from the "opaque" bit of the source data. + private static final int[][] MODIFIERS = { + { 0, 8, 0, -8 }, + { 0, 17, 0, -17 }, + { 0, 29, 0, -29 }, + { 0, 42, 0, -42 }, + { 0, 60, 0, -60 }, + { 0, 80, 0, -80 }, + { 0, 106, 0, -106 }, + { 0, 183, 0, -183 }, + { 2, 8, -2, -8 }, + { 5, 17, -5, -17 }, + { 9, 29, -9, -29 }, + { 13, 42, -13, -42 }, + { 18, 60, -18, -60 }, + { 24, 80, -24, -80 }, + { 33, 106, -33, -106 }, + { 47, 183, -47, -183 }, + }; + + // Distance table for "T" and "H" modes. + private static final int[] DISTANCES = { + 3, 6, 11, 16, 23, 32, 41, 64, + }; + + // Intensity modifier table for alpha values. + private static final int[][] MODIFIERS_ALPHA = { + { -3, -6, -9, -15, 2, 5, 8, 14 }, + { -3, -7, -10, -13, 2, 6, 9, 12 }, + { -2, -5, -8, -13, 1, 4, 7, 12 }, + { -2, -4, -6, -13, 1, 3, 5, 12 }, + { -3, -6, -8, -12, 2, 5, 7, 11 }, + { -3, -7, -9, -11, 2, 6, 8, 10 }, + { -4, -7, -8, -11, 3, 6, 7, 10 }, + { -3, -5, -8, -11, 2, 4, 7, 10 }, + { -2, -6, -8, -10, 1, 5, 7, 9 }, + { -2, -5, -8, -10, 1, 4, 7, 9 }, + { -2, -4, -8, -10, 1, 3, 7, 9 }, + { -2, -5, -7, -10, 1, 4, 6, 9 }, + { -3, -4, -7, -10, 2, 3, 6, 9 }, + { -1, -2, -3, -10, 0, 1, 2, 9 }, + { -4, -6, -8, -9, 3, 5, 7, 8 }, + { -3, -5, -7, -9, 2, 4, 6, 8 }, + }; + + // 2-Bit offset mappings for pixel lookup indices, as used by "individual", "differential", "T" and "H" modes. + private static final int[][] BITS_PIXEL_INDICES = { + {0, 16}, {1, 17}, {2, 18}, {3, 19}, + {4, 20}, {5, 21}, {6, 22}, {7, 23}, + {8, 24}, {9, 25}, {10, 26}, {11, 27}, + {12, 28}, {13, 29}, {14, 30}, {15, 31}, + }; + + // (offset, length)-pairs for 3-bit pixel lookup indices, as used for alpha computation. + // The outer array refers to ofs/len pairs for pixels Pa .. Pp + // (as described in the "Khronos Data Format Specification"). + private static final int[][] BITS_ALPHA_RANGES = { + {45, 3}, {42, 3}, {39, 3}, {36, 3}, + {33, 3}, {30, 3}, {27, 3}, {24, 3}, + {21, 3}, {18, 3}, {15, 3}, {12, 3}, + {9, 3}, {6, 3}, {3, 3}, {0, 3}, + }; + + // 3-Bit offset mappings for "distance" in "T" mode. + private static final int[] BITS_T_D = { 32, 34, 35 }; + // 4-Bit offset mappings for red color in "T" mode. + private static final int[] BITS_T_R = { 56, 57, 59, 60 }; + + // 2-Bit offset mappings for "distance" in "H" mode. + private static final int[] BITS_H_D = { 32, 34 }; + // 4-Bit offset mappings for red color in "H" mode. + private static final int[] BITS_H_B = { 47, 48, 49, 51 }; + // 4-Bit offset mappings for green color in "H" mode. + private static final int[] BITS_H_G = { 52, 56, 57, 58 }; + + // 6-Bit offset mappings for horizontal red color in "planar" mode. + private static final int[] BITS_PLANAR_RH = { 32, 34, 35, 36, 37, 38 }; + // 6-Bit offset mappings for blue color in "planar" mode. + private static final int[] BITS_PLANAR_B = { 39, 40, 41, 43, 44, 48 }; + // 7-Bit offset mappings for green color in "planar" mode. + private static final int[] BITS_PLANAR_G = { 49, 50, 51, 52, 53, 54, 56 }; + + + private final PvrInfo info; + + /** Initializes a new {@code ETC2} decoder from with the specified {@link PvrInfo}. */ + public Etc2Decoder(PvrInfo pvr) { + this.info = Objects.requireNonNull(pvr); + } + + // --------------------- Begin Interface Decodable --------------------- + + @Override + public boolean decode(BufferedImage image, Rectangle region) throws Exception { + return decodeETC(image, region); + } + + @Override + public PvrInfo getPvrInfo() { + return info; + } + + // --------------------- End Interface Decodable --------------------- + + /** + * Decodes PVR data in ETC1 and ETC2 formats as specified by the associated {@link PvrInfo}, + * and draws the specified "region" into "image". + * + * @param image The output image + * @param region The PVR texture region to draw onto "image" + * @return Success state of the operation. + * @throws Exception on error. + */ + private boolean decodeETC(BufferedImage image, Rectangle region) throws Exception { + if (image == null || region == null) { + return false; + } + + final int imgWidth = image.getWidth(); + final int imgHeight = image.getHeight(); + int[] imgData = null; + + // checking region bounds and alignment + if (region.x < 0) { + region.width += -region.x; + region.x = 0; + } + if (region.y < 0) { + region.height += -region.y; + region.y = 0; + } + if (region.x + region.width > info.width) + region.width = info.width - region.x; + if (region.y + region.height > info.height) + region.height = info.height - region.y; + final Rectangle rect = alignRectangle(region, 4, 4); + + // preparing aligned image buffer for faster rendering + BufferedImage alignedImage = null; + final int imgWidthAligned; + if (!region.equals(rect)) { + alignedImage = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB); + imgWidthAligned = alignedImage.getWidth(); + imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); + // translating "region" to be relative to "rect" + region.x -= rect.x; + region.y -= rect.y; + if (imgWidth < region.width) { + region.width = imgWidth; + } + if (imgHeight < region.height) { + region.height = imgHeight; + } + } else { + imgWidthAligned = imgWidth; + imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + + switch (info.pixelFormat) { + case ETC1: + case ETC2_RGB: + decodeData(imgData, rect, imgWidthAligned, false, false); + break; + case ETC2_RGB_A1: + decodeData(imgData, rect, imgWidthAligned, false, true); + break; + case ETC2_RGBA: + decodeData(imgData, rect, imgWidthAligned, true, false); + break; + default: + return false; + } + imgData = null; + + // copying aligned image back to target image + if (alignedImage != null) { + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, + region.y + region.height, null); + } finally { + g.dispose(); + g = null; + } + alignedImage = null; + } + return true; + } + + /** + * Performs decoding of PVR data in ETC1, ETC2 RGB, ETC2 RGB A1 or ETC2 RGBA pixel format on {@code imgData}, + * within the aligned bounds specified by {@code rect} and {@code imgWidth}. + * + * @param imgData The output image data array + * @param rect Defines the rectangular region of pixels to decode from the texture. + * @param imgWidth Width of the output image, in pixels. + * @param hasAlpha Indicates whether the texture data provides an additional alpha channel. + * @param hasTransparency Indicates whether the texture data provides 1 bit punch-through alpha. + */ + private void decodeData(int[] imgData, Rectangle rect, int imgWidth, boolean hasAlpha, boolean hasTransparency) { + final int wordSize = hasAlpha ? 2 : 1; // data size of an encoded 4x4 pixel block, in number of long ints + final int wordImageWidth = info.width >> 2; // the image width, in data blocks + final int wordRectWidth = rect.width >> 2; // the aligned region's width, in data blocks + final int wordRectHeight = rect.height >> 2; // the aligned region's height, in data blocks + final int wordPosX = rect.x >> 2; // X coordinate of region's origin, in data blocks + final int wordPosY = rect.y >> 2; // Y coordinate of region's origin, in data blocks + + // Storage for resulting pixels as 4x4xRGBA-quadruplet matrix. + // As defined by the "Khronos Data Format Specification", the pixels run top to bottom, left to right. + // X and Y coordinates are transposed later when copying pixels to target image. + int[] pixels = new int[4 * 4 * 4]; + + long colorWord = 0, alphaWord = 0; + int pvrOfs = (wordPosY * wordImageWidth + wordPosX) * wordSize; + final LongBuffer longBuf = ByteBuffer.wrap(info.data).order(ByteOrder.BIG_ENDIAN).asLongBuffer(); + for (int y = 0; y < wordRectHeight; y++) { + longBuf.position(pvrOfs); + for (int x = 0; x < wordRectWidth; x++) { + // decoding single ETC data block + if (hasAlpha) { + // fetching alpha data + alphaWord = longBuf.get(); + } + + // decoding color part + colorWord = longBuf.get(); + decodeColorBlock(colorWord, pixels, hasTransparency); + + if (hasAlpha) { + // decoding alpha part + decodeAlphaBlock(alphaWord, pixels); + } + + // writing pixel block to image data + int imgOfs = (y << 2) * imgWidth + (x << 2); + for (int i = 0; i < 16; i++) { + final int py = i >> 2; + final int px = i & 3; + // using transposed coordinates to compensate for pixel order + final int ci = ((px << 2) | py) << 2; + final int c = colorToInt(pixels, ci, 4); + final int ofs = imgOfs + py * imgWidth + px; + imgData[ofs] = c; + } + } + + pvrOfs += wordImageWidth * wordSize; + } + } + + /** + * Decodes the input 64-bit word into color values with optional punch-through alpha, and writes them into + * the specified 4x4 matrix of RGBA pixels. + * + * @param code 64-bit code word with color information. + * @param outPixels A 4x4 BGRA pixel array for decoded pixel data. Color and alpha channels are stored as separate values. + * @param hasTransparency Indicates whether the code word provides 1 bit "punch-through" alpha. + */ + private void decodeColorBlock(long code, int[] outPixels, boolean hasTransparency) { + final boolean diff = isBitSet(code, 33); + + if (!hasTransparency && !diff) { + // Individual mode + final int r1 = getBits(code, 60, 4, false), r2 = getBits(code, 56, 4, false); + final int g1 = getBits(code, 52, 4, false), g2 = getBits(code, 48, 4, false); + final int b1 = getBits(code, 44, 4, false), b2 = getBits(code, 40, 4, false); + + final int[] baseColor1 = { b1, g1, r1 }; + extend4To8Bits(baseColor1, 0, 3); + final int[] baseColor2 = { b2, g2, r2 }; + extend4To8Bits(baseColor2, 0, 3); + decodeColorSubBlocks(code, baseColor1, baseColor2, outPixels, false); + } else { + final int rd = getBits(code, 56, 3, true), r = getBits(code, 59, 5, false); + final int gd = getBits(code, 48, 3, true), g = getBits(code, 51, 5, false); + final int bd = getBits(code, 40, 3, true), b = getBits(code, 43, 5, false); + + if (((r + rd) & ~31) != 0) { // outside [0..31]? + // Mode T + decodeColorModeT(code, outPixels, hasTransparency); + } else if (((g + gd) & ~31) != 0) { // outside [0..31]? + // Mode H + decodeColorModeH(code, outPixels, hasTransparency); + } else if (((b + bd) & ~31) != 0) { // outside [0..31]? + // Planar mode + decodeColorPlanar(code, outPixels); + } else { + // Differential mode + final int[] baseColor1 = { b, g, r }; + extend5To8Bits(baseColor1, 0, 3); + final int[] baseColor2 = { b + bd, g + gd, r + rd }; + extend5To8Bits(baseColor2, 0, 3); + decodeColorSubBlocks(code, baseColor1, baseColor2, outPixels, hasTransparency); + } + } + } + + /** + * Decodes the input 64-bit word in either "individual" or "differential" mode. + * + * @param code 64-bit code word with color information. + * @param baseColor1 Base color for first sub-block, as BGR array. + * @param baseColor2 Base color for second sub-block, as BGR array. + * @param outPixels A 4x4 BGRA pixel array for decoded pixel data. Color and alpha channels are stored as separate values. + * @param hasTransparency Indicates whether the code word provides 1 bit "punch-through" alpha. + */ + private void decodeColorSubBlocks(long code, int[] baseColor1, int[] baseColor2, int[] outPixels, boolean hasTransparency) { + final boolean flipped = isBitSet(code, 32); + final boolean opaque = hasTransparency ? isBitSet(code, 33) : true; + final int[] table = { getBits(code, 37, 3, false), getBits(code, 34, 3, false) }; + + final int table1Idx= (hasTransparency ? 0 : 1 << 3) | table[0]; + final int table2Idx= (hasTransparency ? 0 : 1 << 3) | table[1]; + final int[][] blockTable = { MODIFIERS[table1Idx], MODIFIERS[table2Idx] }; + + final int[][] baseColor = { baseColor1, baseColor2 }; + + for (int i = 0; i < 16; i++) { + final int pixelIdx = getBitsEx(code, false, BITS_PIXEL_INDICES[i]); + final int pofs = i << 2; // pixel offset in outPixels array + if (!opaque && pixelIdx == 2) { // punch-through alpha + outPixels[pofs] = 0; + outPixels[pofs + 1] = 0; + outPixels[pofs + 2] = 0; + outPixels[pofs + 3] = 0; + } else { // opaque color + // using transposed coordinates to compensate for pixel order + final int py = i & 3; + final int px = i >> 2; + // flipped: 4x2 sub-blocks, on top of each other + // not flipped: 2x4 sub-blocks, side-by-side + final int blockIdx = flipped ? (py >> 1) : (px >> 1); + + final int modifier = blockTable[blockIdx][pixelIdx]; + outPixels[pofs] = clamp255(baseColor[blockIdx][0] + modifier); // b + outPixels[pofs + 1] = clamp255(baseColor[blockIdx][1] + modifier); // g + outPixels[pofs + 2] = clamp255(baseColor[blockIdx][2] + modifier); // r + outPixels[pofs + 3] = 255; // alpha + } + } + } + + /** + * Decodes the input 64-bit word in "T" mode. + * + * @param code 64-bit code word with color information. + * @param outPixels A 4x4 BGRA pixel array for decoded pixel data. Color and alpha channels are stored as separate values. + * @param hasTransparency Indicates whether the code word provides 1 bit "punch-through" alpha. + */ + private void decodeColorModeT(long code, int[] outPixels, boolean hasTransparency) { + final int distIdx = getBitsEx(code, false, BITS_T_D); + final boolean opaque = hasTransparency ? isBitSet(code, 33) : true; + final int[] baseColor1 = { + getBits(code, 48, 4, false), // b + getBits(code, 52, 4, false), // g + getBitsEx(code, false, BITS_T_R), // r + }; + extend4To8Bits(baseColor1, 0, 3); + + final int[] baseColor2 = { + getBits(code, 36, 4, false), // b + getBits(code, 40, 4, false), // g + getBits(code, 44, 4, false), // r + }; + extend4To8Bits(baseColor2, 0, 3); + + final int dist= DISTANCES[distIdx]; + + final int[] baseColor2a = { + clamp255(baseColor2[0] + dist), + clamp255(baseColor2[1] + dist), + clamp255(baseColor2[2] + dist) + }; + final int[] baseColor2b = { + clamp255(baseColor2[0] - dist), + clamp255(baseColor2[1] - dist), + clamp255(baseColor2[2] - dist) + }; + + final int[][] paintColor = { baseColor1, baseColor2a, baseColor2, baseColor2b }; + + for (int i = 0; i < 16; i++) { + final int pofs = i << 2; // pixel offset in outPixels array + final int pixelIdx = getBitsEx(code, false, BITS_PIXEL_INDICES[i]); + if (!opaque && pixelIdx == 2) { + // punch-through alpha + outPixels[pofs] = 0; + outPixels[pofs + 1] = 0; + outPixels[pofs + 2] = 0; + outPixels[pofs + 3] = 0; + } else { + // opaque color + outPixels[pofs] = paintColor[pixelIdx][0]; // b + outPixels[pofs + 1] = paintColor[pixelIdx][1]; // g + outPixels[pofs + 2] = paintColor[pixelIdx][2]; // r + outPixels[pofs + 3] = 255; // alpha + } + } + } + + /** + * Decodes the input 64-bit word in "H" mode. + * + * @param code 64-bit code word with color information. + * @param outPixels A 4x4 BGRA pixel array for decoded pixel data. Color and alpha channels are stored as separate values. + * @param hasTransparency Indicates whether the code word provides 1 bit "punch-through" alpha. + */ + private void decodeColorModeH(long code, int[] outPixels, boolean hasTransparency) { + final int dIdx = getBitsEx(code, false, BITS_H_D); + final boolean opaque = hasTransparency ? isBitSet(code, 33) : true; + final int[] baseColor1 = { + getBitsEx(code, false, BITS_H_B), // b + getBitsEx(code, false, BITS_H_G), // g + getBits(code, 59, 4, false), // r + }; + extend4To8Bits(baseColor1, 0, 3); + + final int[] baseColor2 = { + getBits(code, 35, 4, false), // b + getBits(code, 39, 4, false), // g + getBits(code, 43, 4, false), // r + }; + extend4To8Bits(baseColor2, 0, 3); + + final int color1 = colorToInt(baseColor1, 0, 3); + final int color2 = colorToInt(baseColor2, 0, 3); + final int distIdx = (dIdx << 1) | (color1 >= color2 ? 1 : 0); + final int dist = DISTANCES[distIdx]; + + final int[] baseColor1a = { + clamp255(baseColor1[0] + dist), + clamp255(baseColor1[1] + dist), + clamp255(baseColor1[2] + dist), + }; + final int[] baseColor1b = { + clamp255(baseColor1[0] - dist), + clamp255(baseColor1[1] - dist), + clamp255(baseColor1[2] - dist), + }; + final int[] baseColor2a = { + clamp255(baseColor2[0] + dist), + clamp255(baseColor2[1] + dist), + clamp255(baseColor2[2] + dist), + }; + final int[] baseColor2b = { + clamp255(baseColor2[0] - dist), + clamp255(baseColor2[1] - dist), + clamp255(baseColor2[2] - dist), + }; + + final int[][] paintColor = new int[][] { baseColor1a, baseColor1b, baseColor2a, baseColor2b }; + + for (int i = 0; i < 16; i++) { + final int pofs = i << 2; // pixel offset in outPixels array + final int pixelIdx = getBitsEx(code, false, BITS_PIXEL_INDICES[i]); + if (!opaque && pixelIdx == 2) { + // punch-through alpha + outPixels[pofs] = 0; + outPixels[pofs + 1] = 0; + outPixels[pofs + 2] = 0; + outPixels[pofs + 3] = 0; + } else { + // opaque color + outPixels[pofs] = paintColor[pixelIdx][0]; // b + outPixels[pofs + 1] = paintColor[pixelIdx][1]; // g + outPixels[pofs + 2] = paintColor[pixelIdx][2]; // r + outPixels[pofs + 3] = 255; // alpha + } + } + } + + /** + * Decodes the input 64-bit word in "planar" mode. This mode does not support 1 bit "punch-through" alpha. + * + * @param code 64-bit code word with color information. + * @param outPixels A 4x4 BGRA pixel array for decoded pixel data. Color and alpha channels are stored as separate values. + */ + private void decodeColorPlanar(long code, int[] outPixels) { + final int[] color = { + getBitsEx(code, false, BITS_PLANAR_B), // b + getBitsEx(code, false, BITS_PLANAR_G), // g + getBits(code, 57, 6, false), // r + }; + extend676To8Bits(color, 0, 3); + + final int[] colorH = { + getBits(code, 19, 6, false), // b + getBits(code, 25, 7, false), // g + getBitsEx(code, false, BITS_PLANAR_RH), // r + }; + extend676To8Bits(colorH, 0, 3); + + final int[] colorV = { + getBits(code, 0, 6, false), // b + getBits(code, 6, 7, false), // g + getBits(code, 13, 6, false), // r + }; + extend676To8Bits(colorV, 0, 3); + + final int[] outColor = new int[3]; + for (int i = 0; i < 16; i++) { + // using transposed coordinates to compensate for pixel order + final int x = i >> 2; + final int y = i & 3; + final int pofs = i << 2; // pixel offset in outPixels array + interpolate(x, y, color, colorH, colorV, outColor); + outPixels[pofs] = outColor[0]; // b + outPixels[pofs + 1] = outColor[1]; // g + outPixels[pofs + 2] = outColor[2]; // r + outPixels[pofs + 3] = 255; // alpha + } + } + + /** + * Decodes the input 64-bit word into alpha values and writes them into the specified 4x4 matrix of RGBA pixels. + * + * @param code 64-bit code word with alpha information. + * @param outPixels A 4x4 BGRA pixel array for decoded pixel data. Color and alpha channels are stored as separate values. + */ + private void decodeAlphaBlock(long code, int[] outPixels) { + final int tableIdx = getBits(code, 48, 4, false); + final int multiplier = getBits(code, 52, 4, false); + final int base = getBits(code, 56, 8, false); + + for (int i = 0; i < 16; i++) { + final int pixelIdx = getBits(code, BITS_ALPHA_RANGES[i][0], BITS_ALPHA_RANGES[i][1], false); + final int modifier = MODIFIERS_ALPHA[tableIdx][pixelIdx]; + final int alpha = clamp255(base + modifier * multiplier); + outPixels[(i << 2) + 3] = alpha; + } + } + + /** Returns the specified value, clamped between 0 and 255. */ + private static int clamp255(int value) { + return Math.min(Math.max(value, 0), 255); + } + + /** + * Attempts to extract the bit at specified {@code ofs} and returns it as boolean. + * + * @param value A 64-bit source value containing the bit to evaluate. + * @param ofs The bit offset, in range 0..63. + * @return A {@code boolean} which indicates whether the bit at the specified position is set. Returns {@code false} + * if the offset is out of bounds. + */ + private static boolean isBitSet(long value, int ofs) { + if (ofs >= 0 && ofs < 64) { + long mask = 1L << ofs; + return (value & mask) != 0; + } else { + return false; + } + } + + /** + * Attempts to extract {@code len} number of bits, starting at {@code ofs} and expand it to full {@code int} size. + * + * @param value A 64-bit source value containing the bits to extract. + * @param ofs Starting bit offset. + * @param len Number of bits to extract. Allowed range: [0, 32]. + * @param signed Specifies whether the extracted bits should be sign-extended. + * @return The extracted bits as {@code int} value. + */ + private static int getBits(long value, int ofs, int len, boolean signed) { + len = Math.min(64, ofs + len) - ofs; + if (ofs < 0 || len <= 0 || len > 32 || ofs + len > 64) { + return 0; + } + + int mask = (1 << len) - 1; + int maskMsb = 1 << (len - 1); + int retVal = (int)(value >>> ofs) & mask; + if (signed && (retVal & maskMsb) != 0) { + retVal |= ~mask; + } + + return retVal; + } + + /** + * Attempts to extracts all the bits at positions specified by the {@code offsets} array and expand it + * to full {@code int} size. + * + * @param value A 64-bit source value containing the bits to extract. + * @param signed Specifies whether the extracted bits should be sign-extended. + * @param offsets Array of bit positions which are sequentially used to assemble the resulting value. + * @return The extracted bits as {@code int} value. + */ + private static int getBitsEx(long value, boolean signed, int[] offsets) { + if (offsets.length > 32 || offsets.length == 0) { + return 0; + } + + int retVal = 0; + for (int i = offsets.length - 1; i >= 0; i--) { + retVal <<= 1; + retVal |= (value >>> offsets[i]) & 1; + } + + int maskMsb = 1 << (offsets.length - 1); + if (signed && (retVal & maskMsb) != 0) { + int mask = (1 << offsets.length) - 1; + retVal |= ~mask; + } + + return retVal; + } + + /** + * Extends the values in the {@code values} array from RGB:444 to RGB:888 by mirroring the most significant bits + * to the lower bit positions. The result is written back to the {@code values} array. + * + * @param values Array with color values to extend. Each entry specifies a separate color channel. + * @param offset Start offset in the {@code value} array. + * @param count Number of values to extend. + */ + private static void extend4To8Bits(int[] values, int offset, int count) { + for (int i = offset, len = offset + Math.min(values.length - offset, count); i < len; i++) { + values[i] |= values[i] << 4; + } + } + + /** + * Extends the values in the {@code values} array from RGB:555 to RGB:888 by mirroring the most significant bits + * to the lower bit positions. The result is written back to the {@code values} array. + * + * @param values Array with color values to extend. Each entry specifies a separate color channel. + * @param offset Start offset in the {@code value} array. + * @param count Number of values to extend. + */ + private static void extend5To8Bits(int[] values, int offset, int count) { + for (int i = offset, len = offset + Math.min(values.length - offset, count); i < len; i++) { + values[i] = ((values[i] << 3) | ((values[i] & 0x1f) >> 2)) & 0xff; + } + } + + /** + * Extends the values in the {@code values} array from RGB:676 to RGB:888 by mirroring the most significant bits + * to the lower bit positions. The result is written back to the {@code values} array. + * + * @param values Array with color values to extend. Each entry specifies a separate color channel. + * Index of color value, relative to {@code offset} is used to determine if value is a 6-bit value + * (red, blue) or 7-bit value (green). + * @param offset Start offset in the {@code value} array. + * @param count Number of values to extend. + */ + private static void extend676To8Bits(int[] values, int offset, int count) { + for (int i = offset, len = offset + Math.min(values.length - offset, count); i < len; i++) { + if ((i - offset) % 3 == 1) { + // 7 to 8 + values[i] = ((values[i] << 1) | ((values[i] & 0x7f) >> 6)) & 0xff; + } else { + // 6 to 8 + values[i] = ((values[i] << 2) | ((values[i] & 0x3f) >> 4)) & 0xff; + } + } + } + + /** + * Converts the given color array into a combined integer of BGR(A) bytes, in the order from highest to lowest byte: + * (alpha), red, green, blue. + * + * @param color The color triplet as array in sequence { B, G, R, A }. + * @param offset Start offset in the {@code color} array. + * @param count Number of values to combine. + * @return The combined color as {@code int}. + */ + private static int colorToInt(int[] color, int offset, int count) { + int retVal = 0; + for (int i = offset + count - 1; i >= offset; i--) { + retVal <<= 8; + retVal |= color[i] & 0xff; + } + return retVal; + } + + /** + * Computes the interpolated color in a 4x4 matrix, based on the specified coordinates and input colors, + * and stores the result in the {@code outColor} array. + * + * @param x Column of the pixel, in range [0..3]. + * @param y Row of the pixel, in range [0..3]. + * @param c Color value as array of three ints (blue, green, red). + * @param ch Horizontal color modifier as array of three ints (blue, green, red). + * @param cv Vertical color modifier as array of three ints (blue, green, red). + * @param oc Storage for the interpolated color with enough space for three values (blue, green, red). + */ + private static void interpolate(int x, int y, int[] c, int[] ch, int[] cv, int[] oc) { + for (int i = 0; i < 3; i++) { + oc[i] = clamp255((x * (ch[i] - c[i]) + y * (cv[i] - c[i]) + (c[i] << 2) + 2) >> 2); + } + } + + /** Returns a rectangle that is aligned to the values specified as arguments 2 and 3. */ + private static Rectangle alignRectangle(Rectangle rect, int alignX, int alignY) { + if (rect == null) + return null; + + Rectangle retVal = new Rectangle(rect); + if (alignX < 1) + alignX = 1; + if (alignY < 1) + alignY = 1; + if (rect.x < 0) { + rect.width -= -rect.x; + rect.x = 0; + } + if (rect.y < 0) { + rect.height -= -rect.y; + rect.y = 0; + } + + int diffX = retVal.x % alignX; + if (diffX != 0) { + retVal.x -= diffX; + retVal.width += diffX; + } + int diffY = retVal.y % alignY; + if (diffY != 0) { + retVal.y -= diffY; + retVal.height += diffY; + } + + diffX = (alignX - (retVal.width % alignX)) % alignX; + retVal.width += diffX; + + diffY = (alignY - (retVal.height % alignY)) % alignY; + retVal.height += diffY; + + return retVal; + } +} diff --git a/src/org/infinity/resource/graphics/decoder/PvrInfo.java b/src/org/infinity/resource/graphics/decoder/PvrInfo.java new file mode 100644 index 000000000..9a86291fe --- /dev/null +++ b/src/org/infinity/resource/graphics/decoder/PvrInfo.java @@ -0,0 +1,519 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information +// ---------------------------------------------------------------- +// PVRT format specifications and reference implementation: +// Copyright (c) Imagination Technologies Ltd. All Rights Reserved + +package org.infinity.resource.graphics.decoder; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Objects; + +import org.infinity.util.DynamicArray; + +/** + * Storage of preprocessed data for a single PVR resource. + */ +public class PvrInfo { + /** Flags indicates special properties of the color data. */ + public enum Flags { + NONE, PRE_MULTIPLIED + } + + /** Format specifies the pixel format of the color data. */ + public enum PixelFormat { + PVRTC_2BPP_RGB, PVRTC_2BPP_RGBA, PVRTC_4BPP_RGB, PVRTC_4BPP_RGBA, + PVRTC2_2BPP, PVRTC2_4BPP, + ETC1, + DXT1, DXT2, DXT3, DXT4, DXT5, BC4, BC5, BC6, BC7, + UYVY, YUY2, + BW1BPP, R9G9B9E5, RGBG8888, GRGB8888, + ETC2_RGB, ETC2_RGBA, ETC2_RGB_A1, + EAC_R11_RGB_U, EAC_R11_RGB_S, EAC_RG11_RGB_U, EAC_RG11_RGB_S, + CUSTOM + } + + /** Color space of the color data. */ + public enum ColorSpace { + RGB, SRGB + } + + /** Datatype used to describe a color component. */ + public enum ChannelType { + UBYTE_NORM, SBYTE_NORM, UBYTE, SBYTE, + USHORT_NORM, SSHORT_NORM, USHORT, SSHORT, + UINT_NORM, SINT_NORM, UINT, SINT, + FLOAT + } + + // Supported pixel formats + private static final EnumSet SUPPORTED_FORMATS = EnumSet.of( + PixelFormat.DXT1, + PixelFormat.DXT3, + PixelFormat.DXT5, + PixelFormat.PVRTC_2BPP_RGB, + PixelFormat.PVRTC_2BPP_RGBA, + PixelFormat.PVRTC_4BPP_RGB, + PixelFormat.PVRTC_4BPP_RGBA, + PixelFormat.ETC1, + PixelFormat.ETC2_RGB, + PixelFormat.ETC2_RGB_A1, + PixelFormat.ETC2_RGBA + ); + + private final Decodable decoder; + + /** PVR signature ("PVR\u0003"). */ + int signature; + /** PVR flags. */ + Flags flags; + /** (Texture) pixel format of PVR data. */ + PixelFormat pixelFormat; + /** Optional definition of a custom pixel format. */ + byte[] pixelFormatEx; + /** Whether pixel data is encoded in RGB or sRGB color space. */ + ColorSpace colorSpace; + /** Defines the decompressed color value format. */ + ChannelType channelType; + /** Texture height, in pixels. */ + int height; + /** Texture width, in pixels. */ + int width; + /** (Average) color depth of a single decoded pixel. */ + int colorDepth; + /** Average bits per pixel for encoded texture data. */ + int bitsPerInputPixel; + /** Depth of the texture, for 3D textures. */ + int textureDepth; + /** Number of texture surfaces. */ + int numSurfaces; + /** Number of texture faces. */ + int numFaces; + /** Number of mipmaps. */ + int numMipMaps; + /** Size, in bytes, of an optional metadata block. */ + int metaSize; + /** Optional metadata. */ + byte[] metaData; + /** Total PVR header size, including metadata. */ + int headerSize; + /** Encoded texture data. */ + byte[] data; + + /** Returns whether the specified pixel format is supported by the PVR decoder. */ + public static boolean isSupported(PixelFormat pixelFormat) { + try { + return SUPPORTED_FORMATS.contains(pixelFormat); + } catch (Throwable t) { + return false; + } + } + + /** Removes all PvrDecoder objects from the cache. */ + public static void flushCache() { + PvrtcDecoder.flushCache(); + } + + /** + * Initializes PVR information from the specified byte array. + * + * @param buffer Buffer containing PVR header data + * @param size Size of the buffer data. + * @throws Exception Thrown if the buffer doesn't contain valid PVR data. + */ + public PvrInfo(byte[] buffer, int size) throws Exception { + this.decoder = init(buffer, size); + } + + /** Returns flags that indicate special properties of the color data. */ + public Flags getFlags() { + return flags; + } + + /** Returns the pixel format used to encode image data within the PVR file. */ + public PixelFormat getPixelFormat() { + return pixelFormat; + } + + /** Returns meaningful data only if pixelFormat() returns {@code PixelFormat.CUSTOM}. */ + public byte[] getPixelFormatEx() { + return pixelFormatEx; + } + + /** Returns the color space the image data is in. */ + public ColorSpace getColorSpace() { + return colorSpace; + } + + /** Returns the data type used to encode the image data within the PVR file. */ + public ChannelType getChannelType() { + return channelType; + } + + /** Returns the texture width in pixels. */ + public int getWidth() { + return width; + } + + /** Returns the texture height in pixels. */ + public int getHeight() { + return height; + } + + /** Returns the color depth of the pixel type used to encode the color data in bits/pixel. */ + public int getColorDepth() { + return colorDepth; + } + + /** Returns the average number of bits used for each input pixel. */ + public int getAverageBitsPerPixel() { + return bitsPerInputPixel; + } + + /** Returns the depth of the texture stored in the image data, in pixels. */ + public int getTextureDepth() { + return textureDepth; + } + + /** Returns the number of surfaces within the texture array. */ + public int getNumSurfaces() { + return numSurfaces; + } + + /** Returns the number of faces in a cube map. */ + public int getNumFaces() { + return numFaces; + } + + /** Returns the number of MIP-Map levels present including the top level. */ + public int getNumMipMaps() { + return numMipMaps; + } + + /** Returns the total size of meta data embedded in the PVR header. */ + public int getMetaSize() { + return metaSize; + } + + /** Provides access to the content of the meta data, embedded in the PVR header. Can be empty (size = 0). */ + public byte[] getMetaData() { + return metaData; + } + + /** Provides direct access to the content of the encoded pixel data. */ + public byte[] getData() { + return data; + } + + /** + * Decodes PVR data of the given pixel format and draws the specified "region" into "image". + * + * @param image The output image + * @param region The PVR texture region to draw onto "image" + * @return The success state of the operation. + * @throws Exception on error. + */ + public boolean decode(BufferedImage image, Rectangle region) throws Exception { + return decoder.decode(image, region); + } + + /** Returns whether the pixel format of the current texture is supported by the PVR decoder. */ + public boolean isSupported() { + try { + return SUPPORTED_FORMATS.contains(pixelFormat); + } catch (Throwable t) { + return false; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(data); + result = prime * result + Arrays.hashCode(pixelFormatEx); + result = prime * result + Objects.hash(bitsPerInputPixel, channelType, colorDepth, colorSpace, flags, headerSize, + height, numFaces, numMipMaps, numSurfaces, pixelFormat, signature, textureDepth, width); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PvrInfo other = (PvrInfo) obj; + return bitsPerInputPixel == other.bitsPerInputPixel && channelType == other.channelType + && colorDepth == other.colorDepth && colorSpace == other.colorSpace && Arrays.equals(data, other.data) + && flags == other.flags && headerSize == other.headerSize && height == other.height + && numFaces == other.numFaces && numMipMaps == other.numMipMaps && numSurfaces == other.numSurfaces + && pixelFormat == other.pixelFormat && Arrays.equals(pixelFormatEx, other.pixelFormatEx) + && signature == other.signature && textureDepth == other.textureDepth && width == other.width; + } + + // Initializes the PvrInfo structure and returns an associated Decodable object. + private Decodable init(byte[] buffer, int size) throws Exception { + if (buffer == null || size <= 0x34) { + throw new Exception("Invalid or incomplete PVR input data"); + } + + signature = DynamicArray.getInt(buffer, 0); + if (signature != 0x03525650) { + throw new Exception("No PVR signature found"); + } + + int v = DynamicArray.getInt(buffer, 4); + switch (v) { + case 0: + flags = Flags.NONE; + break; + case 1: + flags = Flags.PRE_MULTIPLIED; + break; + default: + throw new Exception(String.format("Unsupported PVR flags: %d", v)); + } + + long l = DynamicArray.getLong(buffer, 8); + if ((l & 0xffffffff00000000L) != 0L) { + // custom pixel format + pixelFormat = PixelFormat.CUSTOM; + pixelFormatEx = new byte[8]; + System.arraycopy(buffer, 8, pixelFormatEx, 0, 8); + } else { + // predefined pixel format + switch ((int) l) { + case 0: + pixelFormat = PixelFormat.PVRTC_2BPP_RGB; + bitsPerInputPixel = 2; + break; + case 1: + pixelFormat = PixelFormat.PVRTC_2BPP_RGBA; + bitsPerInputPixel = 2; + break; + case 2: + pixelFormat = PixelFormat.PVRTC_4BPP_RGB; + bitsPerInputPixel = 4; + break; + case 3: + pixelFormat = PixelFormat.PVRTC_4BPP_RGBA; + bitsPerInputPixel = 4; + break; + case 4: + pixelFormat = PixelFormat.PVRTC2_2BPP; + bitsPerInputPixel = 2; + break; + case 5: + pixelFormat = PixelFormat.PVRTC2_4BPP; + bitsPerInputPixel = 4; + break; + case 6: + pixelFormat = PixelFormat.ETC1; + bitsPerInputPixel = 4; + break; + case 7: + pixelFormat = PixelFormat.DXT1; + bitsPerInputPixel = 4; + break; + case 8: + pixelFormat = PixelFormat.DXT2; + bitsPerInputPixel = 8; + break; + case 9: + pixelFormat = PixelFormat.DXT3; + bitsPerInputPixel = 8; + break; + case 10: + pixelFormat = PixelFormat.DXT4; + bitsPerInputPixel = 8; + break; + case 11: + pixelFormat = PixelFormat.DXT5; + bitsPerInputPixel = 8; + break; + case 12: + pixelFormat = PixelFormat.BC4; + break; + case 13: + pixelFormat = PixelFormat.BC5; + break; + case 14: + pixelFormat = PixelFormat.BC6; + break; + case 15: + pixelFormat = PixelFormat.BC7; + break; + case 16: + pixelFormat = PixelFormat.UYVY; + break; + case 17: + pixelFormat = PixelFormat.YUY2; + break; + case 18: + pixelFormat = PixelFormat.BW1BPP; + break; + case 19: + pixelFormat = PixelFormat.R9G9B9E5; + break; + case 20: + pixelFormat = PixelFormat.RGBG8888; + break; + case 21: + pixelFormat = PixelFormat.GRGB8888; + break; + case 22: + pixelFormat = PixelFormat.ETC2_RGB; + bitsPerInputPixel = 4; + break; + case 23: + pixelFormat = PixelFormat.ETC2_RGBA; + bitsPerInputPixel = 8; + break; + case 24: + pixelFormat = PixelFormat.ETC2_RGB_A1; + bitsPerInputPixel = 4; + break; + case 25: + pixelFormat = PixelFormat.EAC_R11_RGB_U; + break; + case 26: + pixelFormat = PixelFormat.EAC_R11_RGB_S; + break; + case 27: + pixelFormat = PixelFormat.EAC_RG11_RGB_U; + break; + case 28: + pixelFormat = PixelFormat.EAC_RG11_RGB_S; + break; + default: + throw new Exception(String.format("Unsupported pixel format: %s", Integer.toString((int) l))); + } + pixelFormatEx = new byte[0]; + } + + v = DynamicArray.getInt(buffer, 16); + switch (v) { + case 0: + colorSpace = ColorSpace.RGB; + break; + case 1: + colorSpace = ColorSpace.SRGB; + break; + default: + throw new Exception(String.format("Unsupported color space: %d", v)); + } + + v = DynamicArray.getInt(buffer, 20); + switch (v) { + case 0: + channelType = ChannelType.UBYTE_NORM; + break; + case 1: + channelType = ChannelType.SBYTE_NORM; + break; + case 2: + channelType = ChannelType.UBYTE; + break; + case 3: + channelType = ChannelType.SBYTE; + break; + case 4: + channelType = ChannelType.USHORT_NORM; + break; + case 5: + channelType = ChannelType.SSHORT_NORM; + break; + case 6: + channelType = ChannelType.USHORT; + break; + case 7: + channelType = ChannelType.SSHORT; + break; + case 8: + channelType = ChannelType.UINT_NORM; + break; + case 9: + channelType = ChannelType.SINT_NORM; + break; + case 10: + channelType = ChannelType.UINT; + break; + case 11: + channelType = ChannelType.SINT; + break; + case 12: + channelType = ChannelType.FLOAT; + break; + default: + throw new Exception(String.format("Unsupported channel type: %d", v)); + } + + switch (pixelFormat) { + case PVRTC_2BPP_RGB: + case PVRTC_2BPP_RGBA: + case PVRTC_4BPP_RGB: + case PVRTC_4BPP_RGBA: + case DXT1: + case DXT2: + case DXT3: + case DXT4: + case DXT5: + colorDepth = 16; + break; + default: + colorDepth = 32; // most likely wrong, but not important for us anyway + } + + height = DynamicArray.getInt(buffer, 24); + width = DynamicArray.getInt(buffer, 28); + textureDepth = DynamicArray.getInt(buffer, 32); + numSurfaces = DynamicArray.getInt(buffer, 36); + numFaces = DynamicArray.getInt(buffer, 40); + numMipMaps = DynamicArray.getInt(buffer, 44); + metaSize = DynamicArray.getInt(buffer, 48); + if (metaSize > 0) { + if (metaSize + 0x34 > size) { + throw new Exception("Input buffer too small"); + } + metaData = new byte[metaSize]; + System.arraycopy(buffer, 52, metaData, 0, metaSize); + } else { + metaData = new byte[0]; + } + headerSize = 0x34 + metaSize; + + // storing pixel data + data = new byte[size - headerSize]; + System.arraycopy(buffer, headerSize, data, 0, data.length); + + // returning associated Decodable object + switch (pixelFormat) { + case PVRTC_2BPP_RGB: + case PVRTC_2BPP_RGBA: + case PVRTC_4BPP_RGB: + case PVRTC_4BPP_RGBA: + return new PvrtcDecoder(this); + case DXT1: + case DXT3: + case DXT5: + return new DxtDecoder(this); + case ETC1: + case ETC2_RGB: + case ETC2_RGB_A1: + case ETC2_RGBA: + return new Etc2Decoder(this); + default: + return new DummyDecoder(this); + } + } +} diff --git a/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java b/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java new file mode 100644 index 000000000..279232070 --- /dev/null +++ b/src/org/infinity/resource/graphics/decoder/PvrtcDecoder.java @@ -0,0 +1,624 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 - 2023 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics.decoder; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import org.infinity.util.DynamicArray; + +/** + * Texture decoder for PVRTC pixel formats. + */ +public class PvrtcDecoder implements Decodable { + // The local cache list for decoded PVR textures. The "key" has to be a unique PvrInfo structure. + private static final Map TEXTURE_CACHE = Collections + .synchronizedMap(new LinkedHashMap()); + + // The max. number of cache entries to hold + private static final int MAX_CACHE_ENTRIES = 8; + + // Datatypes as used in the reference implementation: + // Pixel32/128S: int[]{red, green, blue, alpha} + // PVRTCWord: int[]{modulation, color} + // PVRTCWordIndices: int[]{p0, p1, q0, q1, r0, r1, s0, s1} + + // color channel indices into an array of pixel values + private static final int CH_R = 0; + private static final int CH_G = 1; + private static final int CH_B = 2; + private static final int CH_A = 3; + // start indices into an array of PVRTC blocks + private static final int IDX_P = 0; + private static final int IDX_Q = 2; + private static final int IDX_R = 4; + private static final int IDX_S = 6; + // indices into a PVRTC data block + private static final int BLK_MOD = 0; + private static final int BLK_COL = 1; + + private final PvrInfo info; + + /** Initializes a new {@code PVRTC} decoder from with the specified {@link PvrInfo}. */ + public PvrtcDecoder(PvrInfo pvr) { + this.info = Objects.requireNonNull(pvr); + } + + // --------------------- Begin Interface Decodable --------------------- + + @Override + public boolean decode(BufferedImage image, Rectangle region) throws Exception { + switch (info.pixelFormat) { + case PVRTC_2BPP_RGB: + case PVRTC_2BPP_RGBA: + return decodePVRT(image, region, true); + case PVRTC_4BPP_RGB: + case PVRTC_4BPP_RGBA: + return decodePVRT(image, region, false); + default: + return false; + } + } + + @Override + public PvrInfo getPvrInfo() { + return info; + } + + // --------------------- End Interface Decodable --------------------- + + // Decodes both 2bpp and 4bpp versions of the PVRT format + private boolean decodePVRT(BufferedImage image, Rectangle region, boolean is2bpp) + throws Exception { + if (image == null || region == null) { + return false; + } + + int imgWidth = image.getWidth(); + int imgHeight = image.getHeight(); + int[] imgData = null; + + // bounds checking + if (region.x < 0) { + region.width += -region.x; + region.x = 0; + } + if (region.y < 0) { + region.height += -region.y; + region.y = 0; + } + if (region.x + region.width > info.width) + region.width = info.width - region.x; + if (region.y + region.height > info.height) + region.height = info.height - region.y; + + // preparing image buffer for faster rendering + BufferedImage alignedImage = getCachedImage(info); + if (alignedImage == null) { + if (!region.equals(new Rectangle(0, 0, info.width, info.height))) { + alignedImage = new BufferedImage(info.width, info.height, BufferedImage.TYPE_INT_ARGB); + imgData = ((DataBufferInt) alignedImage.getRaster().getDataBuffer()).getData(); + if (imgWidth < region.width) { + region.width = imgWidth; + } + if (imgHeight < region.height) { + region.height = imgHeight; + } + } else { + imgData = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + } + + int wordWidth = is2bpp ? 8 : 4; + int wordHeight = 4; + int numXWords = info.width / wordWidth; + int numYWords = info.height / wordHeight; + int[] indices = new int[8]; + int[] p = new int[2], q = new int[2], r = new int[2], s = new int[2]; + int[][] pixels = new int[wordWidth * wordHeight][4]; + + for (int wordY = -1; wordY < numYWords - 1; wordY++) { + for (int wordX = -1; wordX < numXWords - 1; wordX++) { + indices[IDX_P] = wrapWordIndex(numXWords, wordX); + indices[IDX_P + 1] = wrapWordIndex(numYWords, wordY); + indices[IDX_Q] = wrapWordIndex(numXWords, wordX + 1); + indices[IDX_Q + 1] = wrapWordIndex(numYWords, wordY); + indices[IDX_R] = wrapWordIndex(numXWords, wordX); + indices[IDX_R + 1] = wrapWordIndex(numYWords, wordY + 1); + indices[IDX_S] = wrapWordIndex(numXWords, wordX + 1); + indices[IDX_S + 1] = wrapWordIndex(numYWords, wordY + 1); + + // work out the offsets into the twiddle structs, multiply by two as there are two members per word + int[] wordOffsets = new int[] { twiddleUV(numXWords, numYWords, indices[IDX_P], indices[IDX_P + 1]) << 1, + twiddleUV(numXWords, numYWords, indices[IDX_Q], indices[IDX_Q + 1]) << 1, + twiddleUV(numXWords, numYWords, indices[IDX_R], indices[IDX_R + 1]) << 1, + twiddleUV(numXWords, numYWords, indices[IDX_S], indices[IDX_S + 1]) << 1 }; + + // access individual elements to fill out input words + p[BLK_MOD] = DynamicArray.getInt(info.data, wordOffsets[0] << 2); + p[BLK_COL] = DynamicArray.getInt(info.data, (wordOffsets[0] + 1) << 2); + q[BLK_MOD] = DynamicArray.getInt(info.data, wordOffsets[1] << 2); + q[BLK_COL] = DynamicArray.getInt(info.data, (wordOffsets[1] + 1) << 2); + r[BLK_MOD] = DynamicArray.getInt(info.data, wordOffsets[2] << 2); + r[BLK_COL] = DynamicArray.getInt(info.data, (wordOffsets[2] + 1) << 2); + s[BLK_MOD] = DynamicArray.getInt(info.data, wordOffsets[3] << 2); + s[BLK_COL] = DynamicArray.getInt(info.data, (wordOffsets[3] + 1) << 2); + + // assemble four words into struct to get decompressed pixels from + getDecompressedPixels(p, q, r, s, pixels, is2bpp); + mapDecompressedData(imgData, info.width, pixels, indices, is2bpp); + } + } + imgData = null; + registerCachedImage(info, alignedImage); + } else { + if (imgWidth < region.width) { + region.width = imgWidth; + } + if (imgHeight < region.height) { + region.height = imgHeight; + } + } + + // rendering aligned image to target image + if (alignedImage != null) { + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); + g.drawImage(alignedImage, 0, 0, region.width, region.height, region.x, region.y, region.x + region.width, + region.y + region.height, null); + } finally { + g.dispose(); + g = null; + } + alignedImage = null; + } + return true; + } + + // Decodes the first color in a PVRT data word + private static int[] getColorA(int colorData) { + int[] retVal = new int[4]; + if ((colorData & 0x8000) != 0) { + // opaque color mode: RGB554 + retVal[CH_R] = (colorData & 0x7c00) >>> 10; // red: 5->5 bits + retVal[CH_G] = (colorData & 0x3e0) >>> 5; // green: 5->5 bits + retVal[CH_B] = (colorData & 0x1e) | ((colorData & 0x1e) >>> 4); // blue: 4->5 bits + retVal[CH_A] = 0x0f; // alpha: 0->4 bits + } else { + // transparent color mode: ARGB3443 + retVal[CH_R] = ((colorData & 0xf00) >>> 7) | ((colorData & 0xf00) >>> 11); // red: 4->5 bits + retVal[CH_G] = ((colorData & 0xf0) >>> 3) | ((colorData & 0xf0) >>> 7); // green: 4->5 bits + retVal[CH_B] = ((colorData & 0xe) << 1) | ((colorData & 0xe) >>> 2); // blue: 3->5 bits + retVal[CH_A] = ((colorData & 0x7000) >>> 11); // alpha: 3->4 bits + } + return retVal; + } + + // Decodes the second color in a PVRT data word + private static int[] getColorB(int colorData) { + int[] retVal = new int[4]; + if ((colorData & 0x80000000) != 0) { + // opaque color mode: RGB555 + retVal[CH_R] = (colorData & 0x7c000000) >>> 26; // red: 5->5 bits + retVal[CH_G] = (colorData & 0x3e00000) >>> 21; // green: 5->5 bits + retVal[CH_B] = (colorData & 0x1f0000) >>> 16; // blue: 5->5 bits + retVal[CH_A] = 0x0f; // alpha: 0->4 bits + } else { + // transparent color mode: ARGB3444 + retVal[CH_R] = ((colorData & 0xf000000) >>> 23) | ((colorData & 0xf000000) >>> 27); // red: 4->5 bits + retVal[CH_G] = ((colorData & 0xf00000) >>> 19) | ((colorData & 0xf00000) >>> 23); // green: 4->5 bits + retVal[CH_B] = ((colorData & 0xf0000) >>> 15) | ((colorData & 0xf0000) >>> 19); // blue: 4->5 bits + retVal[CH_A] = ((colorData & 0x70000000) >>> 27); // alpha: 3->4 bits + } + return retVal; + } + + // Bilinear upscale from 2x2 pixels to 4x4/8x4 pixels (depending on is2bpp argument) + // p, q, r, s = [channels] + // outBlock = [pixels][channels] + // is2bpp: true=2bpp mode, false=4bpp mode + private static void interpolateColors(int[] p, int[] q, int[] r, int[] s, int[][] outBlock, boolean is2bpp) { + int wordWidth = is2bpp ? 8 : 4; + int wordHeight = 4; + + // making working copy + int[] hp = Arrays.copyOf(p, p.length); + int[] hq = Arrays.copyOf(q, q.length); + int[] hr = Arrays.copyOf(r, r.length); + int[] hs = Arrays.copyOf(s, s.length); + + // get vectors + int[] qmp = new int[] { hq[CH_R] - hp[CH_R], hq[CH_G] - hp[CH_G], hq[CH_B] - hp[CH_B], hq[CH_A] - hp[CH_A] }; + int[] smr = new int[] { hs[CH_R] - hr[CH_R], hs[CH_G] - hr[CH_G], hs[CH_B] - hr[CH_B], hs[CH_A] - hr[CH_A] }; + + // multiply colors + for (int i = 0; i < 4; i++) { + hp[i] *= wordWidth; + hr[i] *= wordWidth; + } + + int[] result = new int[4], dy = new int[4]; + if (is2bpp) { + // loop through pixels to achieve results + for (int x = 0; x < wordWidth; x++) { + for (int i = 0; i < 4; i++) { + result[i] = hp[i] << 2; + dy[i] = hr[i] - hp[i]; + } + + for (int y = 0; y < wordHeight; y++) { + outBlock[y * wordWidth + x][CH_R] = (result[CH_R] >> 7) + (result[CH_R] >> 2); + outBlock[y * wordWidth + x][CH_G] = (result[CH_G] >> 7) + (result[CH_G] >> 2); + outBlock[y * wordWidth + x][CH_B] = (result[CH_B] >> 7) + (result[CH_B] >> 2); + outBlock[y * wordWidth + x][CH_A] = (result[CH_A] >> 5) + (result[CH_A] >> 1); + + result[CH_R] += dy[CH_R]; + result[CH_G] += dy[CH_G]; + result[CH_B] += dy[CH_B]; + result[CH_A] += dy[CH_A]; + } + + hp[CH_R] += qmp[CH_R]; + hp[CH_G] += qmp[CH_G]; + hp[CH_B] += qmp[CH_B]; + hp[CH_A] += qmp[CH_A]; + hr[CH_R] += smr[CH_R]; + hr[CH_G] += smr[CH_G]; + hr[CH_B] += smr[CH_B]; + hr[CH_A] += smr[CH_A]; + } + } else { + // loop through pixels to achieve results + for (int y = 0; y < wordHeight; y++) { + for (int i = 0; i < 4; i++) { + result[i] = hp[i] << 2; + dy[i] = hr[i] - hp[i]; + } + + for (int x = 0; x < wordWidth; x++) { + outBlock[y * wordWidth + x][CH_R] = (result[CH_R] >> 6) + (result[CH_R] >> 1); + outBlock[y * wordWidth + x][CH_G] = (result[CH_G] >> 6) + (result[CH_G] >> 1); + outBlock[y * wordWidth + x][CH_B] = (result[CH_B] >> 6) + (result[CH_B] >> 1); + outBlock[y * wordWidth + x][CH_A] = (result[CH_A] >> 4) + result[CH_A]; + + result[CH_R] += dy[CH_R]; + result[CH_G] += dy[CH_G]; + result[CH_B] += dy[CH_B]; + result[CH_A] += dy[CH_A]; + } + + hp[CH_R] += qmp[CH_R]; + hp[CH_G] += qmp[CH_G]; + hp[CH_B] += qmp[CH_B]; + hp[CH_A] += qmp[CH_A]; + hr[CH_R] += smr[CH_R]; + hr[CH_G] += smr[CH_G]; + hr[CH_B] += smr[CH_B]; + hr[CH_A] += smr[CH_A]; + } + } + } + + // Reads out and decodes the modulation values within the specified data word + // modValues, modModes = [x][y] + private static void unpackModulations(int[] word, int ofsX, int ofsY, int[][] modValues, int[][] modModes, + boolean is2bpp) { + int modMode = word[BLK_COL] & 1; + int modBits = word[BLK_MOD]; + + // unpack differently depending on 2bpp or 4bpp modes + if (is2bpp) { + if (modMode != 0) { + // determine which of the three modes are in use: + if ((modBits & 1) != 0) { + // look at the LSB for the center (V=2, H=4) texel. Its LSB is now actually used to + // indicate whether it's the H-only mode or the V-only + + // the center texel data is at (y=2, x=4) and so its LSB is at bit 20 + if ((modBits & (1 << 20)) != 0) { + // this is V-only mode + modMode = 3; + } else { + // this is H-only mode + modMode = 2; + } + + // create an extra bit for the center pixel so that it looks like we have 2 actual bits + // for this texel. It makes later coding much easier. + if ((modBits & (1 << 21)) != 0) { + modBits |= (1 << 20); + } else { + modBits &= ~(1 << 20); + } + } + + if ((modBits & 2) != 0) { + modBits |= 1; // set it + } else { + modBits &= ~1; // clear it + } + + // run through all the pixels in the block. Note we can now treat all the stored values as + // if they have 2 bits (even when they didn't!) + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 8; x++) { + modModes[x + ofsX][y + ofsY] = modMode; + + // if this is a stored value... + if (((x ^ y) & 1) == 0) { + modValues[x + ofsX][y + ofsY] = modBits & 3; + modBits >>>= 2; + } + } + } + } else { + // if direct encoded 2bpp mode - i.e. mode bit per pixel + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 8; x++) { + modModes[x + ofsX][y + ofsY] = modMode; + + // double the bits, so 0 -> 00, and 1 -> 11 + modValues[x + ofsX][y + ofsY] = ((modBits & 1) != 0) ? 3 : 0; + modBits >>>= 1; + } + } + } + } else { + // much simpler than 2bpp decompression, only two modes, so the n/8 values are set directly + // run through all the pixels in the word + if (modMode != 0) { + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + modValues[y + ofsY][x + ofsX] = modBits & 3; + if (modValues[y + ofsY][x + ofsX] == 1) { + modValues[y + ofsY][x + ofsX] = 4; + } else if (modValues[y + ofsY][x + ofsX] == 2) { + modValues[y + ofsY][x + ofsX] = 14; // +10 tells the decompressor to punch through alpha + } else if (modValues[y + ofsY][x + ofsX] == 3) { + modValues[y + ofsY][x + ofsX] = 8; + } + modBits >>>= 2; + } + } + } else { + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + modValues[y + ofsY][x + ofsX] = modBits & 3; + modValues[y + ofsY][x + ofsX] *= 3; + if (modValues[y + ofsY][x + ofsX] > 3) { + modValues[y + ofsY][x + ofsX] -= 1; + } + modBits >>>= 2; + } + } + } + } + } + + // Gets the effective modulation values for the given pixel + // modValues, modModes = [x][y] + // xPos, yPos = x, y positions within the current data word + private static int getModulationValues(int[][] modValues, int[][] modModes, int xPos, int yPos, boolean is2bpp) { + if (is2bpp) { + final int[] repVals0 = new int[] { 0, 3, 5, 8 }; + + // extract the modulation value... + if (modModes[xPos][yPos] == 0) { + // ...if a simple encoding + return repVals0[modValues[xPos][yPos]]; + } else { + // ...if this is a stored value + if (((xPos ^ yPos) & 1) == 0) { + return repVals0[modValues[xPos][yPos]]; + + // else average from the neighbors + } else if (modModes[xPos][yPos] == 1) { + // if H&V interpolation + return (repVals0[modValues[xPos][yPos - 1]] + repVals0[modValues[xPos][yPos + 1]] + + repVals0[modValues[xPos - 1][yPos]] + repVals0[modValues[xPos + 1][yPos]] + 2) >> 2; + } else if (modModes[xPos][yPos] == 2) { + // if H-only + return (repVals0[modValues[xPos - 1][yPos]] + repVals0[modValues[xPos + 1][yPos]] + 1) >> 1; + } else { + // if V-only + return (repVals0[modValues[xPos][yPos - 1]] + repVals0[modValues[xPos][yPos + 1]] + 1) >> 1; + } + } + } else { + return modValues[xPos][yPos]; + } + } + + // Gets decompressed pixels for a given decompression area + // p, q, r, s = [block word] + // outBlock = [pixels][channels] + // is2bpp: true=2bpp mode, false=4bpp mode + private static void getDecompressedPixels(int[] p, int[] q, int[] r, int[] s, int[][] outData, boolean is2bpp) { + // 4bpp only needs 8*8 values, but 2bpp needs 16*8, so rather than wasting processor time we just statically + // allocate 16*8 + int[][] modValues = new int[16][8]; + // Only 2bpp needs this + int[][] modModes = new int[16][8]; + // 4bpp only needs 16 values, but 2bpp needs 32, so rather than wasting processor time we just statically allocate + // 32. + int[][] upscaledColorA = new int[32][4]; + int[][] upscaledColorB = new int[32][4]; + + int wordWidth = is2bpp ? 8 : 4; + int wordHeight = 4; + + // get modulation from each word + unpackModulations(p, 0, 0, modValues, modModes, is2bpp); + unpackModulations(q, wordWidth, 0, modValues, modModes, is2bpp); + unpackModulations(r, 0, wordHeight, modValues, modModes, is2bpp); + unpackModulations(s, wordWidth, wordHeight, modValues, modModes, is2bpp); + + // bilinear upscale image data from 2x2 -> 4x4 + interpolateColors(getColorA(p[BLK_COL]), getColorA(q[BLK_COL]), getColorA(r[BLK_COL]), getColorA(s[BLK_COL]), + upscaledColorA, is2bpp); + interpolateColors(getColorB(p[BLK_COL]), getColorB(q[BLK_COL]), getColorB(r[BLK_COL]), getColorB(s[BLK_COL]), + upscaledColorB, is2bpp); + + int[] result = new int[4]; + for (int y = 0; y < wordHeight; y++) { + for (int x = 0; x < wordWidth; x++) { + int mod = getModulationValues(modValues, modModes, x + (wordWidth >>> 1), y + (wordHeight >>> 1), is2bpp); + boolean punchThroughAlpha = false; + if (mod > 10) { + punchThroughAlpha = true; + mod -= 10; + } + + result[CH_R] = (upscaledColorA[y * wordWidth + x][CH_R] * (8 - mod) + + upscaledColorB[y * wordWidth + x][CH_R] * mod) >> 3; + result[CH_G] = (upscaledColorA[y * wordWidth + x][CH_G] * (8 - mod) + + upscaledColorB[y * wordWidth + x][CH_G] * mod) >> 3; + result[CH_B] = (upscaledColorA[y * wordWidth + x][CH_B] * (8 - mod) + + upscaledColorB[y * wordWidth + x][CH_B] * mod) >> 3; + if (punchThroughAlpha) { + result[CH_A] = 0; + } else { + result[CH_A] = (upscaledColorA[y * wordWidth + x][CH_A] * (8 - mod) + + upscaledColorB[y * wordWidth + x][CH_A] * mod) >> 3; + } + + // convert the 32bit precision result to 8 bit per channel color + if (is2bpp) { + outData[y * wordWidth + x][CH_R] = result[CH_R]; + outData[y * wordWidth + x][CH_G] = result[CH_G]; + outData[y * wordWidth + x][CH_B] = result[CH_B]; + outData[y * wordWidth + x][CH_A] = result[CH_A]; + } else { + outData[y + x * wordHeight][CH_R] = result[CH_R]; + outData[y + x * wordHeight][CH_G] = result[CH_G]; + outData[y + x * wordHeight][CH_B] = result[CH_B]; + outData[y + x * wordHeight][CH_A] = result[CH_A]; + } + } + } + } + + // Maps decompressed data to the correct location in the output buffer + private static int wrapWordIndex(int numWords, int word) { + return ((word + numWords) % numWords); + } + + // Given the word coordinates and the dimension of the texture in words, this returns the twiddled + // offset of the word from the start of the map + private static int twiddleUV(int xSize, int ySize, int xPos, int yPos) { + // initially assume x is the larger size + int minDimension = xSize; + int maxValue = yPos; + int twiddled = 0; + int srcBitPos = 1; + int dstBitPos = 1; + int shiftCount = 0; + + // if y is the larger dimension - switch the min/max values + if (ySize < xSize) { + minDimension = ySize; + maxValue = xPos; + } + + // step through all the bits in the "minimum" dimension + while (srcBitPos < minDimension) { + if ((yPos & srcBitPos) != 0) { + twiddled |= dstBitPos; + } + + if ((xPos & srcBitPos) != 0) { + twiddled |= (dstBitPos << 1); + } + + srcBitPos <<= 1; + dstBitPos <<= 2; + shiftCount++; + } + + // prepend any unused bits + maxValue >>>= shiftCount; + twiddled |= (maxValue << (shiftCount << 1)); + + return twiddled; + } + + // Maps decompressed data to the correct location in the output buffer + // outBuffer = [pixel] + // inData = [pixel][channel] + // indices = [two per p, q, r, s] + private static void mapDecompressedData(int[] outBuffer, int width, int[][] inData, int[] indices, boolean is2bpp) { + int wordWidth = is2bpp ? 8 : 4; + int wordHeight = 4; + + for (int y = 0; y < (wordHeight >>> 1); y++) { + for (int x = 0; x < (wordWidth >>> 1); x++) { + // map p + int outOfs = (((indices[IDX_P + 1] * wordHeight) + y + (wordHeight >>> 1)) * width + + indices[IDX_P + 0] * wordWidth + x + (wordWidth >>> 1)); + int inOfs = y * wordWidth + x; + outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) + | inData[inOfs][CH_B]; + + // map q + outOfs = (((indices[IDX_Q + 1] * wordHeight) + y + (wordHeight >>> 1)) * width + + indices[IDX_Q + 0] * wordWidth + x); + inOfs = y * wordWidth + x + (wordWidth >>> 1); + outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) + | inData[inOfs][CH_B]; + + // map r + outOfs = (((indices[IDX_R + 1] * wordHeight) + y) * width + indices[IDX_R + 0] * wordWidth + x + + (wordWidth >>> 1)); + inOfs = (y + (wordHeight >>> 1)) * wordWidth + x; + outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) + | inData[inOfs][CH_B]; + + // map s + outOfs = (((indices[IDX_S + 1] * wordHeight) + y) * width + indices[IDX_S + 0] * wordWidth + x); + inOfs = (y + (wordHeight >>> 1)) * wordWidth + x + (wordWidth >>> 1); + outBuffer[outOfs] = (inData[inOfs][CH_A] << 24) | (inData[inOfs][CH_R] << 16) | (inData[inOfs][CH_G] << 8) + | inData[inOfs][CH_B]; + } + } + } + + /** Removes all PvrDecoder objects from the cache. */ + public static void flushCache() { + TEXTURE_CACHE.clear(); + } + + // Returns a PvrDecoder object only if it already exists in the cache. + private static BufferedImage getCachedImage(PvrInfo pvr) { + BufferedImage retVal = null; + if (pvr != null) { + if (TEXTURE_CACHE.containsKey(pvr)) { + retVal = TEXTURE_CACHE.get(pvr); + // re-inserting entry to prevent premature removal from cache + TEXTURE_CACHE.remove(pvr); + TEXTURE_CACHE.put(pvr, retVal); + } + } + return retVal; + } + + // Returns a PvrDecoder object of the specified key if available, or creates and returns a new one otherwise. + private static void registerCachedImage(PvrInfo pvr, BufferedImage image) { + if (pvr != null && getCachedImage(pvr) == null && image != null) { + TEXTURE_CACHE.put(pvr, image); + // removing excess cache entries + while (TEXTURE_CACHE.size() > MAX_CACHE_ENTRIES) { + TEXTURE_CACHE.remove(TEXTURE_CACHE.keySet().iterator().next()); + } + } + } +} diff --git a/src/org/infinity/resource/key/Keyfile.java b/src/org/infinity/resource/key/Keyfile.java index a45324c09..b7af08e7b 100644 --- a/src/org/infinity/resource/key/Keyfile.java +++ b/src/org/infinity/resource/key/Keyfile.java @@ -385,7 +385,7 @@ public BIFFEntry[] getBIFFEntriesSorted() { public BIFFEntry getBIFFEntry(Path keyFile, int index) { List biffs = getBIFFList(keyFile, false); - if (biffs != null) { + if (biffs != null && biffs.size() > 0) { return biffs.get(index); } return null; diff --git a/src/org/infinity/resource/mus/Viewer.java b/src/org/infinity/resource/mus/Viewer.java index 401ae905e..57131ab15 100644 --- a/src/org/infinity/resource/mus/Viewer.java +++ b/src/org/infinity/resource/mus/Viewer.java @@ -11,11 +11,13 @@ import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.HashMap; import java.util.List; import java.util.StringTokenizer; import java.util.Vector; import javax.swing.BorderFactory; +import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JList; @@ -32,6 +34,14 @@ import org.infinity.util.SimpleListModel; public class Viewer extends JPanel implements Runnable, ActionListener { + /** Provides quick access to the "play" and "pause" image icon. */ + private static final HashMap PLAY_ICONS = new HashMap<>(); + + static { + PLAY_ICONS.put(true, Icons.ICON_PLAY_16.getIcon()); + PLAY_ICONS.put(false, Icons.ICON_PAUSE_16.getIcon()); + } + private final SimpleListModel listModel = new SimpleListModel<>(); private final JList list = new JList<>(listModel); private final AudioPlayer player = new AudioPlayer(); @@ -55,10 +65,16 @@ public Viewer(MusResource mus) { @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bPlay) { - new Thread(this).start(); + if (player == null || !player.isRunning()) { + new Thread(this).start(); + } else if (player.isRunning()) { + setPlayButtonState(player.isPaused()); + player.setPaused(!player.isPaused()); + } } else if (event.getSource() == bStop) { bStop.setEnabled(false); bEnd.setEnabled(false); + setPlayButtonState(false); play = false; player.stopPlay(); } else if (event.getSource() == bEnd) { @@ -73,7 +89,7 @@ public void actionPerformed(ActionEvent event) { @Override public void run() { - bPlay.setEnabled(false); + setPlayButtonState(true); bStop.setEnabled(true); bEnd.setEnabled(true); list.setEnabled(false); @@ -89,7 +105,7 @@ public void run() { list.setSelectedIndex(nextnr); list.ensureIndexIsVisible(nextnr); list.repaint(); - player.play(entryList.get(nextnr).getAudioBuffer()); + player.playContinuous(entryList.get(nextnr).getAudioBuffer()); } else if (entryList.get(nextnr).getEndBuffer() != null) { player.play(entryList.get(nextnr).getEndBuffer()); play = false; @@ -105,7 +121,8 @@ public void run() { JOptionPane.showMessageDialog(this, "Error during playback", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } - bPlay.setEnabled(true); + player.stopPlay(); + setPlayButtonState(false); bStop.setEnabled(false); bEnd.setEnabled(false); list.setEnabled(true); @@ -180,7 +197,8 @@ private boolean parseMusFile(MusResource mus) { } private void initGUI() { - bPlay = new JButton("Play", Icons.ICON_PLAY_16.getIcon()); + bPlay = new JButton(); + setPlayButtonState(false); bPlay.addActionListener(this); bEnd = new JButton("Finish", Icons.ICON_END_16.getIcon()); bEnd.setEnabled(false); @@ -234,4 +252,10 @@ private synchronized void setClosed(boolean b) { private synchronized boolean isClosed() { return closed; } + + // Sets icon and text for the Play button according to the specified parameter. + private void setPlayButtonState(boolean paused) { + bPlay.setIcon(PLAY_ICONS.get(!paused)); + bPlay.setText(paused ? "Pause" : "Play"); + } } diff --git a/src/org/infinity/resource/sound/AcmBuffer.java b/src/org/infinity/resource/sound/AcmBuffer.java index 1522054c4..1924bbb56 100644 --- a/src/org/infinity/resource/sound/AcmBuffer.java +++ b/src/org/infinity/resource/sound/AcmBuffer.java @@ -11,6 +11,7 @@ * Decodes ACM encoded audio data into uncompressed PCM WAV audio data. */ public class AcmBuffer extends AudioBuffer { + private AcmReader acm; public AcmBuffer(ResourceEntry entry) throws Exception { super(entry); @@ -28,11 +29,26 @@ public AcmBuffer(byte[] buffer, int offset, AudioOverride override) throws Excep super(buffer, offset, override); } + /** Returns the number of audio channels. */ + public int getChannels() { + return (acm != null) ? acm.getChannels() : 0; + } + + /** Returns the sample rate in Hz. */ + public int getSampleRate() { + return (acm != null) ? acm.getSampleRate() : 0; + } + + /** Returns the bits per sample. */ + public int getBitsPerSample() { + return (acm != null) ? acm.getBitsPerSample() : 0; + } + // --------------------- Begin Class AudioBuffer --------------------- @Override protected void convert(byte[] buffer, int offset, AudioOverride override) throws Exception { - AcmReader acm = new AcmReader(buffer, offset, override); + acm = new AcmReader(buffer, offset, override); int numSamples = acm.getSampleCount(); int numChannels = acm.getChannels(); int sampleRate = acm.getSampleRate(); diff --git a/src/org/infinity/resource/sound/AudioBuffer.java b/src/org/infinity/resource/sound/AudioBuffer.java index 92c239b7c..3d1826dfb 100644 --- a/src/org/infinity/resource/sound/AudioBuffer.java +++ b/src/org/infinity/resource/sound/AudioBuffer.java @@ -5,6 +5,7 @@ package org.infinity.resource.sound; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.io.StreamUtils; @@ -47,6 +48,55 @@ public byte[] getAudioData() { return data; } + /** + * Returns the total duration of the audio data in milliseconds. + * + * @return Duration in milliseconds. + */ + public long getDuration() { + if (data != null && data.length >= 44) { + final ByteBuffer bb = ByteBuffer.wrap(data, 0, 44).asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); + int byteRate = bb.getInt(28); + int totalSize = bb.getInt(40); + return totalSize * 1000L / byteRate; + } + return 0L; + } + + /** + * Returns the sample rate of the current audio clip. + * + * @return Sample rate in Hz. + */ + public int getSampleRate() { + if (data != null && data.length >= 44) { + final ByteBuffer bb = ByteBuffer.wrap(data, 0, 44).asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); + return bb.getInt(24); + } + return 0; + } + + /** + * Returns the number of channels used by the current audio clip. + * + * @return Number of channels. + */ + public int getChannels() { + if (data != null && data.length >= 44) { + final ByteBuffer bb = ByteBuffer.wrap(data, 0, 44).asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); + return bb.getShort(22); + } + return 0; + } + + public int getBitsPerSample() { + if (data != null && data.length >= 44) { + final ByteBuffer bb = ByteBuffer.wrap(data, 0, 44).asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN); + return bb.getShort(34); + } + return 0; + } + /** * Converts the source audio data into uncompressed PCM WAV data. * diff --git a/src/org/infinity/resource/sound/AudioPlayer.java b/src/org/infinity/resource/sound/AudioPlayer.java index da1fa1d90..be37a396d 100644 --- a/src/org/infinity/resource/sound/AudioPlayer.java +++ b/src/org/infinity/resource/sound/AudioPlayer.java @@ -14,20 +14,44 @@ import javax.sound.sampled.UnsupportedAudioFileException; public class AudioPlayer { - private static final byte BUFFER[] = new byte[8196]; + private final byte[] buffer = new byte[8196]; private AudioFormat audioFormat; private SourceDataLine dataLine; private boolean playing = true; + private boolean paused = false; private boolean stopped = true; /** - * Starts playback of audio data associated with the specified audio buffer. + * Starts playback of audio data associated with the specified audio buffer and closes the audio line when + * playback ends. * * @param audioBuffer AudioBuffer object containing audio data. * @throws Exception On error */ public void play(AudioBuffer audioBuffer) throws Exception { + play(audioBuffer, false); + } + + /** + * Starts playback of audio data associated with the specified audio buffer and keeps the audio line open for + * additional sound data. + * + * @param audioBuffer AudioBuffer object containing audio data. + * @throws Exception On error + */ + public void playContinuous(AudioBuffer audioBuffer) throws Exception { + play(audioBuffer, true); + } + + /** + * Starts playback of audio data associated with the specified audio buffer. + * + * @param audioBuffer AudioBuffer object containing audio data. + * @param continuous Whether the audio line should be kept open for additional sound data. + * @throws Exception On error + */ + private void play(AudioBuffer audioBuffer, boolean continuous) throws Exception { if (audioBuffer == null || audioBuffer.getAudioData() == null) { return; } @@ -43,15 +67,22 @@ public void play(AudioBuffer audioBuffer) throws Exception { } dataLine = (SourceDataLine) AudioSystem.getLine(info); dataLine.open(ais.getFormat(), 16384); + dataLine.start(); } - dataLine.start(); while (isPlaying()) { - int numBytesRead = ais.read(BUFFER, 0, BUFFER.length); - if (numBytesRead < 0) { - break; + if (!isPaused()) { + int numBytesRead = ais.read(buffer, 0, buffer.length); + if (numBytesRead < 0) { + if (!continuous) { + dataLine.drain(); + } + break; + } + dataLine.write(buffer, 0, numBytesRead); + } else { + Thread.sleep(25L); } - dataLine.write(BUFFER, 0, numBytesRead); } } catch (Exception e) { setStopped(true); @@ -65,6 +96,34 @@ public void play(AudioBuffer audioBuffer) throws Exception { setStopped(true); } + /** + * Returns whether the player is initialized to play back sound. + */ + public boolean isRunning() { + return dataLine != null && dataLine.isOpen(); + } + + /** + * Sets the current playback mode to "pause" or "playing", depending on the specified parameter. + */ + public void setPaused(boolean pause) { + if (paused != pause) { + paused = pause; + if (pause) { + dataLine.stop(); + } else { + dataLine.start(); + } + } + } + + /** + * Returns whether the player is in pause mode. + */ + public boolean isPaused() { + return paused; + } + /** * Stops audio playback. */ @@ -80,12 +139,26 @@ public void stopPlay() { Thread.sleep(150L); } catch (InterruptedException e) { } + setStopped(true); + if (dataLine != null && dataLine.isOpen()) { + dataLine.close(); + } dataLine = null; } + /** + * Returns the {@link DataLine} instances of the player, or {@code null} if the player has not been initialized. + */ + public DataLine getDataLine() { + return dataLine; + } + private synchronized void setPlaying(boolean b) { if (b != playing) { playing = b; + if (!b) { + paused = false; + } } } diff --git a/src/org/infinity/resource/sound/OggBuffer.java b/src/org/infinity/resource/sound/OggBuffer.java index aa58a5e04..f92554110 100644 --- a/src/org/infinity/resource/sound/OggBuffer.java +++ b/src/org/infinity/resource/sound/OggBuffer.java @@ -23,6 +23,8 @@ */ public class OggBuffer extends AudioBuffer { + private Info oggInfo; + public OggBuffer(ResourceEntry entry) throws Exception { super(entry); } @@ -39,6 +41,10 @@ public OggBuffer(byte[] buffer, int offset, AudioOverride override) throws Excep super(buffer, offset, override); } + public Info getInfo() { + return oggInfo; + } + // --------------------- Begin Class AudioBuffer --------------------- @Override @@ -79,7 +85,7 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { Page og = new Page(); // one Ogg bitstream page. Vorbis packets are inside Packet op = new Packet(); // one raw packet of data for decode - Info vi = new Info(); // struct that stores all the static vorbis bitstream settings + oggInfo = new Info(); // struct that stores all the static vorbis bitstream settings Comment vc = new Comment(); // struct that stores all the bitstream user comments DspState vd = new DspState(); // central working state for the packet->PCM decoder Block vb = new Block(vd); // local working space for packet->PCM decode @@ -131,7 +137,7 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { // header is an easy way to identify a Vorbis bitstream and it's // useful to see that functionality seperated out. - vi.init(); + oggInfo.init(); vc.init(); if (os.pagein(og) < 0) { // error; stream version mismatch perhaps @@ -143,7 +149,7 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { throw new Exception("Error reading initial header packet."); } - if (vi.synthesis_headerin(vc, op) < 0) { + if (oggInfo.synthesis_headerin(vc, op) < 0) { // error case; not a vorbis header throw new Exception("This Ogg bitstream does not contain Vorbis audio data."); } @@ -184,7 +190,7 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { // We can't tolerate that in a header. Die. throw new Exception("Corrupt secondary header."); } - vi.synthesis_headerin(vc, op); + oggInfo.synthesis_headerin(vc, op); i++; } } @@ -217,15 +223,15 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { // System.err.println("Encoded by: "+new String(vc.vendor, 0, vc.vendor.length-1)+"\n"); // } - convSize = 4096 / vi.channels; + convSize = 4096 / oggInfo.channels; // OK, got and parsed all three headers. Initialize the Vorbis packet->PCM decoder. - vd.synthesis_init(vi); // central decode state + vd.synthesis_init(oggInfo); // central decode state vb.init(vd); // local state for most of the decode, so multiple block decodes can proceed // in parallel. We could init multiple vorbis_block structures for vd here float[][][] _pcm = new float[1][][]; - int[] _index = new int[vi.channels]; + int[] _index = new int[oggInfo.channels]; // The rest is just a straight decode loop until end of stream while (eos == 0) { @@ -267,7 +273,7 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { int bout = (samples < convSize) ? samples : convSize; // convert floats to 16 bit signed ints (host order) and interleave - for (i = 0; i < vi.channels; i++) { + for (i = 0; i < oggInfo.channels; i++) { int ptr = i * 2; // int ptr = i; int mono = _index[i]; @@ -287,11 +293,11 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { } convBuffer[ptr] = (byte) (val); convBuffer[ptr + 1] = (byte) (val >>> 8); - ptr += 2 * vi.channels; + ptr += 2 * oggInfo.channels; } } - bos.write(convBuffer, 0, 2 * vi.channels * bout); + bos.write(convBuffer, 0, 2 * oggInfo.channels * bout); // tell libvorbis how // many samples we @@ -312,7 +318,7 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { try { bytes = bis.read(buffer, index, 4096); } catch (Exception e) { - throw new Exception(e.getMessage()); + throw e; } oy.wrote(bytes); if (bytes == 0) { @@ -328,15 +334,15 @@ private byte[] decodeOgg(byte[] inBuf, int offset) throws Exception { // They're never freed or manipulated directly vb.clear(); vd.clear(); - vi.clear(); // must be called last + oggInfo.clear(); // must be called last } // OK, clean up the framer oy.clear(); // create final output buffer - int samplesPerChannel = bos.size() / (vi.channels * 2); - byte[] header = createWAVHeader(samplesPerChannel, vi.channels, vi.rate, 16); + int samplesPerChannel = bos.size() / (oggInfo.channels * 2); + byte[] header = createWAVHeader(samplesPerChannel, oggInfo.channels, oggInfo.rate, 16); byte[] output = new byte[header.length + bos.size()]; System.arraycopy(header, 0, output, 0, header.length); System.arraycopy(bos.toByteArray(), 0, output, header.length, bos.size()); diff --git a/src/org/infinity/resource/sound/SoundResource.java b/src/org/infinity/resource/sound/SoundResource.java index 9dabf4052..5878cd0a7 100644 --- a/src/org/infinity/resource/sound/SoundResource.java +++ b/src/org/infinity/resource/sound/SoundResource.java @@ -15,10 +15,16 @@ import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; import javax.swing.BorderFactory; +import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; +import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; @@ -27,6 +33,7 @@ import org.infinity.NearInfinity; import org.infinity.gui.ButtonPanel; import org.infinity.gui.ButtonPopupMenu; +import org.infinity.gui.ViewerUtil; import org.infinity.icon.Icons; import org.infinity.resource.Closeable; import org.infinity.resource.Referenceable; @@ -41,6 +48,19 @@ * Handles all kinds of supported single track audio files. */ public class SoundResource implements Resource, ActionListener, ItemListener, Closeable, Referenceable, Runnable { + /** Formatted string with 4 placeholders: elapsed minute, elapsed second, total minutes, total seconds */ + private static final String FMT_PLAY_TIME = "%02d:%02d / %02d:%02d"; + + private static final ButtonPanel.Control PROPERTIES = ButtonPanel.Control.CUSTOM_1; + + /** Provides quick access to the "play" and "pause" image icon. */ + private static final HashMap PLAY_ICONS = new HashMap<>(); + + static { + PLAY_ICONS.put(true, Icons.ICON_PLAY_16.getIcon()); + PLAY_ICONS.put(false, Icons.ICON_PAUSE_16.getIcon()); + } + private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); @@ -48,6 +68,7 @@ public class SoundResource implements Resource, ActionListener, ItemListener, Cl private AudioBuffer audioBuffer = null; private JButton bPlay; private JButton bStop; + private JLabel lTime; private JMenuItem miExport; private JMenuItem miConvert; private JPanel panel; @@ -68,13 +89,20 @@ public SoundResource(ResourceEntry entry) throws Exception { @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bPlay) { - new Thread(this).start(); + if (player == null || !player.isRunning()) { + new Thread(this).start(); + } else if (player.isRunning()) { + bPlay.setIcon(PLAY_ICONS.get(!player.isPaused())); + player.setPaused(!player.isPaused()); + } } else if (event.getSource() == bStop) { bStop.setEnabled(false); player.stopPlay(); - bPlay.setEnabled(true); + bPlay.setIcon(PLAY_ICONS.get(true)); } else if (buttonPanel.getControlByType(ButtonPanel.Control.FIND_REFERENCES) == event.getSource()) { searchReferences(panel.getTopLevelAncestor()); + } else if (buttonPanel.getControlByType(PROPERTIES) == event.getSource()) { + showProperties(); } } @@ -140,19 +168,22 @@ public void searchReferences(Component parent) { @Override public void run() { - bPlay.setEnabled(false); + bPlay.setIcon(PLAY_ICONS.get(false)); bStop.setEnabled(true); if (audioBuffer != null) { + final TimerElapsedTask timerTask = new TimerElapsedTask(250L); try { + timerTask.start(); player.play(audioBuffer); } catch (Exception e) { JOptionPane.showMessageDialog(panel, "Error during playback", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); - player.stopPlay(); } + player.stopPlay(); + timerTask.stop(); } bStop.setEnabled(false); - bPlay.setEnabled(true); + bPlay.setIcon(PLAY_ICONS.get(true)); } // --------------------- End Interface Runnable --------------------- @@ -161,23 +192,26 @@ public void run() { @Override public JComponent makeViewer(ViewableContainer container) { - JPanel controlPanel = new JPanel(); - GridBagLayout gbl = new GridBagLayout(); - GridBagConstraints gbc = new GridBagConstraints(); - controlPanel.setLayout(gbl); - gbc.insets = new Insets(3, 3, 3, 3); - gbc.fill = GridBagConstraints.HORIZONTAL; - - bPlay = new JButton(Icons.ICON_PLAY_16.getIcon()); + JPanel controlPanel = new JPanel(new GridBagLayout()); + + bPlay = new JButton(PLAY_ICONS.get(true)); bPlay.addActionListener(this); - gbl.setConstraints(bPlay, gbc); - controlPanel.add(bPlay); + GridBagConstraints c = new GridBagConstraints(); + c = ViewerUtil.setGBC(c, 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, + new Insets(4, 8, 4, 0), 0, 0); + controlPanel.add(bPlay, c); + bStop = new JButton(Icons.ICON_STOP_16.getIcon()); bStop.addActionListener(this); bStop.setEnabled(false); - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbl.setConstraints(bStop, gbc); - controlPanel.add(bStop); + c = ViewerUtil.setGBC(c, 1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.REMAINDER, + new Insets(4, 8, 4, 8), 0, 0); + controlPanel.add(bStop, c); + + lTime = new JLabel(String.format(FMT_PLAY_TIME, 0, 0, 0, 0)); + c = ViewerUtil.setGBC(c, 0, 1, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.REMAINDER, + new Insets(4, 8, 4, 8), 0, 0); + controlPanel.add(lTime, c); JPanel centerPanel = new JPanel(new BorderLayout()); centerPanel.add(controlPanel, BorderLayout.CENTER); @@ -194,6 +228,11 @@ public JComponent makeViewer(ViewableContainer container) { bpmExport.setMenuItems(new JMenuItem[] { miExport, miConvert }); bpmExport.addItemListener(this); + JButton bProperties = new JButton("Properties...", Icons.ICON_EDIT_16.getIcon()); + bProperties.setEnabled(false); + bProperties.addActionListener(this); + buttonPanel.addControl(bProperties, PROPERTIES); + panel = new JPanel(new BorderLayout()); panel.add(centerPanel, BorderLayout.CENTER); panel.add(buttonPanel, BorderLayout.SOUTH); @@ -204,6 +243,34 @@ public JComponent makeViewer(ViewableContainer container) { return panel; } + // Updates the time label with total duration and specified elapsed time (in milliseconds). + private synchronized void updateTimeLabel(long elapsed) { + long duration = (audioBuffer != null) ? audioBuffer.getDuration() : 0L; + long em = elapsed / 1000 / 60; + long es = (elapsed / 1000) - (em * 60); + long dm = duration / 1000 / 60; + long ds = (duration / 1000) - (dm * 60); + lTime.setText(String.format(FMT_PLAY_TIME, em, es, dm, ds)); + } + + /** + * Returns a formatted representation of the total duration of the sound clip. + * + * @param exact Whether the seconds part should contain the fractional amount. + * @return A formatted string representing the sound clip duration. + */ + private String getTotalDurationString(boolean exact) { + long duration = (audioBuffer != null) ? audioBuffer.getDuration() : 0L; + long m = duration / 1000 / 60; + if (exact) { + double s = (duration / 1000.0) - (m * 60); + return String.format("%02d:%06.3f", m, s); + } else { + long s = (duration / 1000) - (m * 60); + return String.format("%02d:%02d", m, s); + } + } + // Returns the top level container associated with this viewer private Container getContainer() { if (panel != null) { @@ -226,8 +293,11 @@ public Boolean doInBackground() { private synchronized void setLoaded(boolean b) { if (bPlay != null) { bPlay.setEnabled(b); + bPlay.setIcon(PLAY_ICONS.get(true)); } + updateTimeLabel(0); miConvert.setEnabled(b); + buttonPanel.getControlByType(PROPERTIES).setEnabled(true); } private synchronized void setClosed(boolean b) { @@ -267,5 +337,127 @@ private boolean loadAudio() { return false; } + /** Shows a message dialog with basic properties of the current sound resource. */ + private void showProperties() { + if (audioBuffer == null) { + return; + } + + final String resName = entry.getResourceName().toUpperCase(Locale.ENGLISH); + String format; + int rate; + int channels; + String channelsDesc; + String duration = getTotalDurationString(true); + String extra = null; + if (audioBuffer instanceof OggBuffer) { + format = "Ogg Vorbis"; + final OggBuffer buf = (OggBuffer) audioBuffer; + rate = buf.getInfo().rate; + channels = buf.getInfo().channels; + double bitrate = entry.getResourceSize(); + bitrate = bitrate * 8.0 / 1000.0; // in kbit + bitrate = bitrate * 1000.0 / buf.getDuration(); // per second + extra = "Bitrate:     " + ((long) bitrate) + " kbps"; + } else if (audioBuffer instanceof AcmBuffer) { + if (audioBuffer instanceof WavcBuffer) { + format = "WAVC/ACM"; + } else { + format = "ACM"; + } + final AcmBuffer buf = (AcmBuffer) audioBuffer; + rate = buf.getSampleRate(); + channels = buf.getChannels(); + extra = "Bits/Sample: " + buf.getBitsPerSample(); + } else { + format = "PCM"; + rate = audioBuffer.getSampleRate(); + channels = audioBuffer.getChannels(); + extra = "Bits/Sample: " + audioBuffer.getBitsPerSample(); + } + + switch (channels) { + case 1: + channelsDesc = " (Mono)"; + break; + case 2: + channelsDesc = " (Stereo)"; + break; + default: + channelsDesc = ""; + break; + } + + final String br = "
"; + final StringBuilder sb = new StringBuilder("
"); + sb.append("Format:      ").append(format).append(br); + sb.append("Duration:    ").append(duration).append(br); + if (extra != null) { + sb.append(extra).append(br); + } + sb.append("Sample Rate: ").append(rate).append(" Hz").append(br); + sb.append("Channels:    ").append(channels).append(channelsDesc).append(br); + sb.append("
"); + JOptionPane.showMessageDialog(panel, sb.toString(), "Properties of " + resName, JOptionPane.INFORMATION_MESSAGE); + } + // --------------------- End Interface Viewable --------------------- + + // -------------------------- INNER CLASSES -------------------------- + + private class TimerElapsedTask extends TimerTask { + private Timer timer; + private long delay; + private boolean paused; + + /** Initializes a new timer task with the given delay, in milliseconds. */ + public TimerElapsedTask(long delay) { + this.delay = Math.max(1L, delay); + this.timer = null; + this.paused = false; + } + + /** + * Starts a new scheduled run. + */ + public void start() { + if (timer == null) { + timer = new Timer(); + timer.schedule(this, 0L, delay); + } + } + +// /** Returns whether a task has been initialized via {@link #start()}. */ +// public boolean isRunning() { +// return (timer != null); +// } + +// /** Pauses or unpauses a scheduled run. */ +// public void setPaused(boolean paused) { +// this.paused = paused; +// } + +// /** Returns whether a scheduled run is in paused state. */ +// public boolean isPaused() { +// return paused; +// } + + /** Stops a scheduled run. */ + public void stop() { + if (timer != null) { + timer.cancel(); + timer = null; + paused = false; + updateTimeLabel(0L); + } + } + + @Override + public void run() { + if (!paused && timer != null && player != null && player.getDataLine() != null) { + updateTimeLabel(player.getDataLine().getMicrosecondPosition() / 1000L); + } + } + + } } diff --git a/src/org/infinity/resource/to/TotResource.java b/src/org/infinity/resource/to/TotResource.java index fec6090f7..5aec391c9 100644 --- a/src/org/infinity/resource/to/TotResource.java +++ b/src/org/infinity/resource/to/TotResource.java @@ -5,14 +5,20 @@ package org.infinity.resource.to; import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.infinity.datatype.IsNumeric; import org.infinity.datatype.Unknown; import org.infinity.resource.AbstractStruct; import org.infinity.resource.Resource; import org.infinity.resource.StructEntry; +import org.infinity.resource.key.FileResourceEntry; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.sav.SavResource; import org.infinity.util.StringTable; +import org.infinity.util.io.FileManager; /** * This resource serves a similar purpose (and has a similar structure to) {@link StringTable TLK} files. The files can @@ -39,11 +45,52 @@ public TotResource(ResourceEntry entry) throws Exception { @Override public int read(ByteBuffer buffer, int offset) throws Exception { if (buffer != null && buffer.limit() > 0) { - // TODO: fetch number of valid string entries from associated TOH resource - for (int i = 0; offset + 524 <= buffer.limit(); i++) { - StringEntry entry = new StringEntry(this, buffer, offset, i); - offset = entry.getEndOffset(); - addField(entry); + final TohResource toh = loadAssociatedToh(getResourceEntry()); + if (toh != null) { + // fetching valid string entries from associated TOH resource + final List tohEntries = new ArrayList<>(toh.getFields(StrRefEntry.class)); + tohEntries.sort((a, b) -> { + StructEntry e1 = ((StrRefEntry) a).getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING); + StructEntry e2 = ((StrRefEntry) b).getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING); + return ((IsNumeric) e1).getValue() - ((IsNumeric) e2).getValue(); + }); + + // handling unmapped region of data + if (tohEntries.size() > 0 && tohEntries.get(0).getOffset() > 0) { + final StrRefEntry entry = (StrRefEntry) tohEntries.get(0); + final int size = ((IsNumeric) entry.getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING)).getValue(); + if (size > 0) { + addField(new Unknown(buffer, offset, size)); + } + } + + // handling string entries defined in TOH resource + int idx = 0; + for (final StructEntry se : tohEntries) { + final StrRefEntry tohEntry = (StrRefEntry) se; + offset = ((IsNumeric) tohEntry.getAttribute(StrRefEntry.TOH_STRREF_OFFSET_TOT_STRING)).getValue(); + // looping through entries to consider split text segments + StringEntry stringEntry = new StringEntry(this, buffer, offset, idx); + while (stringEntry != null) { + offset = stringEntry.getEndOffset(); + addField(stringEntry); + idx++; + int ofsNextEntry = ((IsNumeric) stringEntry.getAttribute(StringEntry.TOT_STRING_OFFSET_NEXT_ENTRY)) + .getValue(); + if (ofsNextEntry >= 0) { + stringEntry = new StringEntry(this, buffer, ofsNextEntry, idx); + } else { + stringEntry = null; + } + } + } + } else { + // guessing string entries (most likely using incorrect offsets) + for (int i = 0; offset + 524 <= buffer.limit(); i++) { + final StringEntry entry = new StringEntry(this, buffer, offset, i); + offset = entry.getEndOffset(); + addField(entry); + } } } else { addField(new Unknown(buffer, offset, 0, TOT_EMPTY)); // Placeholder for empty structure @@ -58,4 +105,33 @@ public int read(ByteBuffer buffer, int offset) throws Exception { return endoffset; } + + /** + * Attempts to find the associated TOH resource in the same directory as the current TOT resource and loads it if + * available. + * + * @param totResource The current TOT resource. + * @return {@code TohResource} instance if loaded successfully, {@code null} otherwise. + */ + private TohResource loadAssociatedToh(ResourceEntry totResource) { + TohResource toh = null; + + if (totResource != null) { + final Path totPath = totResource.getActualPath(); + if (totPath != null) { + String fileName = totPath.getName(totPath.getNameCount() - 1).toString(); + char ch = fileName.charAt(fileName.length() - 1); // last character of file extension (TOT) + ch -= 12; // TOT -> TOH (considers case) + fileName = fileName.substring(0, fileName.length() - 1) + String.valueOf(ch); + final Path tohPath = FileManager.queryExisting(totPath.getParent(), fileName); + try { + toh = new TohResource(new FileResourceEntry(tohPath)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + return toh; + } }