Completed
Push — master ( c5f7b8...442191 )
by Roberto
03:20
created

Graphics::resizeImage()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.9157

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
ccs 12
cts 17
cp 0.7059
rs 8.8571
cc 6
eloc 15
nc 4
nop 2
crap 6.9157
1
<?php
2
3
namespace Posprint\Graphics;
4
5
/**
6
 * Classe Graphics
7
 * 
8
 * @category   NFePHP
9
 * @package    Posprint
10
 * @copyright  Copyright (c) 2016
11
 * @license    http://www.gnu.org/licenses/lesser.html LGPL v3
12
 * @author     Roberto L. Machado <linux dot rlm at gmail dot com>
13
 * @link       http://github.com/nfephp-org/posprint for the canonical source repository
14
 */
15
16
use Posprint\Graphics\Basic;
17
use Endroid\QrCode\QrCode;
18
use RuntimeException;
19
use InvalidArgumentException;
20
21
class Graphics extends Basic
22
{
23
    /**
24
     * Image prixels in BW
25
     * @var string 
26
     */
27
    protected $imgData = null;
28
    /**
29
     * Image Raster bit
30
     * @var string
31
     */
32
    protected $imgRasterData = null;
33
  
34
    /**
35
     * Constructor 
36
     * Load a image, if passed a path to file and adjust dimentions
37
     * 
38
     * @param string $filename
39
     * @param int $width
40
     * @param int $height
41
     * @throws RuntimeException
42
     */
43 10
    public function __construct($filename = null, $width = null, $height = null)
44
    {
45
        // Include option to Imagick
46 10
        if (! $this->isGdSupported()) {
47
            throw new RuntimeException("GD module not found.");
48
        }
49 10
        $this->imgHeight = 0;
50 10
        $this->imgWidth = 0;
51 10
        $this->imgData = null;
52 10
        $this->imgRasterData = null;
53
        // Load the image, if the patch was passed
54 10
        if (! is_null($filename)) {
55 8
            $this->load($filename, $width, $height);
56 6
        }
57 8
    }
58
    
59
    /**
60
     * Return a string of bytes
61
     * 
62
     * @return string
63
     */
64 1
    public function getRasterImage()
65
    {
66 1
        $this->resizeImage($this->imgWidth);
67 1
        $this->convertPixelBW();
68 1
        $this->convertRaster();
69 1
        return $this->imgRasterData;
70
    }
71
72
    /**
73
     * load
74
     * Load image file and adjust dimentions
75
     * @param string $filename path to image file
76
     * @param float $width 
77
     * @param float $height
78
     * @throws InvalidArgumentException
79
     * @throws RuntimeException
80
     */ 
81 8
    public function load($filename, $width = null, $height = null)
82
    {
83 8
        if (! is_file($filename)) {
84 1
            throw new InvalidArgumentException("Image file not found.");
85
        }
86 7
        if (! is_readable($filename)) {
87
            throw new RuntimeException("The file can not be read due to lack of permissions.");
88
        }
89
        //identify type of image and load with GD
90 7
        $tipo = $this->identifyImg($filename);
91 7
        if ($tipo == 'BMP') {
92 1
            $img = $this->loadBMP($filename);
93 1
            if ($img === false) {
94
                throw new InvalidArgumentException("Image file is not a BMP");
95
            }
96 1
            $this->img = $img; 
97 1
        } else {
98 6
            $func = 'imagecreatefrom' . strtolower($tipo);
99 6
            if (! function_exists($func)) {
100 1
                throw new RuntimeException("It is not possible to use or handle this type of image with GD");
101
            }
102 5
            $this->img = $func($filename);
103
        }
104 6
        if (! $this->img) {
105
            throw new RuntimeException("Failed to load image '$filename'.");
106
        }
107
        //get image dimentions
108 6
        $this->getDimImage();
109 6
        if ($width != null || $height != null) {
110
            $this->resizeImage($width, $height);
111
        }
112 6
    }
113
    
114
    /**
115
     * Save image to PNG file 
116
     * @param string $filename
117
     */
118 1
    public function save($filename = null)
119
    {
120 1
        $this->saveImage($filename, $this->img);
121 1
    }
122
    
123
    /**
124
     * Convert a GD image into a BMP string representation
125
     * @param string $filename path to image BMP file
126
     * @return string
127
     */
128 3
    public function convert2BMP($filename = null)
129
    {
130 3
        if (! is_resource($this->img)) {
131
            return '';
132
        }
133
        //to remove alpha color and put white instead
134 3
        $img = $this->img;
135 3
        $imageX = imagesx($img);
136 3
        $imageY = imagesy($img);
137 3
        $bmp = '';
138 3
        for ($yInd = ($imageY - 1); $yInd >= 0; $yInd--) {
139 3
            $thisline = '';
140 3
            for ($xInd = 0; $xInd < $imageX; $xInd++) {
141 3
                $argb = self::getPixelColor($img, $xInd, $yInd);
142
                //change transparency to white color
143 3
                if ($argb['alpha'] == 0 && $argb['blue'] == 0 && $argb['green'] == 0 && $argb['red'] == 0) {
144
                    $thisline .= chr(255).chr(255).chr(255);
145
                } else {
146 3
                    $thisline .= chr($argb['blue']).chr($argb['green']).chr($argb['red']);
147
                }    
148 3
            }
149 3
            while (strlen($thisline) % 4) {
150 3
                $thisline .= "\x00";
151 3
            }
152 3
            $bmp .= $thisline;
153 3
        }
154 3
        $bmpSize = strlen($bmp) + 14 + 40;
155
        // bitMapHeader [14 bytes] - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_62uq.asp
156 3
        $bitMapHeader  = 'BM'; // WORD bfType;
157 3
        $bitMapHeader .= self::littleEndian2String($bmpSize, 4); // DWORD bfSize;
158 3
        $bitMapHeader .= self::littleEndian2String(0, 2); // WORD bfReserved1;
159 3
        $bitMapHeader .= self::littleEndian2String(0, 2); // WORD bfReserved2;
160 3
        $bitMapHeader .= self::littleEndian2String(54, 4); // DWORD bfOffBits;
161
        // bitMapInfoHeader - [40 bytes] http://msdn.microsoft.com/library/en-us/gdi/bitmaps_1rw2.asp
162 3
        $bitMapInfoHeader  = self::littleEndian2String(40, 4); // DWORD biSize;
163 3
        $bitMapInfoHeader .= self::littleEndian2String($imageX, 4); // LONG biWidth;
164 3
        $bitMapInfoHeader .= self::littleEndian2String($imageY, 4); // LONG biHeight;
165 3
        $bitMapInfoHeader .= self::littleEndian2String(1, 2); // WORD biPlanes;
166 3
        $bitMapInfoHeader .= self::littleEndian2String(24, 2); // WORD biBitCount;
167 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biCompression;
168 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biSizeImage;
169 3
        $bitMapInfoHeader .= self::littleEndian2String(2835, 4); // LONG biXPelsPerMeter;
170 3
        $bitMapInfoHeader .= self::littleEndian2String(2835, 4); // LONG biYPelsPerMeter;
171 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biClrUsed;
172 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biClrImportant;
173 3
        $data = $bitMapHeader.$bitMapInfoHeader.$bmp;
174 3
        $this->saveImage($filename, $data);
175 2
        return $data;
176
    }
177
    
178
    /**
179
     * loadBMP
180
     * Create a GD image from BMP file
181
     * @param string $filename
182
     * @return GD object
183
     */
184 1
    public function loadBMP($filename)
185
    {
186
        //Load the image into a string
187 1
        $file = fopen($filename, "rb");
188 1
        $read = fread($file, 10);
189
        //continue at the end of file
190 1
        while (! feof($file) && ($read <> "")) {
191 1
            $read .= fread($file, 1024);
192 1
        }
193 1
        fclose($file);
194 1
        $temp = unpack("H*", $read);
195 1
        $hex = $temp[1];
196 1
        $header = substr($hex, 0, 108);
197
        //Process the header
198
        //Structure: http://www.fastgraph.com/help/bmp_header_format.html
199 1
        if (substr($header, 0, 4) != "424d") {
200
            //is not a BMP file
201
            return false;
202
        }
203
        //Cut it in parts of 2 bytes
204 1
        $headerParts = str_split($header, 2);
205
        //Get the width 4 bytes
206 1
        $width = hexdec($headerParts[19].$headerParts[18]);
207
        //Get the height 4 bytes
208 1
        $height = hexdec($headerParts[23].$headerParts[22]);
209
        // Unset the header params
210 1
        unset($headerParts);
211
        // Define starting X and Y
212 1
        $xPos = 0;
213 1
        $yPos = 1;
214
        //Create new gd image
215 1
        $image = imagecreatetruecolor($width, $height);
216
        //Grab the body from the image
217 1
        $body = substr($hex, 108);
218
        //Calculate if padding at the end-line is needed
219
        //Divided by two to keep overview.
220
        //1 byte = 2 HEX-chars
221 1
        $bodySize = (strlen($body) / 2);
222 1
        $headerSize = ($width * $height);
223
        //Use end-line padding? Only when needed
224 1
        $usePadding = ($bodySize > ($headerSize * 3) + 4);
225
        //Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption
226
        //Calculate the next DWORD-position in the body
227 1
        for ($iCount = 0; $iCount < $bodySize; $iCount += 3) {
228
            //Calculate line-ending and padding
229 1
            if ($xPos >= $width) {
230
                //If padding needed, ignore image-padding
231
                //Shift i to the ending of the current 32-bit-block
232 1
                if ($usePadding) {
233 1
                    $iCount += $width % 4;
234 1
                }
235
                //Reset horizontal position
236 1
                $xPos = 0;
237
                //Raise the height-position (bottom-up)
238 1
                $yPos++;
239
                //Reached the image-height? Break the for-loop
240 1
                if ($yPos > $height) {
241 1
                    break;
242
                }
243 1
            }
244
            //Calculation of the RGB-pixel (defined as BGR in image-data)
245
            //Define $i_pos as absolute position in the body
246 1
            $iPos = $iCount * 2;
247 1
            $red = hexdec($body[$iPos + 4] . $body[$iPos + 5]);
248 1
            $green = hexdec($body[$iPos + 2] . $body[$iPos + 3]);
249 1
            $blue = hexdec($body[$iPos] . $body[$iPos + 1]);
250
            //Calculate and draw the pixel
251 1
            $color = imagecolorallocate($image, $red, $green, $blue);
252 1
            imagesetpixel($image, $xPos, $height-$yPos, $color);
253
            //Raise the horizontal position
254 1
            $xPos++;
255 1
        }
256
        //Unset the body / free the memory
257 1
        unset($body);
258
        //Return GD image-object
259 1
        $this->img = $image;
260 1
        return $image;
261
    }
262
    
263
    /**
264
     * resizeImage
265
     * Resize an image
266
     * NOTE: the width is always set to the multiple of 8 more 
267
     * next, why? printers have a resolution of 8 dots per mm
268
     * 
269
     * @param float $width
270
     * @param float $height
271
     * @throws InvalidArgumentException
272
     */
273 1
    public function resizeImage($width = null, $height = null)
274
    {
275 1
        if ($width == null && $height == null) {
276
            throw new InvalidArgumentException("No dimensions was passed.");
277
        }
278 1
        if ($width != null) {
279 1
            $width = $this->closestMultiple($width);
280 1
            $razao = $width / $this->imgWidth;
281 1
            $height = (int) round($razao * $this->imgHeight);
282 1
        } elseif ($width == null && $height != null) {
283
            $razao = $height / $this->imgHeight;
284
            $width = (int) round($razao * $this->imgWidth);
285
            $width = $this->closestMultiple($width);
286
        }
287 1
        $tempimg = imagecreatetruecolor($width, $height);
288 1
        imagecopyresampled($tempimg, $this->img, 0, 0, 0, 0, $width, $height, $this->imgWidth, $this->imgHeight);
289 1
        $this->img = $tempimg;
290 1
        $this->getDimImage();
291 1
    }
292
    
293
    /**
294
     * Creates a  GD QRCode image
295
     * 
296
     * @param string $dataText
297
     * @param int $width
298
     * @param int $padding
299
     * @param string $errCorretion
300
     */
301 1
    public function imageQRCode(
302
        $dataText = 'NADA NADA NADA NADA NADA NADA NADA NADA NADA NADA NADA NADA',
303
        $width = 200,
304
        $padding = 10,
305
        $errCorretion = 'medium'
306
    ) {
307
        //adjust width for a closest multiple of 8
308 1
        $width = $this->closestMultiple($width, 8);
309 1
        $qrCode = new QrCode();
310 1
        $qrCode->setText($dataText)
311 1
            ->setImageType('png')
312 1
            ->setSize($width)
313 1
            ->setPadding($padding)
314 1
            ->setErrorCorrection($errCorretion)
315 1
            ->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0))
