All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.pawelkrol.Afterimage.Mode.Data.Bitmap.scala Maven / Gradle / Ivy

The newest version!
package com.github.pawelkrol.Afterimage
package Mode.Data

/** Plain hi-resolution bitmap data of an image.
  *
  * @constructor create a new bitmap data
  * @param data an array of bytes
  * @param cols bitmap width counted as a number of 8x8 character columns
  * @param rows bitmap height counted as a number of 8x8 character rows
  */
case class Bitmap(data: Array[Byte], cols: Int, rows: Int) {

  require(
    cols >= 1 && cols <= Bitmap.maxCols && rows >= 1 && rows <= Bitmap.maxRows,
    "Invalid bitmap data size: got %s, but expected anything between 1x1 and 40x25 with row and column values counted as occurences of 8x8 character areas".format(
      "%dx%d".format(cols, rows)
    )
  )

  require(
    data.length == cols * rows * Bitmap.bytesPerChar,
    "Invalid bitmap data length: got %d, but expected %d bytes".format(
      data.length,
      cols * rows * Bitmap.bytesPerChar
    )
  )

  private lazy val pixels = {

    val width = cols * 8
    val height = rows * 8

    (0 to width - 1).map(x => {
      (0 to height - 1).map(y => {

        val bitmapOffset = (y & 0x07) + ((y & 0xf8) >> 3) * width + (x & 0xfff8)
        val bitMask = 0x80 >> (x & 0x0007)

        (data(bitmapOffset) & bitMask) != 0
      })
    })
  }

  /** Returns the entire bitmap data as an array of bytes. */
  def get() = data

  /** Returns a boolean flag indicating if a pixel at a given position is set or not.
    *
    * @param x X coordinate of the pixel
    * @param y Y coordinate of the pixel
    */
  def isPixelSet(x: Int, y: Int) = pixels(x)(y)

  /** Returns an indicated number of pixel values starting at a given position.
    *
    * @param x X coordinate of the first pixel
    * @param y Y coordinate of the first pixel
    * @param numBits number of up to 8 subsequent pixels to test, which are located on the samy Y line
    */
  def getPixels(x: Int, y: Int, numBits: Int) = {

    require(
      numBits >= 1 && numBits <= 8,
      "Invalid number of pixels requested: got %d, but expected an integer between 1 and 8".format(numBits)
    )

    (0 to numBits - 1).foldLeft(0x00)((pixels, bit) => {
      val pixel = if (isPixelSet(x + bit, y)) 0x01 else 0x00
      (pixels << 1) + pixel
    }).toByte
  }

  private def horizontalShift(x: Int, pixels: Seq[Seq[Boolean]], fill: Boolean) =
    if (x < 0)
      pixels.drop(-x) ++ Seq.fill((-x).min(cols * 8)){Seq.fill(rows * 8){fill}}
    else
      Seq.fill((x).min(cols * 8)){Seq.fill(rows * 8){fill}} ++ pixels.dropRight(x)

  private def verticalShift(y: Int, pixels: Seq[Seq[Boolean]], fill: Boolean) =
    pixels.map(col => {
      if (y < 0)
        col.drop(-y) ++ Seq.fill((-y).min(rows * 8)){fill}
      else
        Seq.fill(y.min(rows * 8)){fill} ++ col.dropRight(y)
    })

  private def getData(pixels: Seq[Seq[Boolean]], width: Int, height: Int, cols: Int, rows: Int) = {

    (0 to rows - 1).map(row => {
      (0 to cols - 1).map(col => {
        (0 to 7).map(byte => {
          (0 to 7).foldLeft(0x00)((bits, bit) => {
            val x = col * 8 + bit
            val y = row * 8 + byte
            val pixel = if (x >= width || y >= height)
              0x00
            else
              if (pixels(x)(y)) 0x01 else 0x00
            (bits << 1) + pixel
          }).toByte
        })
      }).flatten
    }).flatten.toArray
  }

  /** Returns the new bitmap data composed from the original image with a shifted content.
    *
    * @param x value of horizontal data shift (positive for shifting to the right, and negative for shifting to the left)
    * @param y value of vertical data shift (positive for shifting to the bottom, and negative for shifting to the top)
    * @param fill optional fill value of newly created empty bits (defaults to a cleared bit)
    */
  def shift(x: Int, y: Int, fill: Boolean = false) = {

    val horizontalShiftPixels = horizontalShift(x, pixels, fill)
    val verticalShiftPixels = verticalShift(y, horizontalShiftPixels, fill)

    val newData = getData(verticalShiftPixels, cols * 8, rows * 8, cols, rows)

    new Bitmap(newData, cols, rows)
  }

  /** Returns the new bitmap data composed from cutting out a slice of the original image.
   *
   * @param x X coordinate of the top-left corner of a rectangular selection area
   * @param y Y coordinate of the top-left corner of a rectangular selection area
   * @param newWidth total width of a rectangular selection area in number of pixels
   * @param newHeight total height of a rectangular selection area in number of pixels
   */
  def slice(x: Int, y: Int, newWidth: Int, newHeight: Int) = {

    val shiftedBitmap = shift(-x, -y)

    val cols = (newWidth.toDouble / 8).ceil.toInt
    val rows = (newHeight.toDouble / 8).ceil.toInt

    val data = getData(shiftedBitmap.pixels, newWidth, newHeight, cols, rows)

    new Bitmap(data, cols, rows)
  }

  def canEqual(that: Any) = that.isInstanceOf[Bitmap]

  override def equals(other: Any) = other match {
    case that: Bitmap =>
      (that canEqual this) && (this.data.toList == that.data.toList) && (this.cols == that.cols) && (this.rows == that.rows)
    case _ =>
      false
  }
}

/** Factory for [[com.github.pawelkrol.Afterimage.Mode.Data.Bitmap]] instances. */
object Bitmap {

  /** Maximum possible number of 8x8 character columns of a Bitmap image data. */
  val maxCols = 40

  /** Maximum possible number of 8x8 character rows of a Bitmap image data. */
  val maxRows = 25

  private val bytesPerChar = 8

  private val maxSize = maxCols * maxRows * bytesPerChar

  /** Creates a default (empty) bitmap of a maximum possible size. */
  def apply(): Bitmap =
    Bitmap(Array.fill(maxSize){0x00}, maxCols, maxRows)

  /** Creates a default (empty) bitmap of a given size. */
  def apply(cols: Int, rows: Int): Bitmap =
    Bitmap(Array.fill(cols * rows * bytesPerChar){0x00}, cols, rows)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy