Skip to content

Commit ed9ded2

Browse files
authored
Merge pull request #52 from mike42/feature/35-bmp
BMP format improvements
2 parents 634c0d7 + fae741e commit ed9ded2

File tree

3 files changed

+82
-32
lines changed

3 files changed

+82
-32
lines changed

src/Mike42/GfxPhp/Codec/Bmp/BmpFile.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public static function fromBinary(DataInputStream $data) : BmpFile
4040
$infoHeader -> bpp != 32) {
4141
throw new Exception("Bit depth " . $infoHeader -> bpp . " not valid.");
4242
} else if ($infoHeader -> bpp === 0) {
43-
// Fail early to give a clearer error for the things which aren't tested yet
44-
throw new Exception("Bit depth " . $infoHeader -> bpp . " not implemented.");
43+
// Fail early to give a clearer error: bit depth 0 is used for embedding PNG/JPEG in a bitmap
44+
throw new Exception("Bit depth " . $infoHeader -> bpp . " not supported.");
4545
}
4646
// See how many colors we expect. 2^n colors in table for bpp <= 8, 0 for higher color depths
4747
$colorCount = $infoHeader -> bpp <= 8 ? 2 ** $infoHeader -> bpp : 0;
@@ -86,6 +86,14 @@ public static function fromBinary(DataInputStream $data) : BmpFile
8686
$data -> advance($fileHeader -> offset - $calculatedOffset);
8787
}
8888
}
89+
if ($infoHeader -> headerSize == BmpInfoHeader::OS22XBITMAPHEADER_FULL_SIZE || $infoHeader -> headerSize == BmpInfoHeader::OS22XBITMAPHEADER_MIN_SIZE) {
90+
// Some compression modes in OS/2 V2 bitmaps use the same numeric ID' as unrelated Windows BMP compression modes, but are not supported.
91+
if ($infoHeader -> compression != BmpInfoHeader::B1_RGB &&
92+
$infoHeader -> compression != BmpInfoHeader::B1_RLE4 &&
93+
$infoHeader -> compression != BmpInfoHeader::B1_RLE8) {
94+
throw new Exception("Compression method not implemented for OS/2 V2 bitmaps");
95+
}
96+
}
8997
// Determine compressed & uncompressed size
9098
$topDown = false;
9199
$height = $infoHeader -> height;
@@ -110,6 +118,7 @@ public static function fromBinary(DataInputStream $data) : BmpFile
110118
switch ($infoHeader -> compression) {
111119
case BmpInfoHeader::B1_RGB:
112120
case BmpInfoHeader::B1_BITFILEDS:
121+
case BmpInfoHeader::B1_ALPHABITFIELDS:
113122
$uncompressedImgData = $compressedImgData;
114123
break;
115124
case BmpInfoHeader::B1_RLE8:
@@ -136,7 +145,6 @@ public static function fromBinary(DataInputStream $data) : BmpFile
136145
break;
137146
case BmpInfoHeader::B1_JPEG:
138147
case BmpInfoHeader::B1_PNG:
139-
case BmpInfoHeader::B1_ALPHABITFIELDS:
140148
case BmpInfoHeader::B1_CMYK:
141149
case BmpInfoHeader::B1_CMYKRLE8:
142150
case BmpInfoHeader::B1_CMYKRLE4:
@@ -164,6 +172,15 @@ public static function fromBinary(DataInputStream $data) : BmpFile
164172
}
165173
// Convert to array of numbers 0-255.
166174
$dataArray = array_values(unpack("C*", $uncompressedImgData));
175+
if ($infoHeader -> profileSize > 0) {
176+
// Skip color profile if present after the image
177+
$imgEnd = $compressedImgSizeBytes + $fileHeader -> offset - BmpFileHeader::FILE_HEADER_SIZE;
178+
$profileStart = $infoHeader -> profileData;
179+
if ($profileStart >= $imgEnd) { // Profile may be before image data, in which case it's already been skipped
180+
$padding = $profileStart - $imgEnd;
181+
$data -> read($infoHeader -> profileSize + $padding);
182+
}
183+
}
167184
if (!$data -> isEof()) {
168185
throw new Exception("BMP image has unexpected trailing data");
169186
}

