CORE-POS/IS4C

View on GitHub
pos/is4c-nf/lib/PrintHandlers/ESCPOSPrintHandler.php

Summary

Maintainability
F
4 days
Test Coverage
A
96%
<?php

namespace COREPOS\pos\lib\PrintHandlers;
use COREPOS\pos\lib\Bitmap;
use COREPOS\pos\lib\ReceiptLib;

/*
ESCPOSPrintHandler
version 1 (2009-09-25)
author Alex Frase

Notes
    Epson TM-H6000II prints 504 pixels per line;
    42 columns with standard 12x24 font, 56 columns with alternate 9x17 font
*/

class ESCPOSPrintHandler extends PrintHandler {
    
    function tab() {
        // "\t"
        return "\x09";
    }
    
    function lineFeed($lines=1) {
        // one line: "\n"
        if ($lines <= 1)
            return "\x0A";
        // multiple lines: ESC "d" num
        return "\x1B\x64".chr( max(0, min(255, (int)$lines)) );
    }
    
    function pageFeed($reset=true) {
        // "(ESC) FF"
        return ($reset?"":"\x1B")."\x0C";
    }
    
    function carriageReturn() {
        // "\r"
        return "\x0D";
    }
    
    function clearPage() {
        // CAN
        return "\x18";
    }
    
    // TODO: realtime status transmission;  DLE(\x10) EOT(\x04) n
    
    // TODO: realtime request to printer;  DLE(\x10) ENQ(\x05) n
    
    // TODO: realtime pulse;  DLE(\x10) DC4(\x14) 1 m t
    
    function characterSpacing($dots=0) {
        // ESC " " space
        return "\x1B\x20".chr( max(0, min(255, $dots)) );
    }

    /**
      Add font style command
        @param $altFont use alternate font
      @param $bold use bold font
      @param $tall use tall characters
      @param $wide use wide characters
      @param $underline use undlerined font      
      @return A printer command string

      Replaces several old printLib.php functions
      TextStyle(true) is equivalent to normalFont()
      TextStyle(true,true) is equivalent to boldFont()
      TextStyle(true,false,true) is equivalent to
        biggerFont().
    */
    function textStyle($altFont=false, $bold=false, $tall=false, $wide=false, $underline=false) {
        // ESC "!" bitfield
        return ("\x1B\x21"
            .chr(
                ($underline ? 128 : 0)
                + ($wide ? 32 : 0)
                + ($tall ? 16 : 0)
                + ($bold ? 8 : 0)
                + ($altFont ? 1 : 0)
            )
        );
    }
    
    function gotoX($dots=0) {
        // ESC "$" xLO xHI
        return ("\x1B\x24"
            .chr( max(0, (int)($dots % 256)) )
            .chr( max(0, min(255, (int)($dots / 256))) )
        );
    } // GotoX()
    
    // TODO: enable downloadable character set;  ESC(\x1B) "%"(\x25) flag
    
    // TODO: define downloadable characters;  ESC(\x1B) "&"(\x26) 3(\x03) char0 char1 data
    
    /*
    Bitmaps are always drawn 24 dots tall (the height of a normal printed line).
    Tall dots are each 3x tall, so they take 1 byte per column (8 bits * 3 dots each = 24).
    Short dots require 3 bytes per column (8 bits * 3 bytes = 24).
    Data must therefore have ($width * ($tallDots ? 3 : 1)) bytes.
    Each data byte specifies dots top-to-bottom, from most- to least-significant-bit:
        \xC0 = \b11000000 = pixels in the top two rows
        \x81 = \b10000001 = pixels on the top and bottom rows
    Multiple bytes are drawn top-to-bottom (if !$tallDots), then left-to-right.
    */
    function inlineBitmap($data, $width, $tallDots=false, $wideDots=false) {
        // ESC "*" bitfield widthLO widthHI data
        $width = (int)max(0, min(1023, $width));
        $bytes = (int)($width * ($tallDots ? 1 : 3));
        return ("\x1B\x2A"
            .chr( ($tallDots ? 0 : 32) + ($wideDots ? 0 : 1) )
            .chr( (int)($width % 256) )
            .chr( (int)($width / 256) )
            .str_pad(substr($data, 0, $bytes), $bytes, "\x00")
        );
    } // InlineBitmap()
    
