// 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.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.infinity.resource.Profile;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.util.io.StreamUtils;
/**
* Handles BAM v1 resources (both BAMC and uncompressed BAM V1).
*/
public class BamV1Decoder extends BamDecoder
{
/**
* Definitions on how to handle palette transparency:<br>
* {@code Normal} looks for the first entry containing RGB(0, 255, 0). It falls back to palette
* index 0 if no entry has been found.<br>
* {@code FirstIndexOnly} automatically uses palette index 0 without looking for entries
* containing RGB(0, 255, 0).
*/
public enum TransparencyMode { NORMAL, FIRST_INDEX_ONLY }
private final List<BamV1FrameEntry> listFrames = new ArrayList<BamV1FrameEntry>();
private final List<CycleEntry> listCycles = new ArrayList<CycleEntry>();
private final BamV1FrameEntry defaultFrameInfo = new BamV1FrameEntry(null, 0);
private BamV1Control defaultControl;
private ByteBuffer bamBuffer; // contains the raw (uncompressed) data of the BAM resource
private int[] bamPalette; // BAM palette
private int rleIndex; // color index for RLE compressed pixels
/**
* Loads and decodes a BAM v1 resource. This includes both compressed (BAMC) and uncompressed BAM
* resource.
* @param bamEntry The BAM resource entry.
*/
public BamV1Decoder(ResourceEntry bamEntry)
{
super(bamEntry);
init();
}
@Override
public BamV1Control createControl()
{
return new BamV1Control(this);
}
@Override
public BamV1FrameEntry getFrameInfo(int frameIdx)
{
if (frameIdx >= 0 && frameIdx < listFrames.size()) {
return listFrames.get(frameIdx);
} else {
return defaultFrameInfo;
}
}
@Override
public void close()
{
bamBuffer = null;
bamPalette = null;
listFrames.clear();
listCycles.clear();
rleIndex = 0;
}
@Override
public boolean isOpen()
{
return (bamBuffer != null);
}
@Override
public void reload()
{
init();
}
@Override
public ByteBuffer getResourceBuffer()
{
return bamBuffer;
}
@Override
public int frameCount()
{
return listFrames.size();
}
@Override
public Image frameGet(BamControl control, int frameIdx)
{
if (frameIdx >= 0 && frameIdx < listFrames.size()) {
if (control == null) {
control = defaultControl;
}
int w, h;
if (control.getMode() == BamDecoder.BamControl.Mode.SHARED) {
Dimension d = control.getSharedDimension();
w = d.width;
h = d.height;
} else {
w = getFrameInfo(frameIdx).getWidth();
h = getFrameInfo(frameIdx).getHeight();
}
if (w > 0 && h > 0) {
BufferedImage image = ColorConvert.createCompatibleImage(w, h, true);
frameGet(control, frameIdx, image);
return image;
}
}
return ColorConvert.createCompatibleImage(1, 1, true);
}
@Override
public void frameGet(BamControl control, int frameIdx, Image canvas)
{
if (canvas != null && frameIdx >= 0 && frameIdx < listFrames.size()) {
if(control == null) {
control = defaultControl;
}
int w, h;
if (control.getMode() == BamDecoder.BamControl.Mode.SHARED) {
Dimension d = control.getSharedDimension();
w = d.width;
h = d.height;
} else {
w = getFrameInfo(frameIdx).getWidth();
h = getFrameInfo(frameIdx).getHeight();
}
if (w > 0 && h > 0 && canvas.getWidth(null) >= w && canvas.getHeight(null) >= h) {
decodeFrame(control, frameIdx, canvas);
}
}
}
/** Returns the compressed color index for compressed BAM v1 resources. */
public int getRleIndex()
{
return rleIndex;
}
// Initializes the current BAM
private void init()
{
// resetting data
close();
if (getResourceEntry() != null) {
try {
bamBuffer = getResourceEntry().getResourceBuffer();
String signature = StreamUtils.readString(bamBuffer, 0, 4);
String version = StreamUtils.readString(bamBuffer, 4, 4);
if ("BAMC".equals(signature)) {
setType(Type.BAMC);
bamBuffer = Compressor.decompress(bamBuffer);
signature = StreamUtils.readString(bamBuffer, 00, 4);
version = StreamUtils.readString(bamBuffer, 4, 4);
} else if ("BAM ".equals(signature) && "V1 ".equals(version)) {
setType(Type.BAMV1);
} else {
throw new Exception("Invalid BAM type");
}
// Data should now be in BAM v1 format
if (!"BAM ".equals(signature) || !"V1 ".equals(version)) {
throw new Exception("Invalid BAM type");
}
// evaluating header data
int framesCount = bamBuffer.getShort(8) & 0xffff;
if (framesCount <= 0) {
throw new Exception("Invalid number of frames");
}
int cyclesCount = bamBuffer.get(0x0a) & 0xff;
if (cyclesCount <= 0) {
throw new Exception("Invalid number of cycles");
}
rleIndex = bamBuffer.get(0x0b) & 0xff;
int ofsFrames = bamBuffer.getInt(0x0c);
if (ofsFrames < 0x18) {
throw new Exception("Invalid frames offset");
}
int ofsPalette = bamBuffer.getInt(0x10);
if (ofsPalette < 0x18) {
throw new Exception("Invalid palette offset");
}
int ofsLookup = bamBuffer.getInt(0x14);
if (ofsLookup < 0x18) {
throw new Exception("Invalid frame lookup table offset");
}
int ofs = ofsFrames;
// initializing frames
for (int i = 0; i < framesCount; i++) {
listFrames.add(new BamV1FrameEntry(bamBuffer, ofs));
ofs += 0x0c;
}
// initializing cycles
for (int i = 0; i < cyclesCount; i++) {
int cnt = bamBuffer.getShort(ofs) & 0xffff;
int idx = bamBuffer.getShort(ofs+2) & 0xffff;
listCycles.add(new CycleEntry(bamBuffer, ofsLookup, cnt, idx));
ofs += 0x04;
}
// initializing palette
bamPalette = new int[256];
int alphaMask = Profile.isEnhancedEdition() ? 0 : 0xff000000;
boolean alphaUsed = false; // determines whether alpha is actually used
for (int i = 0; i < 256; i++) {
bamPalette[i] = alphaMask | bamBuffer.getInt(ofsPalette + 4*i);
alphaUsed |= (bamPalette[i] & 0xff000000) != 0;
}
if (!alphaUsed) {
// fix palette if needed
for (int i = 0; i < bamPalette.length; i++) {
bamPalette[i] |= 0xff000000;
}
}
// creating default bam control instance as a fallback option
defaultControl = new BamV1Control(this);
defaultControl.setMode(BamControl.Mode.SHARED);
defaultControl.setSharedPerCycle(false);
} catch (Exception e) {
e.printStackTrace();
close();
}
}
}
// Draws the absolute frame onto the canvas.
private void decodeFrame(BamControl control, int frameIdx, Image canvas)
{
if (canvas != null && frameIdx >= 0 && frameIdx < listFrames.size()) {
if (control == null) {
control = defaultControl;
}
int[] palette;
if (control instanceof BamV1Control) {
palette = ((BamV1Control)control).getCurrentPalette();
} else {
palette = bamPalette;
}
// decoding frame data
BufferedImage image = ColorConvert.toBufferedImage(canvas, true, false);
byte[] bufferB = null;
int[] bufferI = null;
if (image.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
bufferB = ((DataBufferByte)image.getRaster().getDataBuffer()).getData();
} else {
bufferI = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
}
int dstWidth = image.getWidth();
int dstHeight = image.getHeight();
int srcWidth = listFrames.get(frameIdx).width;
int srcHeight = listFrames.get(frameIdx).height;
boolean isCompressed = listFrames.get(frameIdx).compressed;
int ofsData = listFrames.get(frameIdx).ofsData;
int left, top, maxWidth, maxHeight, srcOfs, dstOfs;
int count = 0, color = 0;
byte pixel = 0;
if (control.getMode() == BamControl.Mode.SHARED) {
left = -control.getSharedRectangle().x - listFrames.get(frameIdx).centerX;
top = -control.getSharedRectangle().y - listFrames.get(frameIdx).centerY;
maxWidth = (dstWidth < srcWidth + left) ? dstWidth : srcWidth;
maxHeight = (dstHeight < srcHeight + top) ? dstHeight : srcHeight;
srcOfs = ofsData;
dstOfs = top*dstWidth + left;
} else {
left = top = 0;
maxWidth = (dstWidth < srcWidth) ? dstWidth : srcWidth;
maxHeight = (dstHeight < srcHeight) ? dstHeight : srcHeight;
srcOfs = ofsData;
dstOfs = 0;
}
for (int y = 0; y < maxHeight; y++) {
for (int x = 0; x < srcWidth; x++, dstOfs++) {
if (count > 0) {
// writing remaining RLE compressed pixels
count--;
if (x < maxWidth) {
if (bufferB != null) bufferB[dstOfs] = pixel;
if (bufferI != null) bufferI[dstOfs] = color;
}
} else {
pixel = bamBuffer.get(srcOfs++);
color = palette[pixel & 0xff];
if (isCompressed && (pixel & 0xff) == rleIndex) {
count = bamBuffer.get(srcOfs++) & 0xff;
}
if (x < maxWidth) {
if (bufferB != null) bufferB[dstOfs] = pixel;
if (bufferI != null) bufferI[dstOfs] = color;
}
}
}
dstOfs += dstWidth - srcWidth;
}
bufferB = null;
bufferI = null;
// rendering resulting image onto the canvas if needed
if (image != canvas) {
Graphics g = canvas.getGraphics();
try {
g.drawImage(image, 0, 0, null);
} finally {
g.dispose();
g = null;
}
image.flush();
image = null;
}
}
}
//-------------------------- INNER CLASSES --------------------------
/** Provides information for a single frame entry */
public class BamV1FrameEntry implements BamDecoder.FrameEntry
{
private int width, height, centerX, centerY, ofsData;
private boolean compressed;
private BamV1FrameEntry(ByteBuffer buffer, int ofs)
{
if (buffer != null && ofs + 12 <= buffer.limit()) {
width = buffer.getShort(ofs + 0) & 0xffff;
height = buffer.getShort(ofs + 2) & 0xffff;
centerX = buffer.getShort(ofs + 4);
centerY = buffer.getShort(ofs + 6);
ofsData = buffer.getInt(ofs + 8) & 0x7fffffff;
compressed = (buffer.getInt(ofs + 8) & 0x80000000) == 0;
} else {
width = height = centerX = centerY = ofsData = 0;
compressed = false;
}
}
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return height; }
@Override
public int getCenterX() { return centerX; }
@Override
public int getCenterY() { return centerY; }
public boolean isCompressed() { return compressed; }
}
/** Provides access to cycle-specific functionality. */
public static class BamV1Control extends BamControl
{
private int[] currentPalette, externalPalette;
private boolean transparencyEnabled;
private TransparencyMode transparencyMode;
private int currentCycle, currentFrame;
protected BamV1Control(BamV1Decoder decoder)
{
super(decoder);
init();
}
/**
* 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 enable)
{
if (enable != transparencyEnabled) {
transparencyEnabled = enable;
preparePalette(externalPalette);
}
}
/**
* Returns the currently used transparency mode for palettes.
*/
public TransparencyMode getTransparencyMode()
{
return transparencyMode;
}
/**
* Sets the mode on how to handle transparency in palettes.
* @param transparencyMode The transparency mode to set.
*/
public void setTransparencyMode(TransparencyMode transparencyMode)
{
if (transparencyMode != null) {
if (this.transparencyMode != transparencyMode) {
this.transparencyMode = transparencyMode;
preparePalette(externalPalette);
}
}
}
/** Returns the transparency index of the current palette. */
public int getTransparencyIndex()
{
for (int i = 0; i < currentPalette.length; i++) {
if ((currentPalette[i] & 0xff000000) == 0) {
return i;
}
}
return 0;
}
/** Returns whether the palette makes use of alpha transparency. */
public boolean isAlphaEnabled()
{
if (Profile.isEnhancedEdition()) {
for (int i = 0; i < currentPalette.length; i++) {
int mask = currentPalette[i] & 0xff000000;
if (mask != 0 && mask != 0xff000000) {
return true;
}
}
}
return false;
}
/**
* Returns the currently assigned external palette.
* @return The currently assigned external palette, or {@code null} if not available.
*/
public int[] getExternalPalette()
{
return externalPalette;
}
/**
* Applies the colors of the specified palette to the active BAM palette.
* <b>Note:</b> Must be called whenever any changes to the external palette have been done.
* @param palette An external palette. Specify {@code null} to use the default palette.
*/
public void setExternalPalette(int[] palette)
{
if (palette != null) {
externalPalette = new int[palette.length];
for (int i = 0; i < palette.length; i++) {
externalPalette[i] = 0xff000000 | palette[i];
}
}
preparePalette(externalPalette);
}
/** Returns the original and unmodified palette as defined in the BAM resource. */
public int[] getPalette()
{
return getDecoder().bamPalette;
}
/**
* Returns the currently used palette. This is either an external palette, the default palette,
* or a combination of both.
*/
public int[] getCurrentPalette()
{
return currentPalette;
}
@Override
public BamV1Decoder getDecoder()
{
return (BamV1Decoder)super.getDecoder();
}
@Override
public int cycleCount()
{
return getDecoder().listCycles.size();
}
@Override
public int cycleFrameCount()
{
if (currentCycle < getDecoder().listCycles.size()) {
return getDecoder().listCycles.get(currentCycle).frames.length;
} else {
return 0;
}
}
@Override
public int cycleFrameCount(int cycleIdx)
{
if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) {
return getDecoder().listCycles.get(cycleIdx).frames.length;
} else {
return 0;
}
}
@Override
public int cycleGet()
{
return currentCycle;
}
@Override
public boolean cycleSet(int cycleIdx)
{
if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size() && currentCycle != cycleIdx) {
currentCycle = cycleIdx;
if (isSharedPerCycle()) {
updateSharedBamSize();
}
return true;
} else {
return false;
}
}
@Override
public boolean cycleHasNextFrame()
{
if (currentCycle < getDecoder().listCycles.size()) {
return (currentFrame < getDecoder().listCycles.get(currentCycle).frames.length - 1);
} else {
return false;
}
}
@Override
public boolean cycleNextFrame()
{
if (cycleHasNextFrame()) {
currentFrame++;
return true;
} else {
return false;
}
}
@Override
public void cycleReset()
{
currentFrame = 0;
}
@Override
public Image cycleGetFrame()
{
int frameIdx = cycleGetFrameIndexAbsolute();
return getDecoder().frameGet(this, frameIdx);
}
@Override
public void cycleGetFrame(Image canvas)
{
int frameIdx = cycleGetFrameIndexAbsolute();
getDecoder().frameGet(this, frameIdx, canvas);
}
@Override
public Image cycleGetFrame(int frameIdx)
{
frameIdx = cycleGetFrameIndexAbsolute(frameIdx);
return getDecoder().frameGet(this, frameIdx);
}
@Override
public void cycleGetFrame(int frameIdx, Image canvas)
{
frameIdx = cycleGetFrameIndexAbsolute(frameIdx);
getDecoder().frameGet(this, frameIdx, canvas);
}
@Override
public int cycleGetFrameIndex()
{
return currentFrame;
}
@Override
public boolean cycleSetFrameIndex(int frameIdx)
{
if (currentCycle < getDecoder().listCycles.size() &&
frameIdx >= 0 && frameIdx < getDecoder().listCycles.get(currentCycle).frames.length) {
currentFrame = frameIdx;
return true;
} else {
return false;
}
}
@Override
public int cycleGetFrameIndexAbsolute()
{
return cycleGetFrameIndexAbsolute(currentCycle, currentFrame);
}
@Override
public int cycleGetFrameIndexAbsolute(int frameIdx)
{
return cycleGetFrameIndexAbsolute(currentCycle, frameIdx);
}
@Override
public int cycleGetFrameIndexAbsolute(int cycleIdx, int frameIdx)
{
if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size() &&
frameIdx >= 0 && frameIdx < getDecoder().listCycles.get(cycleIdx).frames.length) {
return getDecoder().listCycles.get(cycleIdx).frames[frameIdx];
} else {
return -1;
}
}
private void init()
{
this.transparencyEnabled = true;
this.transparencyMode = TransparencyMode.NORMAL;
currentPalette = null;
externalPalette = null;
currentCycle = currentFrame = 0;
// preparing the default palette
preparePalette(null);
updateSharedBamSize();
}
// Prepares the palette to be used for decoding BAM frames
private void preparePalette(int[] externalPalette)
{
if (currentPalette == null) {
currentPalette = new int[256];
}
// some optimizations: don't prepare if the palette hasn't change
boolean isNormalMode = (getTransparencyMode() == TransparencyMode.NORMAL);
int idx = 0;
int transIndex = -1;
int alphaMask = Profile.isEnhancedEdition() ? 0 : 0xff000000;
boolean alphaUsed = false; // determines whether alpha is actually used
if (externalPalette != null) {
// filling palette entries from external palette, as much as possible
for (; idx < externalPalette.length && idx < 256; idx++) {
currentPalette[idx] = alphaMask | externalPalette[idx];
alphaUsed |= (currentPalette[idx] & 0xff000000) != 0;
if (isNormalMode && transIndex < 0 && (currentPalette[idx] & 0x00ffffff) == 0x0000ff00) {
transIndex = idx;
}
}
}
// filling remaining entries with BAM palette
if (getDecoder().bamPalette != null) {
for (; idx < getDecoder().bamPalette.length; idx++) {
currentPalette[idx] = alphaMask | getDecoder().bamPalette[idx];
alphaUsed |= (currentPalette[idx] & 0xff000000) != 0;
if (isNormalMode && transIndex < 0 && (currentPalette[idx] & 0x00ffffff) == 0x0000ff00) {
transIndex = idx;
}
}
}
// removing alpha support if needed
if (!alphaUsed) {
for (int i = 0; i < currentPalette.length; i++) {
currentPalette[i] |= 0xff000000;
}
}
// applying transparent index
if (isNormalMode && transIndex >= 0) {
if (transparencyEnabled) {
currentPalette[transIndex] = 0;
} else {
currentPalette[transIndex] |= 0xff000000;
}
}
// falling back to transparency at color index 0
if (transparencyEnabled && transIndex < 0) {
currentPalette[0] = 0;
}
}
}
// Stores information for a single cycle
private class CycleEntry
{
private final int[] frames; // list of frame indices used in this cycle
private int indexCount; // number of frame indices in this cycle
private int lookupIndex; // index into frame lookup table
/**
* @param buffer The BAM data buffer
* @param ofsLookup Offset of frame lookup table
* @param idxCount Number of frame indices in this cycle
* @param idxLookup Index into frame lookup table of first frame in this cycle
*/
private CycleEntry(ByteBuffer buffer, int ofsLookup, int idxCount, int idxLookup)
{
if (buffer != null && idxCount >= 0 && idxLookup >= 0 &&
ofsLookup + 2*(idxLookup+idxCount) <= buffer.limit()) {
indexCount = idxCount;
lookupIndex = idxLookup;
frames = new int[indexCount];
for (int i = 0; i < indexCount; i++) {
frames[i] = buffer.getShort(ofsLookup + 2*(lookupIndex+i));
}
} else {
frames = new int[0];
indexCount = lookupIndex = 0;
}
}
}
}