src/Mike42/GfxPhp/Codec/Bmp/BmpInfoHeader.php

+58-15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class BmpInfoHeader
88
{
99
const BITMAPCOREHEADER_SIZE = 12;
1010
const OS21XBITMAPHEADER_SIZE = 12;
11+
const OS22XBITMAPHEADER_MIN_SIZE = 16;
12+
const OS22XBITMAPHEADER_FULL_SIZE = 64;
1113
const BITMAPINFOHEADER_SIZE = 40;
1214
const BITMAPV2INFOHEADER_SIZE = 52;
1315
const BITMAPV3INFOHEADER_SIZE = 56;
@@ -40,6 +42,12 @@ class BmpInfoHeader
4042
public $greenMask;
4143
public $blueMask;
4244
public $alphaMask;
45+
public $csType;
46+
public $endpoint;
47+
public $gamma;
48+
public $intent;
49+
public $profileData;
50+
public $profileSize;
4351

4452
public function __construct(
4553
int $headerSize,
@@ -59,7 +67,10 @@ public function __construct(
5967
int $alphaMask = 0,
6068
int $csType = 0,
6169
array $endpoint = [],
62-
array $gamma = []
70+
array $gamma = [],
71+
int $intent = 0,
72+
int $profileData = 0,
73+
int $profileSize = 0
6374
) {
6475
$this -> headerSize = $headerSize;
6576
$this -> width = $width;
@@ -77,6 +88,12 @@ public function __construct(
7788
$this -> greenMask = $greenMask;
7889
$this -> blueMask = $blueMask;
7990
$this -> alphaMask = $alphaMask;
91+
$this -> csType = $csType;
92+
$this -> endpoint = $endpoint;
93+
$this -> gamma = $gamma;
94+
$this -> intent = $intent;
95+
$this -> profileData = $profileData;
96+
$this -> profileSize = $profileSize;
8097
}
8198

8299
public static function fromBinary(DataInputStream $data) : BmpInfoHeader
@@ -86,10 +103,10 @@ public static function fromBinary(DataInputStream $data) : BmpInfoHeader
86103
switch ($infoHeaderSize) {
87104
case self::BITMAPCOREHEADER_SIZE:
88105
return self::readCoreHeader($data);
89-
case 64:
90-
return self::readOs22xBitmapHeader($data);
91-
case 16:
92-
throw new Exception("OS22XBITMAPHEADER not implemented");
106+
case self::OS22XBITMAPHEADER_MIN_SIZE:
107+
case self::OS22XBITMAPHEADER_FULL_SIZE:
108+
// OS/2 v2 bitmap header is technically variable-length, only 16 and 64 are used in practice.
109+
return self::readOs22xBitmapHeader($infoHeaderSize, $data);
93110
case self::BITMAPINFOHEADER_SIZE:
94111
return self::readBitmapInfoHeader($data);
95112
case self::BITMAPV2INFOHEADER_SIZE:
@@ -165,7 +182,7 @@ private static function getV5fields(DataInputStream $data) : array
165182

166183
private static function readBitmapInfoHeader(DataInputStream $data) : BmpInfoHeader
167184
{
168-
$headerSize = self::BITMAPINFOHEADER_SIZE;
185+
$extraBytes = 0;
169186
$infoFields = self::getInfoFields($data);
170187
// Quirk- A BITMAPINFOHEADER specifying B1_BITFIELDS has 12 bytes of masks after it.
171188
// In later versions, this information is part of the header itself, and is read unconditionally.
@@ -179,16 +196,16 @@ private static function readBitmapInfoHeader(DataInputStream $data) : BmpInfoHea
179196
$redMask = $rgbMaskFields['redMask'];
180197
$greenMask = $rgbMaskFields['greenMask'];
181198
$blueMask = $rgbMaskFields['blueMask'];
182-
$headerSize += 12;
199+
$extraBytes += 12;
183200
}
184201
if ($infoFields['compression'] === self::B1_ALPHABITFIELDS) {
185202
// we might or might not need to read a 4-byte alpha mask too, depending on the compression type.
186203
$alphaMaskFields = self::getV3fields($data);
187204
$alphaMask = $alphaMaskFields['alphaMask'];
188-
$headerSize += 4;
205+
$extraBytes += 4;
189206
}
190207
return new BmpInfoHeader(
191-
$headerSize,
208+
self::BITMAPINFOHEADER_SIZE + $extraBytes, // Count any extra bytes as part of the header
192209
$infoFields['width'],
193210
$infoFields['height'],
194211
$infoFields['planes'],
@@ -289,9 +306,6 @@ private static function readBitmapV5Header(DataInputStream $data) : BmpInfoHeade
289306
$v3fields = self::getV3fields($data);
290307
$v4fields = self::getV4fields($data);
291308
$v5fields = self::getV5fields($data);
292-
if ($v5fields['profileSize'] > 0) { // TODO include these fields
293-
throw new Exception("Bitmaps with embedded ICC profile data are not supported.");
294-
}
295309
return new BmpInfoHeader(
296310
self::BITMAPV5HEADER_SIZE,
297311
$infoFields['width'],
@@ -310,12 +324,41 @@ private static function readBitmapV5Header(DataInputStream $data) : BmpInfoHeade
310324
$v3fields['alphaMask'],
311325
$v4fields['csType'],
312326
$v4fields['endpoint'],
313-
$v4fields['gamma']
327+
$v4fields['gamma'],
328+
$v5fields['intent'],
329+
$v5fields['profileData'],
330+
$v5fields['profileSize']
314331
);
315332
}
316333

317-
private static function readOs22xBitmapHeader(DataInputStream $data)
334+
private static function readOs22xBitmapHeader(int $size, DataInputStream $data)
318335
{
319-
throw new Exception("OS22XBITMAPHEADER not implemented");
336+
$coreData = $data -> read(self::OS22XBITMAPHEADER_MIN_SIZE - 4);
337+
$coreFields = unpack("Vwidth/Vheight/vplanes/vbpp", $coreData);
338+
if ($size == self::OS22XBITMAPHEADER_MIN_SIZE) {
339+
return new BmpInfoHeader(
340+
self::OS22XBITMAPHEADER_MIN_SIZE,
341+
$coreFields['width'],
342+
$coreFields['height'],
343+
$coreFields['planes'],
344+
$coreFields['bpp']
345+
);
346+
}
347+
// Read up to the full header size
348+
$extraData = $data -> read(self::OS22XBITMAPHEADER_FULL_SIZE - self::OS22XBITMAPHEADER_MIN_SIZE);
349+
$extraFields = unpack("Vcompression/VcompressedSize/VhorizontalRes/VverticalRes/Vcolors/VimportantColors/vunits/vreserved/vrecording/vrendering/Vsize1/Vsize2/VcolorEncoding/Videntifier", $extraData);
350+
return new BmpInfoHeader(
351+
self::OS22XBITMAPHEADER_FULL_SIZE,
352+
$coreFields['width'],
353+
$coreFields['height'],
354+
$coreFields['planes'],
355+
$coreFields['bpp'],
356+
$extraFields['compression'],
357+
$extraFields['compressedSize'],
358+
$extraFields['horizontalRes'],
359+
$extraFields['verticalRes'],
360+
$extraFields['colors'],
361+
$extraFields['importantColors']
362+
); // Other fields are ignored.
320363
}
321364
}

test/integration/BmpsuiteTest.php

+4-14
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ function test_rgb32bfdef()
330330

331331
function test_pal1huff()
332332
{
333-
// Fails here because of unsupported header type, but compression format is not widely implemented either.
334333
$this -> expectException(Exception::class);
335334
$img = $this -> loadImage("q/pal1huff.bmp");
336335
}
@@ -400,7 +399,6 @@ function test_pal8os2sp()
400399

401400
function test_pal8os2v2_16()
402401
{
403-
$this -> markTestSkipped("Not implemented");
404402
$img = $this -> loadImage("q/pal8os2v2-16.bmp");
405403
$this -> assertEquals(127, $img -> getWidth());
406404
$this -> assertEquals(64, $img -> getHeight());
@@ -415,15 +413,13 @@ function test_pal8os2v2_40sz()
415413

416414
function test_pal8os2v2_sz()
417415
{
418-
$this -> markTestSkipped("Not implemented");
419416
$img = $this -> loadImage("q/pal8os2v2-sz.bmp");
420417
$this -> assertEquals(127, $img -> getWidth());
421418
$this -> assertEquals(64, $img -> getHeight());
422419
}
423420

424421
function test_pal8os2v2()
425422
{
426-
$this -> markTestSkipped("Not implemented");
427423
$img = $this -> loadImage("q/pal8os2v2.bmp");
428424
$this -> assertEquals(127, $img -> getWidth());
429425
$this -> assertEquals(64, $img -> getHeight());
@@ -473,7 +469,7 @@ function test_rgb16faketrns()
473469

474470
function test_rgb24jpeg()
475471
{
476-
$this -> markTestSkipped("Not implemented");
472+
$this -> expectException(Exception::class);
477473
$img = $this -> loadImage("q/rgb24jpeg.bmp");
478474
$this -> assertEquals(127, $img -> getWidth());
479475
$this -> assertEquals(64, $img -> getHeight());
@@ -488,31 +484,28 @@ function test_rgb24largepal()
488484

489485
function test_rgb24lprof()
490486
{
491-
$this -> markTestSkipped("Not implemented");
492487
$img = $this -> loadImage("q/rgb24lprof.bmp");
493488
$this -> assertEquals(127, $img -> getWidth());
494489
$this -> assertEquals(64, $img -> getHeight());
495490
}
496491

497492
function test_rgb24png()
498493
{
499-
$this -> markTestSkipped("Not implemented");
494+
$this -> expectException(Exception::class);
500495
$img = $this -> loadImage("q/rgb24png.bmp");
501496
$this -> assertEquals(127, $img -> getWidth());
502497
$this -> assertEquals(64, $img -> getHeight());
503498
}
504499

505500
function test_rgb24prof()
506501
{
507-
$this -> markTestSkipped("Not implemented");
508502
$img = $this -> loadImage("q/rgb24prof.bmp");
509503
$this -> assertEquals(127, $img -> getWidth());
510504
$this -> assertEquals(64, $img -> getHeight());
511505
}
512506

513507
function test_rgb24prof2()
514508
{
515-
$this -> markTestSkipped("Not implemented");
516509
$img = $this -> loadImage("q/rgb24prof2.bmp");
517510
$this -> assertEquals(127, $img -> getWidth());
518511
$this -> assertEquals(64, $img -> getHeight());
@@ -590,7 +583,6 @@ function test_rgba32_61754()
590583

591584
function test_rgba32_81284()
592585
{
593-
$this -> markTestSkipped("Not implemented");
594586
$img = $this -> loadImage("q/rgba32-81284.bmp");
595587
$this -> assertEquals(127, $img -> getWidth());
596588
$this -> assertEquals(64, $img -> getHeight());
@@ -605,7 +597,6 @@ function test_rgba32()
605597

606598
function test_rgba32abf()
607599
{
608-
$this -> markTestSkipped("Not implemented");
609600
$img = $this -> loadImage("q/rgba32abf.bmp");
610601
$this -> assertEquals(127, $img -> getWidth());
611602
$this -> assertEquals(64, $img -> getHeight());
@@ -620,9 +611,8 @@ function test_rgba32h56()
620611

621612
function test_ba_bm()
622613
{
623-
$this -> markTestSkipped("Not implemented");
614+
// Different container format, not recognised as bitmap at all
615+
$this -> expectException(Exception::class);
624616
$img = $this -> loadImage("x/ba-bm.bmp");
625-
$this -> assertEquals(1, $img -> getWidth());
626-
$this -> assertEquals(1, $img -> getHeight());
627617
}
628618
}

0 commit comments

Comments
 (0)