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

Add methods to write images #248

Merged
merged 7 commits into from
Aug 18, 2022
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 @@ -68,8 +68,8 @@ trait Resource {
/** Provides a [[java.io.OutputStream]] to write data to this resource location.
* The OutputStream is closed in the end, so it should not escape this call.
*/
def withOutputStream(f: OutputStream => Unit): Try[Unit] =
Using[OutputStream, Unit](unsafeOutputStream())(f)
def withOutputStream[A](f: OutputStream => A): Try[A] =
Using[OutputStream, A](unsafeOutputStream())(f)
}

object Resource {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,125 +1,11 @@
package eu.joaocosta.minart.graphics.image

import java.io.InputStream

import scala.annotation.tailrec

import eu.joaocosta.minart.graphics._
import eu.joaocosta.minart.graphics.image.helpers._

/** Image loader for BMP files.
*
* Supports uncompressed 24/32bit Windows BMPs.
*/
final class BmpImageLoader[F[_]](byteReader: ByteReader[F]) extends ImageLoader {
import BmpImageLoader._
import byteReader._

private val loadRgbPixel: ParseState[String, Color] =
readBytes(3)
.collect(
{ case bytes if bytes.size == 3 => Color(bytes(2), bytes(1), bytes(0)) },
_ => "Not enough data to read RGB pixel"
)

private val loadRgbaPixel: ParseState[String, Color] =
readBytes(4)
.collect(
{ case bytes if bytes.size == 4 => Color(bytes(2), bytes(1), bytes(0)) },
_ => "Not enough data to read RGBA pixel"
)

@tailrec
private def loadPixels(
loadColor: ParseState[String, Color],
data: F[Int],
remainingPixels: Int,
acc: List[Color] = Nil
): ParseResult[List[Color]] = {
if (isEmpty(data) || remainingPixels == 0) Right(data -> acc.reverse)
else {
loadColor.run(data) match {
case Left(error) => Left(error)
case Right((remaining, color)) => loadPixels(loadColor, remaining, remainingPixels - 1, color :: acc)
}
}
}

def loadImage(is: InputStream): Either[String, RamSurface] = {
val bytes = fromInputStream(is)
Header.fromBytes(bytes)(byteReader).right.flatMap { case (data, header) =>
val numPixels = header.width * header.height
val pixels = header.bitsPerPixel match {
case 24 =>
loadPixels(loadRgbPixel, data, numPixels)
case 32 =>
loadPixels(loadRgbaPixel, data, numPixels)
case bpp =>
Left(s"Invalid bits per pixel: $bpp")
}
pixels.right.flatMap { case (_, flatPixels) =>
if (flatPixels.size != numPixels) Left(s"Invalid number of pixels: Got ${flatPixels.size}, expected $numPixels")
else Right(new RamSurface(flatPixels.sliding(header.width, header.width).toSeq.reverse))
}
}
}
}
@deprecated("Use eu.joaocosta.minart.graphics.image.bmp.BmpImageFormat instead")
final class BmpImageLoader[F[_]](val byteReader: ByteReader[F]) extends bmp.BmpImageLoader[F]

object BmpImageLoader {
val defaultLoader = new BmpImageLoader[Iterator](ByteReader.IteratorByteReader)

val supportedFormats = Set("BM")

final case class Header(
magic: String,
size: Int,
offset: Int,
width: Int,
height: Int,
bitsPerPixel: Int
)
object Header {
def fromBytes[F[_]](bytes: F[Int])(byteReader: ByteReader[F]): byteReader.ParseResult[Header] = {
import byteReader._
(for {
magic <- readString(2).validate(
supportedFormats,
m => s"Unsupported format: $m. Only windows BMPs are supported"
)
size <- readLENumber(4)
_ <- skipBytes(4)
offset <- readLENumber(4)
dibHeaderSize <- readLENumber(4).validate(
dib => dib >= 40 && dib <= 124,
dib => s"Unsupported DIB header size: $dib"
)
width <- readLENumber(4)
height <- readLENumber(4)
colorPlanes <- readLENumber(2).validate(
_ == 1,
planes => s"Invalid number of color planes (must be 1): $planes"
)
bitsPerPixel <- readLENumber(2).validate(
Set(24, 32),
bpp => s"Unsupported bits per pixel (must be 24 or 32): $bpp"
)
compressionMethod <- readLENumber(4).validate(
c => c == 0 || c == 3 || c == 6,
_ => "Compression is not supported"
)
loadColorMask = compressionMethod == 3 || compressionMethod == 6
_ <- if (loadColorMask) skipBytes(20) else noop
redMask <- if (loadColorMask) readLENumber(4) else State.pure[F[Int], Int](0x00ff0000)
greenMask <- if (loadColorMask) readLENumber(4) else State.pure[F[Int], Int](0x0000ff00)
blueMask <- if (loadColorMask) readLENumber(4) else State.pure[F[Int], Int](0x000000ff)
_ <- if (loadColorMask) skipBytes(4) else noop // Skip alpha mask (or color space)
_ <- State.check(
redMask == 0x00ff0000 && greenMask == 0x0000ff00 && blueMask == 0x000000ff,
"Unsupported color format (must be either RGB or ARGB)"
)
header = Header(magic, size, offset, width, height, bitsPerPixel)
_ <- skipBytes(offset - (if (loadColorMask) 70 else 34))
} yield header).run(bytes)
}
}
@deprecated("Use eu.joaocosta.minart.graphics.image.bmp.BmpImageFormat.defaultFormat instead")
val defaultLoader = bmp.BmpImageFormat.defaultFormat
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,48 @@ object Image {
* @param resource Resource pointing to the image
*/
def loadImage(loader: ImageLoader, resource: Resource): Try[RamSurface] = {
resource
.withInputStream { inputStream =>
loader.loadImage(inputStream)
}
.flatMap {
case Left(error) => Failure(new Exception(error))
case Right(result) => Success(result)
}
loader.loadImage(resource).flatMap {
case Left(error) => Failure(new Exception(error))
case Right(result) => Success(result)
}
}

/** Loads an image in the PPM format.
*/
def loadPpmImage(resource: Resource): Try[RamSurface] =
loadImage(PpmImageLoader.defaultLoader, resource)
loadImage(ppm.PpmImageFormat.defaultFormat, resource)

/** Loads an image in the BMP format.
*/
def loadBmpImage(resource: Resource): Try[RamSurface] =
loadImage(BmpImageLoader.defaultLoader, resource)
loadImage(bmp.BmpImageFormat.defaultFormat, resource)

/** Loads an image in the QOI format.
*/
def loadQoiImage(resource: Resource): Try[RamSurface] =
loadImage(QoiImageLoader.defaultLoader, resource)
loadImage(qoi.QoiImageFormat.defaultFormat, resource)

/** Stores an image using a custom ImageWriter.
*
* @param writer ImageWriter to use
* @param surface Surface to store
* @param resource Resource pointing to the output destination
*/
def storeImage(writer: ImageWriter, surface: Surface, resource: Resource): Try[Unit] = {
writer.storeImage(surface, resource).flatMap {
case Left(error) => Failure(new Exception(error))
case Right(result) => Success(result)
}
}

/** Stores an image in the PPM format.
*/
def storePpmImage(surface: Surface, resource: Resource): Try[Unit] =
storeImage(ppm.PpmImageFormat.defaultFormat, surface, resource)

/** Stores an image in the BMP format.
*/
def storeBmpImage(surface: Surface, resource: Resource): Try[Unit] =
storeImage(bmp.BmpImageFormat.defaultFormat, surface, resource)

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package eu.joaocosta.minart.graphics.image

import java.io.InputStream
import java.io.{ByteArrayInputStream, InputStream}

import scala.util.Try

import eu.joaocosta.minart.graphics._
import eu.joaocosta.minart.runtime.Resource

/** Image loader with a low-level implementation on how to load an image.
*/
Expand All @@ -14,4 +17,22 @@ trait ImageLoader {
* @return Either a RamSurface with the image data or an error string
*/
def loadImage(is: InputStream): Either[String, RamSurface]

/** Loads an image from a Resource.
*
* @param resource Resource with the image data
* @return Either a RamSurface with the image data or an error string, inside a Try capturing the IO exceptions
*/
def loadImage(resource: Resource): Try[Either[String, RamSurface]] =
resource.withInputStream(is => loadImage(is))

/** Loads an image from a byte array.
*
* @param data Byte array
* @return Either a RamSurface with the image data or an error string
*/
def fromByteArray(data: Array[Byte]): Either[String, RamSurface] = {
val is = new ByteArrayInputStream(data)
loadImage(is)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package eu.joaocosta.minart.graphics.image

import java.io.{ByteArrayOutputStream, OutputStream}

import scala.util.Try

import eu.joaocosta.minart.graphics._
import eu.joaocosta.minart.runtime.Resource

/** Image writer with a low-level implementation on how to store an image.
*/
trait ImageWriter {

/** Stores a surface to an OutputStream.
*
* @param surface Surface to store
* @param os OutputStream where to store the data
* @return Either unit or an error string
*/
def storeImage(surface: Surface, os: OutputStream): Either[String, Unit]

/** Stores a surface to a Resource.
*
* @param surface Surface to store
* @param resource Resource where to store the data
* @return Either unit or an error string, inside a Try capturing the IO exceptions
*/
def storeImage(surface: Surface, resource: Resource): Try[Either[String, Unit]] =
resource.withOutputStream(os => storeImage(surface, os))

/** Returns the image data as a byte array.
*
* @param surface Surface to convert
* @return Either a RamSurface with the image data or an error string
*/
def toByteArray(surface: Surface): Either[String, Array[Byte]] = {
val os = new ByteArrayOutputStream()
storeImage(surface, os).right.map(_ => os.toByteArray)
}
}
Loading