316 1
            ->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0))
317 1
            ->setLabel('')
318 1
            ->setLabelFontSize(8);
319 1
        $this->img = $qrCode->getImage();
320 1
        $this->getDimImage();
321 1
    }
322
    
323
    /**
324
     * Convert image from GD resource 
325
     * into Black and White pixels image
326
     * 
327
     * @return string Representation of bytes image in BW 
328
     */
329 1
    protected function convertPixelBW()
330
    {
331
        // Make a string of 1's and 0's 
332 1
        $this->imgData = str_repeat("\0", $this->imgHeight * $this->imgWidth);
333 1
        for ($yInd = 0; $yInd < $this->imgHeight; $yInd++) {
334 1
            for ($xInd = 0; $xInd < $this->imgWidth; $xInd++) {
335
                //get colors from byte image
336 1
                $cols = imagecolorsforindex($this->img, imagecolorat($this->img, $xInd, $yInd));
337
                //convert to greyness color 1 for white, 0 for black
338 1
                $greyness = (int)(($cols['red'] + $cols['green'] + $cols['blue']) / 3) >> 7;
339
                //switch to Black and white
340
                //1 for black, 0 for white, taking into account transparency color
341 1
                $black = (1 - $greyness) >> ($cols['alpha'] >> 6);
342 1
                $this->imgData[$yInd * $this->imgWidth + $xInd] = $black;
343 1
            }
344 1
        }
345 1
        return $this->imgData;
346
    }
347
348
    /**
349
     * Output the image in raster (row) format.
350
     * This can result in padding on the right of the image, 
351
     * if its width is not divisible by 8.
352
     * 
353
     * @throws RuntimeException Where the generated data is unsuitable for the printer (indicates a bug or oversized image).
354
     * @return string The image in raster format.
355
     */
356 1
    protected function convertRaster()
357
    {
358 1
        if (! is_null($this->imgRasterData)) {
359
             return $this->imgRasterData;
360
        }
361 1
        if (is_null($this->imgData)) {
362
            $this->convertPixelBW();
363
        }
364
        //get width in Pixels
365 1
        $widthPixels = $this->getWidth();
366
        //get heightin in Pixels
367 1
        $heightPixels = $this->getHeight();
368
        //get width in Bytes
369 1
        $widthBytes = $this->getWidthBytes();
370
        //initialize vars
371 1
        $xCount = $yCount = $bit = $byte = $byteVal = 0;
372
        //create a string for converted bytes
373 1
        $data = str_repeat("\0", $widthBytes * $heightPixels);
374 1
        if (strlen($data) == 0) {
375
            return $data;
376
        }
377
        /* Loop through and convert format */
378
        do {
379 1
            $byteVal |= (int) $this->imgData[$yCount * $widthPixels + $xCount] << (7 - $bit);
380 1
            $xCount++;
381 1
            $bit++;
382 1
            if ($xCount >= $widthPixels) {
383 1
                $xCount = 0;
384 1
                $yCount++;
385 1
                $bit = 8;
386 1
                if($yCount >= $heightPixels) {
387 1
                    $data[$byte] = chr($byteVal);
388 1
                    break;
389
                }
390 1
            }
391 1
            if ($bit >= 8) {
392 1
                $data[$byte] = chr($byteVal);
393 1
                $byteVal = 0;
394 1
                $bit = 0;
395 1
                $byte++;
396 1
            }
397 1
        } while (true);
398 1
         if (strlen($data) != ($this->getWidthBytes() * $this->getHeight())) {
399
             throw new RuntimeException("Bug in " . __FUNCTION__ . ", wrong number of bytes.");
400
         }
401 1
         $this->imgRasterData = $data;
402 1
         return $this->imgRasterData;
403
    }
404
    
405
    /**
406
     * Save safety binary image file
407
     * 
408
     * @param string $filename
409
     * @param resource|string|null $data
410
     * @return boolean
411
     * @throws InvalidArgumentException
412
     * @throws RuntimeException
413
     */
414 4
    private function saveImage($filename = null, $data = null)
415
    {
416 4
        if (is_null($filename) || is_null($data)) {
417 1
            return false;
418
        }
419 3
        if (is_resource($data)) {
420
            //use GD to save image to file
421 1
            $result = imagepng($data, $filename);
422 1
            if (!$result) {
423
                throw new InvalidArgumentException("Fail to write in $filename.");
424
            }
425 1
            return true;
426
        }
427 2
        $handle = @fopen($filename, 'w');
428 2
        if (!is_resource($handle)) {
429 1
            throw new InvalidArgumentException("Cant open file $filename. Check permissions.");
430
        }
431 1
        $nbytes = fwrite($handle, $data);
432 1
        fclose($handle);
433 1
        if (!$nbytes) {
434
            throw new RuntimeException("Fail to write in $filename.");
435
        }
436 1
        return true;    
437
    }
438
    
439
    /**
440
     * Converts Litte Endian Bytes do String
441
     * 
442
     * @param int $number
443
     * @param int $minbytes
444
     * @return string
445
     */
446 3
    private static function littleEndian2String($number, $minbytes = 1)
447
    {
448 3
        $intstring = '';
449 3
        while ($number > 0) {
450 3
            $intstring = $intstring.chr($number & 255);
451 3
            $number >>= 8;
452 3
        }
453 3
        return str_pad($intstring, $minbytes, "\x00", STR_PAD_RIGHT);
454
    }
455
    
456
    /**
457
     * Get pixel colors
458
     * 
459
     * @param resource $img
460
     * @param int $x
461
     * @param int $y
462
     * @return array
463
     */
464 3
    private static function getPixelColor($img, $x, $y)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $x. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $y. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
465
    {
466 3
        return imageColorsForIndex($img, imageColorAt($img, $x, $y));
467
    }
468
    
469
    /**
470
     * Ajusta o numero para o multiplo mais proximo de base
471
     * 
472
     * @param float $num
473
     * @param int $num
474
     * @return int
475
     */
476 2
    private function closestMultiple($num = 0, $base = 8)
477
    {
478 2
        $iNum = ceil($num);
479 2
        if (($iNum % $base) === 0) {
480 1
            return $iNum;
481
        }
482 1
        return round($num/$base) * $base;
483
    }    
484
}
485