// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.graphics; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.nio.ByteBuffer; import java.util.Arrays; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.io.StreamUtils; public class MosV1Decoder extends MosDecoder { private static final int BlockDimension = 64; // default block dimension private static final int HeaderSize = 24; private ByteBuffer mosBuffer; private int width, height, cols, rows, ofsPalette, ofsLookup, ofsData; private boolean transparencyEnabled; private int[] workingPalette; // storage space for palettes private BufferedImage workingCanvas; // storage space big enough for a single block private int lastBlockIndex; // hold the index of the last data block drawn onto workingCanvas public MosV1Decoder(ResourceEntry mosEntry) { super(mosEntry); init(); transparencyEnabled = false; } /** * Returns whether the transparent palette entry is drawn or not. */ public boolean isTransparencyEnabled() { return transparencyEnabled; } /** * Specify whether to draw the transparent palette entry. */ public void setTransparencyEnabled(boolean set) { transparencyEnabled = set; } /** * Returns the number of data blocks per row. */ public int getColumnCount() { return cols; } /** * Returns the number of data block rows. */ public int getRowCount() { return rows; } /** * Returns the palette of the specified data block as an int array of 256 entries. (Format: ARGB) * @param blockIdx The block index. * @return Palette as int array of 256 entries. Returns {@code null} on error. */ public int[] getPalette(int blockIdx) { if (isValidBlock(blockIdx)) { int[] palette = new int[256]; if (getPalette(blockIdx, palette)) { return palette; } else { palette = null; } } return null; } /** * Writes the palette of the specified MOS block into the buffer. * @param blockIdx The block index. * @param buffer The buffer to write the palette entries into. * @return {@code true} if palette data has been written, {@code false} otherwise. */ public boolean getPalette(int blockIdx, int[] buffer) { if (isValidBlock(blockIdx) && buffer != null) { int ofs = getPaletteOffset(blockIdx); if (ofs > 0) { int maxSize = (buffer.length < 256) ? buffer.length : 256; boolean transSet = false; for (int i = 0; i < maxSize; i++, ofs += 4) { int color = 0xff000000 | mosBuffer.getInt(ofs); if (transparencyEnabled && !transSet && (color & 0x00ffffff) == 0x0000ff00) { color &= 0x00ffffff; } buffer[i] = color; } return true; } } return false; } /** * Returns unprocessed MOS block data. * @param blockIdx The block index. * @return The buffer containing raw block data. */ public byte[] getRawBlockData(int blockIdx) { if (isValidBlock(blockIdx)) { byte[] buffer = new byte[getBlockWidth(blockIdx)*getBlockHeight(blockIdx)]; if (getRawBlockData(blockIdx, buffer)) { return buffer; } buffer = null; } return null; } /** * Writes unprocessed MOS block data into the buffer. The buffer size can be calculated: * <pre>size = {@link #getBlockWidth(int)}*{@link #getBlockHeight(int)}.</pre> * @param blockIdx The block index. * @param buffer The buffer to write the raw block data into. * @return {@code true} if block data has been written, {@code false} otherwise. */ public boolean getRawBlockData(int blockIdx, byte[] buffer) { if (isValidBlock(blockIdx) && buffer != null) { int ofs = getBlockOffset(blockIdx); if (ofs > 0) { int size = getBlockWidth(blockIdx)*getBlockHeight(blockIdx); int maxSize = (buffer.length < size) ? buffer.length : size; mosBuffer.position(ofs); mosBuffer.get(buffer, 0, maxSize); return true; } } return false; } @Override public void close() { mosBuffer = null; width = height = cols = rows = 0; ofsPalette = ofsLookup = ofsData = 0; lastBlockIndex = -1; workingPalette = null; if (workingCanvas != null) { workingCanvas.flush(); workingCanvas = null; } } @Override public void reload() { init(); } @Override public ByteBuffer getResourceBuffer() { return mosBuffer; } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public Image getImage() { if (isInitialized()) { BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); if (renderMos(image)) { return image; } else { image = null; } } return null; } @Override public boolean getImage(Image canvas) { if (canvas != null) { return renderMos(canvas); } return false; } @Override public int[] getImageData() { if (isInitialized()) { int[] buffer = new int[getWidth()*getHeight()]; if (renderMos(buffer, getWidth(), getHeight())) { return buffer; } else { buffer = null; } } return null; } @Override public boolean getImageData(int[] buffer) { if (isInitialized() && buffer != null) { return renderMos(buffer, getWidth(), getHeight()); } return false; } @Override public int getBlockCount() { return cols*rows; } @Override public int getBlockWidth(int blockIdx) { int ofs = getBlockOffset(blockIdx); if (ofs > 0) { int col = blockIdx % cols; if (col < cols - 1) { return BlockDimension; } else { return getWidth() - col*BlockDimension; } } return 0; } @Override public int getBlockHeight(int blockIdx) { int ofs = getBlockOffset(blockIdx); if (ofs > 0) { int row = blockIdx / cols; if (row < rows - 1) { return BlockDimension; } else { return getHeight() - row*BlockDimension; } } return 0; } @Override public Image getBlock(int blockIdx) { if (isValidBlock(blockIdx)) { BufferedImage image = ColorConvert.createCompatibleImage(getBlockWidth(blockIdx), getBlockHeight(blockIdx), true); if (getBlock(blockIdx, image)) { return image; } else { image = null; } } return null; } @Override public boolean getBlock(int blockIdx, Image canvas) { if (canvas != null && updateWorkingCanvas(blockIdx)) { int w = getBlockWidth(blockIdx); int h = getBlockHeight(blockIdx); if (canvas.getWidth(null) < w) w = canvas.getWidth(null); if (canvas.getHeight(null) < h) h = canvas.getHeight(null); Graphics2D g = (Graphics2D)canvas.getGraphics(); try { g.drawImage(workingCanvas, 0, 0, w, h, 0, 0, w, h, null); } finally { g.dispose(); g = null; } return true; } return false; } @Override public int[] getBlockData(int blockIdx) { if (isValidBlock(blockIdx)) { int[] buffer = new int[getBlockWidth(blockIdx)*getBlockHeight(blockIdx)]; if (getBlockData(blockIdx, buffer)) { return buffer; } else { buffer = null; } } return null; } @Override public boolean getBlockData(int blockIdx, int[] buffer) { if (isValidBlock(blockIdx) && buffer != null) { BufferedImage image = ColorConvert.toBufferedImage(getBlock(blockIdx), true); if (image != null) { int[] src = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); int maxSize = (buffer.length < src.length) ? buffer.length : src.length; System.arraycopy(src, 0, buffer, 0, maxSize); image.flush(); image = null; return true; } } return false; } private void init() { close(); if (getResourceEntry() != null) { try { mosBuffer = getResourceEntry().getResourceBuffer(); String signature = StreamUtils.readString(mosBuffer, 0, 4); String version = StreamUtils.readString(mosBuffer, 4, 4); if ("MOSC".equals(signature)) { setType(Type.MOSC); mosBuffer = Compressor.decompress(mosBuffer); signature = StreamUtils.readString(mosBuffer, 0, 4); version = StreamUtils.readString(mosBuffer, 4, 4); } else if ("MOS ".equals(signature) && "V1 ".equals(version)) { setType(Type.MOSV1); } else { throw new Exception("Invalid MOS type"); } // Data should now be in MOS v1 format if (!"MOS ".equals(signature) || !"V1 ".equals(version)) { throw new Exception("Invalid MOS type"); } // evaluating header data width = mosBuffer.getShort(8) & 0xffff; if (width <= 0) { throw new Exception("Invalid MOS width: " + width); } height = mosBuffer.getShort(0x0a) & 0xffff; if (height <= 0) { throw new Exception("Invalid MOS height: " + height); } cols = mosBuffer.getShort(0x0c); rows = mosBuffer.getShort(0x0e); if (cols <= 0 || rows <= 0) { throw new Exception("Invalid number of data blocks: " + (cols*rows)); } int blockSize = mosBuffer.getInt(0x10); if (blockSize != BlockDimension) { throw new Exception("Invalid block size: " + blockSize); } ofsPalette = mosBuffer.getInt(0x14); if (ofsPalette < HeaderSize) { throw new Exception("Invalid palette offset: " + ofsPalette); } ofsLookup = ofsPalette + (getBlockCount() << 10); ofsData = ofsLookup + (getBlockCount() << 2); workingPalette = new int[256]; workingCanvas = new BufferedImage(BlockDimension, BlockDimension, BufferedImage.TYPE_INT_ARGB); } catch (Exception e) { e.printStackTrace(); close(); } } } // Returns if a valid MOS has been initialized private boolean isInitialized() { return (mosBuffer != null && cols > 0 && rows > 0 && width > 0 && height >0); } // Returns whether the specified block index is valid private boolean isValidBlock(int blockIdx) { return (blockIdx >= 0 && blockIdx < cols*rows); } // Returns the starting offset of the specified data block private int getBlockOffset(int blockIdx) { if (isValidBlock(blockIdx)) { return ofsData + mosBuffer.getInt(ofsLookup + (blockIdx << 2)); } return -1; } // Returns the starting offset of the specified palette private int getPaletteOffset(int blockIdx) { if (isValidBlock(blockIdx)) { return ofsPalette + (blockIdx << 10); } return -1; } private boolean renderMos(Image image) { int blockCount = getBlockCount(); if (image != null && blockCount > 0) { Graphics2D g = (Graphics2D)image.getGraphics(); try { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); // overwriting target regardless of transparency int imgWidth = image.getWidth(null); int imgHeight = image.getHeight(null); for (int i = 0; i < blockCount; i++) { int x = (i % cols) * BlockDimension; int y = (i / cols) * BlockDimension; int w = getBlockWidth(i); int h = getBlockHeight(i); if (x + w > imgWidth) w = imgWidth - x; if (y + h > imgHeight) h = imgHeight - y; if (w > 0 && h > 0 && updateWorkingCanvas(i)) { g.drawImage(workingCanvas, x, y, x + w, y + h, 0, 0, w, h, null); } } } finally { g.dispose(); g = null; } return true; } return false; } private boolean renderMos(int[] buffer, int width, int height) { int blockCount = getBlockCount(); if (buffer != null && width > 0 && height > 0 && blockCount > 0) { for (int i = 0; i < blockCount; i++) { int left = (i % cols) * BlockDimension; int top = (i / cols) * BlockDimension; int w = getBlockWidth(i); int h = getBlockHeight(i); if (left + w > width) w = width - left; if (top + h > height) h = height - top; if (w > 0 && h > 0 && updateWorkingCanvas(i)) { int[] src = ((DataBufferInt)workingCanvas.getRaster().getDataBuffer()).getData(); int srcOfs = 0; int dstOfs = top*width + left; for (int y = 0; y < h; y++) { System.arraycopy(src, srcOfs, buffer, dstOfs, w); srcOfs += BlockDimension; dstOfs += width; } } } return true; } return false; } // Draws the specified block onto the working canvas. Returns success state. private boolean updateWorkingCanvas(int blockIdx) { if (blockIdx != lastBlockIndex) { int srcOfs = getBlockOffset(blockIdx); if (srcOfs > 0) { // initializations getPalette(blockIdx, workingPalette); int w = getBlockWidth(blockIdx); int h = getBlockHeight(blockIdx); int maxSrcOfs = srcOfs + h*w; int dstOfs = 0; int maxDstOfs = h*BlockDimension; // removing old content int[] buffer = ((DataBufferInt)workingCanvas.getRaster().getDataBuffer()).getData(); Arrays.fill(buffer, 0); // drawing new content while (srcOfs < maxSrcOfs && dstOfs < maxDstOfs) { for (int x = 0; x < w; x++, srcOfs++, dstOfs++) { buffer[dstOfs] = workingPalette[mosBuffer.get(srcOfs) & 0xff]; } dstOfs += BlockDimension - w; } lastBlockIndex = blockIdx; return true; } return false; } else { return blockIdx >= 0; } } }