Skip to content

Commit

Permalink
Merge pull request #41 from mike42/feature/40-wbmp
Browse files Browse the repository at this point in the history
[WIP] WBMP implementation
  • Loading branch information
mike42 authored Mar 2, 2019
2 parents 2ee1b09 + 502e37e commit ab1ec7a
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 3 deletions.
18 changes: 17 additions & 1 deletion docs/user/formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^
Expand All @@ -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
--------------

Expand Down Expand Up @@ -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
^^^^^^^^^^^^^^

Expand Down
14 changes: 14 additions & 0 deletions example/format-convert.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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");
Binary file added example/resources/bricks.wbmp
Binary file not shown.
6 changes: 4 additions & 2 deletions src/Mike42/GfxPhp/Codec/ImageCodec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
103 changes: 103 additions & 0 deletions src/Mike42/GfxPhp/Codec/WbmpCodec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace Mike42\GfxPhp\Codec;

use Exception;
use Mike42\GfxPhp\BlackAndWhiteRasterImage;
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
use Mike42\GfxPhp\Codec\ImageDecoder;
use Mike42\GfxPhp\Codec\ImageEncoder;
use Mike42\GfxPhp\RasterImage;

class WbmpCodec implements ImageDecoder, ImageEncoder
{
protected static $instance = null;

public function identify(string $blob): string
{
$wbmpMagic = substr($blob, 0, 2);
if ($wbmpMagic == "\x00\x00") {
// Wireless Application Protocol Bitmap
return "wbmp";
}
return "";
}

public function decode(string $blob): RasterImage
{
$data = DataBlobInputStream::fromBlob($blob);
$header = $data -> 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;
}
}
9 changes: 9 additions & 0 deletions test/unit/Codec/GifCodecTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}


}

89 changes: 89 additions & 0 deletions test/unit/Codec/WbmpCodecTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

use Mike42\GfxPhp\BlackAndWhiteRasterImage;
use Mike42\GfxPhp\Codec\Common\DataBlobInputStream;
use Mike42\GfxPhp\Codec\WbmpCodec;
use PHPUnit\Framework\TestCase;

class WbmpCodecTest extends TestCase {
const WBMP_IMAGE = "\x00\x00\x0c\x06\x24\x90\xff\xf0\x49\x20\xff\xf0\x92\x40\xff\xf0";

public function testIdentify() {
$codec = new WbmpCodec();
$this -> 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
}
}

0 comments on commit ab1ec7a

Please sign in to comment.