    function underline($dots=1) {
        // ESC "-" size
        return "\x1B\x2D".chr( max(0, min(2, $dots)) );
    }
    
    function resetLineSpacing() {
        // ESC "2"
        return "\x1B\x32";
    }
    
    function lineSpacing($space=64) {
        // in some small increment; with 12x24 font, space=64 seems to be approximately single-spaced, space=128 double
        // ESC "3" space
        return "\x1B\x33".chr( max(0, min(255, $space)) );
    }
    
    // TODO: return home;  ESC(\x1B) "<"(\x3C)
    
    // TODO: select peripheral device;  ESC(\x1B) "="(\x3D) n
    
    // TODO: delete downloadable characters;  ESC(\x1B) "?"(\x3F) char
    
    function reset() {
        // ESC "@"
        return "\x1B\x40";
    }
    
    function setTabs($tabs=null) {
        // ESC "D" tabs NUL
        if (!is_array($tabs) || !count($tabs))
            return "\x1B\x44\x00";
        $tabs = array_unique(array_map('chr', $tabs), SORT_NUMERIC);
        sort($tabs);
        return ("\x1B\x44"
            .implode('', $tabs)
            ."\x00"
        );
    }
    
    function bold($on=true) {
        // ESC "E" bit
        return "\x1B\x45".chr( $on ? 1 : 0 );
    }
    
    function doublePrint($on=true) {
        // is this like a shadow effect?
        // ESC "G" bit
        return "\x1B\x47".chr( $on ? 1 : 0 );
    }
    
    function paperFeed($space) {
        // in some small increment; with 12x24 font, space=64 seems to be approximately one printed line, space=128 two
        // ESC "J" space
        return "\x1B\x4A".chr( max(0, min(255, $space)) );
    }
    
    function paperFeedBack($space) {
        // in some small increment
        // ESC "K" num
        return "\x1B\x4B".chr( max(0, min(24, $space)) );
    }
    
    function pageMode() {
        // ESC "L"
        return "\x1B\x4C";
    }
    
    function font($font=0) {
        // ESC "M" font
        // (FS "G" font)
        return "\x1B\x4D".chr( max(0, min(2, $font)) );
    }
    
    /*
    international character sets:
        0: America
        1: France
        2: Germany
        3: UK
        4: Denmark I
        5: Sweden
        6: Italy
        7: Spain I
        8: Japan
        9: Norway
        10: Denmark II
        11: Spain II
        12: Latin America
        13: Korea
    */
    function characterSet($set=0) {
        // ESC "R" set
        return "\x1B\x52".chr( max(0, min(13, $set)) );
    }
    
    function lineMode() {
        // ESC "S"
        return "\x1B\x53";
    }
    
    function pageOrient($orient=0) {
        // ESC "T" dir
        return "\x1B\x54".chr( max(0, min(3, (int)$orient)) );
    }
    
    // TODO: unidirectional printing;  ESC(\x1B) "U"(\x55) bit
    
    function rotate($on=true) {
        // ESC "V" bit
        return "\x1B\x56".chr( $on ? 1 : 0 );
    }
    
    function pageRegion($x=0, $y=0, $dx=65535, $dy=65535) {
        // ESC "W" xLO xHI yLO yHI dxLO dxHI dyLO dyHI
        return ("\x1B\x57"
            .chr( max(0, (int)($x % 256)) )
            .chr( max(0, min(255, (int)($x / 245))) )
            .chr( max(0, (int)($y % 256)) )
            .chr( max(0, min(255, (int)($y / 245))) )
            .chr( max(0, (int)($dx % 256)) )
            .chr( max(0, min(255, (int)($dx / 245))) )
            .chr( max(0, (int)($dy % 256)) )
            .chr( max(0, min(255, (int)($dy / 245))) )
        );
    }
    
    function moveX($dots) {
        // ESC "\" dxLO dxHI
        if ($dots < 0 && $dots >= -32768)
            $dots += 65536;
        return ("\x1B\x5C"
            .chr( max(0, (int)($dots % 256)) )
            .chr( max(0, min(255, (int)($dots / 256))) )
        );
    }
    
    function alignLeft() {
        // ESC "a" align
        return "\x1B\x61\x00";
    }
    
    function alignCenter() {
        // ESC "a" align
        return "\x1B\x61\x01";
    }
    
    function alignRight() {
        // ESC "a" align
        return "\x1B\x61\x02";
    }
    
    function paperRoll($receipt=true, $journal=false, $endorse=false, $validation=false) {
        // ESC "c" "0" bitfield
        return ("\x1B\x63\x30"
            .chr(
                ($validation ? 8 : 0) // ??
                + ($endorse ? 4 : 0)
                + ($journal ? 2 : 0) // ??
                + ($receipt ? 1 : 0)
            )
        );
    } // PaperRoll()
    
    // TODO: set paper out warning sensor;  ESC(\x1B) "c"(\x63) "3"(\x33) bitfield
    
    // TODO: set paper out stop sensor;  ESC(\x1B) "c"(\x63) "4"(\x34) bitfield
    
    function panelButtons($on=true) {
        // ESC "c" "5" flag
        return "\x1B\x63\x35".chr( $on ? 0 : 1 );
    }
    
    function lineFeedBack() {
        // ESC "e" 1
        return "\x1B\x65\x01";
    }
    
    // TODO: define macro;  ESC(\x1B) "g"(\x67) NUL(\x00) num lenLO lenHI data
    
    // TODO: run macro;  ESC(\x1B) "g"(\x67) num
    
    function drawerKick($pin=2, $on=100, $off=100) {
        // ESC "p" pin on off
        return ("\x1B\x70"
            .chr( ($pin < 3.5) ? 0 : 1 )
            .chr( max(0, min(255, (int)($on / 2))) ) // times are *2ms
            .chr( max(0, min(255, (int)($off / 2))) )
        );
    }
    
    /*
    code tables:
        0: PC437 (USA: Standard Europe)
        1: Katakana
        2: PC850 (Multilingual)
        3: PC860 (Portugese)
        4: PC863 (Canadian-French)
        5: PC865 (Nordic)
        16: WPC1252
        17: PC866 (Cryllic #2)
        18: PC852 (Latin2)
        19: PC858
        255: blank
    */
    function codeTable($table=0) {
        // ESC "t" table
        return "\x1B\x74".chr( max(0, min(255, $table)) );
    }
    
    // TODO: send peripheral device status;  ESC(\x1B) "u"(\x75) ?
    
    // TODO: send paper sensor status;  ESC(\x1B) "v"(\x76) ?
    
    function upsideDown($on=true) {
        // ESC "{" flag
        return "\x1B\x7B".chr( $on ? 1 : 0 );
    }
    
    // TODO: paper forced feed;  FS(\x1C) "A"(\x41) ?
    
    // TODO: send bitmap file FS(\x1C) "B"(\x42) bitmap
    
    // TODO: pdf 417 aspect definition;  FS(\x1C) "C"(\x43) ?
    
    // TODO: pdf 417 ecc level definition FS(\x1C) "D"(\x44) ?
    
    // TODO: horz/vert bar in page mode FS(\x1C) "E"(\x45) dir lenLO lenHI width
    
    // TODO: set 2d barcode size;  FS(\x1C) "H"(\x48) zoom
    
    // TODO: paper forced return;  FS(\x1C) "R"(\x52) ?
    
    // TODO: store data;  FS(\x1C) "g"(\x67) "1"(\x31) \x00 aLO aML aMH aHI bLO bHI data; max memory address=1024
    
    // TODO: load data;  FS(\x1C) "g"(\x67) "2"(\x32) \x00 aLO aML aMH aHI bLO bHI; max memory address=1024
    
    // TODO: print 2d barcode FS(\x1C) "k"(\x6B) type lenLO lenHI data
    
    // TODO: print stored bitmap;  FS(\x1C) "p"(\x70) num bitfield
    
    // TODO: store bitmaps;  FS(\x1C) "q"(\x71) num [xLO xHI yLO yHI]{num}
    
    function characterZoom($horiz=1, $vert=1) {
        // GS "!" zoom
        return "\x1D\x21".chr(
            16 * (int)(max(1, min(8, $horiz)) - 1)
            + (int)(max(1, min(8, $vert)) - 1)
        );
    }
    
    function gotoY($dots=0) {
        // GS "$" yLO yHI
        return ("\x1D\x24"
            .chr( max(0, (int)($dots % 256)) )
            .chr( max(0, min(255, (int)($dots / 256))) )
        );
    }
    
    // TODO: define downloadable image;  GS(\x1D) "*"(\x2A) x y data
    
