package net.scapeemulator.cache.def; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import net.scapeemulator.cache.util.ByteBufferUtils; /** * Represents a {@link Sprite} which may contain one or more frames. * @author Graham * @author `Discardedx2 */ public final class Sprite { /** * This flag indicates that the pixels should be read vertically instead of * horizontally. */ public static final int FLAG_VERTICAL = 0x01; /** * This flag indicates that every pixel has an alpha, as well as red, green * and blue, component. */ public static final int FLAG_ALPHA = 0x02; /** * Decodes the {@link Sprite} from the specified {@link ByteBuffer}. * @param buffer The buffer. * @return The sprite. */ public static Sprite decode(ByteBuffer buffer) { /* find the size of this sprite set */ buffer.position(buffer.limit() - 2); int size = buffer.getShort() & 0xFFFF; /* allocate arrays to store info */ int[] offsetsX = new int[size]; int[] offsetsY = new int[size]; int[] subWidths = new int[size]; int[] subHeights = new int[size]; /* read the width, height and palette size */ buffer.position(buffer.limit() - size * 8 - 7); int width = buffer.getShort() & 0xFFFF; int height = buffer.getShort() & 0xFFFF; int[] palette = new int[(buffer.get() & 0xFF) + 1]; /* and allocate an object for this sprite set */ Sprite set = new Sprite(width, height, size); /* read the offsets and dimensions of the individual sprites */ for (int i = 0; i < size; i++) { offsetsX[i] = buffer.getShort() & 0xFFFF; } for (int i = 0; i < size; i++) { offsetsY[i] = buffer.getShort() & 0xFFFF; } for (int i = 0; i < size; i++) { subWidths[i] = buffer.getShort() & 0xFFFF; } for (int i = 0; i < size; i++) { subHeights[i] = buffer.getShort() & 0xFFFF; } /* read the palette */ buffer.position(buffer.limit() - size * 8 - 7 - (palette.length - 1) * 3); palette[0] = 0; /* transparent colour (black) */ for (int index = 1; index < palette.length; index++) { palette[index] = ByteBufferUtils.getTriByte(buffer); if (palette[index] == 0) palette[index] = 1; } /* read the pixels themselves */ buffer.position(0); for (int id = 0; id < size; id++) { /* grab some frequently used values */ int subWidth = subWidths[id], subHeight = subHeights[id]; int offsetX = offsetsX[id], offsetY = offsetsY[id]; /* create a BufferedImage to store the resulting image */ BufferedImage image = set.frames[id] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); /* allocate an array for the palette indices */ int[][] indices = new int[subWidth][subHeight]; /* read the flags so we know whether to read horizontally or vertically */ int flags = buffer.get() & 0xFF; /* now read the image */ if (image != null) { /* read the palette indices */ if ((flags & FLAG_VERTICAL) != 0) { for (int x = 0; x < subWidth; x++) { for (int y = 0; y < subHeight; y++) { indices[x][y] = buffer.get() & 0xFF; } } } else { for (int y = 0; y < subHeight; y++) { for (int x = 0; x < subWidth; x++) { indices[x][y] = buffer.get() & 0xFF; } } } /* read the alpha (if there is alpha) and convert values to ARGB */ if ((flags & FLAG_ALPHA) != 0) { if ((flags & FLAG_VERTICAL) != 0) { for (int x = 0; x < subWidth; x++) { for (int y = 0; y < subHeight; y++) { int alpha = buffer.get() & 0xFF; image.setRGB(x + offsetX, y + offsetY, alpha << 24 | palette[indices[x][y]]); } } } else { for (int y = 0; y < subHeight; y++) { for (int x = 0; x < subWidth; x++) { int alpha = buffer.get() & 0xFF; image.setRGB(x + offsetX, y + offsetY, alpha << 24 | palette[indices[x][y]]); } } } } else { for (int x = 0; x < subWidth; x++) { for (int y = 0; y < subHeight; y++) { int index = indices[x][y]; if (index == 0) { image.setRGB(x + offsetX, y + offsetY, 0); } else { image.setRGB(x + offsetX, y + offsetY, 0xFF000000 | palette[index]); } } } } } } return set; } /** * The width of this sprite. */ private final int width; /** * The height of this sprite. */ private final int height; /** * The array of animation frames in this sprite. */ private final BufferedImage[] frames; /** * Creates a new sprite with one frame. * @param width The width of the sprite in pixels. * @param height The height of the sprite in pixels. */ public Sprite(int width, int height) { this(width, height, 1); } /** * Creates a new sprite with the specified number of frames. * @param width The width of the sprite in pixels. * @param height The height of the sprite in pixels. * @param size The number of animation frames. */ public Sprite(int width, int height, int size) { if (size < 1) throw new IllegalArgumentException(); this.width = width; this.height = height; this.frames = new BufferedImage[size]; } /** * Gets the width of this sprite. * @return The width of this sprite. */ public int getWidth() { return width; } /** * Gets the height of this sprite. * @return The height of this sprite. */ public int getHeight() { return height; } /** * Gets the number of frames in this set. * @return The number of frames. */ public int size() { return frames.length; } /** * Gets the frame with the specified id. * @param id The id. * @return The frame. */ public BufferedImage getFrame(int id) { return frames[id]; } /** * Sets the frame with the specified id. * @param id The id. * @param frame The frame. */ public void setFrame(int id, BufferedImage frame) { if (frame.getWidth() != width || frame.getHeight() != height) throw new IllegalArgumentException("The frame's dimensions do not match with the sprite's dimensions."); frames[id] = frame; } /** * Encodes this {@link Sprite} into a {@link ByteBuffer}. * <p /> * Please note that this is a fairly simple implementation which only * supports vertical encoding. It does not attempt to use the offsets * to save space. * @return The buffer. * @throws IOException if an I/O exception occurs. */ @SuppressWarnings("resource") public ByteBuffer encode() throws IOException { try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream os = new DataOutputStream(bout)) { /* set up some variables */ List<Integer> palette = new ArrayList<>(); palette.add(0); /* transparent colour */ /* write the sprites */ for (BufferedImage image : frames) { /* check if we can encode this */ if (image.getWidth() != width || image.getHeight() != height) throw new IOException("All frames must be the same size."); /* loop through all the pixels constructing a palette */ int flags = FLAG_VERTICAL; // TODO: do we need to support horizontal encoding? for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { /* grab the colour of this pixel */ int argb = image.getRGB(x, y); int alpha = (argb >> 24) & 0xFF; int rgb = argb & 0xFFFFFF; if (rgb == 0) rgb = 1; /* we need an alpha channel to encode this image */ if (alpha != 0 && alpha != 255) flags |= FLAG_ALPHA; /* add the colour to the palette if it isn't already in the palette */ if (!palette.contains(rgb)) { if (palette.size() >= 256) throw new IOException("Too many colours in this sprite!"); palette.add(rgb); } } } /* write this sprite */ os.write(flags); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { int argb = image.getRGB(x, y); int alpha = (argb >> 24) & 0xFF; int rgb = argb & 0xFFFFFF; if (rgb == 0) rgb = 1; if ((flags & FLAG_ALPHA) == 0 && alpha == 0) { os.write(0); } else { os.write(palette.indexOf(rgb)); } } } /* write the alpha channel if this sprite has one */ if ((flags & FLAG_ALPHA) != 0) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { int argb = image.getRGB(x, y); int alpha = (argb >> 24) & 0xFF; os.write(alpha); } } } } /* write the palette */ for (int i = 1; i < palette.size(); i++) { int rgb = palette.get(i); os.write((byte) (rgb >> 16)); os.write((byte) (rgb >> 8)); os.write((byte) rgb); } /* write the max width, height and palette size */ os.writeShort(width); os.writeShort(height); os.write(palette.size() - 1); /* write the individual offsets and dimensions */ for (int i = 0; i < frames.length; i++) { os.writeShort(0); // offset X os.writeShort(0); // offset Y os.writeShort(width); os.writeShort(height); } /* write the number of frames */ os.writeShort(frames.length); /* convert the stream to a byte array and then wrap a buffer */ byte[] bytes = bout.toByteArray(); return ByteBuffer.wrap(bytes); } } }