From 9e66e0b552796699a95a0c201f2f667dddecd9c8 Mon Sep 17 00:00:00 2001 From: Graham Date: Wed, 25 Aug 2021 15:46:16 +0100 Subject: [PATCH] Add sprite encoder/decoder This implementation supports: * Encoding in column- or row-major order (based on a very rough heuristic). * Preserving the colours of transparent pixels. * Cutting off the borders of a transparent frame (if there is no colour to preserve). Signed-off-by: Graham --- .../kotlin/org/openrs2/cache/sprite/Sprite.kt | 427 ++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 cache-550/src/main/kotlin/org/openrs2/cache/sprite/Sprite.kt diff --git a/cache-550/src/main/kotlin/org/openrs2/cache/sprite/Sprite.kt b/cache-550/src/main/kotlin/org/openrs2/cache/sprite/Sprite.kt new file mode 100644 index 0000000000..3dc0b55449 --- /dev/null +++ b/cache-550/src/main/kotlin/org/openrs2/cache/sprite/Sprite.kt @@ -0,0 +1,427 @@ +package org.openrs2.cache.sprite + +import io.netty.buffer.ByteBuf +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet +import java.awt.image.BufferedImage + +public class Sprite private constructor( + public val width: Int, + public val height: Int, + private val palette: IntArray, + private val frames: Array +) { + private class Frame( + val xOffset: Int, + val yOffset: Int, + val innerWidth: Int, + val innerHeight: Int, + ) { + val pixels = ByteArray(innerWidth * innerHeight) + var alpha: ByteArray? = null + + fun toImage(width: Int, height: Int, palette: IntArray): BufferedImage { + val rgbPixels = IntArray(width * height) + + var index = 0 + for (y in 0 until innerHeight) { + for (x in 0 until innerWidth) { + val paletteIndex = pixels[index].toInt() and 0xFF + + var color = if (paletteIndex == 0) { + 0 + } else { + palette[paletteIndex - 1] + } + + val alpha = alpha + if (alpha != null) { + color = color or ((alpha[index].toInt() and 0xFF) shl 24) + } else if (paletteIndex != 0) { + color = color or 0xFF000000.toInt() + } + + rgbPixels[(y + yOffset) * width + x + xOffset] = color + index++ + } + } + + val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + image.setRGB(0, 0, width, height, rgbPixels, 0, width) + return image + } + + fun write(buf: ByteBuf) { + var flags = 0 + + val columnMajor = isColumnMajorBest() + if (columnMajor) { + flags = flags or FLAG_COLUMN_MAJOR + } + + val alpha = alpha + if (alpha != null) { + flags = flags or FLAG_ALPHA + } + + buf.writeByte(flags) + + if (columnMajor) { + for (x in 0 until innerWidth) { + for (y in 0 until innerHeight) { + buf.writeByte(pixels[y * innerWidth + x].toInt()) + } + } + + if (alpha != null) { + for (x in 0 until innerWidth) { + for (y in 0 until innerHeight) { + buf.writeByte(alpha[y * innerWidth + x].toInt()) + } + } + } + } else { + buf.writeBytes(pixels) + + if (alpha != null) { + buf.writeBytes(alpha) + } + } + } + + private fun isColumnMajorBest(): Boolean { + var rowMajorScore = 0 + var columnMajorScore = 0 + + // calculate row-major score + var prev = 0 + for (pixel in pixels) { + val current = pixel.toInt() and 0xFF + rowMajorScore += current - prev + prev = current + } + + val alpha = alpha + if (alpha != null) { + for (a in alpha) { + val current = a.toInt() and 0xFF + rowMajorScore += current - prev + prev = current + } + } + + // calculate column-major score + prev = 0 + for (x in 0 until innerWidth) { + for (y in 0 until innerHeight) { + val current = pixels[y * innerWidth + x].toInt() and 0xFF + columnMajorScore += current - prev + prev = current + } + } + + if (alpha != null) { + for (x in 0 until innerWidth) { + for (y in 0 until innerHeight) { + val current = alpha[y * innerWidth + x].toInt() and 0xFF + columnMajorScore += current - prev + prev = current + } + } + } + + // if equal pick row-major, as it's faster to encode/decode + return columnMajorScore < rowMajorScore + } + + companion object { + fun read(buf: ByteBuf, xOffset: Int, yOffset: Int, innerWidth: Int, innerHeight: Int): Frame { + val frame = Frame(xOffset, yOffset, innerWidth, innerHeight) + + val flags = buf.readUnsignedByte().toInt() + + val alpha = if ((flags and FLAG_ALPHA) != 0) { + ByteArray(frame.pixels.size) + } else { + null + } + + if ((flags and FLAG_COLUMN_MAJOR) != 0) { + for (x in 0 until frame.innerWidth) { + for (y in 0 until frame.innerHeight) { + frame.pixels[y * frame.innerWidth + x] = buf.readByte() + } + } + + if (alpha != null) { + for (x in 0 until frame.innerWidth) { + for (y in 0 until frame.innerHeight) { + alpha[y * frame.innerWidth + x] = buf.readByte() + } + } + } + } else { + buf.readBytes(frame.pixels) + + if (alpha != null) { + buf.readBytes(alpha) + } + } + + frame.alpha = alpha + return frame + } + } + } + + public fun write(buf: ByteBuf) { + for (frame in frames) { + frame.write(buf) + } + + for (color in palette) { + buf.writeMedium(color) + } + + buf.writeShort(width) + buf.writeShort(height) + buf.writeByte(palette.size) + + for (frame in frames) { + buf.writeShort(frame.xOffset) + } + + for (frame in frames) { + buf.writeShort(frame.yOffset) + } + + for (frame in frames) { + buf.writeShort(frame.innerWidth) + } + + for (frame in frames) { + buf.writeShort(frame.innerHeight) + } + + buf.writeShort(frames.size) + } + + public fun toImages(): List { + return frames.map { frame -> + frame.toImage(width, height, palette) + } + } + + public companion object { + private const val FLAG_COLUMN_MAJOR = 0x1 + private const val FLAG_ALPHA = 0x2 + + public fun read(buf: ByteBuf): Sprite { + val framesSize = buf.getUnsignedShort(buf.writerIndex() - 2) + var trailerLen = framesSize * 8 + 7 + + buf.markReaderIndex() + buf.readerIndex(buf.writerIndex() - trailerLen) + + val width = buf.readUnsignedShort() + val height = buf.readUnsignedShort() + val paletteSize = buf.readUnsignedByte().toInt() + trailerLen += paletteSize * 3 + + val xOffsets = IntArray(framesSize) { buf.readUnsignedShort() } + val yOffsets = IntArray(framesSize) { buf.readUnsignedShort() } + val innerWidths = IntArray(framesSize) { buf.readUnsignedShort() } + val innerHeights = IntArray(framesSize) { buf.readUnsignedShort() } + + buf.readerIndex(buf.writerIndex() - trailerLen) + + val palette = IntArray(paletteSize) { buf.readUnsignedMedium() } + + buf.resetReaderIndex() + + val frames = Array(framesSize) { i -> + Frame.read(buf, xOffsets[i], yOffsets[i], innerWidths[i], innerHeights[i]) + } + + buf.skipBytes(trailerLen) + + return Sprite(width, height, palette, frames) + } + + public fun fromImage(image: BufferedImage): Sprite { + return fromImages(listOf(image)) + } + + public fun fromImages(images: List): Sprite { + require(images.isNotEmpty()) + + val first = images.first() + val width = first.width + val height = first.height + + val alphaChannels = BooleanArray(images.size) + for ((i, image) in images.withIndex()) { + require(image.width == width && image.height == height) + alphaChannels[i] = image.hasAlphaChannel() + } + + val colors = IntAVLTreeSet() + for ((i, image) in images.withIndex()) { + val alphaChannel = alphaChannels[i] + + for (y in 0 until image.height) { + for (x in 0 until image.width) { + val rgb = image.getRGB(x, y) + + // transparent colours only preserved if FLAG_ALPHA set + val alpha = (rgb shr 24) and 0xFF + if (alphaChannel || alpha != 0) { + val color = rgb and 0xFFFFFF + colors += color + } + } + } + } + + require(colors.size <= 255) + + val palette = colors.toIntArray() + + val frames = Array(images.size) { i -> + val image = images[i] + val alphaChannel = alphaChannels[i] + + var xOffset = 0 + var innerWidth = width + + for (x in 0 until width) { + if (!image.isColumnTransparent(x, alphaChannel)) { + break + } + + xOffset++ + innerWidth-- + } + + for (x in width - 1 downTo xOffset) { + if (!image.isColumnTransparent(x, alphaChannel)) { + break + } + + innerWidth-- + } + + var yOffset = 0 + var innerHeight = height + + for (y in 0 until height) { + if (!image.isRowTransparent(y, alphaChannel)) { + break + } + + yOffset++ + innerHeight-- + } + + for (y in height - 1 downTo yOffset) { + if (!image.isRowTransparent(y, alphaChannel)) { + break + } + + innerHeight-- + } + + val frame = Frame(xOffset, yOffset, innerWidth, innerHeight) + if (alphaChannel) { + frame.alpha = ByteArray(innerWidth * innerHeight) + } + + val alpha = frame.alpha + var index = 0 + for (y in 0 until innerHeight) { + for (x in 0 until innerWidth) { + val rgb = image.getRGB(x + xOffset, y + yOffset) + + val a = (rgb shr 24) and 0xFF + val color = rgb and 0xFFFFFF + + val paletteIndex = if (alpha == null && a == 0) { + 0 + } else { + val paletteIndex = palette.binarySearch(color) + check(paletteIndex >= 0) + paletteIndex + 1 + } + + frame.pixels[index] = paletteIndex.toByte() + + if (alpha != null) { + alpha[index] = a.toByte() + } + + index++ + } + } + + frame + } + + return Sprite(width, height, palette, frames) + } + + private fun BufferedImage.hasAlphaChannel(): Boolean { + for (y in 0 until height) { + for (x in 0 until width) { + val rgb = getRGB(x, y) + + val alpha = (rgb shr 24) and 0xFF + val color = rgb and 0xFFFFFF + + // preserve transparent colours + if (alpha == 0 && color != 0) { + return true + } else if (alpha != 0 && alpha != 0xFF) { + return true + } + } + } + + return false + } + + private fun BufferedImage.isColumnTransparent(x: Int, alphaChannel: Boolean): Boolean { + for (y in 0 until height) { + val rgb = getRGB(x, y) + + val alpha = (rgb shr 24) and 0xFF + val color = rgb and 0xFFFFFF + + // transparent colours only preserved if FLAG_ALPHA set + if (alphaChannel && color != 0) { + return false + } else if (alpha != 0) { + return false + } + } + + return true + } + + private fun BufferedImage.isRowTransparent(y: Int, alphaChannel: Boolean): Boolean { + for (x in 0 until width) { + val rgb = getRGB(x, y) + + val alpha = (rgb shr 24) and 0xFF + val color = rgb and 0xFFFFFF + + // transparent colours only preserved if FLAG_ALPHA set + if (alphaChannel && color != 0) { + return false + } else if (alpha != 0) { + return false + } + } + + return true + } + } +}