    function test($type=3, $paper=0) {
        // GS "(" "A"
        return ("\x1D\x28\x41\x02\x00"
            .chr( max(0, min(2, (int)$paper)) )
            .chr( max(1, min(3, (int)$type)) )
        );
    }
    
    // TODO: edit NV memory;  GS(\x1D) "("(\x28) "C"(\x43) ...
    
    // TODO: toggle realtime command;  GS(\x1D) "("(\x28) "D"(\x44) ...
    
    // TODO: user startup commands;  GS(\x1D) "("(\x28) "E"(\x45) ...
    
    function density($factor=1.0) {
        // GS "(" "K" \x02 \x00 \x31 factor
        // factor = 0.7 (\xFA) - 0.9 (\xFF) ; 1.0 (\x00) - 1.3 (\x06)
        return ("\x1D\x28\x4B\x02\x00\x31"
            .chr( (int)((256 + max(-6, min(6, (($factor - 1.0) * 20)))) % 256) )
        );
    }
    
    function colorBlack() {
        // GS "(" "N" \x02 \x00 \x30 color
        // ESC "r" color
        return "\x1D\x28\x4E\x02\x00\x30\x31";
    }
    
    function colorRed() {
        // GS "(" "N" \x02 \x00 \x30 color
        // ESC "r" color
        return "\x1D\x28\x4E\x02\x00\x30\x32";
    }
    
    // TODO: print downloadable image;  GS(\x1D) "/"(\x2F) bitfield
    
    // TODO: toggle macro recording;  GS(\x1D) ":"(\x3A)
    
    function invert($on=true) {
        // GS "B" flag
        return "\x1D\x42".chr( $on ? 1 : 0 );
    }
    
    // TODO: set counter print mode;  GS(\x1D) "C"(\x43) "0"(\x30) digits align
    
    // TODO: set counter mode;  GS(\x1D) "C"(\x43) "1"(\x31) aLO aHI bLO bHI step repeat
    
    // TODO: set counter mode value;  GS(\x1D) "C"(\x43) "2"(\x32) vLO vHI
    
    // TODO: set counter mode;  GS(\x1D) "C"(\x43) ";"(\x3B) sa ";"(\x3B) sb ";"(\x3B) sn ";"(\x3B) sr ";"(\x3B) sc ";"(\x3B)
    
    function speedHigh() {
        // GS "E" speed
        return "\x1D\x45\x00";
    }
    
    function speedMedium() {
        // GS "E" speed
        return "\x1D\x45\x10";
    }
    
    function speedLow() {
        // GS "E" speed
        return "\x1D\x45\x20";
    }
    
    function barcodeHRI($below=true, $above=false) {
        // GS "H" bitfield
        return ("\x1D\x48"
            .chr( ($below ? 2 : 0) + ($above ? 1 : 0) )
        );
    }
    
    // TODO: send printer ID;  GS(\x1D) "I"(\x49) type
    
    function leftMargin($dots=0) {
        // GS "L" marginLO marginHI
        return ("\x1D\x4C"
            .chr( max(0, (int)($dots % 256)) )
            .chr( max(0, min(255, (int)($dots / 256))) )
        );
    }
    
    function dotPitch($primary=0, $secondary=0) {
        // sets dot pitch to 1/Xth inch (25.4/Xth mm), or 0 for default
        // GS "P" pitch pitch
        return "\x1D\x50".chr($primary).chr($secondary);
    }
    
    function discardLine() {
        // GS "T" printbit
        return "\x1D\x54\x00";
    }
    
    function preCutPaper($full=false) {
        // the cutter is above the print position, so cutting without feeding will put the cut above the last few lines printed
        // GS "V" bit
        // ESC "i"        (partial)
        // ESC "m"        (partial)
        return "\x1D\x56".chr( $full ? 0 : 1 );
    }
    
    function cutPaper($full=false, $feed=0) {
        // this version feeds the paper far enough to put the cutter just below the last line printed, plus the feed distance (in pixels?)
        // GS "V" bit feed
        return ("\x1D\x56"
            .chr( $full ? 65 : 66 )
            .chr( $feed )
        );
    }
    
    function printableWidth($dots=65535) {
        // GS "W" widthLO widthHI
        return ("\x1D\x57"
            .chr( max(0, (int)($dots % 256)) )
            .chr( max(0, min(255, (int)($dots / 256))) )
        );
    }
    
