From d46c9d2da76ebb39ea53d7875af7821540c35cff Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Tue, 26 Feb 2019 22:07:56 +1100 Subject: [PATCH 1/4] initial WBMP implementation, ok for up to 127x127. --- example/format-convert.php | 14 +++++ example/resources/bricks.wbmp | Bin 0 -> 16 bytes src/Mike42/GfxPhp/Codec/ImageCodec.php | 6 +- src/Mike42/GfxPhp/Codec/WbmpCodec.php | 77 +++++++++++++++++++++++++ test/unit/Codec/GifCodecTest.php | 9 +++ test/unit/Codec/WbmpCodecTest.php | 28 +++++++++ 6 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 example/resources/bricks.wbmp create mode 100644 src/Mike42/GfxPhp/Codec/WbmpCodec.php create mode 100644 test/unit/Codec/WbmpCodecTest.php diff --git a/example/format-convert.php b/example/format-convert.php index 20c8147..3ef17ab 100644 --- a/example/format-convert.php +++ b/example/format-convert.php @@ -11,6 +11,7 @@ $img -> write("colorwheel.pgm"); $img -> write("colorwheel.png"); $img -> write("colorwheel.ppm"); +$img -> write("colorwheel.wbmp"); // Write gradient.pgm out as each supported format $img = Image::fromFile(dirname(__FILE__). "/resources/gradient.pgm"); @@ -20,6 +21,7 @@ $img -> write("gradient.pgm"); $img -> write("gradient.png"); $img -> write("gradient.ppm"); +$img -> write("gradient.wbmp"); // Write 5x7hex.pbm out as each supported format $img = Image::fromFile(dirname(__FILE__). "/resources/5x7hex.pbm"); @@ -29,6 +31,7 @@ $img -> write("font.pgm"); $img -> write("font.png"); $img -> write("font.ppm"); +$img -> write("font.wbmp"); // Write abc.png out as each supported format $img = Image::fromFile(dirname(__FILE__). "/resources/abc.png"); @@ -38,3 +41,14 @@ $img -> write("abc.pgm"); $img -> write("abc.png"); $img -> write("abc.ppm"); +$img -> write("abc.wbmp"); + +// Write bricks.wbmp out as each supported format +$img = Image::fromFile(dirname(__FILE__). "/resources/bricks.wbmp"); +$img -> write("bricks.bmp"); +$img -> write("bricks.gif"); +$img -> write("bricks.pbm"); +$img -> write("bricks.pgm"); +$img -> write("bricks.png"); +$img -> write("bricks.ppm"); +$img -> write("bricks.wbmp"); diff --git a/example/resources/bricks.wbmp b/example/resources/bricks.wbmp new file mode 100644 index 0000000000000000000000000000000000000000..821396c3d8fe5b2fd022c5de87a84a2d9c31c165 GIT binary patch literal 16 YcmZQz;9*ml@c)CS!v7DG9R7a*0522=&j0`b literal 0 HcmV?d00001 diff --git a/src/Mike42/GfxPhp/Codec/ImageCodec.php b/src/Mike42/GfxPhp/Codec/ImageCodec.php index 63b5fb9..7101e2e 100644 --- a/src/Mike42/GfxPhp/Codec/ImageCodec.php +++ b/src/Mike42/GfxPhp/Codec/ImageCodec.php @@ -55,12 +55,14 @@ public static function getInstance() : ImageCodec PnmCodec::getInstance(), BmpCodec::getInstance(), PngCodec::getInstance(), - GifCodec::getInstance() + GifCodec::getInstance(), + WbmpCodec::getInstance() ]; $decoders = [ PngCodec::getInstance(), GifCodec::getInstance(), - PnmCodec::getInstance() + PnmCodec::getInstance(), + WbmpCodec::getInstance() ]; self::$instance = new ImageCodec($encoders, $decoders); } diff --git a/src/Mike42/GfxPhp/Codec/WbmpCodec.php b/src/Mike42/GfxPhp/Codec/WbmpCodec.php new file mode 100644 index 0000000..fd0cb69 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/WbmpCodec.php @@ -0,0 +1,77 @@ + read(2); + if($header != "\x00\x00") { + throw new Exception("Not a WBMP file"); + } + $width = ord($data -> read(1)); + if($width > 127) { + throw new Exception("Maximum image width is 127"); + } + $height = ord($data -> read(1)); + $bytesPerRow = intdiv($width + 7, 8); + $expectedBytes = $bytesPerRow * $height; + $binaryData = $data -> read($expectedBytes); + $dataUnpacked = unpack("C*", $binaryData); + $dataValues = array_values($dataUnpacked); + // 1 for white, 0 for black (opposite) + $image = BlackAndWhiteRasterImage::create($width, $height, $dataValues); + $image -> invert(); + return $image; + + } + + public function getDecodeFormats(): array + { + return ["wbmp"]; + } + + public function encode(RasterImage $image, string $format): string + { + $image = $image = $image -> toBlackAndWhite(); + if($image -> getWidth() > 127 || $image -> getHeight() > 127) { + throw new Exception("Maximum image width or height is 127"); + } + $image -> invert(); + return "\x00\x00" . chr($image -> getWidth()) . chr($image -> getHeight()) . $image -> getRasterData(); + } + + public function getEncodeFormats(): array + { + return ["wbmp"]; + } + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new WbmpCodec(); + } + return self::$instance; + } +} \ No newline at end of file diff --git a/test/unit/Codec/GifCodecTest.php b/test/unit/Codec/GifCodecTest.php index 3593786..282447d 100644 --- a/test/unit/Codec/GifCodecTest.php +++ b/test/unit/Codec/GifCodecTest.php @@ -61,5 +61,14 @@ public function testGifEncode() { $this -> assertEquals(self::GIF_IMAGE, $imageStr); } + public function testGifDecode() { + $decoder = new GifCodec(); + $image = $decoder -> decode(self::GIF_IMAGE, 'gif') -> toIndexed(); + $this -> assertEquals(1, $image -> getWidth()); + $this -> assertEquals(1, $image -> getHeight()); + $this -> assertEquals([255, 255, 255], $image -> indexToRgb($image -> getPixel(0, 0))); + } + + } diff --git a/test/unit/Codec/WbmpCodecTest.php b/test/unit/Codec/WbmpCodecTest.php new file mode 100644 index 0000000..bc82cd8 --- /dev/null +++ b/test/unit/Codec/WbmpCodecTest.php @@ -0,0 +1,28 @@ + decode(self::WBMP_IMAGE) -> toBlackAndWhite(); + $this -> assertEquals(12, $image -> getWidth()); + $this -> assertEquals(6, $image -> getHeight()); + $content = "▀▀ ▀▀ ▀▀ ▀▀ \n" . + "▀ ▀▀ ▀▀ ▀▀ ▀\n" . + " ▀▀ ▀▀ ▀▀ ▀▀\n"; + $this -> assertEquals($content, $image -> toString()); + } + + public function testEncode() { + // Raster representation is inverse to WBMP format. + $image = BlackAndWhiteRasterImage::create(12, 6, [0xdb, 0x6f, 0x00, 0x0f, 0xb6, 0xdf, 0x00, 0x0f, 0x6d, 0xbf, 0x00, 0x0f]); + $codec = new WbmpCodec(); + $data = $codec -> encode($image, "wbmp"); + $this -> assertEquals(self::WBMP_IMAGE, $data); + } +} \ No newline at end of file From f0918d9adedfc329bf7335ecd903e5c669c043cd Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Tue, 26 Feb 2019 22:09:27 +1100 Subject: [PATCH 2/4] phpcs fixes --- src/Mike42/GfxPhp/Codec/WbmpCodec.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Mike42/GfxPhp/Codec/WbmpCodec.php b/src/Mike42/GfxPhp/Codec/WbmpCodec.php index fd0cb69..6d8e86e 100644 --- a/src/Mike42/GfxPhp/Codec/WbmpCodec.php +++ b/src/Mike42/GfxPhp/Codec/WbmpCodec.php @@ -27,11 +27,11 @@ public function decode(string $blob): RasterImage { $data = DataBlobInputStream::fromBlob($blob); $header = $data -> read(2); - if($header != "\x00\x00") { + if ($header != "\x00\x00") { throw new Exception("Not a WBMP file"); } $width = ord($data -> read(1)); - if($width > 127) { + if ($width > 127) { throw new Exception("Maximum image width is 127"); } $height = ord($data -> read(1)); @@ -44,7 +44,6 @@ public function decode(string $blob): RasterImage $image = BlackAndWhiteRasterImage::create($width, $height, $dataValues); $image -> invert(); return $image; - } public function getDecodeFormats(): array @@ -55,7 +54,7 @@ public function getDecodeFormats(): array public function encode(RasterImage $image, string $format): string { $image = $image = $image -> toBlackAndWhite(); - if($image -> getWidth() > 127 || $image -> getHeight() > 127) { + if ($image -> getWidth() > 127 || $image -> getHeight() > 127) { throw new Exception("Maximum image width or height is 127"); } $image -> invert(); @@ -74,4 +73,4 @@ public static function getInstance() } return self::$instance; } -} \ No newline at end of file +} From dddbb9529e3ad213f022f35aefc53ac9d733b20e Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Thu, 28 Feb 2019 21:35:53 +1100 Subject: [PATCH 3/4] implement multi-byte integers for WBMP, allowing images > 127 pixels wide/tall --- src/Mike42/GfxPhp/Codec/WbmpCodec.php | 45 ++++++++++++++++---- test/unit/Codec/WbmpCodecTest.php | 61 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/Mike42/GfxPhp/Codec/WbmpCodec.php b/src/Mike42/GfxPhp/Codec/WbmpCodec.php index 6d8e86e..f39ef33 100644 --- a/src/Mike42/GfxPhp/Codec/WbmpCodec.php +++ b/src/Mike42/GfxPhp/Codec/WbmpCodec.php @@ -30,11 +30,8 @@ public function decode(string $blob): RasterImage if ($header != "\x00\x00") { throw new Exception("Not a WBMP file"); } - $width = ord($data -> read(1)); - if ($width > 127) { - throw new Exception("Maximum image width is 127"); - } - $height = ord($data -> read(1)); + $width = $this -> readInt($data); + $height = $this -> readInt($data); $bytesPerRow = intdiv($width + 7, 8); $expectedBytes = $bytesPerRow * $height; $binaryData = $data -> read($expectedBytes); @@ -46,6 +43,39 @@ public function decode(string $blob): RasterImage return $image; } + public function readInt(DataBlobInputStream $data) : int + { + $i = 0; + $ret = 0; + do { + $byte = ord($data -> read(1)); + $ret = ($ret << 7) | ($byte & 0x7F); + $continuation = $byte >> 7 == 1; + $i++; + } while ($continuation && $i < 4); // Limit to 4 bytes to avoid overflow. + if ($continuation) { + throw new Exception("WBMP image size too large, file may be corrupt"); + } + return $ret; + } + + public function writeInt(int $val) : string + { + $i = 0; + $ret = chr($val & 0x7F); + $val >>= 7; + while ($val > 0 && $i < 3) { + $byteVal = ($val & 0x7F) | 0x80; + $ret = chr($byteVal) . $ret; + $val >>= 7; + $i++; + } + if ($val > 0) { + throw new Exception("WBMP image size too large."); + } + return $ret; + } + public function getDecodeFormats(): array { return ["wbmp"]; @@ -54,11 +84,8 @@ public function getDecodeFormats(): array public function encode(RasterImage $image, string $format): string { $image = $image = $image -> toBlackAndWhite(); - if ($image -> getWidth() > 127 || $image -> getHeight() > 127) { - throw new Exception("Maximum image width or height is 127"); - } $image -> invert(); - return "\x00\x00" . chr($image -> getWidth()) . chr($image -> getHeight()) . $image -> getRasterData(); + return "\x00\x00" . $this -> writeInt($image -> getWidth()) . $this -> writeInt($image -> getHeight()) . $image -> getRasterData(); } public function getEncodeFormats(): array diff --git a/test/unit/Codec/WbmpCodecTest.php b/test/unit/Codec/WbmpCodecTest.php index bc82cd8..13a6900 100644 --- a/test/unit/Codec/WbmpCodecTest.php +++ b/test/unit/Codec/WbmpCodecTest.php @@ -1,12 +1,19 @@ assertEquals("wbmp", $codec -> identify(self::WBMP_IMAGE)); + $this -> assertEquals("", $codec -> identify("HELLO")); + } + public function testDecode() { $codec = new WbmpCodec(); $image = $codec -> decode(self::WBMP_IMAGE) -> toBlackAndWhite(); @@ -25,4 +32,58 @@ public function testEncode() { $data = $codec -> encode($image, "wbmp"); $this -> assertEquals(self::WBMP_IMAGE, $data); } + + public function testReadOneByte() { + $data = DataBlobInputStream::fromBlob("\x60"); + $codec = new WbmpCodec(); + $val = $codec -> readInt($data); + $this -> assertEquals(0x60, $val); + } + + public function testReadMultibyte() { + $data = DataBlobInputStream::fromBlob("\x81\x20"); + $codec = new WbmpCodec(); + $val = $codec -> readInt($data); + $this -> assertEquals(0xA0, $val); + } + + public function testReadMax() { + $data = DataBlobInputStream::fromBlob("\xFF\xFF\xFF\x7F\x00"); // Final byte not used + $codec = new WbmpCodec(); + $val = $codec -> readInt($data); + $this -> assertEquals(268435455, $val); + } + + public function testReadMultibyteOverflow() { + // Appears to be no limit in WBMP to image dimensions, but we stop reading the multibyte-ints after 28 bits. + $this -> expectException(Exception::class); + $data = DataBlobInputStream::fromBlob("\xFF\xFF\xFF\x80\x00"); // (value in testMax()) + 1 + $codec = new WbmpCodec(); + $codec -> readInt($data); + } + + public function testWriteOneByte() { + $codec = new WbmpCodec(); + $val = $codec -> writeInt(0x60); + $this -> assertEquals("\x60", $val); + } + + public function testWriteMultibyte() { + $codec = new WbmpCodec(); + $val = $codec -> writeInt(0xA0); + $this -> assertEquals("\x81\x20", $val); + } + + public function testWriteMax() { + $codec = new WbmpCodec(); + $val = $codec -> writeInt(268435455); + $this -> assertEquals("\xFF\xFF\xFF\x7F", $val); + + } + + public function testWriteMultibyteOverflow() { + $this -> expectException(Exception::class); + $codec = new WbmpCodec(); + $val = $codec -> writeInt(268435456); // As testWriteMax(), +1 + } } \ No newline at end of file From 502e37ec4b081080c98a24ca023a9802a25995d9 Mon Sep 17 00:00:00 2001 From: Michael Billington Date: Sat, 2 Mar 2019 14:40:03 +1100 Subject: [PATCH 4/4] add docs --- docs/user/formats.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/user/formats.rst b/docs/user/formats.rst index 405c4c1..30e0a24 100644 --- a/docs/user/formats.rst +++ b/docs/user/formats.rst @@ -48,7 +48,7 @@ The GIF codec is used where the input has the ``gif`` file extension. Any well-f - If a GIF file contains multiple images, then only the first one will be loaded - If a transparent color is present, then this will be mixed to white -A GIF image will always be loaded into an omstamce of :class:`IndexedRasterImage`, which makes palette information available. +A GIF image will always be loaded into an instance of :class:`IndexedRasterImage`, which makes palette information available. Netpbm Formats ^^^^^^^^^^^^^^ @@ -62,6 +62,11 @@ The Netpbm formats are a series of uncompressed bitmap formats, which can repres Each of these formats has both a binary and text encoding. ``gfx-php`` only supports the binary encodings at this stage. +WBMP +^^^ + +The WBMP codec is used where the input has the ``wbmp`` file extension. A WBMP image will always be loaded into a :class:`BlackAndWhiteRasterImage` object. + Output formats -------------- @@ -109,6 +114,17 @@ The BMP format is selected by using the ``bmp`` file extension. This library will currently output BMP files using an uncompressed 24-bit RGB representation of the image. +WBMP +^^^ + +The WBMP format is selected by using the ``wbmp`` file extension. + +.. code-block:: php + + $tux -> write("tux.wbmp"); + +The image will be converted to a 1-bit monochrome representation, which is the only type of image supported by WBMP. + Netpbm Formats ^^^^^^^^^^^^^^