Skip to content

Commit

Permalink
Merge pull request mike42#47 from mike42/feature/35-bmp
Browse files Browse the repository at this point in the history
Add support for additional BMP file types
  • Loading branch information
mike42 authored Sep 22, 2019
2 parents 049b9a2 + 173c9da commit 5492d8e
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 29 deletions.
54 changes: 46 additions & 8 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,37 @@ public static function fromBinary(DataInputStream $data) : BmpFile
}
// See how many colors we expect. 2^n colors in table for bpp <= 8, 0 for higher color depths
$colorCount = $infoHeader -> bpp <= 8 ? 2 ** $infoHeader -> bpp : 0;
if ($infoHeader -> colors > 0) {
// .. unless otherwise specified
$colorCount = $infoHeader -> colors;
}
$colorTable = [];
for ($i = 0; $i < $colorCount; $i++) {
$entryData = $data -> read(4);
$color = unpack("C*", $entryData);
$colorTable[] = [$color[3], $color[2], $color[1]];
if (self::isOs21XBitmap($fileHeader, $infoHeader, $colorCount)) {
// This type of image may only be 1, 4, 8 or 24 bit
if ($infoHeader -> bpp != 1 &&
$infoHeader -> bpp != 4 &&
$infoHeader -> bpp != 8 &&
$infoHeader -> bpp != 24) {
throw new Exception("Bit depth " . $infoHeader->bpp . " not valid for OS/2 1.x bitmap.");
}
$calculatedTableSize = intdiv($fileHeader -> offset - (BmpInfoHeader::OS21XBITMAPHEADER_SIZE + BmpFileHeader::FILE_HEADER_SIZE), 3);
if ($calculatedTableSize < $colorCount) {
// Downsize the palette based on observed offset: only non-standard files do this.
$colorCount = $calculatedTableSize;
}
// OS/2 1.x bitmaps use 3-bytes per color
for ($i = 0; $i < $colorCount; $i++) {
$entryData = $data->read(3);
$color = unpack("C*", $entryData);
$colorTable[] = [$color[3], $color[2], $color[1]];
}
// In the case of 1bpp or small palettes, it is possible that we are not aligned to a multiple of 4 bytes now.
} else {
if ($infoHeader -> colors > 0) {
// .. unless otherwise specified
$colorCount = $infoHeader -> colors;
}
for ($i = 0; $i < $colorCount; $i++) {
$entryData = $data->read(4);
$color = unpack("C*", $entryData);
$colorTable[] = [$color[3], $color[2], $color[1]];
}
}
// May need to skip here if header shows pixel data later than we expect
// Determine compressed & uncompressed size
Expand Down Expand Up @@ -121,6 +143,22 @@ public static function fromBinary(DataInputStream $data) : BmpFile
return new BmpFile($fileHeader, $infoHeader, $dataArray, $colorTable);
}

private static function isOs21XBitmap(BmpFileHeader $fileHeader, BmpInfoHeader $infoHeader, int $colorCount)
{
// OS/2 1.x bitmaps use 24 bits per entry in the color palette, rather than 32, but share the same 12-byte
// header as original Windows bitmaps. If the header size, color count and offset to the bitmap data are
// consistent with 24-bit color table, then this function returns true.
if ($infoHeader -> headerSize !== BmpInfoHeader::OS21XBITMAPHEADER_SIZE) {
// Wrong header size
return false;
}
if ($fileHeader -> offset > $colorCount * 3 + BmpInfoHeader::OS21XBITMAPHEADER_SIZE + BmpFileHeader::FILE_HEADER_SIZE) {
// Data starts later than we expect
return false;
}
return true;
}