    function moveY($dots) {
        // GS "\" dyLO dyHI
        if ($dots < 0 && $dots >= -32768)
            $dots += 65536;
        return ("\x1D\x5C"
            .chr( max(0, (int)($dots % 256)) )
            .chr( max(0, min(255, (int)($dots / 256))) )
        );
    }
    
    // TODO: run macro;  GS(\x1D) "^"(\x5E) repeat delay mode
    
    // TODO: enable automatic status back;  GS(\x1D) "a"(\x61) bitfield
    
    function smooth($on=true) {
        // GS "b" flag
        return "\x1D\x62".chr( $on ? 1 : 0 );
    }
    
    // TODO: print counter;  GS(\x1D) "c"(\x63)
    
    function barcodeHRIFont($font=0) {
        // GS "f" font
        return "\x1D\x66".chr( max(0, min(2, $font)) );
    }
    
    function barcodeHeight($dots=162) {
        // GS "h" height
        return "\x1D\x68".chr( max(1, min(255, $dots)) );
    }

    public function printBarcode($type, $data)
    {
        switch ($type) {
            case PrintHandler::BARCODE_UPCA:
                return $this->barcodeUPC($data, 65);
            case PrintHandler::BARCODE_UPCE:
                return $this->barcodeUPC($data, 66);
            case PrintHandler::BARCODE_EAN13:
                return $this->barcodeEAN($data, false);
            case PrintHandler::BARCODE_EAN8:
                return $this->barcodeEAN($data, true);
            case PrintHandler::BARCODE_CODE39:
                return $this->barcodeCODE39($data);
            case PrintHandler::BARCODE_ITF:
                return $this->barcodeITF($data);
            case PrintHandler::BARCODE_CODEABAR:
                return $this->barcodeCODEABAR($data);
            case PrintHandler::BARCODE_CODE93:
                return $this->barcodeCODE93($data);
            case PrintHandler::BARCODE_CODE128:
                return $this->barcodeCODE128($data);
        }

        return '';
    }
    
    private function barcodeUPC($data, $subtype) {
        $bytes = max(11, min(12, strlen($data)));
        return ("\x1D\x6B"
            .chr( $subtype )
            .chr( $bytes )
            .str_pad(preg_replace('|[^0-9]|', '0', substr($data, 0, $bytes)), $bytes, '0', STR_PAD_LEFT)
        );
    }
    
    private function barcodeEAN($data, $ean8=false) {
        $bytes = $ean8 ? max(7, min(8, strlen($data))) : max(12, min(13, strlen($data)));
        return ("\x1D\x6B"
            .chr( $ean8 ? 68 : 67 )
            .chr( $bytes )
            .str_pad(preg_replace('|[^0-9]|', '0', substr($data, 0, $bytes)), $bytes, '0', STR_PAD_LEFT)
        );
    }
    
    private function barcodeCODE39($data) {
        $bytes = max(1, min(255, strlen($data)));
        return ("\x1D\x6B"
            .chr(69)
            .chr($bytes)
            .str_pad(preg_replace('|[^0-9A-Z $%+./-]|', ' ', substr($data, 0, $bytes)), $bytes, ' ', STR_PAD_LEFT)
        );
    }
    
    private function barcodeITF($data) {
        $bytes = max(2, min(254, (int)(strlen($data) / 2) * 2));
        return ("\x1D\x6B"
            .chr(70)
            .chr($bytes)
            .str_pad(preg_replace('|[^0-9]|', '0', substr($data, 0, $bytes)), $bytes, '0', STR_PAD_LEFT)
        );
    }
    
    private function barcodeCODEABAR($data) {
        $bytes = max(1, min(255, strlen($data)));
        return ("\x1D\x6B"
            .chr(71)
            .chr($bytes)
            .str_pad(preg_replace('|[^0-9A-D$+./:-]|', '0', substr($data, 0, $bytes)), $bytes, '0', STR_PAD_LEFT)
        );
    }
    
    private function barcodeCODE93($data) {
        $bytes = max(1, min(255, strlen($data)));
        return ("\x1D\x6B"
            .chr(72)
            .chr($bytes)
            .str_pad(preg_replace('|[^\\x00-\\x7f]|', "\x00", substr($data, 0, $bytes)), $bytes, "\x00", STR_PAD_LEFT)
        );
    }
    
