Skip to content

Commit

Permalink
Supports generating code for .TTF files and .ASE files (with tags/ani…
Browse files Browse the repository at this point in the history
…mations and slices information) (#1741)

* Supports generating code for .TTF files and .ASE files (with tags/animations and slices information)

* Adds ImageDataView. play and stop helpers
  • Loading branch information
soywiz authored Jul 1, 2023
1 parent 8c1ddc0 commit 514d29d
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,34 @@ class TypedResourcesGenerator {

fun generateForFolders(resourcesFolder: SFile): String {
return Indenter {
line("import korlibs.image.atlas.Atlas")
line("import korlibs.io.file.VfsFile")
line("import korlibs.io.file.std.resourcesVfs")
line("import korlibs.image.atlas.readAtlas")
line("import korlibs.audio.sound.readSound")
line("import korlibs.image.format.readBitmap")
line("import korlibs.audio.sound.*")
line("import korlibs.io.file.*")
line("import korlibs.io.file.std.*")
line("import korlibs.image.bitmap.*")
line("import korlibs.image.atlas.*")
line("import korlibs.image.font.*")
line("import korlibs.image.format.*")
line("")
line("// AUTO-GENERATED FILE! DO NOT MODIFY!")
line("")
line("@Retention(AnnotationRetention.BINARY) annotation class ResourceVfsPath(val path: String)")
line("inline class TypedVfsFile(val __file: VfsFile)")
line("inline class TypedVfsFileBitmap(val __file: VfsFile) { suspend fun read(): korlibs.image.bitmap.Bitmap = this.__file.readBitmap() }")
line("inline class TypedVfsFileSound(val __file: VfsFile) { suspend fun read(): korlibs.audio.sound.Sound = this.__file.readSound() }")
line("inline class TypedVfsFileTTF(val __file: VfsFile) {")
line(" suspend fun read(): korlibs.image.font.TtfFont = this.__file.readTtfFont()")
line("}")
line("inline class TypedVfsFileBitmap(val __file: VfsFile) {")
line(" suspend fun read(): korlibs.image.bitmap.Bitmap = this.__file.readBitmap()")
line(" suspend fun readSlice(atlas: MutableAtlasUnit? = null, name: String? = null): BmpSlice = this.__file.readBitmapSlice(name, atlas)")
line("}")
line("inline class TypedVfsFileSound(val __file: VfsFile) {")
line(" suspend fun read(): korlibs.audio.sound.Sound = this.__file.readSound()")
line("}")
line("interface TypedAtlas<T>")

data class AtlasInfo(val file: SFile, val className: String)
data class ExtraInfo(val file: SFile, val className: String)

val atlases = arrayListOf<AtlasInfo>()
val atlases = arrayListOf<ExtraInfo>()
val ases = arrayListOf<ExtraInfo>()

val exploredFolders = LinkedHashSet<SFile>()
val foldersToExplore = ArrayDeque<SFile>()
Expand Down Expand Up @@ -78,11 +88,17 @@ class TypedResourcesGenerator {
val type: String? = when (extension) {
"png", "jpg" -> "TypedVfsFileBitmap"
"mp3", "wav" -> "TypedVfsFileSound"
"ttf", "otf" -> "TypedVfsFileTTF"
"ase" -> {
val className = "Ase${fullVarName.textCase().pascalCase()}"
ases += ExtraInfo(file, className)
"$className.TypedAse"
}
"atlas" -> {
if (isDirectory) {
extraSuffix += ".json"
val className = "Atlas${fullVarName.textCase().pascalCase()}"
atlases += AtlasInfo(file, className)
atlases += ExtraInfo(file, className)
"$className.TypedAtlas"
} else {
"TypedVfsFile"
Expand Down Expand Up @@ -123,6 +139,51 @@ class TypedResourcesGenerator {
}
}
}

for (ase in ases) {
line("")
line("inline class ${ase.className}(val data: korlibs.image.format.ImageDataContainer)") {
line("inline class TypedAse(val __file: VfsFile) { suspend fun read(atlas: korlibs.image.atlas.MutableAtlasUnit? = null): ${ase.className} = ${ase.className}(this.__file.readImageDataContainer(korlibs.image.format.ASE.toProps(), atlas)) }")
val aseFile = ase.file

try {
val info = ASEInfo.getAseInfo(ase.file.readBytes())

line("enum class TypedAnimation(val animationName: String)") {
for (tag in info.tags) {
line("${tag.tagName.nameToVariable().uppercase()}(${tag.tagName.quoted}),")
}
line(";")
line("companion object") {
line("val list: List<TypedAnimation> = values().toList()")
for (tag in info.tags) {
line("val ${tag.tagName.nameToVariable().lowercase()}: TypedAnimation get() = TypedAnimation.${tag.tagName.nameToVariable().uppercase()}")
}
}
}

line("inline class TypedImageData(val data: ImageData)") {
line("val animations: TypedAnimation.Companion get() = TypedAnimation")
}

line("val animations: TypedAnimation.Companion get() = TypedAnimation")
line("val default: TypedImageData get() = TypedImageData(data.default)")
for (slice in info.slices) {
val varName = slice.sliceName.nameToVariable()
line("val `$varName`: TypedImageData get() = TypedImageData(data[${slice.sliceName.quoted}]!!)")
}
// @TODO: We could

//println("wizardFemale=${wizardFemale.imageDatasByName.keys}")
//println("wizardFemale.animations=${wizardFemale.imageDatas.first().animationsByName.keys}")
} catch (e: Throwable) {
System.err.println("FILE: aseFile=$aseFile")
e.printStackTrace()

throw e // @TODO: Remove this
}
}
}
}
}
}
Expand Down
129 changes: 129 additions & 0 deletions buildSrc/src/main/kotlin/korlibs/korge/gradle/util/ASEInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package korlibs.korge.gradle.util

data class ASEInfo(
val slices: List<AseSlice> = emptyList(),
val tags: List<AseTag> = emptyList(),
) {
data class AseSlice(
val sliceName: String,
val hasNinePatch: Boolean,
val hasPivotInfo: Boolean,
)

data class AseTag(
val fromFrame: Int,
val toFrame: Int,
val direction: Int,
val tagColor: Int,
val tagName: String
)

companion object {
fun getAseInfo(file: SFile): ASEInfo {
return getAseInfo(file.readBytes())
}

fun getAseInfo(data: ByteArray): ASEInfo {
return getAseInfo(ByteArraySimpleInputStream(ByteArraySlice(data)))
}

fun getAseInfo(s: ByteArraySimpleInputStream): ASEInfo {
if (s.length == 0) return ASEInfo()

val slices = arrayListOf<AseSlice>()
val tags = arrayListOf<AseTag>()

val fileSize = s.readS32LE()
if (s.length < fileSize) error("File too short")
val headerMagic = s.readU16LE()
if (headerMagic != 0xA5E0) error("Not an Aseprite file : headerMagic=$headerMagic")
val numFrames = s.readU16LE()
val imageWidth = s.readU16LE()
val imageHeight = s.readU16LE()
val bitsPerPixel = s.readU16LE()
val bytesPerPixel = bitsPerPixel / 8
val flags = s.readU32LE()
val speed = s.readU16LE()
s.skip(4)
s.skip(4)
val transparentIndex = s.readU8()
s.skip(3)
val numColors = s.readU16LE()
val pixelWidth = s.readU8()
val pixelHeight = s.readU8()
val gridX = s.readS16LE()
val gridY = s.readS16LE()
val gridWidth = s.readU16LE()
val gridHeight = s.readU16LE()
s.skip(84)

//println("ASE fileSize=$fileSize, headerMagic=$headerMagic, numFrames=$numFrames, $imageWidth x $imageHeight, bitsPerPixel=$bitsPerPixel, numColors=$numColors, gridWidth=$gridWidth, gridHeight=$gridHeight")

for (frameIndex in 0 until numFrames) {
//println("FRAME: $frameIndex")
val bytesInFrame = s.readS32LE()
val fs = s.readStream(bytesInFrame - 4)
val frameMagic = fs.readU16LE()
//println(" bytesInFrame=$bytesInFrame, frameMagic=$frameMagic")
if (frameMagic != 0xF1FA) error("Invalid ASE sprite file or error parsing : frameMagic=$frameMagic")
fs.readU16LE()
val frameDuration = fs.readU16LE()
fs.skip(2)
val numChunks = fs.readS32LE()

//println(" - $numChunks")

for (nc in 0 until numChunks) {
val chunkSize = fs.readS32LE()
val chunkType = fs.readU16LE()
val cs = fs.readStream(chunkSize - 6)

//println(" chunkType=$chunkType, chunkSize=$chunkSize")

when (chunkType) {
0x2022 -> { // SLICE KEYS
val numSliceKeys = cs.readS32LE()
val sliceFlags = cs.readS32LE()
cs.skip(4)
val sliceName = cs.readAseString()
val hasNinePatch = sliceFlags.hasBitSet(0)
val hasPivotInfo = sliceFlags.hasBitSet(1)
val aslice = AseSlice(sliceName, hasNinePatch, hasPivotInfo)
slices += aslice
}
0x2018 -> { // TAGS
// Tags
val numTags = cs.readU16LE()
cs.skip(8)
//println(" tags: numTags=$numTags")

for (tag in 0 until numTags) {
val fromFrame = cs.readU16LE()
val toFrame = cs.readU16LE()
val direction = cs.readU8()
cs.skip(8)
val tagColor = cs.readS32LE()
val tagName = cs.readAseString()
val atag = AseTag(fromFrame, toFrame, direction, tagColor, tagName)
tags += atag
//println(" tag[$tag]=$atag")
}
}
// Unsupported tag
else -> {

}
}
}
}

return ASEInfo(
slices = slices,
tags = tags,
)
}

fun ByteArraySimpleInputStream.readAseString(): String = readBytes(readU16LE()).toString(Charsets.UTF_8)
public infix fun Int.hasBitSet(index: Int): Boolean = ((this ushr index) and 1) != 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package korlibs.korge.gradle.util

import java.io.*

class ByteArraySlice(val ba: ByteArray, val pos: Int = 0, val size: Int = ba.size - pos) {
fun sliceRange(range: IntRange): ByteArraySlice {
return ByteArraySlice(ba, pos + range.first, range.last - range.first - 1)
}
val length: Int get() = size
operator fun get(index: Int): Byte = ba[pos + index]
fun sliceArray(range: IntRange): ByteArray {
return ba.sliceArray((pos + range.first) .. (pos + range.last))
}

override fun toString(): String= "ByteArraySlice[$size]"
}

class ByteArraySimpleInputStream(
val data: ByteArraySlice,
var pos: Int = 0
) {
val available: Int get() = length - pos
val length: Int get() = data.size

fun read(): Int {
if (pos >= length) return -1
return data[pos++].toInt() and 0xFF
}

fun readU8(): Int {
val v = this.read()
if (v < 0) error("Can't read byte at $pos in $data")
return v
}

fun readU16LE(): Int {
val v0 = readU8()
val v1 = readU8()
return (v0 shl 0) or (v1 shl 8)
}

fun readS16LE(): Int = (readU16LE() shl 16) shr 16

fun skip(count: Int): Int {
val oldPos = pos
pos += count
return oldPos
}

fun readS32LE(): Int {
val v0 = readU8()
val v1 = readU8()
val v2 = readU8()
val v3 = readU8()
return (v0 shl 0) or (v1 shl 8) or (v2 shl 16) or (v3 shl 24)
}

fun readU32LE(): Long {
return readS32LE().toLong() and 0xFFFFFFFFL
}

fun readStream(count: Int): ByteArraySimpleInputStream {
val pos = skip(count)
return ByteArraySimpleInputStream(data.sliceRange(pos until (pos + count)), 0)
}

fun readBytes(count: Int): ByteArray {
val start = skip(count)
return data.sliceArray(start until (start + count))
}
}

fun InputStream.readU8(): Int {
val v = this.read()
if (v < 0) error("Can't read byte")
return v
}

fun InputStream.readU16LE(): Int {
val v0 = readU8()
val v1 = readU8()
return (v0 shl 0) or (v1 shl 8)
}

fun InputStream.readS32LE(): Int {
val v0 = readU8()
val v1 = readU8()
val v2 = readU8()
val v3 = readU8()
return (v0 shl 0) or (v1 shl 8) or (v2 shl 16) or (v3 shl 24)
}

fun InputStream.readU32LE(): Long {
return readS32LE().toLong() and 0xFFFFFFFFL
}

Loading

0 comments on commit 514d29d

Please sign in to comment.