Skip to content

Commit

Permalink
Merge pull request mike42#43 from mike42/feature/35-bmp
Browse files Browse the repository at this point in the history
Add BMP file reader
  • Loading branch information
mike42 authored Jul 8, 2019
2 parents 417526b + ab3cf1a commit 4847807
Show file tree
Hide file tree
Showing 98 changed files with 870 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ As a small project, we can't do everything. In particular, `gfx-php` is not like

This repository uses test files from other projects:

- [BMP Suite](http://entropymine.com/jason/bmpsuite/) by Jason Summers.
- [PyGIF](https://github.com/robert-ancell/pygif) test suite by Robert Ancell.
- [pngsuite](http://www.schaik.com/pngsuite/) by Willem van Schaik.

Expand Down
8 changes: 8 additions & 0 deletions docs/user/formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ The GIF codec is used where the input has the ``gif`` file extension. Any well-f

A GIF image will always be loaded into an instance of :class:`IndexedRasterImage`, which makes palette information available.

PNG
^^^

The BMP codec is used where the input has the ``bmp`` or ``dib`` file extensions.

Only uncompressed 24-bit color bitmap images may currently be read. The returned object will be an
instance of instance of :class:`RgbRasterImage`.

Netpbm Formats
^^^^^^^^^^^^^^

Expand Down
106 changes: 106 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
namespace Mike42\GfxPhp\Codec\Bmp;

use Exception;
use Mike42\GfxPhp\Codec\Common\DataInputStream;
use Mike42\GfxPhp\RasterImage;
use Mike42\GfxPhp\RgbRasterImage;

class BmpFile
{
const BMP_SIGNATURE = "BM";

private $fileHeader;
private $infoHeader;
private $uncompressedData;

public function __construct(BmpFileHeader $fileHeader, BmpInfoHeader $infoHeader, array $data)
{
$this -> fileHeader = $fileHeader;
$this -> infoHeader = $infoHeader;
$this -> uncompressedData = $data;
}

public static function fromBinary(DataInputStream $data) : BmpFile
{
// Read two different headers
$fileHeader = BmpFileHeader::fromBinary($data);
$infoHeader = BmpInfoHeader::fromBinary($data);
if ($infoHeader -> bpp != 0 &&
$infoHeader -> bpp != 1 &&
$infoHeader -> bpp != 4 &&
$infoHeader -> bpp != 8 &&
$infoHeader -> bpp != 16 &&
$infoHeader -> bpp != 24 &&
$infoHeader -> bpp != 32) {
throw new Exception("Bit depth " . $infoHeader -> bpp . " not valid.");
} else if ($infoHeader -> bpp != 24) {
// Fail early to give a clearer error for the things which aren't tested yet
throw new Exception("Bit depth " . $infoHeader -> bpp . " not implemented.");
}
// Skip color table (allowed in a true color image, but not useful)
if ($infoHeader -> colors > 0) {
// for non-truecolor images, 0 will mean 2^bpp.
// Size of each entry may also be variable
$data -> read($infoHeader -> colors * 4);
}
// Determine compressed & uncompressed size
$rowSizeBytes = intdiv(($infoHeader -> bpp * $infoHeader -> width + 31), 32) * 4;
$uncompressedImgSizeBytes = $rowSizeBytes * $infoHeader -> height;
if ($infoHeader -> compression == BmpInfoHeader::B1_RGB) {
$compressedImgSizeBytes = $uncompressedImgSizeBytes;
} else {
$compressedImgSizeBytes = $infoHeader -> compressedSize;
}
$compressedImgData = $data -> read($compressedImgSizeBytes);
// De-compress if necessary
switch ($infoHeader -> compression) {
case BmpInfoHeader::B1_RGB:
$uncompressedImgData = $compressedImgData;
break;
case BmpInfoHeader::B1_RLE8:
case BmpInfoHeader::B1_RLE4:
case BmpInfoHeader::B1_BITFILEDS:
case BmpInfoHeader::B1_JPEG:
case BmpInfoHeader::B1_PNG:
case BmpInfoHeader::B1_ALPHABITFIELDS:
case BmpInfoHeader::B1_CMYK:
case BmpInfoHeader::B1_CMYKRLE8:
case BmpInfoHeader::B1_CMYKRLE4:
throw new Exception("Compression method not implemented");
default:
throw new Exception("Bad compression method");
}
// Account for padding, row order
$paddedLines = str_split($uncompressedImgData, $rowSizeBytes);
$dataLines = [];
$rowDataBytes = intdiv($infoHeader -> bpp * $infoHeader -> width + 7, 8); // Excludes padding bytes
for ($i = count($paddedLines) - 1; $i >= 0; $i--) { // Iterate lines backwards
$dataLines[] = substr($paddedLines[$i], 0, $rowDataBytes);
}
$uncompressedImgData = implode("", $dataLines);
// Account for RGB vs BGR in file format
if ($infoHeader -> bpp == 24) {
$pixels = str_split($uncompressedImgData, 3);
array_walk($pixels, ["\\Mike42\\GfxPhp\\Codec\\Bmp\\BmpFile", "transformRevString"]);
$uncompressedImgData = implode("", $pixels);
}
// Convert to array of numbers 0-255.
$dataArray = array_values(unpack("c*", $uncompressedImgData));
return new BmpFile($fileHeader, $infoHeader, $dataArray);
}

public function toRasterImage() : RasterImage
{
if ($this -> infoHeader -> bpp == 24) {
return RgbRasterImage::create($this -> infoHeader -> width, $this -> infoHeader -> height, $this -> uncompressedData);
}
throw new Exception("Unknown bit depth " . $this -> infoHeader -> bpp);
}

public static function transformRevString(&$item, $key)
{
// Convert RGB to BGR
$item = strrev($item);
}
}
31 changes: 31 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpFileHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php


namespace Mike42\GfxPhp\Codec\Bmp;

use Exception;
use Mike42\GfxPhp\Codec\Common\DataInputStream;

class BmpFileHeader
{
public $offset;
public $size;

public function __construct(string $fileType, int $size, int $offset)
{
$this -> fileType = $fileType;
$this -> size = $size;
$this -> offset = $offset;
}

public static function fromBinary(DataInputStream $data) : BmpFileHeader
{
$fileType = $data->read(2);
if (array_search($fileType, ["BM", "BA", "CI", "CP", "IC", "PT", "OS"]) === false) {
throw new Exception("Not a bitmap image");
}
$fileHeaderData = $data->read(12);
$fields = unpack("Vsize/vreserved1/vreserved2/Voffset", $fileHeaderData);
return new BmpFileHeader($fileType, $fields['size'], $fields['offset']);
}
}
90 changes: 90 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpInfoHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
namespace Mike42\GfxPhp\Codec\Bmp;

use Exception;
use Mike42\GfxPhp\Codec\Common\DataInputStream;

class BmpInfoHeader
{
const BITMAPCOREHEADER_SIZE = 12;
const OS21XBITMAPHEADER_SIZE = 12;
const BITMAPINFOHEADER_SIZE = 40;

const B1_RGB = 0;
const B1_RLE8 = 1;
const B1_RLE4 = 2;
const B1_BITFILEDS = 3;
const B1_JPEG = 4;
const B1_PNG = 5;
const B1_ALPHABITFIELDS = 6;
const B1_CMYK = 11;
const B1_CMYKRLE8 = 12;
const B1_CMYKRLE4 = 13;

public $bpp;
public $colors;
public $compressedSize;
public $compression;
public $headerSize;
public $height;
public $hprizontalRes;
public $importantColors;
public $planes;
public $verticalRes;
public $width;

public function __construct(int $headerSize, int $width, int $height, int $planes, int $bpp, int $compression = 0, int $compressedSize = 0, int $horizontalRes = 0, int $verticalRes = 0, int $colors = 0, int $importantColors = 0)
{
$this -> headerSize = $headerSize;
$this -> width = $width;
$this -> height = $height;
$this -> planes = $planes;
$this -> bpp = $bpp;
$this -> compression = $compression;
$this -> compressedSize = $compressedSize;
$this -> hprizontalRes = $horizontalRes;
$this -> verticalRes = $verticalRes;
$this -> colors = $colors;
$this -> importantColors = $importantColors;
}

public static function fromBinary(DataInputStream $data) : BmpInfoHeader
{
$infoHeaderSizeData = $data -> read(4);
$infoHeaderSize = unpack("V", $infoHeaderSizeData)[1];
switch ($infoHeaderSize) {
case self::BITMAPCOREHEADER_SIZE:
return self::readCoreHeader($data);
case 64:
throw new Exception("OS22XBITMAPHEADER not implemented");
case 16:
throw new Exception("OS22XBITMAPHEADER not implemented");
case self::BITMAPINFOHEADER_SIZE:
return self::readBitmapInfoHeader($data);
case 52:
throw new Exception("BITMAPV2INFOHEADER not implemented");
case 56:
throw new Exception("BITMAPV3INFOHEADER not implemented");
case 108:
throw new Exception("BITMAPV4HEADER not implemented");
case 124:
throw new Exception("BITMAPV5HEADER not implemented");
default:
throw new Exception("Info header size " . $infoHeaderSize . " is not supported.");
}
}

private static function readCoreHeader(DataInputStream $data) : BmpInfoHeader
{
$infoData = $data -> read(self::BITMAPCOREHEADER_SIZE - 4);
$fields = unpack("vwidth/vheight/vplanes/vbpp", $infoData);
return new BmpInfoHeader(self::BITMAPCOREHEADER_SIZE, $fields['width'], $fields['height'], $fields['planes'], $fields['bpp']);
}

private static function readBitmapInfoHeader(DataInputStream $data) : BmpInfoHeader
{
$infoData = $data -> read(self::BITMAPINFOHEADER_SIZE - 4);
$fields = unpack("Vwidth/Vheight/vplanes/vbpp/Vcompression/VcompressedSize/VhorizontalRes/VverticalRes/Vcolors/VimportantColors", $infoData);
return new BmpInfoHeader(self::BITMAPINFOHEADER_SIZE, $fields['width'], $fields['height'], $fields['planes'], $fields['bpp'], $fields['compression'], $fields['compressedSize'], $fields['horizontalRes'], $fields['verticalRes'], $fields['colors'], $fields['importantColors']);
}
}
25 changes: 24 additions & 1 deletion src/Mike42/GfxPhp/Codec/BmpCodec.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace Mike42\GfxPhp\Codec;

use Mike42\GfxPhp\Codec\Bmp\BmpFile;
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
use Mike42\GfxPhp\Codec\Gif\GifDataStream;
use Mike42\GfxPhp\RasterImage;
use Mike42\GfxPhp\RgbRasterImage;

class BmpCodec implements ImageEncoder
class BmpCodec implements ImageEncoder, ImageDecoder
{
protected static $instance = null;
const INFO_HEADER_SIZE = 40;
Expand Down Expand Up @@ -71,6 +74,26 @@ public function getEncodeFormats(): array
return ["bmp", "dib"];
}

public function decode(string $blob): RasterImage
{
$data = DataBlobInputStream::fromBlob($blob);
$bmp = BmpFile::fromBinary($data);
return $bmp -> toRasterImage();
}

public function identify(string $blob): string
{
if (substr($blob, 0, 2) == BmpFile::BMP_SIGNATURE) {
return "bmp";
}
return "";
}

public function getDecodeFormats(): array
{
return ["bmp", "dib"];
}

public static function getInstance()
{
if (self::$instance === null) {
Expand Down
1 change: 1 addition & 0 deletions src/Mike42/GfxPhp/Codec/ImageCodec.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static function getInstance() : ImageCodec
$decoders = [
PngCodec::getInstance(),
GifCodec::getInstance(),
BmpCodec::getInstance(),
PnmCodec::getInstance(),
WbmpCodec::getInstance()
];
Expand Down
Loading

0 comments on commit 4847807

Please sign in to comment.