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 ^^^^^^^^^^^^^^ 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 0000000..821396c Binary files /dev/null and b/example/resources/bricks.wbmp differ 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..f39ef33 --- /dev/null +++ b/src/Mike42/GfxPhp/Codec/WbmpCodec.php @@ -0,0 +1,103 @@ + read(2); + if ($header != "\x00\x00") { + throw new Exception("Not a WBMP file"); + } + $width = $this -> readInt($data); + $height = $this -> readInt($data); + $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 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"]; + } + + public function encode(RasterImage $image, string $format): string + { + $image = $image = $image -> toBlackAndWhite(); + $image -> invert(); + return "\x00\x00" . $this -> writeInt($image -> getWidth()) . $this -> writeInt($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; + } +} 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..13a6900 --- /dev/null +++ b/test/unit/Codec/WbmpCodecTest.php @@ -0,0 +1,89 @@ + 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(); + $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); + } + + 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