public function toRasterImage() : RasterImage
{
if ($this -> infoHeader -> bpp == 1) {
Expand Down
2 changes: 2 additions & 0 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpFileHeader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

class BmpFileHeader
{
const FILE_HEADER_SIZE = 14;

public $offset;
public $size;

Expand Down
227 changes: 212 additions & 15 deletions src/Mike42/GfxPhp/Codec/Bmp/BmpInfoHeader.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class BmpInfoHeader
const BITMAPCOREHEADER_SIZE = 12;
const OS21XBITMAPHEADER_SIZE = 12;
const BITMAPINFOHEADER_SIZE = 40;
const BITMAPV2INFOHEADER_SIZE = 52;
const BITMAPV3INFOHEADER_SIZE = 56;
const BITMAPV4HEADER_SIZE = 108;
const BITMAPV5HEADER_SIZE = 124;

const B1_RGB = 0;
const B1_RLE8 = 1;
Expand All @@ -33,8 +37,26 @@ class BmpInfoHeader
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)
{
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,
int $redMask = 0,
int $greenMask = 0,
int $blueMask = 0,
int $alphaMask = 0,
int $csType = 0,
array $endpoint = [],
array $gamma = []
) {
$this -> headerSize = $headerSize;
$this -> width = $width;
$this -> height = $height;
Expand All @@ -56,19 +78,19 @@ public static function fromBinary(DataInputStream $data) : BmpInfoHeader
case self::BITMAPCOREHEADER_SIZE:
return self::readCoreHeader($data);
case 64:
throw new Exception("OS22XBITMAPHEADER not implemented");
return self::readOs22xBitmapHeader($data);
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");
case self::BITMAPV2INFOHEADER_SIZE:
return self::readBitmapV2InfoHeader($data);
case self::BITMAPV3INFOHEADER_SIZE:
return self::readBitmapV3InfoHeader($data);
case self::BITMAPV4HEADER_SIZE:
return self::readBitmapV4Header($data);
case self::BITMAPV5HEADER_SIZE:
return self::readBitmapV5Header($data);
default:
throw new Exception("Info header size " . $infoHeaderSize . " is not supported.");
}
Expand All @@ -78,13 +100,188 @@ 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']);
return new BmpInfoHeader(
self::BITMAPCOREHEADER_SIZE,
$fields['width'],
$fields['height'],
$fields['planes'],
$fields['bpp']
);
}

private static function readBitmapInfoHeader(DataInputStream $data) : BmpInfoHeader
private static function getInfoFields(DataInputStream $data) : array
{
$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']);
return unpack("Vwidth/Vheight/vplanes/vbpp/Vcompression/VcompressedSize/VhorizontalRes/VverticalRes/Vcolors/VimportantColors", $infoData);
}

private static function getV2fields(DataInputStream $data) : array
{
$infoData = $data -> read(self::BITMAPV2INFOHEADER_SIZE - self::BITMAPINFOHEADER_SIZE);
return unpack("VredMask/VgreenMask/VblueMask", $infoData);
}

private static function getV3fields(DataInputStream $data) : array
{
$infoData = $data -> read(self::BITMAPV3INFOHEADER_SIZE - self::BITMAPV2INFOHEADER_SIZE);
return unpack("ValphaMask", $infoData);
}

private static function getV4fields(DataInputStream $data) : array
{
// color space
$csTypeData = $data -> read(4);
$csType = unpack("VcsType", $csTypeData);
// endpoint
$csEndpoints = [];
foreach (['red', 'green', 'blue'] as $channel) {
$channelData = $data -> read(12);
$csEndpoints[$channel] = unpack('Vx/Vy/Vz', $channelData);
}
// gamma
$gammaData = $data -> read(12);
$gamma = unpack("Vred/Vgreen/Vblue", $gammaData);
return [
'csType' => $csType['csType'],
'endpoint' => $csEndpoints,
'gamma' => $gamma
];
}

private static function getV5fields(DataInputStream $data) : array
{
$infoData = $data -> read(self::BITMAPV5HEADER_SIZE - self::BITMAPV4HEADER_SIZE);
return unpack("Vintent/VprofileData/VprofileSize/Vreserved", $infoData);
}

private static function readBitmapInfoHeader(DataInputStream $data) : BmpInfoHeader
{
$infoFields = self::getInfoFields($data);
return new BmpInfoHeader(
self::BITMAPINFOHEADER_SIZE,
$infoFields['width'],
$infoFields['height'],
$infoFields['planes'],
$infoFields['bpp'],
$infoFields['compression'],
$infoFields['compressedSize'],
$infoFields['horizontalRes'],
$infoFields['verticalRes'],
$infoFields['colors'],
$infoFields['importantColors']
);
}

private static function readBitmapV2InfoHeader(DataInputStream $data) : BmpInfoHeader
{
$infoFields = self::getInfoFields($data);
$v2fields = self::getV2fields($data);
return new BmpInfoHeader(
self::BITMAPV2INFOHEADER_SIZE,
$infoFields['width'],
$infoFields['height'],
$infoFields['planes'],
$infoFields['bpp'],
$infoFields['compression'],
$infoFields['compressedSize'],
$infoFields['horizontalRes'],
$infoFields['verticalRes'],
$infoFields['colors'],
$infoFields['importantColors'],
$v2fields['redMask'],
$v2fields['greenMask'],
$v2fields['blueMask']
);
}

private static function readBitmapV3InfoHeader(DataInputStream $data) : BmpInfoHeader
{
$infoFields = self::getInfoFields($data);
$v2fields = self::getV2fields($data);
$v3fields = self::getV3fields($data);
return new BmpInfoHeader(
self::BITMAPV3INFOHEADER_SIZE,
$infoFields['width'],
$infoFields['height'],
$infoFields['planes'],
$infoFields['bpp'],
$infoFields['compression'],
$infoFields['compressedSize'],
$infoFields['horizontalRes'],
$infoFields['verticalRes'],
$infoFields['colors'],
$infoFields['importantColors'],
$v2fields['redMask'],
$v2fields['greenMask'],
$v2fields['blueMask'],
$v3fields['alphaMask']
);
}

private static function readBitmapV4Header(DataInputStream $data) : BmpInfoHeader
{
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv4header
$infoFields = self::getInfoFields($data);
$v2fields = self::getV2fields($data);
$v3fields = self::getV3fields($data);
$v4fields = self::getV4fields($data);
return new BmpInfoHeader(
self::BITMAPV4HEADER_SIZE,
$infoFields['width'],
$infoFields['height'],
$infoFields['planes'],
$infoFields['bpp'],
$infoFields['compression'],
$infoFields['compressedSize'],
$infoFields['horizontalRes'],
$infoFields['verticalRes'],
$infoFields['colors'],
$infoFields['importantColors'],
$v2fields['redMask'],
$v2fields['greenMask'],
$v2fields['blueMask'],
$v3fields['alphaMask'],
$v4fields['csType'],
$v4fields['endpoint'],
$v4fields['gamma']
);
}

private static function readBitmapV5Header(DataInputStream $data) : BmpInfoHeader
{
// Structure documented @ https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
$infoFields = self::getInfoFields($data);
$v2fields = self::getV2fields($data);
$v3fields = self::getV3fields($data);
$v4fields = self::getV4fields($data);
$v5fields = self::getV5fields($data);
if ($v5fields['profileSize'] > 0) { // TODO include these fields
throw new Exception("Bitmaps with embedded ICC profile data are not supported.");
}
return new BmpInfoHeader(
self::BITMAPV5HEADER_SIZE,
$infoFields['width'],
$infoFields['height'],
$infoFields['planes'],
$infoFields['bpp'],
$infoFields['compression'],
$infoFields['compressedSize'],
$infoFields['horizontalRes'],
$infoFields['verticalRes'],
$infoFields['colors'],
$infoFields['importantColors'],
$v2fields['redMask'],
$v2fields['greenMask'],
$v2fields['blueMask'],
$v3fields['alphaMask'],
$v4fields['csType'],
$v4fields['endpoint'],
$v4fields['gamma']
);
}

private static function readOs22xBitmapHeader(DataInputStream $data)
{
throw new Exception("OS22XBITMAPHEADER not implemented");
}
}
6 changes: 0 additions & 6 deletions test/integration/BmpsuiteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ function test_pal8nonsquare()

