forked from openrs2/openrs2
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 <gpe@openrs2.org>
parent
aaea619550
commit
9e66e0b552
@ -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<Frame> |
||||
) { |
||||
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<BufferedImage> { |
||||
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<BufferedImage>): 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 |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue