CORE-POS/IS4C

View on GitHub
pos/is4c-nf/lib/Bitmap.php

Summary

Maintainability
F
1 wk
Test Coverage
F
30%
<?php

namespace COREPOS\pos\lib;
use \Exception;

/*
Bitmap.class.php
version 1 (2009-09-24)
author Alex Frase

References
    http://en.wikipedia.org/wiki/BMP_file_format
    http://atlc.sourceforge.net/bmp.html
    http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#Optimization
*/

class Bitmap 
{
    private $DIB1 = 12;
    private $DIB2 = 64;
    private $DIB3 = 40;
    private $DIB4 = 108;
    private $DIB5 = 124;
    
    private $error;
    
    private $magic;
    private $dibVersion;
    private $width;
    private $height;
    private $bpp; // bits per pixel
    private $hppm; // horiz. pixels per meter
    private $vppm; // vert. pixels per meter
    private $palSize;
    private $palSizeImp;
    private $palette;
    private $image;
    private $rowBytes;
    
    /*
    * INTERNAL METHODS
    */
    
    private function returnError($err=null, $ret=null) 
    {
        $this->error = is_string($err) ? $err : "Unknown error";

        return $ret;
    } // returnError()
    
    private function parseInt($data, $left=0, $right=-1, $signed=false, $bigEndian=false) 
    {
        if (is_string($data)) {
            $isarray = false;
            if ($right < 0) {
                $right = strlen($data) - 1;
            }
        } elseif (is_array($data)) {
            $isarray = true;
            $data = array_values($data);
            if ($right < 0) {
                $right = count($data) - 1;
            }
        } else {
            return null;
        }
        if ($right < $left) {
            return null;
        }
        // set traversal range and direction
        $start = $bigEndian ? $right : $left;
        $end   = $bigEndian ? $left  : $right;
        $delta = $bigEndian ? -1     : 1;
        // process bytes
        $num = $start;
        $val = $isarray ? (int)$data[$num] : ord($data[$num]);
        $factor = 256;
        while ($num != $end) {
            $num += $delta;
            $val += $factor * ($isarray ? (int)$data[$num] : ord($data[$num]));
            $factor <<= 8; // *= 256
        }
        // check sign bit
        if ($signed && (($isarray ? (int)$data[$end] : ord($data[$end])) & 0x80)) {
            $val = -$factor + $val;
        }
        // done
        return $val;
    } // parseInt()
    
    private function renderInt($val, $bytes, $signed=false, $bigEndian=false) 
    {
        $val = (int)$val;
        $bytes = (int)$bytes;
        $range = pow(2, $bytes * 8);
        $neg = false;
        if ($signed && $val < 0) {
            $val = (~ (-$val)) + 1;
        }
        // set traversal range and direction
        $start = $bigEndian ? ($bytes - 1) : 0;
        $end   = $bigEndian ? 0            : ($bytes - 1);
        $delta = $bigEndian ? -1           : 1;
        // set bytes
        $data = str_repeat("\x00", $bytes);
        for ($num = $start;  $num != $end;  $num += $delta) {
            $data[$num] = chr($val & 0xFF);
            $val >>= 8; // /= 256
        }
        // done

        return $data;
    } // renderInt()
    
    
    private function lastError($reset=true) 
    {
        $err = $this->error;
        if ($reset) {
            $this->error = null;
        }
        return $err;
    } // lastError()
    
    public function load($filename, $filedata=null) 
    {
        $data = "";
        if ($filename === true) {
            $data = $filedata;
        } else {
            $data = file_get_contents($filename);
            if (!$data) {
                return $this->returnError("load(): failed reading file \"".$filename."\"");
            }
        }
        $datasize = strlen($data);
        
        // read the BMP header
        
        if ($datasize < 18) {
            return $this->returnError("load(): incomplete BMP header (file is ".$datasize." bytes)");
        }
        $magic = substr($data, 0, 2);
        $fileSize = $this->parseInt($data, 2, 5);
        // bytes 6-9 are unused (application specific)
        $imageAt = $this->parseInt($data, 10, 13);
        
        // validate the BMP header
        
        switch ($magic) {
            case "BM": // Windows 3.1x, 95, NT, ...
            case "BA": // OS/2 Bitmap Array
            case "CI": // OS/2 Color Icon
            case "CP": // OS/2 Color Pointer
            case "IC": // OS/2 Icon
            case "PT": // OS/2 Pointer
                break;
            default:
                return $this->returnError("load(): unknown magic numbers \"".dechex(ord($magic[0]))." ".dechex(ord($magic[1]))."\"");
        }
        if ($fileSize != $datasize) {
            return $this->returnError("load(): incorrect file size (".$fileSize." reported, ".$datasize." actual)");
        }
        
        // read the DIB header
        
        $headerSize = $this->parseInt($data, 14, 17);
        if ($datasize < (14 + $headerSize)) {
            return $this->returnError("load(): incomplete DIB header (file is ".$datasize." bytes)");
        }
        switch ($headerSize) {
            case $this->DIB1: // OS/2 V1 "BITMAPCOREHEADER" 12 bytes
                $width = $this->parseInt($data, 18, 19);
                $height = $this->parseInt($data, 20, 21);
                $colorPlanes = $this->parseInt($data, 22, 23);
                $bpp = $this->parseInt($data, 24, 25);
                if ($bpp == 16 || $bpp == 32) { // not supported in this header
                    return $this->returnError("load(): ".$bpp." bits-per-pixel invalid in V1 header");
                }
                $compression = 0;
                $imgDataSize = null;
                $hppm = null;
                $vppm = null;
                $palSize = 0;
                $palSizeImp = 0;
                break;
            
            case $this->DIB3: // Windows V3 "BITMAPINFOHEADER" 40 bytes
                $width = $this->parseInt($data, 18, 21, true);
                $height = $this->parseInt($data, 22, 25, true);
                $colorPlanes = $this->parseInt($data, 26, 27);
                $bpp = $this->parseInt($data, 28, 29);
                $compression = $this->parseInt($data, 30, 33);
                $imgDataSize = $this->parseInt($data, 34, 37);
                $hppm = $this->parseInt($data, 38, 41, true);
                $vppm = $this->parseInt($data, 42, 45, true);
                $palSize = $this->parseInt($data, 46, 49);
                $palSizeImp = $this->parseInt($data, 50, 53);
                break;
                
            // TODO: more header formats
            
            default:
                return $this->returnError("load(): unknown DIB header size (".$headerSize." bytes)");
        }
        
        // validate the DIB header
        
        if ($width < 1) {
            return $this->returnError("load(): invalid image width ".$width);
        }
        if ($height == 0) {// height can be negative, meaning data is top-to-bottom instead of bottom-to-top
            return $this->returnError("load(): invalid image height ".$height);
        }
        if ($colorPlanes != 1) {
            return $this->returnError("load(): invalid color plane count ".$colorPlanes);
        }
        if ($bpp != 1 && $bpp != 4 && $bpp != 8 && $bpp != 16 && $bpp != 24 && $bpp != 32) {
            return $this->returnError("load(): invalid bits-per-pixel ".$bpp);
        }

        switch ($compression) {
            case 0: // BI_RGB (uncompressed)
                break;
            case 1: // BI_RLE8 (RLE; 8 bpp only)
            case 2: // BI_RLE4 (RLE; 4 bpp only)
            case 3: // BI_BITFIELDS (bitfield; 16 and 32 bpp only)
            case 4: // BI_JPEG (JPEG)
            case 5: // BI_PNG (PNG)
                // TODO: support compression?
                return $this->returnError("load(): image data compression not supported");
            default:
                return $this->returnError("load(): invalid compression code ".$compression);
        }

        $rowDataSize = (int)((($width * $bpp) + 31) / 32) * 4;
        if ($imgDataSize === null || $imgDataSize === 0) {
            $imgDataSize = abs($height) * $rowDataSize;
        } elseif ($imgDataSize != (abs($height) * $rowDataSize)){
            /** modification by Andy 09Aug13
                I think this makes more sense and it's incorrect
                to assume all zero bytes at the end of the
                image are padding **/
            $padding = $imgDataSize % 4;
            if ($padding > 0) {
                $imgDataSize -= $padding;
                $data = substr($data,0,strlen($data)-$padding);
            }
            /* previous method for removing padding
            while(ord($data[strlen($data)-1])===0){
                $imgDataSize--;
                $data = substr($data,0,strlen($data)-1);
            }
            */
            if($imgDataSize != (abs($height) * $rowDataSize)) {
                return $this->returnError("load(): incorrect image data size (".$imgDataSize." reported, ".(abs($height) * $rowDataSize)." expected)");
            }
        }
        if ($hppm !== null && $hppm <= 0) {
            return $this->returnError("load(): invalid horizontal pixels-per-meter ".$hppm);
        }
        if ($vppm !== null && $vppm <= 0) {
            return $this->returnError("load(): invalid vertical pixels-per-meter ".$hppm);
        }
        
        // read the palette
        
        if (!$palSize && $bpp < 16) {
            $palSize = pow(2, $bpp);
        }
        $palColorSize = 0;
        $palDataSize = 0;
        $palette = null;
        if ($palSize) {
            $palColorSize = ($headerSize == $this->DIB1) ? 3 : 4;
            $palDataSize = $palSize * $palColorSize;
            if ($datasize < (14 + $headerSize + $palDataSize)) {
                return $this->returnError("load(): incomplete palette (file is ".$datasize." bytes)");
            }
            $palette = substr($data, 14 + $headerSize, $palDataSize);
            if ($palColorSize < 4) {
                // pad each palette color to 4 bytes for simpler lookup (remember "." doesn't match newline, hence "|\\n")
                $palette = preg_replace('/(.|\\n){'.$palColorSize.'}/', '\\1'.str_repeat("\x00",(4-$palColorSize)), $palette);
            }
        }
        
        // read the image
        
        if ($datasize < ($imageAt + $imgDataSize)) {
            return $this->returnError("load(): incomplete image (file is ".$datasize." bytes)");
        }
        $image = substr($data, $imageAt, $imgDataSize);
        $rowBytes = (int)((($width * $bpp) + 7) / 8);
        if ($rowBytes > $rowDataSize) {
            return $this->returnError("load(): consistency error (calculated byte width ".$rowBytes.", data width ".$rowDataSize.")");
        }
        if ($rowBytes < $rowDataSize) {
            // strip off word-alignment padding (remember "." doesn't match newline, hence "|\\n")
            $image = preg_replace('/((?:.|\\n){'.$rowBytes.'})((?:.|\\n){'.($rowDataSize-$rowBytes).'})/', '\\1', $image);
        }
        if ($height < 0) {
            $height = abs($height);
        } else {
            // flip image rows vertically (BMPs are stored upside-down by default)
            $str_split = array();
            $val = "";
            for($i=0; $i<strlen($image); $i++) {
                if ($i % $rowBytes == 0) {
                    if (strlen($val) > 0) {
                        $str_split[] = $val;
                    }
                    $val = "";
                }
                $val .= $image[$i];
            }
            if (strlen($val) > 0) {
                $str_split[] = $val;
            }
            $image = implode('', array_reverse($str_split));
        }
        
        // initialize the object
        
        $this->magic = $magic;
        $this->dibVersion = $headerSize;
        $this->width = $width;
        $this->height = $height;
        $this->bpp = $bpp;
        $this->rowBytes = $rowBytes;
        $this->hppm = $hppm;
        $this->vppm = $vppm;
        $this->palSize = $palSize;
        $this->palSizeImp = $palSizeImp;
        $this->palette = $palette;
        $this->image = $image;
        
        // all done
        return true;
    } // load()
    
    
    /*
    * OBJECT METHODS
    */
    
    public function __construct($width=1, $height=1, $bpp=1, $dpi=72) 
    {
        $this->error = null;
        
        if (!is_numeric($width) || (int)$width < 1) {
            throw('Bitmap width must be at least 1');
        }
        if (!is_numeric($height) || (int)$height < 1) {
            throw('Bitmap height must be at least 1');
        }
        if (!is_numeric($bpp) || (int)$bpp != 1) {
            throw('Color bitmaps not yet supported');
        }
        if (!is_numeric($dpi) || (int)$dpi < 1) {
            throw('Bitmap DPI must be at least 1');
        }
        $this->magic = "BM";
        $this->dibVersion = $this->DIB3;
        $this->width = (int)$width;
        $this->height = (int)$height;
        $this->bpp = (int)$bpp;
        $this->rowBytes = (int)((($this->width * $this->bpp) + 7) / 8);
        $this->hppm = (int)(($dpi * 39.37) + 0.5); // 39.37 inches per meter
        $this->vppm = $this->hppm;
        $this->palSize = 2;
        $this->palSizeImp = 0;
        $this->palette = "\xFF\xFF\xFF\x00\x00\x00\x00\x00";
        $rowBytes = (int)((($this->width * $this->bpp) + 7) / 8);
        $this->image = str_repeat("\x00", $rowBytes * $this->height);
    } // __construct()
    
    public function save($filename) {
        // prepare the image
        $rowBytes = (int)((($this->width * $this->bpp) + 7) / 8);
        $rowDataSize = (int)((($this->width * $this->bpp) + 31) / 32) * 4;
        if ($rowBytes > $rowDataSize) {
            return $this->returnError("save(): consistency error (calculated byte width ".$rowBytes.", data width ".$rowDataSize.")");
        }
        $pad = str_repeat("\x00", $rowDataSize - $rowBytes); // might be 0 -> "" pad, which is ok
        $image = implode($pad, array_reverse(str_split($this->image, $rowBytes))) . $pad;
        $imgDataSize = $this->height * $rowDataSize;
        if ($imgDataSize != strlen($image)) {
            return $this->returnError("save(): consistency error (calculated image data size ".$imgDataSize.", prepared ".strlen($image).")");
        }
        
        // prepare the palette
        $palette = "";
        if ($this->palSize) {
            $palette = $this->palette;
            // if ($this->dibVersion == self::DIB1)  // strip padding...
        }
        $palDataSize = strlen($palette);
        
        // prepare the DIB header
        $dibHead = "";
        switch ($this->dibVersion) {
            case $this->DIB1: // OS/2 V1 "BITMAPCOREHEADER" 12 bytes
                return $this->returnError("save(): only DIB3 is supported for writing");
            
            case $this->DIB3: // Windows V3 "BITMAPINFOHEADER" 40 bytes
                $dibHead .= $this->renderInt($this->DIB3, 4);
                $dibHead .= $this->renderInt($this->width, 4, true);
                $dibHead .= $this->renderInt($this->height, 4, true);
                $dibHead .= $this->renderInt(1, 2); // colorPlanes
                $dibHead .= $this->renderInt($this->bpp, 2);
                $dibHead .= $this->renderInt(0, 4); // compression
                $dibHead .= $this->renderInt($imgDataSize, 4);
                $dibHead .= $this->renderInt($this->hppm, 4, true);
                $dibHead .= $this->renderInt($this->vppm, 4, true);
                $dibHead .= $this->renderInt($this->palSize, 4);
                $dibHead .= $this->renderInt($this->palSizeImp, 4);
                break;
            
            // TODO: more header formats
            
            default:
                return $this->returnError("save(): only DIB3 is supported for writing");
        }
        if (strlen($dibHead) != $this->dibVersion) {
            return $this->returnError("save(): consistency error (calculated DIB size ".$this->dibVersion.", prepared ".strlen($dibHead).")");
        }
        
        // prepare the BMP header
        $imageAt = 14 + $this->dibVersion + $palDataSize;
        $fileSize = $imageAt + $imgDataSize;
        $bmpHead = "";
        $bmpHead .= $this->magic;
        $bmpHead .= $this->renderInt($fileSize, 4);
        $bmpHead .= $this->renderInt(0, 4); // bytes 6-9 are unused (application specific)
        $bmpHead .= $this->renderInt($imageAt, 4);
        
        // write or return the file
        $data = $bmpHead . $dibHead . $palette . $image;
        if ($filename === true) {
            return $data;
        }
        $bytes = file_put_contents($filename, $data);
        if ($bytes != strlen($data)) {
            return $this->returnError("save(): failed writing file \"".$filename."\" (".$bytes." of ".strlen($data)." bytes written)");
        }

        return true;
    } // save()
    
    public function getWidth() 
    {
        return $this->width;
    }
    
    public function getHeight()
    {
        return $this->height;
    }
    
    private function getColorDepth()
    {
        return $this->bpp;
    }
    
    private function getHorizResolution($asDPI=false){
        return $asDPI ? (int)(($this->hppm / 39.37) + 0.5) : $this->hppm;
    }
    
    private function getVertResolution($asDPI=false)
    {
        return $asDPI ? (int)(($this->vppm / 39.37) + 0.5) : $this->vppm;
    }
    
    private function getPaletteSize()
    {
        return ($this->bpp < 16 && $this->palSize) ? $this->palSize : null;
    }
    
    private function getPaletteColor($index, $channel=null) 
    {
        if (!$this->palSize || $this->palette === null || $index < 0 || $index >= $this->palSize) {
            return null;
        }
        $byte = $index << 2; // (int)($index * 4)
        if ($channel !== null) {
            if ($channel < 0 || $channel > 2) {
                return null;
            }
            return ord($this->palette[$byte + $channel]);
        }

        return $this->parseInt($this->palette, $byte, $byte + 2);
    } // getPaletteColor()
    
    private function getPixelValue($x, $y) 
    {
        // validate coordinates
        if ($x < 0) {
            $x += $this->width;
        } elseif ($x >= $this->width) {
            return null;
        }
        if ($y < 0) {
            $y += $this->height;
        } elseif ($y >= $this->height) {
            return null;
        }
        // fetch pixel
        switch ($this->bpp) {
            case 32:
                $byte = ($y * $this->rowBytes) + ($x << 2); // (int)($x * 4)
                return $this->parseInt($this->image, $byte, $byte + 2); // BMPs don't actually have an alpha channel, so ignore the 4th byte
            case 24:
                $byte = ($y * $this->rowBytes) + ((int)($x * 3));
                return $this->parseInt($this->image, $byte, $byte + 2);
            case 16:
                $byte = ($y * $this->rowBytes) + ($x << 1); // (int)($x * 2)
                return $this->parseInt($this->image, $byte, $byte + 1);
            case 8:
                $byte = ($y * $this->rowBytes) + $x;
                return ord($this->image[$byte]);
            case 4:
                $byte = ($y * $this->rowBytes) + ($x >> 1); // (int)($x / 2)
                $shift = 1 - ($x % 2);
                return ((ord($this->image[$byte]) >> $shift) & 0x0F);
            case 1:
                $byte = ($y * $this->rowBytes) + ($x >> 3); // (int)($x / 8)
                $shift = 7 - ($x % 8);
                return ((ord($this->image[$byte]) >> $shift) & 0x01);
        }

        return null;
    } // getPixelValue()
    
    private function setPixelValue($x, $y, $val) 
    {
        // validate coordinates
        if ($x < 0) {
            $x += $this->width;
        } elseif ($x >= $this->width) {
            return null;
        }
        if ($y < 0) {
            $y += $this->height;
        } elseif ($y >= $this->height) {
            return null;
        }
        // validate new pixel value
        if ($val < 0 || $val >= pow(2, $this->bpp) || ($this->palSize && $val >= $this->palSize)) {
            return null;
        }
        // set pixel
        switch ($this->bpp) {
            case 32:
                $byte = ($y * $this->rowBytes) + ($x << 2); // (int)($x * 4)
                $this->image = substr_replace($this->image, $this->renderInt($val,3), $byte, 3); // BMPs don't actually have an alpha channel, so ignore the 4th byte
                return true;
            case 24:
                $byte = ($y * $this->rowBytes) + ((int)($x * 3));
                $this->image = substr_replace($this->image, $this->renderInt($val,3), $byte, 3);
                return true;
            case 16:
                $byte = ($y * $this->rowBytes) + ($x << 1); // (int)($x * 2)
                $this->image = substr_replace($this->image, $this->renderInt($val,2), $byte, 2);
                return true;
            case 8:
                $byte = ($y * $this->rowBytes) + $x;
                $this->image[$byte] = chr($val);
                return true;
            case 4:
                $byte = ($y * $this->rowBytes) + ($x >> 1); // (int)($x / 2)
                $shift = 1 - ($x % 2);
                $mask = (0x0F << $shift);
                $this->image[$byte] = chr( (ord($this->image[$byte]) & (~ $mask)) | ($val & $mask) );
                return true;
            case 1:
                $byte = ($y * $this->rowBytes) + ($x >> 3); // (int)($x / 8)
                $shift = 7 - ($x % 8);
                if ($val) {
                    $this->image[$byte] = chr( ord($this->image[$byte]) | (0x01 << $shift) );
                } else {
                    $this->image[$byte] = chr( ord($this->image[$byte]) & (0xFF ^ (0x01 << $shift)) );
                }
                return true;
        }

        return null;
    } // setPixelValue()

    private function validateCoord($val, $max)
    {
        if ($val < 0) {
            return $val + $max;
        } elseif ($val >= $max) {
            throw new Exception('Coordinate out of range');
        }
        return $val;
    }
    
    // @hintable
    public function drawLine($pt1, $pt2, $val) 
    {
        // validate coordinates
        try {
            $pt1[0] = $this->validateCoord($pt1[0], $this->width);
            $pt2[0] = $this->validateCoord($pt1[0], $this->width);
            $pt1[1] = $this->validateCoord($pt1[1], $this->height);
            $pt2[1] = $this->validateCoord($pt2[1], $this->height);
        } catch (Exception $ex) {
            return null;
        }
        // validate new pixel(s) value
        if ($val < 0 || $val >= pow(2, $this->bpp) || ($this->palSize && $val >= $this->palSize)) {
            return null;
        }
        // draw!
        // http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#Optimization
        $steep = (abs($pt2[1] - $pt1[1]) > abs($pt2[0] - $pt1[0]));
        if ($steep) {
            $tmp=$pt1[0]; $pt1[0]=$pt1[1]; $pt1[1]=$tmp; // swap $pt1[0],$pt1[1]
            $tmp=$pt2[0]; $pt2[0]=$pt2[1]; $pt2[1]=$tmp; // swap $pt2[0],$pt2[1]
        }
        if ($pt1[0] > $pt2[0]) {
            $tmp=$pt1; $pt1=$pt2; $pt2=$tmp; // swap $pt1,$pt2
        }
        $divx = ($pt2[0] - $pt1[0]);
        $divy = abs($pt2[1] - $pt1[1]);
        $err = $divx >> 1;
        $s_y = ($pt1[1] < $pt2[1]) ? 1 : -1;
        while ($pt1[0] <= $pt2[0]) {
            if ($steep) {
                $this->setPixelValue($pt1[1], $pt1[0], 1);
            } else {
                $this->setPixelValue($pt1[0], $pt1[1], 1);
            }
            $err -= $divy;
            if ($err < 0) {
                $pt1[1] += $s_y;
                $err += $divx;
            }
            $pt1[0]++;
        }

        return true;
    } // DrawLine()
    
    private function getPixelColor($x, $y, $channel=null) 
    {
        $val = $this->getPixelValue($x, $y);
        if ($val !== null && $this->palette !== null) {
            return $this->getPaletteColor($val, $channel);
        }
        if ($this->bpp == 16) {
            $val =
                (((((($val >> 10) & 0x1F) / 0x1F) * 0xFF) + 0.5) << 16)
                + (((((($val >> 5) & 0x1F) / 0x1F) * 0xFF) + 0.5) << 8)
                + (int)(((($val & 0x1F) / 0x1F) * 0xFF) + 0.5);
        }

        return $val;
    } // getPixelColor()
    
    public function getRawData() 
    {
        return $this->image;
    } // GetRawData()
    
    private function getRawBytesPerRow() 
    {
        return $this->rowBytes;
    } // GetRawBytesPerRow()

    /**
      Generate a bar graph bitmap
      @param $percent (0.05 and 5 both represent 5%)
      @param $width default 200
      @param $height default 40
      @return Bitmap object
    */
    public function barGraph($percent, $width=200, $height=40)
    {
        $graph = new Bitmap($width, $height, 1);
        $black = 1;
        $spacing = 5;

        // border top
        $graph->drawLine(array(0,0), array($width-1,0), $black);
        // border bottom
        $graph->drawLine(array(0,$height-1), array($width-1,$height-1), $black);
        // border left
        $graph->drawLine(array(0,1), array(0,$height-2), $black);
        // border right
        $graph->drawLine(array($width-1,1), array($width-1,$height-2), $black);

        $full_bar_size = $width - ($spacing*2);
        if ($percent > 1) $percent = (float)($percent / 100.00);
        if ($percent > 1) $percent = 1.0;
        $bar_size = round($percent * $full_bar_size);

        for($line=$spacing;$line<$height-$spacing;$line++){
            $graph->drawLine(array($spacing,$line), array($spacing+$bar_size,$line), $black);    
        }

        return $graph;
    }

    /**
      Turn bitmap into receipt string
      @param $arg string filename OR Bitmap obj
      @return receipt-formatted string
    */
    static private function renderBitmap($arg)
    {
        global $PRINT_OBJ;
        $slip = "";

        $bmp = null;
        if (is_object($arg) && is_a($arg, 'Bitmap')) {
            $bmp = $arg;
        } elseif (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);

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

        $slip .= $printer->AlignCenter();
        if (count($stripes) > 1) {
            $slip .= $printer->LineSpacing(0);
        }
        $slip .= implode("\n",$stripes);
        if (count($stripes) > 1) {
            $slip .= $printer->ResetLineSpacing()."\n";
        }
        $slip .= $printer->AlignLeft();

        return $slip;
    }
    
} // Bitmap