Skip to content

Commit

Permalink
Merge pull request mike42#46 from mike42/feature/35-bmp
Browse files Browse the repository at this point in the history
Implement RLE8 decode for BMP files
  • Loading branch information
mike42 authored Sep 22, 2019
2 parents 4390c84 + 1da1f68 commit 049b9a2
Show file tree
Hide file tree
Showing 7 changed files with 521 additions and 100 deletions.
2 changes: 1 addition & 1 deletion docs/user/formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ A GIF image will always be loaded into an instance of :class:`IndexedRasterImage
BMP
^^^

The BMP codec is used where the input has the ``bmp`` or ``dib`` file extensions. Most uncompressed bitmap files cn be read.
The BMP codec is used where the input has the ``bmp`` or ``dib`` file extensions. Most uncompressed or run-length encoded bitmap files can be read by this library.

The returned object will be an instance of instance of :class:`IndexedRasterImage` if the color depth is 8 or lower.

Expand Down
14 changes: 14 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public static function fromBinary(DataInputStream $data) : BmpFile
$compressedImgSizeBytes = $uncompressedImgSizeBytes;
} else {
$compressedImgSizeBytes = $infoHeader -> compressedSize;
// Limit height to prevent insane allocations during decompression if file is corrupt
if ($infoHeader -> width > 65535 || $infoHeader -> height > 65535 || $infoHeader -> width < 0 || $infoHeader -> height < 0) {
throw new Exception("Image size " . $infoHeader -> width . "x" . $infoHeader -> height . " is outside the supported range.");
}
}
$compressedImgData = $data -> read($compressedImgSizeBytes);
// De-compress if necessary
Expand All @@ -73,6 +77,16 @@ public static function fromBinary(DataInputStream $data) : BmpFile
$uncompressedImgData = $compressedImgData;
break;
case BmpInfoHeader::B1_RLE8:
if ($infoHeader -> bpp !== 8) {
throw new Exception("RLE8 compression only valid for 8-bit images");
}
$decoder = new Rle8Decoder();
$uncompressedImgData = $decoder -> decode($compressedImgData, $infoHeader -> width, $infoHeader -> height);
$actualSize = strlen($uncompressedImgData);
if ($uncompressedImgSizeBytes !== $actualSize) {
throw new Exception("RLE8 decode failed. Expected $uncompressedImgSizeBytes bytes uncompressed, got $actualSize");
}
break;
case BmpInfoHeader::B1_RLE4:
case BmpInfoHeader::B1_BITFILEDS:
case BmpInfoHeader::B1_JPEG:
Expand Down
73 changes: 73 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/Rle8Decoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
namespace Mike42\GfxPhp\Codec\Bmp;

use Exception;

class Rle8Decoder
{
const RLE_ESCAPE = 0;
const RLE_END_LINE = 0;
const RLE_END_BITMAP = 1;
const RLE_JUMP = 2;

public function decode(string $compressedImgData, int $width, int $height) : string
{
// Read into numeric input.
$inpNum = array_values(unpack("C*", $compressedImgData));
$outpNum = $this -> decodeNumbers($inpNum, $width, $height);
// Back to string
$outStringArr = [];
$rowWidth = intdiv((8 * $width + 31), 32) * 4; // Padding to 4-byte boundary: Can this be simplified?
$padding = str_repeat("\0", $rowWidth - $width);
foreach ($outpNum as $row) {
$outStringArr[] = pack("C*", ...$row);
}
return implode($padding, $outStringArr) . $padding;
}

public function decodeNumbers(array $inpNum, int $width, int $height) : array
{
// Initialize canvas
$buffer = new RleCanvas($width, $height);
// read input data, 2 byes at a time
$i = 0;
$len = intdiv(count($inpNum), 2) * 2;
while ($i < $len) {
$firstByte = $inpNum[$i];
$secondByte = $inpNum[$i + 1];
$i += 2;
if ($firstByte === self::RLE_ESCAPE) {
if ($secondByte === self::RLE_END_LINE) {
$buffer -> endOfLine();
} else if ($secondByte === self::RLE_END_BITMAP) {
$buffer -> endOfBitmap();
} else if ($secondByte === self::RLE_JUMP) {
// "Delta".
if ($i + 2 > $len) { // Need 2 more bytes to find out how far to jump
throw new Exception("Unexpected EOF");
}
$deltaX = $inpNum[$i];
$deltaY = $inpNum[$i + 1];
$i += 2;
$buffer -> delta($deltaX, $deltaY);
} else {
// "Absolute run". Paste the requested number of bytes onto the canvas
$absoluteLen = $secondByte;
if ($i + $absoluteLen > $len) {
throw new Exception("Unexpected EOF");
}
$bytesToPaste = array_slice($inpNum, $i, $absoluteLen);
$i += $absoluteLen;
if ($absoluteLen % 2 != 0) {
// skip a padding byte too
$i++;
}
$buffer -> absolute($bytesToPaste);
}
} else {
$buffer -> repeat($secondByte, $firstByte);
}
}
return $buffer -> getContents();
}
}
87 changes: 87 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/RleCanvas.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace Mike42\GfxPhp\Codec\Bmp;

use Exception;

/**
* 2D canvas for writing RLE-encoded BMP data into. A cursor is moved based on the commands issued, and an Exception
* is thrown if the data goes outside the canvas boundary, or if the data continues after endOfBitmap is called.
*/
class RleCanvas
{
private $buffer;
private $cursorX;
private $cursorY;
private $width;
private $height;
private $complete;

public function __construct(int $width, int $height)
{
$this -> cursorX = 0;
$this -> cursorY = 0;
$this -> width = $width;
$this -> height = $height;
$this -> complete = false;
// Initialise empty space
$tmp = [];
for ($y = 0; $y < $height; $y++) {
$tmp[] = array_fill(0, $width, 0);
}
$this->buffer = $tmp;
}

public function delta(int $deltaX, int $deltaY)
{
$this -> cursorX += $deltaX;
$this -> cursorY += $deltaY;
}

public function endOfLine()
{
$this -> cursorY++;
$this -> cursorX = 0;
}

public function endOfBitmap()
{
$this -> complete = true;
}

public function set(int $val)
{
// Range check when we attempt to write the pixel.
if ($this -> cursorY < 0 || $this -> cursorY >= $this -> height) {
throw new Exception("Bitmap compressed data exceeds image boundary; file is not valid. Y-overflow");
}
if ($this -> cursorX < 0 || $this -> cursorX >= $this -> width) {
throw new Exception("Bitmap compressed data exceeds image boundary; file is not valid. X-overflow");
}
if ($this -> complete) {
throw new Exception("Bitmap compressed data continued after end-of-bitmap code was found; file is not valid.");
}
// Write the pixel
$this -> buffer[$this -> cursorY][$this -> cursorX] = $val;
$this -> cursorX++;
}

public function absolute(array $values)
{
for ($i = 0; $i < count($values); $i++) {
$this -> set($values[$i]);
}
}

public function repeat(int $val, int $times)
{
for ($j = 0; $j < $times; $j++) {
$this -> set($val);
}
}

public function getContents()
{
return $this -> buffer;
}
}
Loading

0 comments on commit 049b9a2

Please sign in to comment.