-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #498 from JD557/playdate-image-support
Add Playdate Image (PDI) support
- Loading branch information
Showing
11 changed files
with
230 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
image/shared/src/main/scala/eu/joaocosta/minart/graphics/image/pdi/CellHeader.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package eu.joaocosta.minart.graphics.image.pdi | ||
|
||
private[pdi] final case class CellHeader( | ||
clipWidth: Int, | ||
clipHeight: Int, | ||
stride: Int, | ||
clipLeft: Int, | ||
clipRight: Int, | ||
clipTop: Int, | ||
clipBottom: Int, | ||
transparency: Boolean | ||
) |
6 changes: 6 additions & 0 deletions
6
image/shared/src/main/scala/eu/joaocosta/minart/graphics/image/pdi/Header.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package eu.joaocosta.minart.graphics.image.pdi | ||
|
||
private[pdi] final case class Header( | ||
magic: String, | ||
compressed: Boolean | ||
) |
13 changes: 13 additions & 0 deletions
13
image/shared/src/main/scala/eu/joaocosta/minart/graphics/image/pdi/PdiImageFormat.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package eu.joaocosta.minart.graphics.image.pdi | ||
|
||
/** Image reader and writer for PDI files. | ||
* | ||
* Supports reading and writing uncompressed Playdate PDIs. | ||
*/ | ||
final class PdiImageFormat() extends PdiImageReader with PdiImageWriter | ||
|
||
object PdiImageFormat { | ||
val defaultFormat = new PdiImageFormat() | ||
|
||
val supportedFormats = Set("Playdate IMG") | ||
} |
101 changes: 101 additions & 0 deletions
101
image/shared/src/main/scala/eu/joaocosta/minart/graphics/image/pdi/PdiImageReader.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package eu.joaocosta.minart.graphics.image.pdi | ||
|
||
import java.io.InputStream | ||
|
||
import scala.annotation.tailrec | ||
|
||
import eu.joaocosta.minart.graphics.* | ||
import eu.joaocosta.minart.graphics.image.* | ||
import eu.joaocosta.minart.internal.* | ||
|
||
/** Image reader for PDI files. | ||
* | ||
* Supports uncompressed Playdate PDIs. | ||
*/ | ||
trait PdiImageReader extends ImageReader { | ||
import ByteReader.* | ||
|
||
private def bitsFromByte(byte: Byte, entries: Int): List[Boolean] = | ||
if (entries > 0) { | ||
val colorBit = (byte & 0x80) >> 7 | ||
(colorBit == 1) :: bitsFromByte(((byte << 1) & 0xff).toByte, entries - 1) | ||
} else Nil | ||
|
||
private def loadBitLine( | ||
data: CustomInputStream, | ||
width: Int, | ||
lineBytes: Int | ||
): ParseResult[Array[Boolean]] = { | ||
readBytes(lineBytes).map(_.flatMap(b => bitsFromByte(b.toByte, 8)).take(width)).run(data) | ||
} | ||
|
||
@tailrec | ||
private def loadBits( | ||
data: CustomInputStream, | ||
remainingLines: Int, | ||
width: Int, | ||
lineBytes: Int, | ||
acc: Vector[Array[Boolean]] = Vector() | ||
): ParseResult[Vector[Array[Boolean]]] = { | ||
if (isEmpty(data) || remainingLines == 0) Right(data -> acc) | ||
else { | ||
loadBitLine(data, width, lineBytes) match { | ||
case Left(error) => Left(error) | ||
case Right((remaining, line)) => | ||
loadBits(remaining, remainingLines - 1, width, lineBytes, acc :+ line) | ||
} | ||
} | ||
} | ||
|
||
private def loadHeader(bytes: CustomInputStream): ParseResult[Header] = { | ||
(for { | ||
magic <- readString(12).validate( | ||
PdiImageFormat.supportedFormats, | ||
m => s"Unsupported format: $m." | ||
) | ||
flags <- readLENumber(4) | ||
compression <- State.cond((flags & 0x80000000) == 0, false, "Unsuported compression") | ||
header = Header(magic, compression) | ||
} yield header).run(bytes) | ||
} | ||
|
||
private def loadCellHeader(bytes: CustomInputStream): ParseResult[CellHeader] = { | ||
(for { | ||
clipWidth <- readLENumber(2) | ||
clipHeight <- readLENumber(2) | ||
cellStride <- readLENumber(2) | ||
clipLeft <- readLENumber(2) | ||
clipRight <- readLENumber(2) | ||
clipTop <- readLENumber(2) | ||
clipBottom <- readLENumber(2) | ||
flags <- readLENumber(2) | ||
transparency = (flags & 0x0003) != 0 | ||
header = CellHeader(clipWidth, clipHeight, cellStride, clipLeft, clipRight, clipTop, clipBottom, transparency) | ||
} yield header).run(bytes) | ||
} | ||
|
||
final def loadImage(is: InputStream): Either[String, RamSurface] = { | ||
val bytes = fromInputStream(is) | ||
loadHeader(bytes).flatMap(_ => loadCellHeader(bytes)).flatMap { case (data, cellHeader) => | ||
val emptyColor = Color(0, 0, 0, 0) | ||
val width = cellHeader.clipLeft + cellHeader.clipWidth + cellHeader.clipRight | ||
val leftPad = Array.fill(cellHeader.clipLeft)(emptyColor) | ||
val rightPad = Array.fill(cellHeader.clipRight)(emptyColor) | ||
for { | ||
centerColors <- loadBits(data, cellHeader.clipHeight, cellHeader.clipWidth, cellHeader.stride) | ||
centerMask <- loadBits(data, cellHeader.clipHeight, cellHeader.clipWidth, cellHeader.stride) | ||
centerPixels = centerColors._2.zip(centerMask._2).map { (colorLine, maskLine) => | ||
colorLine.zip(maskLine).map { | ||
case (_, false) => Color(0, 0, 0, 0) | ||
case (false, true) => Color(0, 0, 0) | ||
case (true, true) => Color(255, 255, 255) | ||
} | ||
} | ||
pixels = | ||
Vector.fill(cellHeader.clipTop)(Array.fill(width)(emptyColor)) ++ | ||
centerPixels.map(center => leftPad ++ center ++ rightPad) ++ | ||
Vector.fill(cellHeader.clipBottom)(Array.fill(width)(emptyColor)) | ||
} yield (new RamSurface(pixels)) | ||
} | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
image/shared/src/main/scala/eu/joaocosta/minart/graphics/image/pdi/PdiImageWriter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package eu.joaocosta.minart.graphics.image.pdi | ||
|
||
import java.io.OutputStream | ||
|
||
import scala.annotation.tailrec | ||
|
||
import eu.joaocosta.minart.graphics.* | ||
import eu.joaocosta.minart.graphics.image.* | ||
import eu.joaocosta.minart.internal.* | ||
|
||
/** Image writer for Pdi files. | ||
* | ||
* Stores data as uncompressed PlayDate PDIs. | ||
*/ | ||
trait PdiImageWriter extends ImageWriter { | ||
import ByteWriter.* | ||
|
||
private def colorsToByte(acc: Byte, colors: List[Color], f: Color => Boolean): Byte = colors match { | ||
case Nil => acc | ||
case color :: rem => | ||
if (f(color)) colorsToByte(((acc << 1) | 0x01).toByte, rem, f) | ||
else colorsToByte((acc << 1).toByte, rem, f) | ||
} | ||
|
||
private def lineToBytes(colors: List[Color], f: Color => Boolean): List[Byte] = | ||
colors.sliding(8, 8).map(cs => colorsToByte(0.toByte, cs.padTo(8, Color(0, 0, 0, 0)), f)).toList | ||
|
||
private def storeLine(colors: Array[Color], f: Color => Boolean): ByteStreamState[String] = | ||
writeBytes(lineToBytes(colors.toList, f).map(java.lang.Byte.toUnsignedInt)) | ||
|
||
@tailrec | ||
private def storePixels( | ||
lines: Vector[Array[Color]], | ||
f: Color => Boolean, | ||
acc: ByteStreamState[String] = emptyStream | ||
): ByteStreamState[String] = { | ||
if (lines.isEmpty) acc | ||
else storePixels(lines.tail, f, acc.flatMap(_ => storeLine(lines.head, f))) | ||
} | ||
|
||
private val storeHeader: ByteStreamState[String] = { | ||
(for { | ||
_ <- writeString("Playdate IMG") | ||
_ <- writeLENumber(0, 4) // Disable compression | ||
} yield ()) | ||
} | ||
|
||
private def storeCellHeader(surface: Surface): ByteStreamState[String] = { | ||
(for { | ||
_ <- writeLENumber(surface.width, 2) | ||
_ <- writeLENumber(surface.height, 2) | ||
_ <- writeLENumber(math.ceil(surface.width / 8.0).toInt, 2) | ||
_ <- writeLENumber(0, 2) // No padding | ||
_ <- writeLENumber(0, 2) // No padding | ||
_ <- writeLENumber(0, 2) // No padding | ||
_ <- writeLENumber(0, 2) // No padding | ||
_ <- writeLENumber(0x03, 2) // Always enable transparency | ||
} yield ()) | ||
} | ||
|
||
final def storeImage(surface: Surface, os: OutputStream): Either[String, Unit] = { | ||
val state = for { | ||
_ <- storeHeader | ||
_ <- storeCellHeader(surface) | ||
_ <- storePixels(surface.getPixels(), color => math.max(math.max(color.r, color.g), color.b) >= 127) | ||
_ <- storePixels(surface.getPixels(), color => color.a > 0) | ||
} yield () | ||
toOutputStream(state, os) | ||
} | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters