// 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.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.InputStream; import java.nio.ByteBuffer; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.io.StreamUtils; /** * Common base class for handling BAM resources. */ public abstract class BamDecoder { /** Recognized BAM resource types */ public enum Type { INVALID, BAMC, BAMV1, BAMV2, CUSTOM } private final ResourceEntry bamEntry; private Type type; /** * Returns whether the specified resource entry points to a valid BAM resource. */ public static boolean isValid(ResourceEntry bamEntry) { return getType(bamEntry) != Type.INVALID; } /** * Returns the type of the specified resource entry. * @return One of the BAM {@code Type}s. */ public static Type getType(ResourceEntry bamEntry) { Type retVal = Type.INVALID; if (bamEntry != null) { try { InputStream is = bamEntry.getResourceDataAsStream(); if (is != null) { String signature = StreamUtils.readString(is, 4); String version = StreamUtils.readString(is, 4); is.close(); if ("BAMC".equals(signature)) { retVal = Type.BAMC; } else if ("BAM ".equals(signature)) { if ("V1 ".equals(version)) { retVal = Type.BAMV1; } else if ("V2 ".equals(version)) { retVal = Type.BAMV2; } } } } catch (Exception e) { e.printStackTrace(); } } return retVal; } /** * Returns a new BamDecoder object based on the specified Bam resource entry. * @param bamEntry The BAM resource entry. * @return Either {@code BamV1Decoder} or {@code BamV2Decoder}, depending on the * BAM resource type. Returns {@code null} if the resource doesn't contain valid * BAM data. */ public static BamDecoder loadBam(ResourceEntry bamEntry) { Type type = getType(bamEntry); switch (type) { case BAMC: case BAMV1: return new BamV1Decoder(bamEntry); case BAMV2: return new BamV2Decoder(bamEntry); default: return null; } } /** * Returns a new BamDecoder object of type PseudoBamDecoder. * @param image The input image to create a pseudo BAM of one frame. * @return A PseudoBamDecoder object. */ public static BamDecoder loadBam(BufferedImage image) { return loadBam(new BufferedImage[]{image}); } /** * Returns a new BamDecoder object of type PseudoBamDecoder. * @param images An array of input images to create the pseudo BAM structure. * @return A PseudoBamDecoder object. */ public static BamDecoder loadBam(BufferedImage[] images) { return new PseudoBamDecoder(images); } /** * Returns the ResourceEntry object of the BAM resource. */ public ResourceEntry getResourceEntry() { return bamEntry; } /** * Returns {@code true} if the BAM has been closed, is invalid or does not contain any frames. */ public boolean isEmpty() { return frameCount() == 0; } /** Returns the type of the BAM resource. */ public Type getType() { return type; } /** Returns an interface that provides access to cycle-specific functionality. */ public abstract BamControl createControl(); /** Returns a frame info object containing basic properties of the specified frame. */ public abstract FrameEntry getFrameInfo(int frameIdx); /** Removes all data from the decoder. Use this to free up memory. */ public abstract void close(); /** Returns whether the BamDecoder instance has been initialized correctly. */ public abstract boolean isOpen(); /** Clears existing data and reloads the current BAM resource entry. */ public abstract void reload(); /** Returns the raw data of the BAM resource. */ public abstract ByteBuffer getResourceBuffer(); /** Returns the total number of available frames. */ public abstract int frameCount(); /** Returns the specified frame as Image. */ public abstract Image frameGet(BamControl control, int frameIdx); /** Draws the specified frame onto the canvas. */ public abstract void frameGet(BamControl control, int frameIdx, Image canvas); protected BamDecoder(ResourceEntry bamEntry) { this.bamEntry = bamEntry; this.type = Type.INVALID; } // Sets the current BAM type protected void setType(Type type) { this.type = type; } //-------------------------- INNER CLASSES -------------------------- /** * Interface for basic BAM frame properties */ public interface FrameEntry { public int getWidth(); public int getHeight(); public int getCenterX(); public int getCenterY(); } /** * Provides access to cycle-specific functionality. */ public static abstract class BamControl { /** * Definitions of how to render BAM frames.<br> * <b>Individual:</b> Each frame is drawn individually. The resulting image dimension is defined * by the drawn frame. Does not take frame centers into account.<br> * <b>Shared:</b> Each frame is drawn onto a canvas of fixed dimension that is big enough to hold * every single frame without cropping or resizing. Takes frame centers into account. */ public enum Mode { INDIVIDUAL, SHARED } private final BamDecoder parent; private Mode mode; // Contains the image dimension of all BAMs used in Shared mode. // Dimension(width, height) defines the total image dimension. // Point(x, y) defines the position for center (0, 0). private Rectangle sharedBamSize; private boolean sharedPerCycle; protected BamControl(BamDecoder parent) { this.parent = parent; this.mode = Mode.INDIVIDUAL; this.sharedBamSize = new Rectangle(); this.sharedPerCycle = true; } /** * Returns the currently selected drawing mode. */ public Mode getMode() { return mode; } /** * Specify how to draw graphical frame data. It affects all methods that draw a frame into an * Image object or an int array. * @param mode The drawing mode. */ public void setMode(Mode mode) { if (mode != null && mode != this.mode) { this.mode = mode; updateSharedBamSize(); } } /** * Returns whether the calculated image dimension in shared mode is based on the current cycle. */ public boolean isSharedPerCycle() { return sharedPerCycle; } /** * Sets whether the calculated image dimension in shared mode is based on the current cycle. */ public void setSharedPerCycle(boolean set) { if (set != sharedPerCycle) { sharedPerCycle = set; updateSharedBamSize(); } } /** * Calculates the rectangle of the current BAM animation. Takes {@link #isSharedPerCycle()} into account. * @param isMirrored If {@code true}, returns a rectangle that is based on the * animation mirrored along the x axis. * @return A rectangle containing information about the base offset (x, y) and * overall dimension (width, height). */ public Rectangle calculateSharedCanvas(boolean isMirrored) { return calculateSharedBamSize(null, isSharedPerCycle(), isMirrored); } /** * Returns the dimension of the image for frames to be drawn in shared mode. */ public Dimension getSharedDimension() { if (sharedBamSize != null) { return new Dimension(sharedBamSize.getSize()); } else { return new Dimension(1, 1); } } /** * Returns the point of origin for frames that are drawn in shared mode.<br> * The top-left corner of the selected frame is calculated as:<br> * {@code topleft = getSharedOrigin() - frameCenter()} */ public Point getSharedOrigin() { if (sharedBamSize != null) { return new Point(sharedBamSize.getLocation()); } else { return new Point(); } } /** Returns the BamDecoder instance this control is associated with. */ public BamDecoder getDecoder() { return parent; } /** Returns {@code true} if, and only if {@link #cycleCount()} is 0. */ public boolean isEmpty() { return (cycleCount() == 0); } /** Returns the total number of available cycles. */ public abstract int cycleCount(); /** Returns the number of frames in the currently selected cycle. */ public abstract int cycleFrameCount(); /** Returns the number of frames in the specified cycle. */ public abstract int cycleFrameCount(int cycleIdx); /** Returns the index of the active cycle. */ public abstract int cycleGet(); /** Sets the active cycle. (Default: first available cycle) */ public abstract boolean cycleSet(int cycleIdx); /** Returns whether the active cycle can be advanced by at least one more frame. */ public abstract boolean cycleHasNextFrame(); /** Selects the next available frame in the active cycle if available. Returns whether a next frame has been selected. */ public abstract boolean cycleNextFrame(); /** Selects the first frame in the active cycle. */ public abstract void cycleReset(); /** Returns the currently selected frame of the active cycle as Image. (Takes current mode into account.) */ public abstract Image cycleGetFrame(); /** Draws the currently selected frame of the active cycle onto the specified canvas. (Takes current mode into account.) */ public abstract void cycleGetFrame(Image canvas); /** Returns the specified frame of the active cycle as Image. (Takes current mode into account.) */ public abstract Image cycleGetFrame(int frameIdx); /** Draws the specified frame of the active cycle onto the specified canvas. (Takes current mode into account.) */ public abstract void cycleGetFrame(int frameIdx, Image canvas); /** Returns the index of the currently selected frame in the active cycle. */ public abstract int cycleGetFrameIndex(); /** Selects the specified frame in the active cycle. */ public abstract boolean cycleSetFrameIndex(int frameIdx); /** Translates the active cycle's frame index into an absolute frame index. Returns -1 if cycle doesn't contain frames. */ public abstract int cycleGetFrameIndexAbsolute(); /** Translates the specified active cycle's frame index into an absolute frame index. Returns -1 if cycle doesn't contain frames. */ public abstract int cycleGetFrameIndexAbsolute(int frameIdx); /** Translates the cycle's frame index into an absolute frame index. Returns -1 if cycle doesn't contain frames. */ public abstract int cycleGetFrameIndexAbsolute(int cycleIdx, int frameIdx); // Updates the shared canvas size for the current BAM protected void updateSharedBamSize() { sharedBamSize = calculateSharedBamSize(sharedBamSize, isSharedPerCycle(), false); } // Calculates a shared canvas size for the current BAM. // cycleBased: true=for current cycle only, false=for all available frames // isMirrored: true=mirror along the x axis, false=no mirroring // To get the top-left corner of the selected frame: // For unmirrored frames: // left = -sharedBamSize.x - frameCenterX() // top = -sharedBamSize.y - frameCenterY() // For mirrored frames: // left = -sharedBamSize.x - (frameWidth() - frameCenterX() - 1) // top = -sharedBamSize.y - frameCenterY() protected Rectangle calculateSharedBamSize(Rectangle rect, boolean cycleBased, boolean isMirrored) { if (rect == null) { rect = new Rectangle(); } int x1 = Integer.MAX_VALUE, x2 = Integer.MIN_VALUE; int y1 = Integer.MAX_VALUE, y2 = Integer.MIN_VALUE; if (cycleBased) { for (int i = 0; i < cycleFrameCount(); i++) { int frame = cycleGetFrameIndexAbsolute(i); int cx = isMirrored ? (parent.getFrameInfo(frame).getWidth() - parent.getFrameInfo(frame).getCenterX() - 1) : parent.getFrameInfo(frame).getCenterX(); x1 = Math.min(x1, -cx); y1 = Math.min(y1, -parent.getFrameInfo(frame).getCenterY()); x2 = Math.max(x2, parent.getFrameInfo(frame).getWidth() - cx); y2 = Math.max(y2, parent.getFrameInfo(frame).getHeight() - parent.getFrameInfo(frame).getCenterY()); } } else { for (int i = 0; i < parent.frameCount(); i++) { int cx = isMirrored ? (parent.getFrameInfo(i).getWidth() - parent.getFrameInfo(i).getCenterX() - 1) : parent.getFrameInfo(i).getCenterX(); x1 = Math.min(x1, -cx); y1 = Math.min(y1, -parent.getFrameInfo(i).getCenterY()); x2 = Math.max(x2, parent.getFrameInfo(i).getWidth() - cx); y2 = Math.max(y2, parent.getFrameInfo(i).getHeight() - parent.getFrameInfo(i).getCenterY()); } } if (x1 == Integer.MAX_VALUE) x1 = 0; if (y1 == Integer.MAX_VALUE) y1 = 0; if (x2 == Integer.MIN_VALUE) x2 = 0; if (y2 == Integer.MIN_VALUE) y2 = 0; rect.x = x1; rect.y = y1; rect.width = x2 - x1 + 1; rect.height = y2 - y1 + 1; return rect; } // Returns the shared rectangle object protected Rectangle getSharedRectangle() { return sharedBamSize; } } }