package com.riiablo.codec; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.glutils.PixmapTextureData; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.StreamUtils; import com.riiablo.codec.util.BBox; import com.riiablo.graphics.PaletteIndexedPixmap; import com.riiablo.util.BufferUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; public class DC6 extends com.riiablo.codec.DC { private static final String TAG = "DC6"; private static final boolean DEBUG = true; private static final boolean DEBUG_DIRECTIONS = DEBUG && false; private static final boolean DEBUG_FRAMES = DEBUG && false; private static final boolean DEBUG_SHEETS = DEBUG && false; public static final String EXT = "dc6"; public static final int PAGE_SIZE = 256; Header header; Direction directions[]; Frame frames[][]; BBox box; Pixmap pixmaps[][]; Texture textures[][]; private static DC6 obtain(Header header, Direction[] directions, Frame[][] frames, BBox box) { return new DC6().set(header, directions, frames, box); } private DC6() {} private DC6 set(Header header, Direction[] directions, Frame[][] frames, BBox box) { this.header = header; this.directions = directions; this.frames = frames; this.box = box; this.regions = new TextureRegion[header.directions][]; return this; } @Override public void dispose() { disposeFrames(); disposePixmaps(); disposeTextures(); } private void disposeFrames() { final int numDirections = header.directions; for (int d = 0; d < numDirections; d++) { Frame[] frames = this.frames[d]; if (frames != null) for (Frame frame : frames) { frame.pixmap.dispose(); } } } private void disposePixmaps() { if (pixmaps == null) return; final int numDirections = header.directions; for (int d = 0; d < numDirections; d++) { Frame[] frames = this.frames[d]; Pixmap[] pixmaps = this.pixmaps[d]; if (pixmaps != null && (frames == null || frames.length != pixmaps.length)) { for (Pixmap pixmap : pixmaps) pixmap.dispose(); } } pixmaps = null; } private void disposeTextures() { if (textures == null) return; final int numDirections = header.directions; for (int d = 0; d < numDirections; d++) { Texture[] frames = textures[d]; if (frames != null) for (Texture frame : frames) frame.dispose(); } textures = null; } @Override public int getNumPages(int d) { return textures[d].length; } @Override public Pixmap getPixmap(int d, int f) { return frames[d][f].pixmap; } @Override public TextureRegion getTexture(int d, int i) { assert regions[d] != null : "loadDirection(d) must be called before getTexture(d,i)"; return regions[d][i]; } @Override public boolean isPreloaded(int d) { return pixmaps != null && pixmaps[d] != null; } @Override public void preloadDirections(boolean combineFrames) { final int numDirections = header.directions; for (int d = 0; d < numDirections; d++) preloadDirection(d, combineFrames); } @Override public void preloadDirection(int d, boolean combineFrames) { if (pixmaps == null) pixmaps = new Pixmap[header.directions][]; else if (pixmaps[d] != null) return; if (!combineFrames) { pixmaps[d] = new Pixmap[header.framesPerDir]; for (int f = 0; f < header.framesPerDir; f++) pixmaps[d][f] = frames[d][f].pixmap; return; } final Frame[] frames = this.frames[d]; final int numFrames = header.framesPerDir; int columns = 0, rows = 0; int width = 0; for (int w = 0, tmp; w < numFrames; w++) { columns++; tmp = frames[w].width; width += tmp; if (tmp < PAGE_SIZE) { break; } } int height = 0; for (int h = 0, tmp; h < numFrames; h += columns) { rows++; tmp = frames[h].height; height += tmp; if (tmp < PAGE_SIZE) { break; } } final int numPages = numFrames / (rows * columns); Pixmap[] pixmaps = this.pixmaps[d] = new Pixmap[numPages]; if (numPages == numFrames) { for (int f = 0; f < numFrames; f++) pixmaps[f] = frames[f].pixmap; return; } for (int p = 0, f = 0; p < numPages; p++) { int y = 0, x = 0; Frame frame; Pixmap pixmap = pixmaps[p] = new PaletteIndexedPixmap(width, height); for (int r = 0; r < rows; r++, x = 0) { for (int c = 0; c < columns; c++) { frame = frames[f++]; pixmap.drawPixmap(frame.pixmap, x, y); x += frame.width; } y += PAGE_SIZE; } } } @Override public boolean isLoaded(int d) { return textures != null && textures[d] != null; } @Override public void loadDirections(boolean combineFrames) { final int numDirections = header.directions; for (int d = 0; d < numDirections; d++) loadDirection(d, combineFrames); } @Override public void loadDirection(int d, boolean combineFrames) { if (textures == null) textures = new Texture[header.directions][]; else if (textures[d] != null) return; preloadDirection(d, combineFrames); Pixmap[] pixmaps = this.pixmaps[d]; textures[d] = new Texture[pixmaps.length]; for (int p = 0; p < pixmaps.length; p++) { Pixmap pixmap = pixmaps[p]; Texture texture = new Texture(new PixmapTextureData(pixmap, null, false, false, false)); //texture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear); texture.setWrap(Texture.TextureWrap.ClampToEdge, Texture.TextureWrap.ClampToEdge); textures[d][p] = texture; } regions[d] = new TextureRegion[pixmaps.length]; for (int p = 0; p < pixmaps.length; p++) { regions[d][p] = new TextureRegion(textures[d][p]); } } @Override public Direction getDirection(int d) { return directions[d]; } @Override public Frame getFrame(int d, int f) { return frames[d][f]; } @Override public int getNumDirections() { return header.directions; } @Override public int getNumFramesPerDir() { return header.framesPerDir; } @Override public BBox getBox() { return box; } @Override public BBox getBox(int d) { return directions[d].box; } @Override public BBox getBox(int d, int f) { return frames[d][f].box; } public static DC6 loadFromFile(FileHandle handle) { return loadFromStream(handle.read()); } public static DC6 loadFromStream(InputStream in) { try { final int fileSize = in.available(); Header header = Header.obtain(in); if (DEBUG) Gdx.app.debug(TAG, header.toString()); int numDirections = header.directions; int numFrames = header.framesPerDir; int totalFrames = numDirections * numFrames; int[] frameOffsets = new int[totalFrames + 1]; ByteBuffer.wrap(IOUtils.readFully(in, totalFrames << 2)) .order(ByteOrder.LITTLE_ENDIAN) .asIntBuffer() .get(frameOffsets, 0, totalFrames); frameOffsets[totalFrames] = fileSize; if (DEBUG) Gdx.app.debug(TAG, "frame offsets = " + Arrays.toString(frameOffsets)); Frame[][] frames = new Frame[numDirections][numFrames]; int start = frameOffsets[0], end; for (int d = 0, df = 0; d < numDirections; d++) { for (int f = 0; f < numFrames; f++, df++) { end = frameOffsets[df + 1]; frames[d][f] = Frame.obtain(in, end - start); if (DEBUG_FRAMES) Gdx.app.debug(TAG, frames[d][f].toString()); start = end; } } BBox box = new BBox(); box.xMin = box.yMin = Integer.MAX_VALUE; box.xMax = box.yMax = Integer.MIN_VALUE; Direction[] directions = new Direction[header.directions]; for (int d = 0; d < header.directions; d++) { Direction dir = directions[d] = Direction.obtain(frames[d]); if (DEBUG_DIRECTIONS) Gdx.app.debug(TAG, dir.toString()); if (dir.box.xMin < box.xMin) box.xMin = dir.box.xMin; if (dir.box.yMin < box.yMin) box.yMin = dir.box.yMin; if (dir.box.xMax > box.xMax) box.xMax = dir.box.xMax; if (dir.box.yMax > box.yMax) box.yMax = dir.box.yMax; } box.width = box.xMax - box.xMin + 1; box.height = box.yMax - box.yMin + 1; return DC6.obtain(header, directions, frames, box); } catch (Throwable t) { throw new GdxRuntimeException("Couldn't load DC6 from stream.", t); } finally { StreamUtils.closeQuietly(in); } } static class Header { static final int SIZE = 24; static final int MAGIC_NUMBER_1 = 0xEEEEEEEE; static final int MAGIC_NUMBER_2 = 0xCDCDCDCD; int version; int flags; int format; int termination; int directions; int framesPerDir; static Header obtain(InputStream in) throws IOException { return new Header().read(in); } Header read(InputStream in) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(IOUtils.readFully(in, SIZE)).order(ByteOrder.LITTLE_ENDIAN); version = buffer.getInt(); flags = buffer.getInt(); format = buffer.getInt(); termination = buffer.getInt(); directions = buffer.getInt(); framesPerDir = buffer.getInt(); assert !buffer.hasRemaining(); return this; } @Override public String toString() { return new ToStringBuilder(this) .append("version", version) .append("flags", flags) .append("format", format) .append("termination", "0x" + Integer.toHexString(termination)) .append("directions", directions) .append("framesPerDir", framesPerDir) .toString(); } } static class Direction extends com.riiablo.codec.DC.Direction { //BBox box; static Direction obtain(Frame[] frames) { return new Direction().read(frames); } Direction read(Frame[] frames) { box = new BBox(); box.xMin = box.yMin = Integer.MAX_VALUE; box.xMax = box.yMax = Integer.MIN_VALUE; for (int f = 0; f < frames.length; f++) { Frame frame = frames[f]; if (frame.box.xMin < box.xMin) box.xMin = frame.box.xMin; if (frame.box.yMin < box.yMin) box.yMin = frame.box.yMin; if (frame.box.xMax > box.xMax) box.xMax = frame.box.xMax; if (frame.box.yMax > box.yMax) box.yMax = frame.box.yMax; } box.width = box.xMax - box.xMin + 1; box.height = box.yMax - box.yMin + 1; return this; } @Override public String toString() { return new ToStringBuilder(this) .append("box", box) .toString(); } } static class Frame extends DC.Frame { static final int SIZE = 32; //int flip; //int width; //int height; //int xOffset; //int yOffset; int allocSize; int nextBlock; int length; //BBox box; //byte colormap[]; Pixmap pixmap; static Frame obtain(InputStream in, int size) throws IOException { return new Frame().read(in, size); } Frame read(InputStream in, int size) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(IOUtils.readFully(in, size)).order(ByteOrder.LITTLE_ENDIAN); flip = buffer.getInt(); width = buffer.getInt(); height = buffer.getInt(); xOffset = buffer.getInt(); yOffset = buffer.getInt(); allocSize = buffer.getInt(); nextBlock = buffer.getInt(); length = buffer.getInt(); box = new BBox(); box.xMin = xOffset; box.xMax = box.xMin + width - 1; if (flip > 0) { // bottom-up box.yMin = yOffset; box.yMax = box.yMin + height - 1; } else { // top-down box.yMax = yOffset; box.yMin = box.yMax - height + 1; } box.width = box.xMax - box.xMin + 1; box.height = box.yMax - box.yMin + 1; colormap = new byte[width * height]; decompress(buffer); pixmap = new PaletteIndexedPixmap(width, height, colormap); return this; } void decompress(ByteBuffer in) throws IOException { assert width > 0 && height > 0; // TODO: Support flipping? int x = 0, y = height - 1; int rawIndex = 0; while (rawIndex < length) { int chunkSize = BufferUtils.readUnsignedByte(in); rawIndex++; if (chunkSize == 0x80) { // eol x = 0; y--; } else if ((chunkSize & 0x80) != 0) { // number of transparent pixels x += (chunkSize & 0x7F); } else { // number of colors to read assert chunkSize + x <= width; in.get(colormap, x + width * y, chunkSize); rawIndex += chunkSize; x += chunkSize; } } assert length == rawIndex; } @Override public String toString() { return new ToStringBuilder(this) .append("flip", flip) .append("width", width) .append("height", height) .append("xOffset", xOffset) .append("yOffset", yOffset) .append("allocSize", allocSize) .append("nextBlock", nextBlock) .append("length", length) .append("box", box) .toString(); } } }