    private function barcodeCODE128($data) {
        $bytes = max(2, min(255, strlen($data)));

        return $this->barcodeWidth(2) . $this->barcodeHeight(81)
            . ("\x1D\x6B"
            .chr(73)
            .chr($bytes)
            .str_pad(preg_replace('|[^\\x00-\\x7f]|', "\x00", substr($data, 0, $bytes)), $bytes, "\x00", STR_PAD_LEFT)
        );
    }
    
    // TODO: send status;  GS(\x1D) "r"(\x72) bitfield
    
    /*
    Raster bitmaps may be up to 524280 pixels wide ((255+255*256)*8), and up to 2303 pixels tall (255+8*256).
    Tall dots are 2 pixels tall, and wide dots are 2 pixels wide.
    This function takes $width and $height as pixels, but the printer takes width as bytes (= 8 pixels).
    Data must have (($width * $height) / 8) bytes.
    The bits in each data byte specify dots from left-to-right, and bytes are left-to-right, top-to-bottom.
        \xC0 = \b11000000 = pixels in the left two columns
        \x81 = \b10000001 = pixels in the left and right columns
    */
    function rasterBitmap($data, $width, $height, $tallDots=false, $wideDots=false) {
        // GS "v" 0 bits widthLO widthHI heightLO heightHI data
        $width = (int)(($width + 7) / 8);
        $bytes = (int)($width * $height);
        return ("\x1D\x76\x00"
            .chr( ($tallDots ? 2 : 0) + ($wideDots ? 1 : 0) )
            .chr( (int)($width % 256) )
            .chr( max(0, min(255, (int)($width / 256))) )
            .chr( (int)($height % 256) )
            .chr( max(0, min(8, (int)($height / 256))) )
            .str_pad(substr($data, 0, $bytes), $bytes, "\x00")
        );
    }
    
    function barcodeWidth($scale=3) {
        // ($scale * 0.141mm) is the width per single code line, not the total code width
        // GS "w" scale
        return "\x1D\x77".chr( max(1, min(6, $scale)) );
    }

    function writeLine($text){
        ReceiptLib::writeLine($text);
    }

    function renderBitmapFromFile($fn, $align='C')
    {
        return $this->RenderBitmap($fn, $align);
    }

    /**
      Turn bitmap into receipt string
      @param $arg string filename OR Bitmap obj
      @return receipt-formatted string
    */
    function renderBitmap($arg, $align='C'){
        $slip = "";

        if (!class_exists('COREPOS\\pos\\lib\\Bitmap')) return "";

        $bmp = null;
        if (is_object($arg) && is_a($arg, 'COREPOS\\pos\\lib\\Bitmap')){
            $bmp = $arg;
        }
        else if (file_exists($arg)){
            $bmp = new Bitmap();
            $bmp->load($arg);
        }

        // argument was invalid
        if ($bmp === null)
            return "";

        $bmpData = $bmp->getRawData();
        $bmpWidth = $bmp->getWidth();
        $bmpHeight = $bmp->getHeight();
        $bmpRawBytes = (int)(($bmpWidth + 7)/8);

        $stripes = $this->TransposeBitmapData($bmpData, $bmpWidth);
        for($i=0; $i<count($stripes); $i++)
            $stripes[$i] = $this->InlineBitmap($stripes[$i], $bmpWidth);

        if ($align == 'C')
            $slip .= $this->AlignCenter();
        if (count($stripes) > 1)
            $slip .= $this->LineSpacing(0);
        $slip .= implode("\n",$stripes);
        if (count($stripes) > 1)
            $slip .= $this->ResetLineSpacing();
        if ($align == 'C') $slip .= "\n";
        $slip .= $this->AlignLeft();

        return $slip;
    }

    /**
      Show bitmap stored on the printer device itself
      @param $image_id [int|array] storage location ID
        OR storage position (start, end)
      @return [string] receipt text
    */
    public function renderBitmapFromRam($image_id)
    {
        if (is_array($image_id) && count($image_id) >= 2) {
            return chr(29) . '(L' . chr(6) . chr(0) . '0E' 
                . chr($image_id[0]) // Start position defined in utility
                . chr($image_id[1]) // End position defined in utility
                . chr(1) . chr(1);
        }

        return chr(28) . 'p' . chr($image_id) . chr(0);
    }
    
} // ESCPOSPrintHandler