Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify codecs #500

Merged
merged 4 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ trait BmpImageReader extends ImageReader {
_ => "Not enough data to read RGBA pixel"
)

private def loadColor(bitsPerPixel: Int): Either[String, ParseState[String, Color]] =
bitsPerPixel match {
case 24 =>
Right(loadRgbPixel)
case 32 =>
Right(loadRgbaPixel)
case bpp =>
Left(s"Invalid bits per pixel: $bpp")
}

@tailrec
private def loadPixelLine(
loadColor: ParseState[String, Color],
Expand Down Expand Up @@ -91,7 +101,7 @@ trait BmpImageReader extends ImageReader {
bpp => s"Unsupported bits per pixel (must be 24 or 32): $bpp"
)
compressionMethod <- readLENumber(4).validate(
c => c == 0 || c == 3 || c == 6,
Set(0, 3, 6),
_ => "Compression is not supported"
)
loadColorMask = compressionMethod == 3 || compressionMethod == 6
Expand All @@ -112,25 +122,14 @@ trait BmpImageReader extends ImageReader {
final def loadImage(is: InputStream): Either[String, RamSurface] = {
val bytes = fromInputStream(is)
loadHeader(bytes).flatMap { case (data, header) =>
val pixels = header.bitsPerPixel match {
case 24 =>
loadPixels(
loadRgbPixel,
data,
header.height,
header.width,
BmpImageFormat.linePadding(header.width, header.bitsPerPixel)
)
case 32 =>
loadPixels(
loadRgbaPixel,
data,
header.height,
header.width,
BmpImageFormat.linePadding(header.width, header.bitsPerPixel)
)
case bpp =>
Left(s"Invalid bits per pixel: $bpp")
val pixels = loadColor(header.bitsPerPixel).flatMap { loadColor =>
loadPixels(
loadColor,
data,
header.height,
header.width,
BmpImageFormat.linePadding(header.width, header.bitsPerPixel)
)
}
pixels.flatMap { case (_, pixelMatrix) =>
if (pixelMatrix.size != header.height)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package eu.joaocosta.minart.graphics.image.bmp

import java.io.OutputStream

import scala.annotation.tailrec

import eu.joaocosta.minart.graphics.*
import eu.joaocosta.minart.graphics.image.*
import eu.joaocosta.minart.internal.*
Expand All @@ -15,30 +13,8 @@ import eu.joaocosta.minart.internal.*
trait BmpImageWriter extends ImageWriter {
import ByteWriter.*

private def storeBgrPixel(color: Color): ByteStreamState[String] =
writeBytes(List(color.b, color.g, color.r))

@tailrec
private def storePixels(
storeColor: Color => ByteStreamState[String],
surface: Surface,
width: Int,
padding: Int,
currentPixel: Int = 0,
acc: ByteStreamState[String] = emptyStream
): ByteStreamState[String] = {
if (currentPixel >= surface.width * surface.height) acc
else {
val x = currentPixel % surface.width
val y = (surface.height - 1) - (currentPixel / surface.width) // lines are stored upside down
val color = surface.unsafeGetPixel(x, y)
val nextAcc = acc.flatMap { _ =>
if (x == width - 1) storeColor(color).flatMap(_ => writeBytes(List.fill(padding)(0)))
else storeColor(color)
}
storePixels(storeColor, surface, width, padding, currentPixel + 1, nextAcc)
}
}
private def colorToBytes(color: Color): Array[Byte] =
Array(color.b.toByte, color.g.toByte, color.r.toByte)

private def storeHeader(surface: Surface): ByteStreamState[String] = {
(for {
Expand All @@ -63,7 +39,9 @@ trait BmpImageWriter extends ImageWriter {
final def storeImage(surface: Surface, os: OutputStream): Either[String, Unit] = {
val state = for {
_ <- storeHeader(surface)
_ <- storePixels(storeBgrPixel, surface, surface.width, BmpImageFormat.linePadding(surface.width, 24))
padding = Array.fill(BmpImageFormat.linePadding(surface.width, 24))(0.toByte)
byteIterator = surface.getPixels().reverseIterator.flatMap(_.iterator.map(colorToBytes) ++ Iterator(padding))
_ <- append(byteIterator)
} yield ()
toOutputStream(state, os)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ 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.*
Expand All @@ -22,21 +20,8 @@ trait PdiImageWriter extends ImageWriter {
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 def lineToBytes(colors: Array[Color], f: Color => Boolean): Array[Byte] =
colors.sliding(8, 8).map(cs => colorsToByte(0.toByte, cs.toList.padTo(8, Color(0, 0, 0, 0)), f)).toArray

private val storeHeader: ByteStreamState[String] = {
(for {
Expand All @@ -62,8 +47,11 @@ trait PdiImageWriter extends ImageWriter {
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)
pixels = surface.getPixels()
_ <- append(
pixels.iterator.map(line => lineToBytes(line, color => math.max(math.max(color.r, color.g), color.b) >= 127))
)
_ <- append(pixels.iterator.map(line => lineToBytes(line, color => color.a > 0)))
} yield ()
toOutputStream(state, os)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ trait PpmImageReader extends ImageReader {
_ => "Not enough data to read RGB pixel"
)

private def loadColor(magic: String): Either[String, ParseState[String, Color]] = magic match {
case "P1" => Right(loadStringBWPixel)
case "P2" => Right(loadStringGrayscalePixel)
case "P3" => Right(loadStringRgbPixel)
// case "P4" => // P4 requires special logic
case "P5" => Right(loadBinaryGrayscalePixel)
case "P6" => Right(loadBinaryRgbPixel)
case fmt => Left(s"Invalid pixel format: $fmt")
}

@tailrec
private def loadPixelLine(
loadColor: ParseState[String, Color],
Expand Down Expand Up @@ -138,22 +148,12 @@ trait PpmImageReader extends ImageReader {
val bytes = fromInputStream(is)
loadHeader(bytes).flatMap { case (data, header) =>
val pixels = header.magic match {
case "P1" =>
loadPixels(loadStringBWPixel, data, header.height, header.width)
case "P2" =>
loadPixels(loadStringGrayscalePixel, data, header.height, header.width)
case "P3" =>
loadPixels(loadStringRgbPixel, data, header.height, header.width)
case "P4" =>
loadBits(data, header.height, header.width, math.ceil(header.width / 8.0).toInt).map((state, bits) =>
(state, bits.map(_.map(b => if (b) Color.grayscale(0) else Color.grayscale(255))))
)
case "P5" =>
loadPixels(loadBinaryGrayscalePixel, data, header.height, header.width)
case "P6" =>
loadPixels(loadBinaryRgbPixel, data, header.height, header.width)
case fmt =>
Left(s"Invalid pixel format: $fmt")
case _ =>
loadColor(header.magic).flatMap(loadColor => loadPixels(loadColor, data, header.height, header.width))
}
pixels.flatMap { case (_, pixelMatrix) =>
if (pixelMatrix.size != header.height)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package eu.joaocosta.minart.graphics.image.ppm

import java.io.OutputStream

import scala.annotation.tailrec

import eu.joaocosta.minart.graphics.*
import eu.joaocosta.minart.graphics.image.*
import eu.joaocosta.minart.internal.*
Expand All @@ -15,24 +13,8 @@ import eu.joaocosta.minart.internal.*
trait PpmImageWriter extends ImageWriter {
import ByteWriter.*

private def storeBinaryRgbPixel(color: Color): ByteStreamState[String] =
writeBytes(List(color.r, color.g, color.b))

@tailrec
private def storePixels(
storeColor: Color => ByteStreamState[String],
surface: Surface,
currentPixel: Int = 0,
acc: ByteStreamState[String] = emptyStream
): ByteStreamState[String] = {
if (currentPixel >= surface.width * surface.height) acc
else {
val x = currentPixel % surface.width
val y = currentPixel / surface.width
val color = surface.unsafeGetPixel(x, y)
storePixels(storeColor, surface, currentPixel + 1, acc.flatMap(_ => storeColor(color)))
}
}
private def colorToBytes(color: Color): Array[Byte] =
Array(color.r.toByte, color.g.toByte, color.b.toByte)

private def storeHeader(surface: Surface): ByteStreamState[String] =
for {
Expand All @@ -44,7 +26,7 @@ trait PpmImageWriter extends ImageWriter {
final def storeImage(surface: Surface, os: OutputStream): Either[String, Unit] = {
val state = for {
_ <- storeHeader(surface)
_ <- storePixels(storeBinaryRgbPixel, surface)
_ <- append(surface.getPixels().iterator.flatten.map(colorToBytes))
} yield ()
toOutputStream(state, os)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,32 @@ trait QoiImageReader extends ImageReader {
import Op.*
readByte
.collect(
{ case Some(tag) =>
(tag & 0xc0, tag & 0x3f)
},
{ case Some(tag) => (tag & 0xc0, tag & 0x3f) },
_ => "Corrupted file, expected a Op but got nothing"
)
.flatMap {
case (0xc0, 0x3e) =>
readBytes(3)
.validate(_.size == 3, _ => "Not enough data for OP_RGB")
.map(data => OpRgb(data(0), data(1), data(2)))
.collect(
{ case bytes if bytes.size == 3 => OpRgb(bytes(0), bytes(1), bytes(2)) },
_ => "Not enough data for OP_RGB"
)
case (0xc0, 0x3f) =>
readBytes(4)
.validate(_.size == 4, _ => "Not enough data for OP_RGBA")
.map(data => OpRgba(data(0), data(1), data(2), data(3)))
.collect(
{ case bytes if bytes.size == 4 => OpRgba(bytes(0), bytes(1), bytes(2), bytes(3)) },
_ => "Not enough data for OP_RGBA"
)
case (0x00, index) =>
State.pure(OpIndex(index))
case (0x40, diffs) =>
State.pure(OpDiff(load2Bits(diffs >> 4), load2Bits(diffs >> 2), load2Bits(diffs)))
case (0x80, dg) =>
readByte.collect(
{ case Some(byte) => OpLuma(load6Bits(dg), load4Bits(byte >> 4), load4Bits(byte)) },
_ => "Not enough data for OP_LUMA"
)
readByte
.collect(
{ case Some(byte) => OpLuma(load6Bits(dg), load4Bits(byte >> 4), load4Bits(byte)) },
_ => "Not enough data for OP_LUMA"
)
case (0xc0, run) =>
State.pure(OpRun(run + 1))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,8 @@ trait QoiImageWriter extends ImageWriter {
.reverse

private def writeOp(op: Op): ByteStreamState[String] = op match {
case Op.OpRgb(r, g, b) =>
for {
_ <- writeByte(254)
_ <- writeByte(r)
_ <- writeByte(g)
_ <- writeByte(b)
} yield ()
case Op.OpRgba(r, g, b, a) =>
for {
_ <- writeByte(254)
_ <- writeByte(r)
_ <- writeByte(g)
_ <- writeByte(b)
_ <- writeByte(a)
} yield ()
case Op.OpRgb(r, g, b) => writeBytes(Vector(254, r, g, b))
case Op.OpRgba(r, g, b, a) => writeBytes(Vector(255, r, g, b, a))
case Op.OpIndex(index) =>
if (index < 0 || index > 63) State.error(s"Invalid index: $index")
else writeByte(index)
Expand All @@ -69,7 +56,7 @@ trait QoiImageWriter extends ImageWriter {
storeOps(ops.tail, nextStream)
}

private val storeTrail: ByteStreamState[String] = writeBytes(List(0, 0, 0, 0, 0, 0, 0, 1))
private val storeTrail: ByteStreamState[String] = writeBytes(Vector(0, 0, 0, 0, 0, 0, 0, 1))

final def storeImage(surface: Surface, os: OutputStream): Either[String, Unit] = {
val state = for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package eu.joaocosta.minart.audio.sound.aiff

import java.io.OutputStream

import scala.annotation.tailrec

import eu.joaocosta.minart.audio.*
import eu.joaocosta.minart.audio.sound.*
import eu.joaocosta.minart.internal.*
Expand All @@ -14,33 +12,22 @@ import eu.joaocosta.minart.internal.*
*/
trait AiffAudioWriter(sampleRate: Int, bitRate: Int) extends AudioClipWriter {
private val chunkSize = 128
require(Set(8, 16, 32).contains(bitRate))
require(Set(8, 16, 32).contains(bitRate), "Unsupported bit rate")

import AiffAudioWriter.*
import ByteWriter.*
import ByteFloatOps.*

private def convertSample(x: Double): List[Int] = bitRate match {
private def convertSample(x: Double): Array[Byte] = bitRate match {
case 8 =>
List(java.lang.Byte.toUnsignedInt((Math.min(Math.max(-1.0, x), 1.0) * Byte.MaxValue).toByte))
val byte = Math.min(Math.max(-1.0, x), 1.0) * Byte.MaxValue
Array(byte.toByte)
case 16 =>
val short = (Math.min(Math.max(-1.0, x), 1.0) * Short.MaxValue).toInt
List((short >> 8) & 0xff, short & 0xff)
Array(((short >> 8) & 0xff).toByte, (short & 0xff).toByte)
case 32 =>
val int = (Math.min(Math.max(-1.0, x), 1.0) * Int.MaxValue).toInt
List((int >> 24) & 0xff, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff)
}

@tailrec
private def storeData(
iterator: Iterator[Seq[Double]],
acc: ByteStreamState[String] = emptyStream
): ByteStreamState[String] = {
if (!iterator.hasNext) acc
else {
val chunk = iterator.next().flatMap(convertSample)
storeData(iterator, acc.flatMap(_ => writeBytes(chunk)))
}
Array(((int >> 24) & 0xff).toByte, ((int >> 16) & 0xff).toByte, ((int >> 8) & 0xff).toByte, (int & 0xff).toByte)
}

private def storeSsndChunk(clip: AudioClip): ByteStreamState[String] =
Expand All @@ -51,7 +38,9 @@ trait AiffAudioWriter(sampleRate: Int, bitRate: Int) extends AudioClipWriter {
_ <- writeBENumber(8 + numBytes, 4) // The padding is not included in the chunk size
_ <- writeBENumber(0, 4)
_ <- writeBENumber(0, 4)
_ <- storeData(Sampler.sampleClip(clip, sampleRate).grouped(chunkSize))
_ <- append(
Sampler.sampleClip(clip, sampleRate).grouped(chunkSize).map(_.iterator.flatMap(convertSample).toArray)
)
_ <- writeBytes(List.fill(numBytes % 2)(0))
} yield ()

Expand Down
Loading
Loading