Repository: robgridley/zebra Branch: master Commit: aed6c02c1976 Files: 16 Total size: 19.6 KB Directory structure: gitextract_rwnbuiod/ ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpspec.yml ├── spec/ │ ├── Zpl/ │ │ ├── BuilderSpec.php │ │ └── ImageSpec.php │ ├── test_1000.txt │ └── test_150.txt └── src/ ├── Client.php ├── CommunicationException.php ├── Contracts/ │ └── Zpl/ │ ├── Decoder.php │ └── Image.php └── Zpl/ ├── Builder.php ├── GdDecoder.php └── Image.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store /.idea /vendor /composer.lock /test.php ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Rob Gridley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Zebra PHP ZPL builder, image conversion and a basic client for network-connected Zebra label printers. Requires: PHP 7.1.0+ * Convert images to ASCII hex. * Create ZPL code in PHP that is easy to read. * Simple wrapper for PHP sockets to send ZPL to the printer via raw TCP/IP (port 9100). ## Example The following example will print a label with an image positioned 50 dots from the top left. ```php use Zebra\Client; use Zebra\Zpl\Image; use Zebra\Zpl\Builder; use Zebra\Zpl\GdDecoder; $decoder = GdDecoder::fromPath('example.png'); $image = new Image($decoder); $zpl = new Builder(); $zpl->fo(50, 50)->gf($image)->fs(); $client = new Client('10.0.0.50'); $client->send($zpl); ``` ## Installation with Composer ``` $ composer require robgridley/zebra ``` ================================================ FILE: composer.json ================================================ { "name": "robgridley/zebra", "description": "PHP ZPL builder, image conversion and a basic client for network-connected Zebra label printers.", "keywords": ["zebra", "zpl", "image"], "license": "MIT", "require": { "php": ">=7.1.0" }, "suggest": { "ext-gd": "*" }, "require-dev": { "phpspec/phpspec": "^7.1" }, "authors": [ { "name": "Rob Gridley", "email": "me@robgridley.com" } ], "autoload": { "psr-4": { "Zebra\\": "src/" } }, "scripts": { "test": "phpspec --ansi run" } } ================================================ FILE: phpspec.yml ================================================ suites: zebra_suite: namespace: Zebra psr4_prefix: Zebra ================================================ FILE: spec/Zpl/BuilderSpec.php ================================================ command('BC', 'N', 100, true)->toZpl()->shouldReturn("^XA^BCN,100,Y^XZ"); } function it_creates_zpl_commands_from_magic_methods_and_their_arguments() { $this->fo(50, 50)->toZpl()->shouldReturn("^XA^FO50,50^XZ"); } function it_accepts_a_single_argument_of_image_for_gf_command(ImageContract $image) { $image->toAscii()->willReturn('FF00'); $image->width()->willReturn(2); $image->height()->willReturn(16); $this->gf($image)->toZpl()->shouldReturn("^XA^GFA,32,32,2,FF00^XZ"); } function it_can_be_converted_to_a_string() { $this->fo(50, 50)->__toString()->shouldReturn("^XA^FO50,50^XZ"); } } ================================================ FILE: spec/Zpl/ImageSpec.php ================================================ beConstructedWith($decoder); } function it_converts_images_to_compressed_ascii_hexadecimal_bitmaps() { $this->toAscii()->shouldReturn(file_get_contents(__DIR__ . '/../test_150.txt')); } function it_converts_large_images_to_compressed_ascii_hexadecimal_bitmaps() { $decoder = GdDecoder::fromPath(__DIR__ . '/../test_1000.png'); $this->beConstructedWith($decoder); $this->toAscii()->shouldReturn(file_get_contents(__DIR__ . '/../test_1000.txt')); } function it_gets_the_height_of_the_image_in_rows() { $this->height()->shouldReturn(75); } function it_gets_the_width_of_the_image_in_bytes() { $this->width()->shouldReturn(19); } } ================================================ FILE: spec/test_1000.txt ================================================ !::::::::::::::::::::::::::::::::::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!::::::: ================================================ FILE: spec/test_150.txt ================================================ gWFC::::gWF,gVF,3gTF,07gRF,007gOFC,I03gMF,K0gJF,M07XFC,P03QFE,T0FF8,:::::T0FF8g04T0FF8Y03CT0FF8X07FCCS0FF8W07FFCF8R0FF8V0JFCFF8Q0FF8T03KFCIF8P0FF8R01MFCKFO0FF8P03OFCMFEL0FF8L01SFCOFEJ0FF8I07VFC::::::::::::::::::::OFEI07FFEI07VFCOFEI03FFEI07VFCOFEI03FFCI07VFC:OFE0023FFC4007VFCOFE0033FFCC007VFCOFE0F39FFDEF07VFCOFE07FDFFBFF07VFCOFE07LFE07VFC:::OFE0NF07VFC:OFE07LFE07VFCOFE03LFC07VFCOFE00LF807VFCOFE007JFE007VFCOFE003JFC007VFCOFE001JF8007VFC::OFE001E1878007VFCOFEJ018J07VFC: ================================================ FILE: src/Client.php ================================================ connect($host, $port); } /** * Destroy an instance. */ public function __destruct() { $this->disconnect(); } /** * Create an instance statically. * * @param string $host * @param int $port * @return Client */ public static function printer(string $host, int $port = 9100): self { return new static($host, $port); } /** * Connect to printer. * * @param string $host * @param int $port * @throws CommunicationException if the connection fails. */ protected function connect(string $host, int $port): void { $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (!$this->socket || !@socket_connect($this->socket, $host, $port)) { $error = $this->getLastError(); throw new CommunicationException($error['message'], $error['code']); } } /** * Close connection to printer. */ protected function disconnect(): void { @socket_close($this->socket); } /** * Send ZPL data to printer. * * @param string $zpl * @throws CommunicationException if writing to the socket fails. */ public function send(string $zpl): void { if (false === @socket_write($this->socket, $zpl)) { $error = $this->getLastError(); throw new CommunicationException($error['message'], $error['code']); } } /** * Get the last socket error. * * @return array */ protected function getLastError(): array { $code = socket_last_error($this->socket); $message = socket_strerror($code); return compact('code', 'message'); } } ================================================ FILE: src/CommunicationException.php ================================================ zpl[] = '^' . $command . implode(',', $parameters); return $this; } /** * Convert native types to their ZPL representations. * * @param mixed $parameter * @return mixed */ protected function parameter($parameter) { if (is_bool($parameter)) { return $parameter ? 'Y' : 'N'; } return $parameter; } /** * Handle dynamic method calls. * * @param string $method * @param array $arguments * @return Builder */ public function __call($method, $arguments) { array_unshift($arguments, $method); return call_user_func_array([$this, 'command'], $arguments); } /** * Add GF command. * * @return Builder */ public function gf(): self { $arguments = func_get_args(); if (func_num_args() === 1 && ($image = $arguments[0]) instanceof ImageContract) { $bytesPerRow = $image->width(); $byteCount = $fieldCount = $bytesPerRow * $image->height(); return $this->command('GF', 'A', $byteCount, $fieldCount, $bytesPerRow, $image->toAscii()); } array_unshift($arguments, 'GF'); return call_user_func_array([$this, 'command'], $arguments); } /** * Convert instance to ZPL. * * @param bool $newlines * @return string */ public function toZpl(bool $newlines = false): string { return implode($newlines ? "\n" : '', array_merge(['^XA'], $this->zpl, ['^XZ'])); } /** * Convert instance to string. * * @return string */ public function __toString() { return $this->toZpl(); } } ================================================ FILE: src/Zpl/GdDecoder.php ================================================ isGdResource($image)) { throw new InvalidArgumentException('Invalid resource'); } if (!imageistruecolor($image)) { imagepalettetotruecolor($image); } imagefilter($image, IMG_FILTER_GRAYSCALE); $this->image = $image; } /** * Destroy the instance. */ public function __destruct() { imagedestroy($this->image); } /** * Determine if specified image is a GD resource. * * @param mixed $image * @return bool */ public function isGdResource($image): bool { if (is_resource($image)) { return get_resource_type($image) === 'gd'; } elseif (is_object($image) && $image instanceOf \GdImage) { return true; } return false; } /** * Create a new decoder instance from the specified GD resource. * * @param resource $image * @return GdDecoder */ public static function fromResource($image): self { return new static($image); } /** * Create a new decoder instance from the specified file path. * * @param string $path * @return GdDecoder */ public static function fromPath(string $path): self { return static::fromString(file_get_contents($path)); } /** * Create a new decoder instance from the specified string. * * @param string $data * @return GdDecoder */ public static function fromString(string $data): self { if (false === $image = imagecreatefromstring($data)) { throw new InvalidArgumentException('Could not read image'); } return new static($image); } /** * {@inheritdoc} */ public function width(): int { return imagesx($this->image); } /** * {@inheritdoc} */ public function height(): int { return imagesy($this->image); } /** * {@inheritdoc} */ public function getBitAt(int $x, int $y): int { return (imagecolorat($this->image, $x, $y) & 0xFF) < 127 ? 1 : 0; } } ================================================ FILE: src/Zpl/Image.php ================================================ width = $decoder->width(); $this->height = $decoder->height(); $this->decoder = $decoder; } /** * {@inheritdoc} * * @return int */ public function width(): int { return (int)ceil($this->width / 8); } /** * {@inheritdoc} * * @return int */ public function height(): int { return $this->height; } /** * {@inheritdoc} * * @return string */ public function toAscii(): string { return $this->encoded ?: $this->encoded = $this->encode(); } /** * Encode the image in ASCII hexadecimal by looping over every pixel. * * @return string */ protected function encode(): string { $bitmap = null; $lastRow = null; for ($y = 0; $y < $this->height; $y++) { $bits = null; for ($x = 0; $x < $this->width; $x++) { $bits .= $this->decoder->getBitAt($x, $y); } $bytes = str_split($bits, 8); $bytes[] = str_pad(array_pop($bytes), 8, '0'); $row = null; foreach ($bytes as $byte) { $row .= sprintf('%02X', bindec($byte)); } $bitmap .= $this->compress($row, $lastRow); $lastRow = $row; } return $bitmap; } /** * Compress a row of ASCII hexadecimal data. * * @param string $row * @param string $lastRow * @return string */ protected function compress(string $row, ?string $lastRow): string { if ($row === $lastRow) { return ':'; } $row = $this->compressTrailingZerosOrOnes($row); $row = $this->compressRepeatingCharacters($row); return $row; } /** * Replace trailing zeros or ones with a comma (,) or exclamation (!) respectively. * * @param string $row * @return string */ protected function compressTrailingZerosOrOnes(string $row): string { return preg_replace(['/0+$/', '/F+$/'], [',', '!'], $row); } /** * Compress characters which repeat. * * @param string $row * @return string */ protected function compressRepeatingCharacters(string $row): string { $callback = function ($matches) { $original = $matches[0]; $repeat = strlen($original); $count = null; if ($repeat > 400) { $count .= str_repeat('z', floor($repeat / 400)); $repeat %= 400; } if ($repeat > 19) { $count .= chr(ord('f') + floor($repeat / 20)); $repeat %= 20; } if ($repeat > 0) { $count .= chr(ord('F') + $repeat); } return $count . substr($original, 1, 1); }; return preg_replace_callback('/(.)(\1{2,})/', $callback, $row); } }