From b835749bc65955df11704d633386a6c72dc03b69 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Fri, 21 Jul 2023 14:48:12 +0200 Subject: [PATCH] Outsource BMP graphics decoding to dedicated class Fixes original cause of 07e83faf525176dd0182e5a35d8f07203a145d43 --- .../resource/graphics/BmpDecoder.java | 308 ++++++++++++++++++ .../resource/graphics/GraphicsResource.java | 95 +----- src/org/infinity/util/IconCache.java | 6 +- 3 files changed, 318 insertions(+), 91 deletions(-) create mode 100644 src/org/infinity/resource/graphics/BmpDecoder.java diff --git a/src/org/infinity/resource/graphics/BmpDecoder.java b/src/org/infinity/resource/graphics/BmpDecoder.java new file mode 100644 index 000000000..144d3e1b9 --- /dev/null +++ b/src/org/infinity/resource/graphics/BmpDecoder.java @@ -0,0 +1,308 @@ +// Near Infinity - An Infinity Engine Browser and Editor +// Copyright (C) 2001 Jon Olav Hauglid +// See LICENSE.txt for license information + +package org.infinity.resource.graphics; + +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +import javax.imageio.ImageIO; + +import org.infinity.resource.key.ResourceEntry; +import org.infinity.util.io.ByteBufferInputStream; +import org.infinity.util.io.FileManager; +import org.infinity.util.io.StreamUtils; + +/** + * Decodes a BMP file. + */ +public class BmpDecoder { + private BufferedImage image; + private Palette palette; + private Info info; + + /** + * Returns an initialized {@code BmpDecoder} object with the specified {@code ResourceEntry} instance. + * + * @param entry The {@link ResourceEntry} instance of the BMP resource. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(ResourceEntry entry) throws Exception { + return new BmpDecoder(Objects.requireNonNull(entry).getResourceBuffer()); + } + + /** + * Returns an initialized {@code BmpDecoder} object with the specified file. + * + * @param file Filename of the BMP resource as {@code String}. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(String file) throws Exception { + return loadBmp(FileManager.resolve(file)); + } + + /** + * Returns an initialized {@code BmpDecoder} object with the specified {@code Path} instance. + * + * @param file Filename of the BMP resource as {@link Path} instance. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(Path file) throws Exception { + try (InputStream is = StreamUtils.getInputStream(Objects.requireNonNull(file))) { + return loadBmp(is); + } + } + + /** + * Returns an initialized {@code BmpDecoder} object with data from the specified {@code InputStream} instance. + * + * @param input The {@link InputStream} instance containing BMP data. + * @return {@link BmpDecoder} instance with decoded BMP data. + * @throws Exception If the BMP resource could not be loaded. + */ + public static BmpDecoder loadBmp(InputStream input) throws Exception { + Objects.requireNonNull(input); + final int bufSize = 1024; + final ArrayList bufList = new ArrayList<>(); + while (true) { + final byte[] buf = new byte[bufSize]; + int len = input.read(buf); + if (len == bufSize) { + bufList.add(buf); + } else if (len > 0) { + bufList.add(Arrays.copyOf(buf, len)); + } else { + break; + } + } + + int bufferSize = bufList.stream().mapToInt(b -> b.length).sum(); + final ByteBuffer bb = StreamUtils.getByteBuffer(bufferSize); + bufList.forEach(b -> bb.put(b)); + bb.rewind(); + + return new BmpDecoder(bb); + } + + private BmpDecoder(ByteBuffer buffer) throws Exception { + init(buffer); + } + + /** Returns information about the BMP image. */ + public Info getInfo() { + return info; + } + + /** Returns the decoded BMP resource as {@link Image} object. */ + public BufferedImage getImage() { + return image; + } + + /** + * Returns the palette for indexed BMP resources. + * + * @return {@link Palette} object for indexed BMP resources, {@code null} otherwise. + */ + public Palette getPalette() { + return palette; + } + + private void init(ByteBuffer buffer) throws Exception { + Objects.requireNonNull(buffer); + + // Checking signature + boolean isBMP = false; + if ("BM".equals(StreamUtils.readString(buffer, 0, 2))) { + isBMP = true; + } + + image = null; + palette = null; + if (isBMP) { + int rasteroff = buffer.getInt(10); + + int width = buffer.getInt(18); + int height = buffer.getInt(22); + int bitcount = buffer.getShort(28); + int compression = buffer.getInt(30); + if ((compression == 0 || compression == 3) && bitcount <= 32) { + int colsUsed = buffer.getInt(46); // Colorsused + + if (bitcount <= 8) { + if (colsUsed == 0) { + colsUsed = 1 << bitcount; + } + int palSize = 4 * colsUsed; + palette = new Palette(buffer, rasteroff - palSize, palSize); + } + + int bytesprline = bitcount * width / 8; + int padded = 4 - bytesprline % 4; + if (padded == 4) { + padded = 0; + } + + image = ColorConvert.createCompatibleImage(width, height, bitcount >= 32); + int offset = rasteroff; + for (int y = height - 1; y >= 0; y--) { + setPixels(buffer, offset, bitcount, bytesprline, y, palette); + offset += bytesprline + padded; + } + + info = new Info(image, compression, bitcount); + } + } + + if (image == null) { + buffer.rewind(); + try (ByteBufferInputStream bbis = new ByteBufferInputStream(buffer)) { + image = ImageIO.read(bbis); + // extracting palette + if (image.getColorModel() instanceof IndexColorModel) { + final IndexColorModel icm = (IndexColorModel) image.getColorModel(); + int numColors = icm.getMapSize(); + final int[] colors = new int[numColors]; + icm.getRGBs(colors); + final ByteBuffer pb = StreamUtils.getByteBuffer(colors.length * 4); + for (int i = 0; i < colors.length; i++) { + pb.putInt(colors[i]); + } + pb.rewind(); + palette = new Palette(pb, 0, pb.capacity()); + } + info = new Info(image); + } catch (Exception e) { + image = null; + palette = null; + throw new Exception("Unsupported graphics format"); + } + } + } + + private void setPixels(ByteBuffer buffer, int offset, int bitcount, int width, int y, Palette palette) { + if (bitcount == 4) { + int pix = 0; + for (int x = 0; x < width; x++) { + int color = buffer.get(offset + x) & 0xff; + int color1 = (color >> 4) & 0x0f; + image.setRGB(pix++, y, palette.getColor(color1)); + int color2 = color & 0x0f; + image.setRGB(pix++, y, palette.getColor(color2)); + } + } else if (bitcount == 8) { + for (int x = 0; x < width; x++) { + image.setRGB(x, y, palette.getColor(buffer.get(offset + x) & 0xff)); + } + } else if (bitcount == 24) { + for (int x = 0; x < width / 3; x++) { + int rgb = (buffer.get(offset + 3 * x + 2) & 0xff) << 16; + rgb |= (buffer.get(offset + 3 * x + 1) & 0xff) << 8; + rgb |= buffer.get(offset + 3 * x) & 0xff; + image.setRGB(x, y, rgb); + } + } else if (bitcount == 32) { + for (int x = 0; x < width / 4; x++) { + int rgb = buffer.getInt(offset + 4 * x); + image.setRGB(x, y, rgb); + } + } + } + + // -------------------------- INNER CLASSES -------------------------- + + /** + * Provides basic information about the BMP resource. + */ + public static class Info { + /** Available bitmap compression types. */ + public enum Compression { + /** Compression type could not be determined. */ + UNKNOWN(-1, "Unknown"), + /** Uncompressed RGB pixel data. */ + RGB(0, "No compression"), + /** RLE-encoded 8-bit paletted pixel data. */ + RLE8(1, "RLE encoded (8-bit)"), + /** RLE-encoded 4-bit paletted pixel data. */ + RLE4(2, "RLE encoded (4-bit)"), + /** Color components are defined by component masks. */ + BITFIELD(3, "Bitfield encoded"), + ; + + private final int code; + private final String label; + + private Compression(int code, String label) { + this.code = code; + this.label = label; + } + + /** Returns the numeric BMP compression code. Returns -1 for undetermined compression. */ + public int getCode() { + return code; + } + + /** Returns a descriptive label for the compression. */ + public String getLabel() { + return label; + } + + @Override + public String toString() { + return String.format("%s (%d)", getLabel(), getCode()); + } + } + + private Compression compression; + private int width; + private int height; + private int bpp; + + private Info(BufferedImage image) { + this(image, -1, 0); + } + + private Info(BufferedImage image, int compression, int bpp) { + Objects.requireNonNull(image); + + this.compression = Arrays + .stream(Compression.values()) + .filter(c -> c.ordinal() == compression) + .findFirst() + .orElse(Compression.UNKNOWN); + this.width = image.getWidth(); + this.height = image.getHeight(); + this.bpp = (bpp > 0) ? bpp : image.getColorModel().getPixelSize(); + } + + /** Returns the compression type of the BMP resource. */ + public Compression getCompression() { + return compression; + } + + /** Returns the image width, in pixels. */ + public int getWidth() { + return width; + } + + /** Returns the image height, in pixels. */ + public int getHeight() { + return height; + } + + /** Returns the number of bits per pixel. */ + public int getBitsPerPixel() { + return bpp; + } + } +} diff --git a/src/org/infinity/resource/graphics/GraphicsResource.java b/src/org/infinity/resource/graphics/GraphicsResource.java index 98dcd8815..351db1d2d 100644 --- a/src/org/infinity/resource/graphics/GraphicsResource.java +++ b/src/org/infinity/resource/graphics/GraphicsResource.java @@ -9,10 +9,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; -import java.io.InputStream; -import java.nio.ByteBuffer; -import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; @@ -27,19 +24,17 @@ import org.infinity.resource.ViewableContainer; import org.infinity.resource.key.ResourceEntry; import org.infinity.search.ReferenceSearcher; -import org.infinity.util.io.StreamUtils; public final class GraphicsResource implements Resource, Referenceable, ActionListener { private final ResourceEntry entry; + private final BmpDecoder decoder; private final ButtonPanel buttonPanel = new ButtonPanel(); - private BufferedImage image; private JPanel panel; - private Palette palette; public GraphicsResource(ResourceEntry entry) throws Exception { this.entry = entry; - init(); + this.decoder = BmpDecoder.loadBmp(entry); } // --------------------- Begin Interface ActionListener --------------------- @@ -82,7 +77,7 @@ public void searchReferences(Component parent) { @Override public JComponent makeViewer(ViewableContainer container) { - RenderCanvas rcCanvas = new RenderCanvas(image); + RenderCanvas rcCanvas = new RenderCanvas(decoder.getImage()); JScrollPane scroll = new JScrollPane(rcCanvas); scroll.getVerticalScrollBar().setUnitIncrement(16); scroll.getHorizontalScrollBar().setUnitIncrement(16); @@ -101,90 +96,14 @@ public JComponent makeViewer(ViewableContainer container) { // --------------------- End Interface Viewable --------------------- public BufferedImage getImage() { - return image; + return decoder.getImage(); } public Palette getPalette() { - return palette; + return decoder.getPalette(); } - private void init() throws Exception { - ByteBuffer buffer = entry.getResourceBuffer(); - // Checking signature - boolean isBMP = false; - if ("BM".equals(StreamUtils.readString(buffer, 0, 2))) { - isBMP = true; - } - - image = null; - if (isBMP) { - int rasteroff = buffer.getInt(10); - - int width = buffer.getInt(18); - int height = buffer.getInt(22); - int bitcount = buffer.getShort(28); - int compression = buffer.getInt(30); - if ((compression == 0 || compression == 3) && bitcount <= 32) { - int colsUsed = buffer.getInt(46); // Colorsused - - if (bitcount <= 8) { - if (colsUsed == 0) { - colsUsed = 1 << bitcount; - } - int palSize = 4 * colsUsed; - palette = new Palette(buffer, rasteroff - palSize, palSize); - } - - int bytesprline = bitcount * width / 8; - int padded = 4 - bytesprline % 4; - if (padded == 4) { - padded = 0; - } - - image = ColorConvert.createCompatibleImage(width, height, bitcount >= 32); - int offset = rasteroff; - for (int y = height - 1; y >= 0; y--) { - setPixels(buffer, offset, bitcount, bytesprline, y, palette); - offset += bytesprline + padded; - } - } - } - if (image == null) { - try (InputStream is = entry.getResourceDataAsStream()) { - image = ImageIO.read(is); - } catch (Exception e) { - image = null; - throw new Exception("Unsupported graphics format"); - } - } - } - - private void setPixels(ByteBuffer buffer, int offset, int bitcount, int width, int y, Palette palette) { - if (bitcount == 4) { - int pix = 0; - for (int x = 0; x < width; x++) { - int color = buffer.get(offset + x) & 0xff; - int color1 = (color >> 4) & 0x0f; - image.setRGB(pix++, y, palette.getColor(color1)); - int color2 = color & 0x0f; - image.setRGB(pix++, y, palette.getColor(color2)); - } - } else if (bitcount == 8) { - for (int x = 0; x < width; x++) { - image.setRGB(x, y, palette.getColor(buffer.get(offset + x) & 0xff)); - } - } else if (bitcount == 24) { - for (int x = 0; x < width / 3; x++) { - int rgb = (buffer.get(offset + 3 * x + 2) & 0xff) << 16; - rgb |= (buffer.get(offset + 3 * x + 1) & 0xff) << 8; - rgb |= buffer.get(offset + 3 * x) & 0xff; - image.setRGB(x, y, rgb); - } - } else if (bitcount == 32) { - for (int x = 0; x < width / 4; x++) { - int rgb = buffer.getInt(offset + 4 * x); - image.setRGB(x, y, rgb); - } - } + public BmpDecoder.Info getInfo() { + return decoder.getInfo(); } } diff --git a/src/org/infinity/util/IconCache.java b/src/org/infinity/util/IconCache.java index a52f24976..7c5f43302 100644 --- a/src/org/infinity/util/IconCache.java +++ b/src/org/infinity/util/IconCache.java @@ -19,8 +19,8 @@ import org.infinity.resource.ResourceFactory; import org.infinity.resource.graphics.BamDecoder; import org.infinity.resource.graphics.BamDecoder.BamControl; +import org.infinity.resource.graphics.BmpDecoder; import org.infinity.resource.graphics.ColorConvert; -import org.infinity.resource.graphics.GraphicsResource; import org.infinity.resource.key.ResourceEntry; /** @@ -295,8 +295,8 @@ private static synchronized Image getBmpImage(ResourceEntry bmpEntry) { if (bmpEntry != null) { try { - final GraphicsResource res = new GraphicsResource(bmpEntry); - retVal = res.getImage(); + final BmpDecoder decoder = BmpDecoder.loadBmp(bmpEntry); + retVal = decoder.getImage(); } catch (Exception e) { // No log output; catches lots of false positives }