function test_pal8os2()
{
$this -> markTestSkipped("Not implemented");
$img = $this -> loadImage("g/pal8os2.bmp");
$this -> assertEquals(127, $img -> getWidth());
$this -> assertEquals(64, $img -> getHeight());
Expand All @@ -234,15 +233,13 @@ function test_pal8topdown()

function test_pal8v4()
{
$this -> markTestSkipped("Not implemented");
$img = $this -> loadImage("g/pal8v4.bmp");
$this -> assertEquals(127, $img -> getWidth());
$this -> assertEquals(64, $img -> getHeight());
}

function test_pal8v5()
{
$this -> markTestSkipped("Not implemented");
$img = $this -> loadImage("g/pal8v5.bmp");
$this -> assertEquals(127, $img -> getWidth());
$this -> assertEquals(64, $img -> getHeight());
Expand Down Expand Up @@ -394,23 +391,20 @@ function test_pal8offs()

function test_pal8os2_hs()
{
$this -> markTestSkipped("Not implemented");
$img = $this -> loadImage("q/pal8os2-hs.bmp");
$this -> assertEquals(127, $img -> getWidth());
$this -> assertEquals(64, $img -> getHeight());
}

function test_pal8os2_sz()
{
$this -> markTestSkipped("Not implemented");
$img = $this -> loadImage("q/pal8os2-sz.bmp");
$this -> assertEquals(127, $img -> getWidth());
$this -> assertEquals(64, $img -> getHeight());
}

function test_pal8os2sp()
{
$this -> markTestSkipped("Not implemented");
$img = $this -> loadImage("q/pal8os2sp.bmp");
$this -> assertEquals(127, $img -> getWidth());
$this -> assertEquals(64, $img -> getHeight());
Expand Down

0 comments on commit 5492d8e

Please sign in to comment.