[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/.idea\n/vendor\n/composer.lock\n/test.php\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Rob Gridley <me@robgridley.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Zebra\n\nPHP ZPL builder, image conversion and a basic client for network-connected Zebra label printers.\n\nRequires: PHP 7.1.0+\n\n* Convert images to ASCII hex.\n* Create ZPL code in PHP that is easy to read.\n* Simple wrapper for PHP sockets to send ZPL to the printer via raw TCP/IP (port 9100).\n\n## Example\n\nThe following example will print a label with an image positioned 50 dots from the top left.\n\n```php\nuse Zebra\\Client;\nuse Zebra\\Zpl\\Image;\nuse Zebra\\Zpl\\Builder;\nuse Zebra\\Zpl\\GdDecoder;\n\n$decoder = GdDecoder::fromPath('example.png');\n$image = new Image($decoder);\n\n$zpl = new Builder();\n$zpl->fo(50, 50)->gf($image)->fs();\n\n$client = new Client('10.0.0.50');\n$client->send($zpl);\n```\n\n## Installation with Composer\n\n```\n$ composer require robgridley/zebra\n```\n"
  },
  {
    "path": "composer.json",
    "content": "{\n\t\"name\": \"robgridley/zebra\",\n\t\"description\": \"PHP ZPL builder, image conversion and a basic client for network-connected Zebra label printers.\",\n\t\"keywords\": [\"zebra\", \"zpl\", \"image\"],\n\t\"license\": \"MIT\",\n\t\"require\": {\n\t\t\"php\": \">=7.1.0\"\n\t},\n\t\"suggest\": {\n\t\t\"ext-gd\": \"*\"\n\t},\n\t\"require-dev\": {\n\t\t\"phpspec/phpspec\": \"^7.1\"\n\t},\n\t\"authors\": [\n\t\t{\n\t\t\t\"name\": \"Rob Gridley\",\n\t\t\t\"email\": \"me@robgridley.com\"\n\t\t}\n\t],\n\t\"autoload\": {\n\t\t\"psr-4\": {\n\t\t\t\"Zebra\\\\\": \"src/\"\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"test\": \"phpspec --ansi run\"\n\t}\n}\n"
  },
  {
    "path": "phpspec.yml",
    "content": "suites:\n    zebra_suite:\n        namespace: Zebra\n        psr4_prefix: Zebra\n"
  },
  {
    "path": "spec/Zpl/BuilderSpec.php",
    "content": "<?php\n\nnamespace spec\\Zebra\\Zpl;\n\nuse Prophecy\\Argument;\nuse PhpSpec\\ObjectBehavior;\nuse Zebra\\Contracts\\Zpl\\Image as ImageContract;\n\nclass BuilderSpec extends ObjectBehavior\n{\n    function it_creates_zpl_commands_with_string_number_and_boolean_parameters()\n    {\n        $this->command('BC', 'N', 100, true)->toZpl()->shouldReturn(\"^XA^BCN,100,Y^XZ\");\n    }\n\n    function it_creates_zpl_commands_from_magic_methods_and_their_arguments()\n    {\n        $this->fo(50, 50)->toZpl()->shouldReturn(\"^XA^FO50,50^XZ\");\n    }\n\n    function it_accepts_a_single_argument_of_image_for_gf_command(ImageContract $image)\n    {\n        $image->toAscii()->willReturn('FF00');\n        $image->width()->willReturn(2);\n        $image->height()->willReturn(16);\n\n        $this->gf($image)->toZpl()->shouldReturn(\"^XA^GFA,32,32,2,FF00^XZ\");\n    }\n\n    function it_can_be_converted_to_a_string()\n    {\n        $this->fo(50, 50)->__toString()->shouldReturn(\"^XA^FO50,50^XZ\");\n    }\n}\n"
  },
  {
    "path": "spec/Zpl/ImageSpec.php",
    "content": "<?php\n\nnamespace spec\\Zebra\\Zpl;\n\nuse Prophecy\\Argument;\nuse Zebra\\Zpl\\GdDecoder;\nuse PhpSpec\\ObjectBehavior;\n\nclass ImageSpec extends ObjectBehavior\n{\n    function let()\n    {\n        $decoder = GdDecoder::fromPath(__DIR__ . '/../test_150.png');\n        $this->beConstructedWith($decoder);\n    }\n\n    function it_converts_images_to_compressed_ascii_hexadecimal_bitmaps()\n    {\n        $this->toAscii()->shouldReturn(file_get_contents(__DIR__ . '/../test_150.txt'));\n    }\n\n    function it_converts_large_images_to_compressed_ascii_hexadecimal_bitmaps()\n    {\n        $decoder = GdDecoder::fromPath(__DIR__ . '/../test_1000.png');\n        $this->beConstructedWith($decoder);\n\n        $this->toAscii()->shouldReturn(file_get_contents(__DIR__ . '/../test_1000.txt'));\n    }\n\n    function it_gets_the_height_of_the_image_in_rows()\n    {\n        $this->height()->shouldReturn(75);\n    }\n\n    function it_gets_the_width_of_the_image_in_bytes()\n    {\n        $this->width()->shouldReturn(19);\n    }\n}\n"
  },
  {
    "path": "spec/test_1000.txt",
    "content": "!::::::::::::::::::::::::::::::::::rOFCrNFE,rNF8,rMFC,rLFE,rLF,rKF8,rJF8,rIFC,rHFE,rGFE,rGF,rF,qYF,7qWF,1qVF,07qTF,01qSF,003qQF,I07qOF,I01qMFE,J03qKFE,K07qIFC,L07qGF8,M0qF,N0pWFE,N01pUFC,O01pSF8,P01pQF,Q01pNFC,S0pLF8,T0pIFE,U07pF8,V03oWFE,W01oUF8,Y0oRFC,g07oOF,gG01oLF8,gI07oHFC,gJ01nYFE,gL07nUFE,gN0nRFE,gO01nNFE,gQ03nJFE,gS07nFE,gU07mVFC,gW07mRF,gY07mMFE,hG03mIF8,hJ0lXFC,hL07lRFE,hO0lMFE,hQ01lGFE,hT03kUFC,hW01kNFE,iG0kHF,iJ03jSFE,iN07jKF8,iR03iUFE,iW07iJFE,jH03hRF,jN07gTFE,jR0VFE,::::::::::::::::::::::::::::::::::jR0VFEm03jR0VFElY03!jR0VFElX01!jR0VFElW01!jR0VFElV01!jR0VFElV0!jR0VFElU0!jR0VFElT0!jR0VFElS0!jR0VFElR0!jR0VFElQ0!jR0VFElO01!jR0VFElN01!jR0VFElM01!jR0VFElL01!CjQ0VFElK03!F8jP0VFElJ03!FEjP0VFElI07!FFCjO0VFElH0!IFjO0VFElG0!IFEjN0VFEkY01!JFCjM0VFEkX03!KFjM0VFEkW07!KFEjL0VFEkV0!LFCjK0VFEkT01!MF8jJ0VFEkS03!NFjJ0VFEkR07!NFEjI0VFEkQ0!OFEjH0VFEkO03!PFCjG0VFEkN07!QF8j0VFEkL01!RF8iY0VFEkK07!SFiY0VFEkI01!TFiX0VFEkH07!UFiW0VFEk01!VFiV0VFEjY07!WFiU0VFEjW03!XFiT0VFEjV0!YF8iR0VFEjT07!gF8iQ0VFEjR03!gGFCiP0VFEjP01!gHFEiO0VFEjN01!gJFiN0VFEjM0!gKF8iL0VFEjK0!gLFEiK0VFEjI0!gNF8iI0VFEj01!gOFEiH0VFEiX03!gQFCi0VFEiV07!gSF8hX0VFEiS01!gUF8hV0VFEiQ0!gWF8hT0VFEiN07!gYFChR0VFEiK0!hHFhP0VFEiG01!hKFhM0VFEhW01!hNFChI0VFEhS07!hSFCgX0VFEhM0!iLF8gK0VFEgK07!:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::iLF8gH01C0VFE1CgI07!iLF8gH01F0VFE3CgI07!iLF8gH01FCVFEFCgI07!iLF8gH01gFCgI07!iLF8gI0gFCgI07!iLF8gI0gF8gI07!:::iLF8gI07YF8gI07!iLF8gI07YFgJ07!:::iLF8gI03YFgJ07!:iLF8gI03XFEgJ07!:::iLF8gI01XFEgJ07!iLF8gI01XFCgJ07!:::iLF8gJ0XFCgJ07!iLF8gJ0XF8gJ07!:::iLF8gJ07WF8gJ07!:iLF8W06L07WFL03X07!iLF8W07L07WFL07X07!iLF8W078K07WFL0FX07!iLF8W07CK07WFK01F8W07!iLF8W0FEK03WFK03F8W07!iLF8W0FFK03VFEK07F8W07!iLF8W0FF8J03VFEK0FF8W07!iLF8V01FFCJ03VFEK0FFCW07!iLF8V01FFEJ03VFEJ01FFCW07!iLF8V01FFEJ01VFEJ03FFCW07!iLF8M01EM01IFJ01VFCJ07FFEM03CN07!iLF8N0FFL03IF8I01VFCJ0IFEL07FCN07!iLF8N0IF8J03IFCI01VFCI01IFEK0IF8N07!iLF8N0JFCI03IFEI01VFCI03JFJ0JF8N07!iLF8N0KFC007JFJ0VFCI07JF001KF8N07!iLF8N07KFE0KF8I0VFCI0KF83LF8N07!iLF8N07RFCI0VF8I0SFO07!iLF8N07RFEI0VF8001SFO07!iLF8N07RFEI0VF8003SFO07!iLF8N03SFI0VF8007SFO07!iLF8N03SF8007UF800SFEO07!iLF8N03SFC007UF001SFEO07!iLF8N01SFE007UF003SFEO07!iLF8N01TF007UF007SFCO07!iLF8N01TF807UF00TFCO07!iLF8N01TFC07UF00TFCO07!iLF8O0TFE07UF81TFCO07!iLF8O0UF0VF87TF8O07!iLF8O0hNF8O07!:iLF8O07hMFP07!::iLF8O03hMFP07!iLF8O03hLFEP07!::iLF8O01hLFEP07!iLF8O01hLFCP07!:iLF8P0hLFCP07!:iLF8P0hLF8P07!::iLF8P07hKF8P07!iLF8P0hLF8P07!iLF8O01hLFCP07!iLF8O07hMFP07!iLF8N01hNFCO07!iLF8N07hOFO07!iLF8M01hPFCN07!iLF8M07hQFN07!iLF8L01hRFCM07!:iLF8M0hRF8M07!iLF8M03hQFN07!iLF8M01hPFEN07!iLF8N0hPF8N07!iLF8N07hOFO07!iLF8N03hNFEO07!iLF8O0hNFCO07!iLF8O07hMFP07!iLF8O03hLFEP07!iLF8O01hLFCP07!iLF8P07hKF8P07!iLF8P03hJFEQ07!iLF8P01hJFCQ07!iLF8Q0hJF8Q07!iLF8Q07hIFR07!iLF8Q01hHFER07!iLF8R0hHF8R07!iLF8R07hGFS07!iLF8R03hFES07!iLF8S0hFCS07!iLF8S07gYFT07!iLF8S03gXFET07!iLF8S01gXFCT07!iLF8T07gWF8T07!iLF8T03gVFEU07!iLF8T01gVFCU07!iLF8U0gVF8U07!iLF8U07gUFV07!iLF8U01gTFEV07!iLF8V0gTF8V07!iLF8V07gSFW07!iLF8V03gRFEW07!iLF8W0gRFCW07!iLF8W07gQFX07!iLF8W03gPFEX07!iLF8W01gPFCX07!iLF8X0gPF8X07!iLF8X03gOFY07!iLF8X01gNFCY07!iLF8Y0gNF8Y07!iLF8Y07gMFg07!iLF8Y03gLFEg07!iLF8Y01gLFCg07!iLF8g0gLFCg07!iLF8g0gLF8g07!::iLF8Y01gLFCg07!::iLF8Y01gLFEg07!iLF8Y03gLFEg07!:iLF8Y03gMFg07!iLF8Y07gMFg07!:iLF8Y07QF1FFC7QF8Y07!iLF8Y0OFEI0FF8003OF8Y07!iLF8Y0MFEK0FF8J01MF8Y07!iLF8Y0KFCM0FF8L01KF8Y07!iLF8Y0IF8O0FF8O0IFCY07!iLF8X01F8Q0FF8Q07CY07!iLF8gR0FF8gR07!::::iLF8gR0FFCgR07!:::iLF8gQ01FFCgR07!:::::::"
  },
  {
    "path": "spec/test_150.txt",
    "content": "gWFC::::gWF,gVF,3gTF,07gRF,007gOFC,I03gMF,K0gJF,M07XFC,P03QFE,T0FF8,:::::T0FF8g04T0FF8Y03CT0FF8X07FCCS0FF8W07FFCF8R0FF8V0JFCFF8Q0FF8T03KFCIF8P0FF8R01MFCKFO0FF8P03OFCMFEL0FF8L01SFCOFEJ0FF8I07VFC::::::::::::::::::::OFEI07FFEI07VFCOFEI03FFEI07VFCOFEI03FFCI07VFC:OFE0023FFC4007VFCOFE0033FFCC007VFCOFE0F39FFDEF07VFCOFE07FDFFBFF07VFCOFE07LFE07VFC:::OFE0NF07VFC:OFE07LFE07VFCOFE03LFC07VFCOFE00LF807VFCOFE007JFE007VFCOFE003JFC007VFCOFE001JF8007VFC::OFE001E1878007VFCOFEJ018J07VFC:"
  },
  {
    "path": "src/Client.php",
    "content": "<?php\n\nnamespace Zebra;\n\nclass Client\n{\n    /**\n     * The endpoint.\n     *\n     * @var resource\n     */\n    protected $socket;\n\n    /**\n     * Create an instance.\n     *\n     * @param string $host\n     * @param int $port\n     */\n    public function __construct($host, $port = 9100)\n    {\n        $this->connect($host, $port);\n    }\n\n    /**\n     * Destroy an instance.\n     */\n    public function __destruct()\n    {\n        $this->disconnect();\n    }\n\n    /**\n     * Create an instance statically.\n     *\n     * @param string $host\n     * @param int $port\n     * @return Client\n     */\n    public static function printer(string $host, int $port = 9100): self\n    {\n        return new static($host, $port);\n    }\n\n    /**\n     * Connect to printer.\n     *\n     * @param string $host\n     * @param int $port\n     * @throws CommunicationException if the connection fails.\n     */\n    protected function connect(string $host, int $port): void\n    {\n        $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);\n\n        if (!$this->socket || !@socket_connect($this->socket, $host, $port)) {\n            $error = $this->getLastError();\n            throw new CommunicationException($error['message'], $error['code']);\n        }\n    }\n\n    /**\n     * Close connection to printer.\n     */\n    protected function disconnect(): void\n    {\n        @socket_close($this->socket);\n    }\n\n    /**\n     * Send ZPL data to printer.\n     *\n     * @param string $zpl\n     * @throws CommunicationException if writing to the socket fails.\n     */\n    public function send(string $zpl): void\n    {\n        if (false === @socket_write($this->socket, $zpl)) {\n            $error = $this->getLastError();\n            throw new CommunicationException($error['message'], $error['code']);\n        }\n    }\n\n    /**\n     * Get the last socket error.\n     *\n     * @return array\n     */\n    protected function getLastError(): array\n    {\n        $code = socket_last_error($this->socket);\n        $message = socket_strerror($code);\n\n        return compact('code', 'message');\n    }\n}\n"
  },
  {
    "path": "src/CommunicationException.php",
    "content": "<?php\n\nnamespace Zebra;\n\nuse RuntimeException;\n\nclass CommunicationException extends RuntimeException\n{\n    //\n}\n"
  },
  {
    "path": "src/Contracts/Zpl/Decoder.php",
    "content": "<?php\n\nnamespace Zebra\\Contracts\\Zpl;\n\ninterface Decoder\n{\n    /**\n     * Get the width of the image (in pixels).\n     *\n     * @return int\n     */\n    public function width(): int;\n\n    /**\n     * Get the height of the image (in pixels).\n     *\n     * @return int\n     */\n    public function height(): int;\n\n    /**\n     * Get the bit at the specified position.\n     *\n     * @param int $x\n     * @param int $y\n     * @return int\n     */\n    public function getBitAt(int $x, int $y): int;\n}\n"
  },
  {
    "path": "src/Contracts/Zpl/Image.php",
    "content": "<?php\n\nnamespace Zebra\\Contracts\\Zpl;\n\ninterface Image\n{\n    /**\n     * Get the image width in bytes.\n     *\n     * @return int\n     */\n    public function width(): int;\n\n    /**\n     * Get the image height in pixels.\n     *\n     * @return int\n     */\n    public function height(): int;\n\n    /**\n     * Get the ASCII hex representation of the image.\n     *\n     * @return string\n     */\n    public function toAscii(): string;\n}\n"
  },
  {
    "path": "src/Zpl/Builder.php",
    "content": "<?php\n\nnamespace Zebra\\Zpl;\n\nuse Zebra\\Contracts\\Zpl\\Image as ImageContract;\n\nclass Builder\n{\n    /**\n     * ZPL commands.\n     *\n     * @var array\n     */\n    protected $zpl = [];\n\n    /**\n     * Add a command.\n     *\n     * @return Builder\n     */\n    public function command(): self\n    {\n        $parameters = func_get_args();\n        $command = strtoupper(array_shift($parameters));\n        $parameters = array_map([$this, 'parameter'], $parameters);\n\n        $this->zpl[] = '^' . $command . implode(',', $parameters);\n\n        return $this;\n    }\n\n    /**\n     * Convert native types to their ZPL representations.\n     *\n     * @param mixed $parameter\n     * @return mixed\n     */\n    protected function parameter($parameter)\n    {\n        if (is_bool($parameter)) {\n            return $parameter ? 'Y' : 'N';\n        }\n\n        return $parameter;\n    }\n\n    /**\n     * Handle dynamic method calls.\n     *\n     * @param string $method\n     * @param array $arguments\n     * @return Builder\n     */\n    public function __call($method, $arguments)\n    {\n        array_unshift($arguments, $method);\n\n        return call_user_func_array([$this, 'command'], $arguments);\n    }\n\n    /**\n     * Add GF command.\n     *\n     * @return Builder\n     */\n    public function gf(): self\n    {\n        $arguments = func_get_args();\n\n        if (func_num_args() === 1 && ($image = $arguments[0]) instanceof ImageContract) {\n            $bytesPerRow = $image->width();\n            $byteCount = $fieldCount = $bytesPerRow * $image->height();\n\n            return $this->command('GF', 'A', $byteCount, $fieldCount, $bytesPerRow, $image->toAscii());\n        }\n\n        array_unshift($arguments, 'GF');\n\n        return call_user_func_array([$this, 'command'], $arguments);\n    }\n\n    /**\n     * Convert instance to ZPL.\n     *\n     * @param bool $newlines\n     * @return string\n     */\n    public function toZpl(bool $newlines = false): string\n    {\n        return implode($newlines ? \"\\n\" : '', array_merge(['^XA'], $this->zpl, ['^XZ']));\n    }\n\n    /**\n     * Convert instance to string.\n     *\n     * @return string\n     */\n    public function __toString()\n    {\n        return $this->toZpl();\n    }\n}\n"
  },
  {
    "path": "src/Zpl/GdDecoder.php",
    "content": "<?php\n\nnamespace Zebra\\Zpl;\n\nuse InvalidArgumentException;\nuse Zebra\\Contracts\\Zpl\\Decoder;\n\nclass GdDecoder implements Decoder\n{\n    /**\n     * The GD image resource.\n     *\n     * @var resource|\\GdImage\n     */\n    protected $image;\n\n    /**\n     * Create a new decoder instance.\n     *\n     * @param resource|\\GdImage $image\n     */\n    public function __construct($image)\n    {\n        if (!$this->isGdResource($image)) {\n            throw new InvalidArgumentException('Invalid resource');\n        }\n\n        if (!imageistruecolor($image)) {\n            imagepalettetotruecolor($image);\n        }\n\n        imagefilter($image, IMG_FILTER_GRAYSCALE);\n\n        $this->image = $image;\n    }\n\n    /**\n     * Destroy the instance.\n     */\n    public function __destruct()\n    {\n        imagedestroy($this->image);\n    }\n\n    /**\n     * Determine if specified image is a GD resource.\n     *\n     * @param mixed $image\n     * @return bool\n     */\n    public function isGdResource($image): bool\n    {\n        if (is_resource($image)) {\n            return get_resource_type($image) === 'gd';\n        } elseif (is_object($image) && $image instanceOf \\GdImage) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Create a new decoder instance from the specified GD resource.\n     *\n     * @param resource $image\n     * @return GdDecoder\n     */\n    public static function fromResource($image): self\n    {\n        return new static($image);\n    }\n\n\n    /**\n     * Create a new decoder instance from the specified file path.\n     *\n     * @param string $path\n     * @return GdDecoder\n     */\n    public static function fromPath(string $path): self\n    {\n        return static::fromString(file_get_contents($path));\n    }\n\n    /**\n     * Create a new decoder instance from the specified string.\n     *\n     * @param string $data\n     * @return GdDecoder\n     */\n    public static function fromString(string $data): self\n    {\n        if (false === $image = imagecreatefromstring($data)) {\n            throw new InvalidArgumentException('Could not read image');\n        }\n\n        return new static($image);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function width(): int\n    {\n        return imagesx($this->image);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function height(): int\n    {\n        return imagesy($this->image);\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    public function getBitAt(int $x, int $y): int\n    {\n        return (imagecolorat($this->image, $x, $y) & 0xFF) < 127 ? 1 : 0;\n    }\n}\n"
  },
  {
    "path": "src/Zpl/Image.php",
    "content": "<?php\n\nnamespace Zebra\\Zpl;\n\nuse Zebra\\Contracts\\Zpl\\Decoder;\nuse Zebra\\Contracts\\Zpl\\Image as ImageContract;\n\nclass Image implements ImageContract\n{\n    /**\n     * The decoder instance.\n     *\n     * @var resource\n     */\n    protected $decoder;\n\n    /**\n     * The ASCII hexadecimal encoded image data.\n     *\n     * @var string\n     */\n    protected $encoded;\n\n    /**\n     * The image width (in pixels).\n     *\n     * @var int\n     */\n    protected $width;\n\n    /**\n     * The image height (in pixels).\n     *\n     * @var int\n     */\n    protected $height;\n\n    /**\n     * Create a new image instance.\n     *\n     * @param Decoder $decoder\n     */\n    public function __construct(Decoder $decoder)\n    {\n        $this->width = $decoder->width();\n        $this->height = $decoder->height();\n        $this->decoder = $decoder;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @return int\n     */\n    public function width(): int\n    {\n        return (int)ceil($this->width / 8);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @return int\n     */\n    public function height(): int\n    {\n        return $this->height;\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @return string\n     */\n    public function toAscii(): string\n    {\n        return $this->encoded ?: $this->encoded = $this->encode();\n    }\n\n    /**\n     * Encode the image in ASCII hexadecimal by looping over every pixel.\n     *\n     * @return string\n     */\n    protected function encode(): string\n    {\n        $bitmap = null;\n        $lastRow = null;\n\n        for ($y = 0; $y < $this->height; $y++) {\n            $bits = null;\n\n            for ($x = 0; $x < $this->width; $x++) {\n                $bits .= $this->decoder->getBitAt($x, $y);\n            }\n\n            $bytes = str_split($bits, 8);\n            $bytes[] = str_pad(array_pop($bytes), 8, '0');\n\n            $row = null;\n\n            foreach ($bytes as $byte) {\n                $row .= sprintf('%02X', bindec($byte));\n            }\n\n            $bitmap .= $this->compress($row, $lastRow);\n            $lastRow = $row;\n        }\n\n        return $bitmap;\n    }\n\n    /**\n     * Compress a row of ASCII hexadecimal data.\n     *\n     * @param string $row\n     * @param string $lastRow\n     * @return string\n     */\n    protected function compress(string $row, ?string $lastRow): string\n    {\n        if ($row === $lastRow) {\n            return ':';\n        }\n\n        $row = $this->compressTrailingZerosOrOnes($row);\n        $row = $this->compressRepeatingCharacters($row);\n\n        return $row;\n    }\n\n    /**\n     * Replace trailing zeros or ones with a comma (,) or exclamation (!) respectively.\n     *\n     * @param string $row\n     * @return string\n     */\n    protected function compressTrailingZerosOrOnes(string $row): string\n    {\n        return preg_replace(['/0+$/', '/F+$/'], [',', '!'], $row);\n    }\n\n    /**\n     * Compress characters which repeat.\n     *\n     * @param string $row\n     * @return string\n     */\n    protected function compressRepeatingCharacters(string $row): string\n    {\n        $callback = function ($matches) {\n            $original = $matches[0];\n            $repeat = strlen($original);\n            $count = null;\n\n            if ($repeat > 400) {\n                $count .= str_repeat('z', floor($repeat / 400));\n                $repeat %= 400;\n            }\n\n            if ($repeat > 19) {\n                $count .= chr(ord('f') + floor($repeat / 20));\n                $repeat %= 20;\n            }\n\n            if ($repeat > 0) {\n                $count .= chr(ord('F') + $repeat);\n            }\n\n            return $count . substr($original, 1, 1);\n        };\n\n        return preg_replace_callback('/(.)(\\1{2,})/', $callback, $row);\n    }\n}\n"
  }
]