// 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.Color; import java.awt.Dimension; import java.awt.Graphics; 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.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; import java.awt.image.IndexColorModel; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import javax.swing.ProgressMonitor; import org.infinity.gui.converter.ConvertToPvrz; import org.infinity.util.BinPack2D; import org.infinity.util.DynamicArray; import org.infinity.util.Pair; import org.infinity.util.io.FileManager; import org.infinity.util.io.StreamUtils; /** * A decoder that takes individual images as input and simulates a BAM structure. * Furthermore, this class provides methods for manipulating the frame and cycle structure. */ public class PseudoBamDecoder extends BamDecoder { // A list of helpful options that can be applied globally, to frames or to cycles. /** A label of some kind for identification purposes. [String] */ public static final String OPTION_STRING_LABEL = "Label"; /** A flag specifying a compression state. (BAM v1 specific) [Boolean] */ public static final String OPTION_BOOL_COMPRESSED = "Compressed"; /** A value specifying a compressed pixel value. (BAM v1 specific) [Integer] */ public static final String OPTION_INT_RLEINDEX = "RLEIndex"; /** A value specifying the start index of data blocks (BAM v2 specific) [Integer] */ public static final String OPTION_INT_BLOCKINDEX = "BlockIndex"; /** A value specifying the number of data blocks (BAM v2 specific) [Integer] */ public static final String OPTION_INT_BLOCKCOUNT = "BlockCount"; private static final Color TransparentColor = new Color(0, true); private final PseudoBamFrameEntry defaultFrameInfo = new PseudoBamFrameEntry(null, 0, 0); private final HashMap<String, Object> mapOptions = new HashMap<String, Object>(); private List<PseudoBamCycleEntry> listCycles = new ArrayList<PseudoBamCycleEntry>(); private List<PseudoBamFrameEntry> listFrames; private PseudoBamControl defaultControl; public PseudoBamDecoder() { this(null, (BufferedImage[])null, (Point[])null); } public PseudoBamDecoder(List<PseudoBamFrameEntry> framesList) { this(framesList, (BufferedImage[])null, (Point[])null); } public PseudoBamDecoder(BufferedImage image) { this(null, new BufferedImage[]{image}, new Point[0]); } public PseudoBamDecoder(List<PseudoBamFrameEntry> framesList, BufferedImage image) { this(framesList, new BufferedImage[]{image}, new Point[0]); } public PseudoBamDecoder(BufferedImage image, Point center) { this(null, new BufferedImage[]{image}, new Point[]{center}); } public PseudoBamDecoder(List<PseudoBamFrameEntry> framesList, BufferedImage image, Point center) { this(framesList, new BufferedImage[]{image}, new Point[]{center}); } public PseudoBamDecoder(BufferedImage[] images) { this(null, images, new Point[0]); } public PseudoBamDecoder(List<PseudoBamFrameEntry> framesList, BufferedImage[] images) { this(framesList, images, new Point[0]); } public PseudoBamDecoder(BufferedImage[] images, Point[] centers) { this(null, images, centers); } public PseudoBamDecoder(List<PseudoBamFrameEntry> framesList, BufferedImage[] images, Point[] centers) { super(null); setFramesList(framesList); init(images, centers); } /** Returns all available options by name. */ public String[] getOptionNames() { String[] retVal = new String[mapOptions.keySet().size()]; Iterator<String> iter = mapOptions.keySet().iterator(); int idx = 0; while (iter.hasNext()) { retVal[idx++] = iter.next(); } return retVal; } /** Returns the value of the specified global BAM option. */ public Object getOption(String name) { if (name != null) { return mapOptions.get(name); } return null; } /** Sets a custom option for the whole BAM. */ public void setOption(String name, Object value) { if (name != null) { mapOptions.put(name, value); } } /** Returns the currently used frames list. */ public List<PseudoBamFrameEntry> getFramesList() { return listFrames; } /** * Attaches a custom list of frame entries to the object. * Caution: Methods don't check explicitly for {@code null} entries in the list. * @param framesList The new frames list to attach. Specifying {@code null} will create * a new list automatically. */ public void setFramesList(List<PseudoBamFrameEntry> framesList) { if (framesList != null) { listFrames = framesList; } else { listFrames = new ArrayList<PseudoBamFrameEntry>(); } } /** Returns the currently used cycles list. */ public List<PseudoBamCycleEntry> getCyclesList() { return listCycles; } /** * Attaches a custom list of cycle entries to the object. * Caution: Methods don't check explicitely for {@code null} entries in the list. * @param cyclesList The new cycles list to attach. Specifying {@code null} will create * a new list automatically. */ public void setCyclesList(List<PseudoBamCycleEntry> cyclesList) { if (cyclesList != null) { listCycles = cyclesList; } else { listCycles = new ArrayList<PseudoBamCycleEntry>(); } } /** * Adds a new frame to the end of the frame list. Center position defaults to (0, 0). * @param image The image to add. */ public void frameAdd(BufferedImage image) { frameInsert(listFrames.size(), new BufferedImage[]{image}, new Point[0]); } /** * Adds a new frame to the end of the frame list. * @param image The image to add. * @param center The center position of the image. */ public void frameAdd(BufferedImage image, Point center) { frameInsert(listFrames.size(), new BufferedImage[]{image}, new Point[]{center}); } /** * Adds the list of frames to the end of the frame list. Center positions default to (0, 0). * @param images An array containing the images to add. */ public void frameAdd(BufferedImage[] images) { frameInsert(listFrames.size(), images, new Point[0]); } /** * Adds the list of frames to the end of the frame list. * @param images An array containing the images to add. * @param centers An array of center positions corresponding with the images. */ public void frameAdd(BufferedImage[] images, Point[] centers) { frameInsert(listFrames.size(), images, centers); } /** * Inserts a frame at the specified position. Center position defaults to (0, 0). * @param frameIdx The position for the frame to insert. * @param image The image to insert. */ public void frameInsert(int frameIdx, BufferedImage image) { frameInsert(frameIdx, new BufferedImage[]{image}, new Point[0]); } /** * Inserts a frame at the specified position. * @param frameIdx The position for the frame to insert. * @param image The image to insert. * @param center The center position of the image. */ public void frameInsert(int frameIdx, BufferedImage image, Point center) { frameInsert(frameIdx, new BufferedImage[]{image}, new Point[]{center}); } /** * Inserts an array of frames at the specified position. Center positions default to (0, 0). * @param frameIdx The position for the frames to insert. * @param images An array containing the images to insert. */ public void frameInsert(int frameIdx, BufferedImage[] images) { frameInsert(frameIdx, images, new Point[0]); } /** * Inserts an array of frames at the specified position. * @param frameIdx The position for the frames to insert. * @param images An array containing the images to insert. * @param centers An array of center positions corresponding with the images. */ public void frameInsert(int frameIdx, BufferedImage[] images, Point[] centers) { if (frameIdx >= 0 && frameIdx <= listFrames.size() && images != null) { for (int i = 0; i < images.length; i++) { int x = 0, y = 0; if (centers != null && centers.length > i && centers[i] != null) { x = centers[i].x; y = centers[i].y; } listFrames.add(frameIdx+i, new PseudoBamFrameEntry(images[i], x, y)); } } } /** * Removes the frame at the specified position. * @param frameIdx The frame position. */ public void frameRemove(int frameIdx) { frameRemove(frameIdx, 1); } /** * Removes a number of frames, start at the specified position. * @param frameIdx The frame position. * @param count The number of frames to remove. */ public void frameRemove(int frameIdx, int count) { if (frameIdx >= 0 && frameIdx < listFrames.size() && count > 0) { if (frameIdx + count > listFrames.size()) { count = listFrames.size() - frameIdx; } for (int i = 0; i < count; i++) { listFrames.remove(frameIdx); } } } /** * Removes all frames from the BAM structure. */ public void frameClear() { listCycles.clear(); listFrames.clear(); } /** * Moves the frame by the specified (positive or negative) offset. * @return The new frame index, or -1 on error. */ public int frameMove(int frameIdx, int offset) { if (frameIdx >= 0 && frameIdx < listFrames.size()) { int ofsAbs = frameIdx + offset; if (ofsAbs < 0) ofsAbs = 0; if (ofsAbs >= listFrames.size()) ofsAbs = listFrames.size() - 1; if (ofsAbs != frameIdx) { PseudoBamFrameEntry entry = listFrames.get(frameIdx); listFrames.remove(frameIdx); listFrames.add(ofsAbs, entry); } return ofsAbs; } return -1; } @Override public PseudoBamControl createControl() { return new PseudoBamControl(this); } @Override public PseudoBamFrameEntry getFrameInfo(int frameIdx) { if (frameIdx >= 0 && frameIdx < listFrames.size()) { return listFrames.get(frameIdx); } else { return defaultFrameInfo; } } @Override public void close() { if (defaultControl != null) { defaultControl.cycleSet(0); } listCycles.clear(); listFrames.clear(); } @Override public boolean isOpen() { return !listFrames.isEmpty(); } @Override public void reload() { // does nothing } @Override public ByteBuffer getResourceBuffer() { return StreamUtils.getByteBuffer(0); } @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) { control.updateSharedBamSize(); 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) { renderFrame(control, frameIdx, canvas); } } } private void init(BufferedImage[] images, Point[] centers) { // resetting data close(); if (images != null) { for (int i = 0; i < images.length; i++) { int x = 0, y = 0; if (centers != null && centers.length > i && centers[i] != null) { x = centers[i].x; y = centers[i].y; } listFrames.add(new PseudoBamFrameEntry(images[i], x, y)); } // creating a default cycle int[] indices = new int[listFrames.size()]; for (int i = 0; i < indices.length; i++) { indices[i] = i; } listCycles.add(new PseudoBamCycleEntry(indices)); } // creating default bam control instance as a fallback option defaultControl = new PseudoBamControl(this); defaultControl.setMode(BamControl.Mode.SHARED); defaultControl.setSharedPerCycle(false); } // Draws the absolute frame onto the canvas. Takes BAM mode into account. private void renderFrame(BamControl control, int frameIdx, Image canvas) { if (canvas != null && frameIdx >= 0 && frameIdx < listFrames.size()) { if (control == null) { control = defaultControl; } // decoding frame data BufferedImage srcImage = listFrames.get(frameIdx).frame; BufferedImage dstImage = ColorConvert.toBufferedImage(canvas, true, false); int srcPixelStride = srcImage.getRaster().getSampleModel().getNumDataElements(); int srcBufferType = srcImage.getRaster().getDataBuffer().getDataType(); int dstBufferType = dstImage.getRaster().getDataBuffer().getDataType(); byte[] srcBufferB = null, dstBufferB = null; int[] srcBufferI = null, dstBufferI = null; IndexColorModel cm = null; if (srcBufferType == DataBuffer.TYPE_BYTE) { srcBufferB = ((DataBufferByte)srcImage.getRaster().getDataBuffer()).getData(); if (srcImage.getType() == BufferedImage.TYPE_BYTE_INDEXED) { cm = (IndexColorModel)srcImage.getColorModel(); } else if (srcPixelStride == 3 || srcPixelStride == 4) { // XXX: a hack to convert non-paletted pixel types on-the-fly srcBufferI = new int[srcImage.getWidth()*srcImage.getHeight()]; int[] shift; int mask; if (srcPixelStride == 3) { shift = new int[]{0, 8, 16}; mask = 0xff000000; } else { shift = new int[]{24, 0, 8, 16}; mask = 0; } for (int si = 0, di = 0, numPixels = srcBufferI.length; di < numPixels; si += srcPixelStride, di++) { int px = 0; for (int i = 0, cnt = shift.length; i < cnt; i++) { px |= (srcBufferB[si+i] & 0xff) << shift[i]; } px |= mask; srcBufferI[di] = px; } srcBufferB = null; } else { // not supported return; } } else if (srcBufferType == DataBuffer.TYPE_INT) { srcBufferI = ((DataBufferInt)srcImage.getRaster().getDataBuffer()).getData(); } if (dstBufferType == DataBuffer.TYPE_BYTE) { dstBufferB = ((DataBufferByte)dstImage.getRaster().getDataBuffer()).getData(); } else if (dstBufferType == DataBuffer.TYPE_INT) { dstBufferI = ((DataBufferInt)dstImage.getRaster().getDataBuffer()).getData(); } if (srcBufferI != null && dstBufferB != null) { // incompatible combination return; } int dstWidth = dstImage.getWidth(); int dstHeight = dstImage.getHeight(); int srcWidth = listFrames.get(frameIdx).width; int srcHeight = listFrames.get(frameIdx).height; if (control.getMode() == BamControl.Mode.SHARED) { // drawing on shared canvas Rectangle shared = control.getSharedRectangle(); int srcCenterX = listFrames.get(frameIdx).centerX; int srcCenterY = listFrames.get(frameIdx).centerY; int left = -shared.x - srcCenterX; int top = -shared.y - srcCenterY; int maxWidth = (dstWidth < srcWidth + left) ? dstWidth : srcWidth; int maxHeight = (dstHeight < srcHeight + top) ? dstHeight : srcHeight; int srcOfs = 0, dstOfs = top*dstWidth + left; for (int y = 0; y < maxHeight; y++) { for (int x = 0; x < maxWidth; x++) { if (srcBufferB != null) { if (dstBufferB != null) { dstBufferB[dstOfs+x] = srcBufferB[srcOfs+x]; } else { dstBufferI[dstOfs+x] = cm.getRGB(srcBufferB[srcOfs+x] & 0xff); } } else { // only one possible combination left dstBufferI[dstOfs+x] = srcBufferI[srcOfs+x]; } } srcOfs += srcWidth; dstOfs += dstWidth; } } else { // drawing on individual canvas int srcOfs = 0, dstOfs = 0; int maxWidth = (dstWidth < srcWidth) ? dstWidth : srcWidth; int maxHeight = (dstHeight < srcHeight) ? dstHeight : srcHeight; for (int y = 0; y < maxHeight; y++) { for (int x = 0; x < maxWidth; x++) { if (srcBufferB != null) { if (dstBufferB != null) { dstBufferB[dstOfs+x] = srcBufferB[srcOfs+x]; } else { dstBufferI[dstOfs+x] = cm.getRGB(srcBufferB[srcOfs+x] & 0xff); } } else { // only one possible combination left dstBufferI[dstOfs+x] = srcBufferI[srcOfs+x]; } } srcOfs += srcWidth; dstOfs += dstWidth; } } srcBufferB = null; dstBufferB = null; srcBufferI = null; dstBufferI = null; // rendering resulting image onto the canvas if needed if (dstImage != canvas) { Graphics g = canvas.getGraphics(); try { g.drawImage(dstImage, 0, 0, null); } finally { g.dispose(); g = null; } dstImage.flush(); dstImage = null; } } } /** * Determines whether "color" can be classified as RLE-compressed color. * @param color The color to check. * @param rleColor The RLE color index. * @param threshold The amount of alpha allowed for opaque colors. * @return {@code true} if the color is determined as the RLE-compressed color. */ public static boolean isRleColor(int color, int rleColor, int threshold) { final int Green = 0x0000ff00; rleColor &= 0x00ffffff; if (threshold < 0) threshold = 0; else if (threshold > 255) threshold = 255; boolean inThreshold = (((color >>> 24) & 0xff) < (255 - threshold)); color &= 0x00ffffff; return (color == rleColor || (inThreshold && rleColor == Green)); } /** * Determines whether "color" is interpreted as "transparent". * @param color The color to check. * @param threshold The amount of alpha allowed for opaque colors. * @return {@code true} if the color is determined as "transparent". */ public static boolean isTransparentColor(int color, int threshold) { final int Green = 0x0000ff00; if (threshold < 0) threshold = 0; else if (threshold > 255) threshold = 255; boolean isAlpha = (((color >>> 24) & 0xff) < (255 - threshold)); boolean isGreen = ((color & 0x00ffffff) == Green); return (isAlpha || isGreen); } /** * Creates a BAM v1 resource from the current BAM structure. Requires paletted source frames * using a common palette for all frames. * @param fileName The filename of the BAM file to export. * @param progress An optional progress monitor to display the state of the export progress. * @param curProgress The current progress state of the progress monitor. * @return {@code true} if the export was successfull, {@code false} otherwise. * @throws Exception If an unrecoverable error occured. */ public boolean exportBamV1(Path fileName, ProgressMonitor progress, int curProgress) throws Exception { final int FrameEntrySize = 12; final int CycleEntrySize = 4; if (!listFrames.isEmpty() && !listCycles.isEmpty()) { // sanity checks if (fileName == null) { throw new Exception("Invalid filename specified."); } if (listFrames.size() > 65535) { throw new Exception("No more than 65535 frames supported."); } if (listCycles.size() > 255) { throw new Exception("No more than 255 cycles supported."); } for (int i = 0; i < listCycles.size(); i++) { if (listCycles.get(i).size() > 65535) { throw new Exception(String.format("No more than 65535 frames per cycle supported. " + "Cycle %1$d contains %2$d entries.", i, listCycles.get(i).size())); } } int[] palette = null; int transIndex = -1; for (int i = 0; i < listFrames.size(); i++) { PseudoBamFrameEntry entry = listFrames.get(i); // checking source frame type if (entry.getFrame().getType() != BufferedImage.TYPE_BYTE_INDEXED) { throw new Exception("Unsupported source frame image type."); } // checking frame properties if (entry.width <= 0 || entry.width > 65535 || entry.height <= 0 || entry.height > 65535 || entry.centerX < Short.MIN_VALUE || entry.centerX > Short.MAX_VALUE || entry.centerY < Short.MIN_VALUE || entry.centerY > Short.MAX_VALUE) { throw new Exception("Dimensions are out of range for frame index " + i); } // rudimentary palette check if (palette == null) { final int Green = 0x0000ff00;; IndexColorModel cm = (IndexColorModel)entry.getFrame().getColorModel(); palette = new int[1 << cm.getPixelSize()]; cm.getRGBs(palette); for (int j = 0; j < palette.length; j++) { int c = palette[i] & 0x00ffffff; if (transIndex < 0 && c == Green) { transIndex = j; break; } } if (transIndex < 0) { transIndex = 0; } } else { IndexColorModel cm = (IndexColorModel)entry.getFrame().getColorModel(); if (palette.length != (1 <<cm.getPixelSize())) { throw new Exception("Incompatible palette found in source frame " + i); } } } // initializing progress monitor if (progress != null) { if (curProgress < 0) curProgress = 0; progress.setMaximum(progress.getMaximum() + 2); progress.setProgress(curProgress++); progress.setNote("Encoding frames"); } // calculating the max. space required for a single frame PseudoBamControl control = createControl(); control.setMode(BamDecoder.BamControl.Mode.SHARED); control.setSharedPerCycle(false); Dimension dimFrame = control.calculateSharedCanvas(false).getSize(); int maxImageSize = (dimFrame.width*dimFrame.height*3) / 2; // about 1.5x of max. size List<byte[]> listFrameData = new ArrayList<byte[]>(listFrames.size()); // encoding frames Object o = getOption(OPTION_INT_RLEINDEX); byte rleIndex = (byte)(((o != null) ? ((Integer)o).intValue() : 0) & 0xff); byte[] dstData = new byte[maxImageSize]; for (int idx = 0; idx < listFrames.size(); idx++) { o = listFrames.get(idx).getOption(OPTION_BOOL_COMPRESSED); boolean frameCompressed = (o != null) ? ((Boolean)o).booleanValue() : false; PseudoBamFrameEntry entry = listFrames.get(idx); byte[] srcBuffer = ((DataBufferByte)entry.frame.getRaster().getDataBuffer()).getData(); if (frameCompressed) { // creating RLE compressed frame int srcIdx = 0, dstIdx = 0, srcMax = srcBuffer.length; while (srcIdx < srcMax) { if (rleIndex == srcBuffer[srcIdx]) { // color to compress int cnt = 0; srcIdx++; while (srcIdx < srcMax && cnt < 255 && rleIndex == srcBuffer[srcIdx]) { cnt++; srcIdx++; } dstData[dstIdx++] = (byte)rleIndex; dstData[dstIdx++] = (byte)cnt; } else { // uncompressed pixels dstData[dstIdx++] = srcBuffer[srcIdx++]; } } // storing the resulting frame data byte[] outData = new byte[dstIdx]; System.arraycopy(dstData, 0, outData, 0, dstIdx); listFrameData.add(outData); } else { // creating uncompressed frame System.arraycopy(srcBuffer, 0, dstData, 0, srcBuffer.length); // storing the resulting frame data byte[] outData = new byte[srcBuffer.length]; System.arraycopy(dstData, 0, outData, 0, srcBuffer.length); listFrameData.add(outData); } srcBuffer = null; } if (progress != null) { progress.setProgress(curProgress++); progress.setNote("Generating BAM"); } // creating cycles table and frame lookup table List<Integer> listFrameLookup = new ArrayList<Integer>(); int lookupSize = 0; for (int i = 0; i < listCycles.size(); i++) { listFrameLookup.add(Integer.valueOf(lookupSize)); lookupSize += listCycles.get(i).size(); } // putting it all together int ofsFrameEntries = 0x18; int ofsPalette = ofsFrameEntries + listFrames.size()*FrameEntrySize + listCycles.size()*CycleEntrySize; int ofsLookup = ofsPalette + 1024; int ofsFrameData = ofsLookup + lookupSize*2; int bamSize = ofsFrameData; // updating frame offsets int[] frameDataOffsets = new int[listFrameData.size()]; for (int i = 0; i < listFrameData.size(); i++) { frameDataOffsets[i] = bamSize; o = listFrames.get(i).getOption(OPTION_BOOL_COMPRESSED); if (o == null || ((Boolean)o).booleanValue() == false) { frameDataOffsets[i] |= 0x80000000; } bamSize += listFrameData.get(i).length; } byte[] bamData = new byte[bamSize]; System.arraycopy("BAM V1 ".getBytes(), 0, bamData, 0, 8); DynamicArray.putShort(bamData, 0x08, (short)listFrames.size()); DynamicArray.putByte(bamData, 0x0a, (byte)listCycles.size()); DynamicArray.putByte(bamData, 0x0b, (byte)rleIndex); DynamicArray.putInt(bamData, 0x0c, ofsFrameEntries); DynamicArray.putInt(bamData, 0x10, ofsPalette); DynamicArray.putInt(bamData, 0x14, ofsLookup); // adding frame entries int curOfs = ofsFrameEntries; for (int i = 0; i < listFrames.size(); i++) { DynamicArray.putShort(bamData, curOfs, (short)listFrames.get(i).width); DynamicArray.putShort(bamData, curOfs + 2, (short)listFrames.get(i).height); DynamicArray.putShort(bamData, curOfs + 4, (short)listFrames.get(i).centerX); DynamicArray.putShort(bamData, curOfs + 6, (short)listFrames.get(i).centerY); DynamicArray.putInt(bamData, curOfs + 8, frameDataOffsets[i]); curOfs += FrameEntrySize; } // adding cycle entries for (int i = 0; i < listCycles.size(); i++) { DynamicArray.putShort(bamData, curOfs, (short)listCycles.get(i).size()); DynamicArray.putShort(bamData, curOfs + 2, listFrameLookup.get(i).shortValue()); curOfs += CycleEntrySize; } // adding palette for (int i = 0; i < palette.length; i++) { DynamicArray.putByte(bamData, curOfs, (byte)(palette[i] & 0xff)); // red DynamicArray.putByte(bamData, curOfs + 1, (byte)((palette[i] >>> 8) & 0xff)); // green DynamicArray.putByte(bamData, curOfs + 2, (byte)((palette[i] >>> 16) & 0xff)); // blue DynamicArray.putByte(bamData, curOfs + 3, (byte)0); // unused curOfs += 4; } // adding frame lookup table for (int i = 0; i < listCycles.size(); i++) { for (int j = 0; j < listCycles.get(i).frames.size(); j++) { DynamicArray.putShort(bamData, curOfs, listCycles.get(i).frames.get(j).shortValue()); curOfs += 2; } } // adding frame graphics data for (int i = 0; i < listFrameData.size(); i++) { System.arraycopy(listFrameData.get(i), 0, bamData, curOfs, listFrameData.get(i).length); curOfs += listFrameData.get(i).length; } // compressing BAM (optional) o = getOption(OPTION_BOOL_COMPRESSED); boolean isCompressed = (o != null) ? ((Boolean)o).booleanValue() : false; if (isCompressed) { bamData = Compressor.compress(bamData, "BAMC", "V1 "); } // writing BAM to disk try (OutputStream os = StreamUtils.getOutputStream(fileName, true)) { os.write(bamData); } catch (Exception e) { e.printStackTrace(); throw e; } bamData = null; return true; } return false; } /** * Creates a BAM v2 resource from the current BAM structure. * @param fileName The BAM filename. Path is also used for associated PVRZ files. * @param dxtType The desired DXTn compression type to use. * @param pvrzIndex The start index of PVRZ files. * @param progress An optional progress monitor to display the state of the export progress. * @param curProgress The current progress state of the progress monitor. * @return {@code true} if the export was successful, {@code false} otherwise. * @throws Exception If an unrecoverable error occured. */ public boolean exportBamV2(Path fileName, DxtEncoder.DxtType dxtType, int pvrzIndex, ProgressMonitor progress, int curProgress) throws Exception { final int FrameEntrySize = 12; final int CycleEntrySize = 4; final int BlockEntrySize = 28; if (!listFrames.isEmpty() && !listCycles.isEmpty()) { // sanity checks if (fileName == null) { throw new Exception("Invalid filename specified."); } if (dxtType != DxtEncoder.DxtType.DXT1 && dxtType != DxtEncoder.DxtType.DXT5) { dxtType = DxtEncoder.DxtType.DXT5; } if (pvrzIndex < 0 || pvrzIndex > 99999) { throw new Exception("PVRZ start index is out of range [0..99999]."); } // preparing output path for PVRZ files Path pvrzFilePath = fileName.toAbsolutePath().getParent(); List<FrameDataV2> listFrameData = new ArrayList<FrameDataV2>(listFrames.size()); List<BinPack2D> listGrid = new ArrayList<BinPack2D>(); // initializing progress monitor if (progress != null) { if (curProgress < 0) curProgress = 0; progress.setMaximum(progress.getMaximum() + 5); progress.setProgress(curProgress++); progress.setNote("Calculating PVRZ layout"); } // preparations // generating block data list if (!buildFrameDataList(listFrameData, listGrid, pvrzIndex)) { return false; } // generating remaining info blocks List<FrameDataV2> listFrameDataBlocks = new ArrayList<FrameDataV2>(); List<PseudoBamFrameEntry> listFrameEntries = new ArrayList<PseudoBamFrameEntry>(); List<Pair<Short>> listCycleData = new ArrayList<Pair<Short>>(listCycles.size()); int frameStartIndex = 0; // keeps track of current start index of frame entries int blockStartIndex = 0; // keeps track of current start index of frame data blocks for (int i = 0; i < listCycles.size(); i++) { List<Integer> cycleFrames = listCycles.get(i).frames; // generating cycle entries Pair<Short> cycle = new Pair<Short>(Short.valueOf((short)cycleFrames.size()), Short.valueOf((short)frameStartIndex)); listCycleData.add(cycle); for (int j = 0; j < cycleFrames.size(); j++) { int idx = cycleFrames.get(j).intValue(); try { FrameDataV2 frame = listFrameData.get(idx); PseudoBamFrameEntry bfe = listFrames.get(idx); PseudoBamFrameEntry entry = new PseudoBamFrameEntry(bfe.frame, bfe.centerX, bfe.centerY); entry.setOption(OPTION_INT_BLOCKINDEX, Integer.valueOf(blockStartIndex)); entry.setOption(OPTION_INT_BLOCKCOUNT, Integer.valueOf(1)); listFrameEntries.add(entry); blockStartIndex++; listFrameDataBlocks.add(frame); } catch (IndexOutOfBoundsException e) { throw new IndexOutOfBoundsException( String.format("Invalid frame index %1$d found in cycle %2$d", idx, i)); } } frameStartIndex += cycleFrames.size(); } // putting it all together int ofsFrameEntries = 0x20; int ofsCycleEntries = ofsFrameEntries + listFrameEntries.size()*FrameEntrySize; int ofsFrameData = ofsCycleEntries + listCycleData.size()*CycleEntrySize; int bamSize = ofsFrameData + listFrameDataBlocks.size()*BlockEntrySize; byte[] bamData = new byte[bamSize]; // writing main header System.arraycopy("BAM V2 ".getBytes(), 0, bamData, 0, 8); DynamicArray.putInt(bamData, 0x08, listFrameEntries.size()); DynamicArray.putInt(bamData, 0x0c, listCycleData.size()); DynamicArray.putInt(bamData, 0x10, listFrameDataBlocks.size()); DynamicArray.putInt(bamData, 0x14, ofsFrameEntries); DynamicArray.putInt(bamData, 0x18, ofsCycleEntries); DynamicArray.putInt(bamData, 0x1c, ofsFrameData); // writing frame entries int ofs = ofsFrameEntries; Object o; short v; for (int i = 0; i < listFrameEntries.size(); i++) { PseudoBamFrameEntry fe = listFrameEntries.get(i); DynamicArray.putShort(bamData, ofs, (short)fe.width); DynamicArray.putShort(bamData, ofs + 2, (short)fe.height); DynamicArray.putShort(bamData, ofs + 4, (short)fe.centerX); DynamicArray.putShort(bamData, ofs + 6, (short)fe.centerY); o = fe.getOption(OPTION_INT_BLOCKINDEX); v = (o != null) ? ((Integer)o).shortValue() : 0; DynamicArray.putShort(bamData, ofs + 8, v); o = fe.getOption(OPTION_INT_BLOCKCOUNT); v = (o != null) ? ((Integer)o).shortValue() : 0; DynamicArray.putShort(bamData, ofs + 10, v); ofs += FrameEntrySize; } // writing cycle entries for (int i = 0; i < listCycleData.size(); i++) { Pair<Short> entry = listCycleData.get(i); DynamicArray.putShort(bamData, ofs, entry.getFirst().shortValue()); DynamicArray.putShort(bamData, ofs + 2, entry.getSecond().shortValue()); ofs += CycleEntrySize; } // writing frame data blocks for (int i = 0; i < listFrameDataBlocks.size(); i++) { FrameDataV2 entry = listFrameDataBlocks.get(i); DynamicArray.putInt(bamData, ofs, entry.page); DynamicArray.putInt(bamData, ofs + 4, entry.sx); DynamicArray.putInt(bamData, ofs + 8, entry.sy); DynamicArray.putInt(bamData, ofs + 12, entry.width); DynamicArray.putInt(bamData, ofs + 16, entry.height); DynamicArray.putInt(bamData, ofs + 20, entry.dx); DynamicArray.putInt(bamData, ofs + 24, entry.dy); ofs += BlockEntrySize; } // writing BAM to disk try (OutputStream os = StreamUtils.getOutputStream(fileName, true)) { os.write(bamData); } catch (Exception e) { e.printStackTrace(); throw e; } bamData = null; // generating PVRZ files if (!createPvrzPages(pvrzFilePath, dxtType, listGrid, listFrameData, progress, curProgress)) { return false; } return true; } return false; } /** * Creates an array of max. 255 colors that can be used to create a global palette for all available frames. * Makes use of the specified color map if available. Does not consider transparent color. * @param colorMap An optional color map that will be used if available. Can be {@code null}. * @return An int array containing up to 255 colors without the transparent color entry. */ public int[] createGlobalPalette(HashMap<Integer, Integer> colorMap) { final int Green = 0x0000ff00; int[] retVal; if (!listFrames.isEmpty() && !listCycles.isEmpty()) { // adding pixels of all available frames to the hashset HashMap<Integer, Integer> newMap; if (colorMap == null) { newMap = new HashMap<Integer, Integer>(); for (int i = 0; i < listFrames.size(); i++) { registerColors(newMap, listFrames.get(i).frame); } } else { newMap = new HashMap<Integer, Integer>(colorMap); } // transparent color does not count if (newMap.containsKey(Integer.valueOf(Green))) { newMap.remove(Integer.valueOf(Green)); } // creating palette int numColors = newMap.size(); int[] colorBuffer = new int[numColors]; Iterator<Integer> iter = newMap.keySet().iterator(); int idx = 0; while (iter.hasNext()) { colorBuffer[idx] = iter.next(); idx++; } if (colorBuffer.length > 255) { retVal = ColorConvert.medianCut(colorBuffer, 255, true); } else { retVal = colorBuffer; } // removing duplicate entries from the palette HashSet<Integer> colorSet = new HashSet<Integer>(); for (int i = 0; i < retVal.length; i++) { colorSet.add(Integer.valueOf(retVal[i])); } if (colorSet.size() != retVal.length) { retVal = new int[colorSet.size()]; idx = 0; iter = colorSet.iterator(); while (iter.hasNext()) { retVal[idx] = iter.next().intValue(); idx++; } } } else { retVal = new int[0]; } return retVal; } /** Maps all color values of the specified image. */ public static void registerColors(HashMap<Integer, Integer> colorMap, BufferedImage image) { final int Green = 0x0000ff00; if (image != null) { if (image.getType() == BufferedImage.TYPE_BYTE_INDEXED && image.getColorModel() instanceof IndexColorModel) { IndexColorModel cm = (IndexColorModel)image.getColorModel(); boolean hasAlpha = cm.hasAlpha(); int numColors = 1 << cm.getPixelSize(); for (int i = 0; i < numColors; i++) { int color = cm.getRGB(i); // determining transparency if (hasAlpha && ((color >>> 24) < 255)) { color = Green; } // registering color in map Integer key = Integer.valueOf(color); Integer count = colorMap.get(key); if (count == null) { count = Integer.valueOf(1); } else { ++count; } colorMap.put(key, count); } } else if (image.getRaster().getDataBuffer().getDataType() == DataBuffer.TYPE_INT) { int[] buffer = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); for (int i = 0; i < buffer.length; i++) { int color = buffer[i]; // determining transparency if ((color & 0xff000000) == 0) { color = Green; } else { color &= 0x00ffffff; } // registering color in map Integer key = Integer.valueOf(color); Integer count = colorMap.get(key); if (count == null) { count = Integer.valueOf(1); } else { ++count; } colorMap.put(key, count); } } } } /** Unmaps all color values of the specified image. */ public static void unregisterColors(HashMap<Integer, Integer> colorMap, BufferedImage image) { final int Green = 0x0000ff00; if (image != null) { if (image.getType() == BufferedImage.TYPE_BYTE_INDEXED && image.getColorModel() instanceof IndexColorModel) { IndexColorModel cm = (IndexColorModel)image.getColorModel(); byte[] buffer = ((DataBufferByte)image.getRaster().getDataBuffer()).getData(); boolean hasAlpha = cm.hasAlpha(); for (int i = 0; i < buffer.length; i++) { int pixel = buffer[i] & 0xff; int color = (cm.getRed(pixel) << 16) | (cm.getGreen(pixel) << 8) | cm.getBlue(pixel); // determining transparency if (hasAlpha) { int a = cm.getAlpha(pixel); if (a > 0) { color = Green; } } // unregistering color in map Integer key = Integer.valueOf(color); Integer count = colorMap.get(key); if (count != null) { --count; if (count == 0) { colorMap.remove(key); } else { colorMap.put(key, count); } } } } else if (image.getRaster().getDataBuffer().getDataType() == DataBuffer.TYPE_INT) { int[] buffer = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); for (int i = 0; i < buffer.length; i++) { int color = buffer[i]; // determining transparency if ((color & 0xff000000) == 0) { color = Green; } else { color &= 0x00ffffff; } // unregistering color in map Integer key = Integer.valueOf(color); Integer count = colorMap.get(key); if (count != null) { --count; if (count == 0) { colorMap.remove(key); } else { colorMap.put(key, count); } } } } } } // Calculates the locations of all frames on PVRZ textures and stores the results in framesList and gridList. private boolean buildFrameDataList(List<FrameDataV2> framesList, List<BinPack2D> gridList, int pvrzPageIndex) throws Exception { if (framesList != null && gridList != null && pvrzPageIndex >= 0 && pvrzPageIndex < 99999) { final int pageDim = 1024; final BinPack2D.HeuristicRules binPackRule = BinPack2D.HeuristicRules.BOTTOM_LEFT_RULE; for (int frameIdx = 0; frameIdx < listFrames.size(); frameIdx++) { int imgWidth = listFrames.get(frameIdx).frame.getWidth() + 2; int imgHeight = listFrames.get(frameIdx).frame.getHeight() + 2; // use multiple of 4 to take advantage of texture compression algorithm Dimension space = new Dimension((imgWidth+3) & ~3, (imgHeight+3) & ~3); int pageIdx = -1; Rectangle rectMatch = null; for (int i = 0; i < gridList.size(); i++) { BinPack2D packer = gridList.get(i); rectMatch = packer.insert(space.width, space.height, binPackRule); if (rectMatch.height > 0) { pageIdx = i; break; } } // create new page? if (pageIdx < 0) { BinPack2D packer = new BinPack2D(pageDim, pageDim); gridList.add(packer); pageIdx = gridList.size() - 1; rectMatch = packer.insert(space.width, space.height, binPackRule); } // registering page entry (centering frame in padded region) FrameDataV2 entry = new FrameDataV2(pvrzPageIndex + pageIdx, rectMatch.x + 1, rectMatch.y + 1, imgWidth - 2, imgHeight - 2, 0, 0); framesList.add(entry); } if (pvrzPageIndex + gridList.size() > 100000) { throw new Exception(String.format("The number of required PVRZ files exceeds the max. index of 99999.\n" + "Please choose a PVRZ start index smaller than or equal to %1$d.", 100000 - gridList.size())); } return true; } return false; } // Creates all PVRZ files defined in the method arguments. private boolean createPvrzPages(Path path, DxtEncoder.DxtType dxtType, List<BinPack2D> gridList, List<FrameDataV2> framesList, ProgressMonitor progress, int curProgress) throws Exception { if (path == null) { path = FileManager.resolve(""); } int dxtCode = (dxtType == DxtEncoder.DxtType.DXT5) ? 11 : 7; byte[] output = new byte[DxtEncoder.calcImageSize(1024, 1024, dxtType)]; int pageMin = Integer.MAX_VALUE; int pageMax = -1; for (int i = 0; i < framesList.size(); i++) { FrameDataV2 entry = framesList.get(i); pageMin = Math.min(pageMin, entry.page); pageMax = Math.max(pageMax, entry.page); } String note = "Generating PVRZ file %1$s / %2$s"; if (progress != null) { if (curProgress < 0) curProgress = 0; progress.setMaximum(curProgress + pageMax - pageMin + 1); progress.setProgress(curProgress++); } // processing each PVRZ page for (int i = pageMin; i <= pageMax; i++) { if (progress != null) { if (progress.isCanceled()) { throw new Exception("Conversion has been cancelled by the user."); } progress.setProgress(curProgress); progress.setNote(String.format(note, curProgress, pageMax - pageMin + 1)); curProgress++; } Path pvrzName = path.resolve(String.format("MOS%1$04d.PVRZ", i)); BinPack2D packer = gridList.get(i - pageMin); packer.shrinkBin(true); // generating texture image int tw = packer.getBinWidth(); int th = packer.getBinHeight(); BufferedImage texture = ColorConvert.createCompatibleImage(tw, th, true); Graphics2D g = texture.createGraphics(); try { g.setColor(Color.BLACK); g.fillRect(0, 0, texture.getWidth(), texture.getHeight()); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); g.setColor(TransparentColor); for (int frameIdx = 0; frameIdx < listFrames.size(); frameIdx++) { BufferedImage image = listFrames.get(frameIdx).frame; FrameDataV2 frame = framesList.get(frameIdx); if (frame.page == i) { int sx = frame.dx, sy = frame.dy; int dx = frame.sx, dy = frame.sy; int w = frame.width, h = frame.height; g.fillRect(dx - 1, dy - 1, w + 2, h + 2); // compensating for padding done in buildFrameDataList() g.drawImage(image, dx, dy, dx+w, dy+h, sx, sy, sx+w, sy+h, null); } } } finally { g.dispose(); g = null; } // compressing PVRZ String errorMsg = null; int[] textureData = ((DataBufferInt)texture.getRaster().getDataBuffer()).getData(); try { int outSize = DxtEncoder.calcImageSize(texture.getWidth(), texture.getHeight(), dxtType); DxtEncoder.encodeImage(textureData, texture.getWidth(), texture.getHeight(), output, dxtType); byte[] header = ConvertToPvrz.createPVRHeader(texture.getWidth(), texture.getHeight(), dxtCode); byte[] pvrz = new byte[header.length + outSize]; System.arraycopy(header, 0, pvrz, 0, header.length); System.arraycopy(output, 0, pvrz, header.length, outSize); header = null; pvrz = Compressor.compress(pvrz, 0, pvrz.length, true); // writing PVRZ to disk try (OutputStream os = StreamUtils.getOutputStream(pvrzName, true)) { os.write(pvrz); } catch (Exception e) { errorMsg = String.format("Error writing PVRZ file \"%1$s\" to disk.", pvrzName); e.printStackTrace(); } textureData = null; pvrz = null; } catch (Exception e) { e.printStackTrace(); errorMsg = String.format("Error generating PVRZ files:\n%1$s.", e.getMessage()); } if (errorMsg != null) { throw new Exception(errorMsg); } } output = null; return true; } //-------------------------- INNER CLASSES -------------------------- /** Provides information for a single frame entry */ public static class PseudoBamFrameEntry implements FrameEntry { private final HashMap<String, Object> mapOptions = new HashMap<String, Object>(); private int width, height, centerX, centerY; private BufferedImage frame; public PseudoBamFrameEntry(BufferedImage image, int centerX, int centerY) { setFrame(image); setCenterX(centerX); setCenterY(centerY); } @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 void setCenterX(int value) { if (value < Short.MIN_VALUE) value = Short.MIN_VALUE; else if (value > Short.MAX_VALUE) value = Short.MAX_VALUE; centerX = value; } public void setCenterY(int value) { if (value < Short.MIN_VALUE) value = Short.MIN_VALUE; else if (value > Short.MAX_VALUE) value = Short.MAX_VALUE; centerY = value; } /** Returns the image object of this frame entry. */ public BufferedImage getFrame() { return frame; } /** Assigns a new image object to this frame entry. */ public void setFrame(BufferedImage image) { if (image != null) { frame = image; width = frame.getWidth(); height = frame.getHeight(); } else { frame = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); width = height = 1; } } @Override public Object clone() { return new PseudoBamFrameEntry(frame, centerX, centerY); } @Override public String toString() { String s = (String)getOption(PseudoBamDecoder.OPTION_STRING_LABEL); if (s != null) { return s; } else { return String.format("Frame@%1$dx%2$d", width, height); } } /** Returns all available options by name. */ public String[] getOptionNames() { String[] retVal = new String[mapOptions.keySet().size()]; Iterator<String> iter = mapOptions.keySet().iterator(); int idx = 0; while (iter.hasNext()) { retVal[idx++] = iter.next(); } return retVal; } /** Returns the value of the specified option for this frame. */ public Object getOption(String name) { if (name != null) { return mapOptions.get(name); } return null; } /** Sets a custom option for this frame. */ public void setOption(String name, Object value) { if (name != null) { mapOptions.put(name, value); } } } /** Provides access to cycle-specific functionality. */ public static class PseudoBamControl extends BamControl { private int currentCycle, currentFrame; protected PseudoBamControl(PseudoBamDecoder decoder) { super(decoder); init(); } /** Returns all available options by name for the current cycle. */ public String[] cycleGetOptionNames() { return cycleGetOptionsNames(currentCycle); } /** Returns all available options by name for the specified cycle. */ public String[] cycleGetOptionsNames(int cycleIdx) { update(); if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { return getDecoder().listCycles.get(cycleIdx).getOptionNames(); } return null; } /** Returns the specified option associated with the current cycle. */ public Object cycleGetOption(String name) { return cycleGetOption(currentCycle, name); } /** Returns the option associated with the specified cycle. */ public Object cycleGetOption(int cycleIdx, String name) { update(); if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { return getDecoder().listCycles.get(cycleIdx).getOption(name); } return null; } /** Assigns a custom option to the current cycle. */ public void cycleSetOption(String name, Object value) { cycleSetOption(currentCycle, name, value); } /** Assigns a custom option to the specified cycle. */ public void cycleSetOption(int cycleIdx, String name, Object value) { update(); if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { getDecoder().listCycles.get(cycleIdx).setOption(name, value); } } /** Adds a new empty cycle. */ public void cycleAdd() { cycleInsert(getDecoder().listCycles.size(), null); } /** Adds a new cycle and initializes it with an array of frame indices. */ public void cycleAdd(int[] indices) { cycleInsert(getDecoder().listCycles.size(), indices); } /** Inserts a new empty cycle at the specified position. */ public void cycleInsert(int cycleIdx) { cycleInsert(cycleIdx, null); } /** Inserts a new cycle at the specified position and initializes it with an array of frame indices. */ public void cycleInsert(int cycleIdx, int[] indices) { if (cycleIdx >= 0 && cycleIdx <= getDecoder().listCycles.size()) { PseudoBamCycleEntry ce = new PseudoBamCycleEntry(indices); getDecoder().listCycles.add(cycleIdx, ce); update(); } } /** Removes the cycle at the specified position. */ public void cycleRemove(int cycleIdx) { cycleRemove(cycleIdx, 1); } /** Removes a number of cycles at the specified position. */ public void cycleRemove(int cycleIdx, int count) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size() && count > 0) { if (cycleIdx + count > getDecoder().listCycles.size()) { count = getDecoder().listCycles.size() - cycleIdx; } for (int i = 0; i < count; i++) { getDecoder().listCycles.remove(cycleIdx); } update(); } } /** Removes all available cycles. */ public void cycleClear() { getDecoder().listCycles.clear(); update(); } /** * Moves the current cycle by the specified (positive or negative) offset. * @return The new cycle index, or -1 on error. */ public int cycleMove(int offset) { return cycleMove(currentCycle, offset); } /** * Moves the specified cycle by the specified (positive or negative) offset. * @return The new cycle index, or -1 on error. */ public int cycleMove(int cycleIdx, int offset) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { int ofsAbs = cycleIdx + offset; if (ofsAbs < 0) ofsAbs = 0; if (ofsAbs >= getDecoder().listCycles.size()) ofsAbs = getDecoder().listCycles.size() - 1; if (ofsAbs != cycleIdx) { PseudoBamCycleEntry ce = getDecoder().listCycles.get(cycleIdx); getDecoder().listCycles.remove(cycleIdx); getDecoder().listCycles.add(ofsAbs, ce); } return ofsAbs; } return -1; } /** Adds frame indices to the specified cycle. */ public void cycleAddFrames(int cycleIdx, int[] indices) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { cycleInsertFrames(cycleIdx, getDecoder().listCycles.get(cycleIdx).size(), indices); } } /** Inserts frame indices to the cycle at the specified position. */ public void cycleInsertFrames(int cycleIdx, int pos, int[] indices) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { getDecoder().listCycles.get(cycleIdx).insert(pos, indices); update(); } } /** Removes one frame index from the cycle at the specified position. */ public void cycleRemoveFrames(int cycleIdx, int pos) { cycleRemoveFrames(cycleIdx, pos, 1); } /** Removes frame indices from the cycle at the specified position. */ public void cycleRemoveFrames(int cycleIdx, int pos, int count) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { getDecoder().listCycles.get(cycleIdx).remove(pos, count); update(); } } /** Removes all frame indices from the current cycle. */ public void cycleClearFrames() { cycleClearFrames(currentCycle); } /** Removes all frame indices from the specified cycle. */ public void cycleClearFrames(int cycleIdx) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { getDecoder().listCycles.get(cycleIdx).clear(); update(); } } /** * Moves the current frame of the current cycle by the specified (positive or negative) offset. * Sets the current frame to the new position afterwards. * @return The new frame index within the cycle, or -1 on error. */ public int cycleMoveFrame(int offset) { int pos = cycleMoveFrame(currentCycle, currentFrame, offset); if (pos >= 0) { currentFrame = pos; } return pos; } /** * Moves the frame of the current cycle by the specified (positive or negative) offset. * @return The new frame index within the cycle, or -1 on error. */ public int cycleMoveFrame(int frameIdx, int offset) { return cycleMoveFrame(currentCycle, frameIdx, offset); } /** * Moves the frame of the cycle by the specified (positive or negative) offset. * @return The new frame index within the cycle, or -1 on error. */ public int cycleMoveFrame(int cycleIdx, int frameIdx, int offset) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { PseudoBamCycleEntry ce = getDecoder().listCycles.get(cycleIdx); if (frameIdx >= 0 && frameIdx < ce.size()) { int ofsAbs = frameIdx + offset; if (ofsAbs < 0) ofsAbs = 0; if (ofsAbs >= ce.size()) ofsAbs = ce.size() - 1; if (ofsAbs != frameIdx) { int index = ce.get(frameIdx); ce.remove(frameIdx, 1); ce.insert(ofsAbs, new int[]{index}); } return ofsAbs; } } return -1; } /** Returns whether the current frame in the current cycle includes a palette. */ public boolean cycleFrameHasPalette() { return cycleFrameHasPalette(currentCycle, currentFrame); } /** Returns whether the specified frame in the current cycle includes a palette. */ public boolean cycleFrameHasPalette(int frameIdx) { return cycleFrameHasPalette(currentCycle, frameIdx); } /** Returns whether the frame in the specified cycles includes a palette. */ public boolean cycleFrameHasPalette(int cycleIdx, int frameIdx) { int index = cycleGetFrameIndexAbsolute(cycleIdx, frameIdx); if (index >= 0) { BufferedImage image = getDecoder().listFrames.get(index).frame; if (image != null && image.getType() == BufferedImage.TYPE_BYTE_INDEXED) { return true; } } return false; } /** Returns the palette of the current frame in the current cycle. Returns {@code null} if no palette is available. */ public int[] cycleFrameGetPalette() { return cycleFrameGetPalette(currentCycle, currentFrame); } /** Returns the palette of the specified frame in the current cycle. Returns {@code null} if no palette is available. */ public int[] cycleFrameGetPalette(int frameIdx) { return cycleFrameGetPalette(currentCycle, frameIdx); } /** Returns the palette of the frame in the specified cycle. Returns {@code null} if no palette is available. */ public int[] cycleFrameGetPalette(int cycleIdx, int frameIdx) { int index = cycleGetFrameIndexAbsolute(cycleIdx, frameIdx); if (index >= 0) { BufferedImage image = getDecoder().listFrames.get(index).frame; if (image != null && image.getType() == BufferedImage.TYPE_BYTE_INDEXED) { if (image.getColorModel() instanceof IndexColorModel) { IndexColorModel cm = (IndexColorModel)image.getColorModel(); int[] palette = new int[256]; int size = 1 << cm.getPixelSize(); if (size > 256) size = 256; for (int i = 0; i < size; i++) { palette[i] = (cm.getAlpha(i) << 24) | (cm.getRed(i) << 16) | (cm.getGreen(i) << 8) | cm.getBlue(i); } return palette; } } } return null; } /** * Validates the current cycle configuration. This method should be called whenever changes * have been made to the frames and/or cycle structure outside of this control instance. */ public void validate() { update(); } /** Returns a CycleEntry structure for the specified cycle. */ public PseudoBamCycleEntry getCycleInfo(int cycleIdx) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { return getDecoder().listCycles.get(cycleIdx); } else { return null; } } @Override public PseudoBamDecoder getDecoder() { return (PseudoBamDecoder)super.getDecoder(); } @Override public int cycleCount() { return getDecoder().listCycles.size(); } @Override public int cycleFrameCount() { return cycleFrameCount(currentCycle); } @Override public int cycleFrameCount(int cycleIdx) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { return getDecoder().listCycles.get(cycleIdx).size(); } return 0; } @Override public int cycleGet() { return currentCycle; } @Override public boolean cycleSet(int cycleIdx) { if (cycleIdx >= 0 && cycleIdx < getDecoder().listCycles.size()) { currentCycle = cycleIdx; if (isSharedPerCycle()) { updateSharedBamSize(); } return true; } return false; } @Override public boolean cycleHasNextFrame() { if (currentCycle >= 0 && currentCycle < getDecoder().listCycles.size()) { return (currentFrame >= 0 && currentFrame < getDecoder().listCycles.get(currentCycle).size() - 1); } 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 >= 0 && currentCycle < getDecoder().listCycles.size() && frameIdx >= 0 && frameIdx < getDecoder().listCycles.get(currentCycle).size()) { 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).size()) { return getDecoder().listCycles.get(cycleIdx).get(frameIdx); } else { return -1; } } private void init() { currentCycle = currentFrame = -1; update(); updateSharedBamSize(); } // Updates current cycle and frame pointers private void update() { if (getDecoder().listCycles.isEmpty()) { currentCycle = currentFrame = -1; } else { if (currentCycle < 0) { currentCycle = 0; if (getDecoder().listCycles.get(currentCycle).size() == 0) { currentFrame = -1; } else { currentFrame = 0; } } else if (currentCycle >= getDecoder().listCycles.size()) { currentCycle = getDecoder().listCycles.size() - 1; if (getDecoder().listCycles.get(currentCycle).size() == 0) { currentFrame = -1; } else { currentFrame = 0; } } } updateSharedBamSize(); } } /** Stores information for a single cycle */ public static class PseudoBamCycleEntry { private final List<Integer> frames; // stores abs. frame indices that define this cycle private final HashMap<String, Object> mapOptions = new HashMap<String, Object>(); protected PseudoBamCycleEntry(int[] indices) { frames = new ArrayList<Integer>(); add(indices); } /** Returns all available options by name. */ public String[] getOptionNames() { String[] retVal = new String[mapOptions.keySet().size()]; Iterator<String> iter = mapOptions.keySet().iterator(); int idx = 0; while (iter.hasNext()) { retVal[idx++] = iter.next(); } return retVal; } /** Returns the value of the specified option. */ public Object getOption(String name) { if (name != null) { return mapOptions.get(name); } return null; } /** Adds a custom option to this cycle. */ public void setOption(String name, Object value) { if (name != null) { mapOptions.put(name, value); } } /** Returns the number of stored frame indices. */ public int size() { return frames.size(); } /** Returns the frame index at specified position. Returns -1 on error. */ public int get(int pos) { if (pos >= 0 && pos < frames.size()) { return frames.get(pos).intValue(); } else { return -1; } } /** Replaces the frame index value at the specified position. Note: Does not validate frameIdx! */ public void set(int pos, int frameIdx) { if (pos >= 0 && pos < frames.size()) { frames.set(pos, frameIdx); } } /** Removes all frame indices. */ public void clear() { frames.clear(); } /** Appends specified indices to list. */ public void add(int[] indices) { insert(frames.size(), indices); } /** Inserts indices at specified position. */ public boolean insert(int pos, int[] indices) { if (indices != null && pos >= 0 && pos <= frames.size()) { for (int i = 0; i < indices.length; i++) { frames.add(pos + i, indices[i]); } return true; } return false; } /** Removes count indices at specified position. */ public boolean remove(int pos, int count) { if (pos >= 0 && pos < frames.size()) { if (pos + count > frames.size()) { count = frames.size() - pos; } for (int i = 0; i < count; i++) { frames.remove(pos); } return count > 0; } return false; } @Override public String toString() { StringBuilder sb = new StringBuilder("["); for (int i = 0; i < frames.size(); i++) { sb.append(Integer.toString(frames.get(i))); if (i < frames.size() - 1) { sb.append(", "); } } sb.append("]"); return sb.toString(); } } // Storage for BAM v2 frame data blocks private static class FrameDataV2 { public int page, sx, sy, width, height, dx, dy; public FrameDataV2(int page, int sx, int sy, int width, int height, int dx, int dy) { this.page = page; this.sx = sx; this.sy = sy; this.width = width; this.height = height; this.dx = dx; this.dy = dy; } @Override public boolean equals(Object o) { if (o instanceof FrameDataV2) { FrameDataV2 fd = (FrameDataV2)o; return (fd.page == page && fd.sx == sx && fd.sy == sy && fd.width == width && fd.height == height && fd.dx == dx && fd.dy == dy); } return false; } } }