Completed
Branch master (fd4537)
by Roberto
06:49 queued 03:43
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
     */
42 10
    public function __construct($filename = null, $width = null, $height = null)
43
    {
44
        // Include option to Imagick
45 10
        if (! $this->isGdSupported()) {
46
            throw new Exception("GD module not found.");
47
        }
48 10
        $this->imgHeight = 0;
49 10
        $this->imgWidth = 0;
50 10
        $this->imgData = null;
51 10
        $this->imgRasterData = null;
52
        // Load the image, if the patch was passed
53 10
        if (! is_null($filename)) {
54 8
            $this->load($filename, $width, $height);
55 6
        }
56 8
    }
57
    
58
    /**
59
     * Return a string of bytes
60
     * 
61
     * @return string
62
     */
63 1
    public function getRasterImage()
64
    {
65 1
        $this->resizeImage($this->imgWidth);
66 1
        $this->convertPixelBW();
67 1
        $this->convertRaster();
68 1
        return $this->imgRasterData;
69
    }
70
71
    /**
72
     * load
73
     * Load image file and adjust dimentions
74
     * @param string $filename path to image file
75
     * @param float $width 
76
     * @param float $height
77
     * @throws InvalidArgumentException
78
     * @throws RuntimeException
79
     */ 
80 8
    public function load($filename, $width = null, $height = null)
81
    {
82 8
        if (! is_file($filename)) {
83 1
            throw new InvalidArgumentException("Image file not found.");
84
        }
85 7
        if (! is_readable($filename)) {
86
            throw new RuntimeException("The file can not be read due to lack of permissions.");
87
        }
88
        //identify type of image and load with GD
89 7
        $tipo = $this->identifyImg($filename);
90 7
        if ($tipo == 'BMP') {
91 1
            $img = $this->loadBMP($filename);
92 1
            if ($img === false) {
93
                throw new InvalidArgumentException("Image file is not a BMP");
94
            }
95 1
            $this->img = $img; 
96 1
        } else {
97 6
            $func = 'imagecreatefrom' . strtolower($tipo);
98 6
            if (! function_exists($func)) {
99 1
                throw new RuntimeException("It is not possible to use or handle this type of image with GD");
100
            }
101 5
            $this->img = $func($filename);
102
        }
103 6
        if (! $this->img) {
104
            throw new RuntimeException("Failed to load image '$filename'.");
105
        }
106
        //get image dimentions
107 6
        $this->getDimImage();
108 6
        if ($width != null || $height != null) {
109
            $this->resizeImage($width, $height);
110
        }
111 6
    }
112
    
113
    /**
114
     * Save image to PNG file 
115
     * @param string $filename
116
     */
117 1
    public function save($filename = null)
118
    {
119 1
        $this->saveImage($filename, $this->img);
0 ignored issues
show
Documentation introduced by
$this->img is of type resource, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
120 1
    }
121
    
122
    /**
123
     * Convert a GD image into a BMP string representation
124
     * @param string $filename path to image BMP file
125
     * @return string
126
     */
127 3
    public function convert2BMP($filename = null)
128
    {
129 3
        if (! is_resource($this->img)) {
130
            return '';
131
        }
132
        //to remove alpha color and put white instead
133 3
        $img = $this->img;
134 3
        $imageX = imagesx($img);
135 3
        $imageY = imagesy($img);
136 3
        $bmp = '';
137 3
        for ($yInd = ($imageY - 1); $yInd >= 0; $yInd--) {
138 3
            $thisline = '';
139 3
            for ($xInd = 0; $xInd < $imageX; $xInd++) {
140 3
                $argb = self::getPixelColor($img, $xInd, $yInd);
141
                //change transparency to white color
142 3
                if ($argb['alpha'] == 0 && $argb['blue'] == 0 && $argb['green'] == 0 && $argb['red'] == 0) {
143
                    $thisline .= chr(255).chr(255).chr(255);
144
                } else {
145 3
                    $thisline .= chr($argb['blue']).chr($argb['green']).chr($argb['red']);
146
                }    
147 3
            }
148 3
            while (strlen($thisline) % 4) {
149 3
                $thisline .= "\x00";
150 3
            }
151 3
            $bmp .= $thisline;
152 3
        }
153 3
        $bmpSize = strlen($bmp) + 14 + 40;
154
        // bitMapHeader [14 bytes] - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_62uq.asp
155 3
        $bitMapHeader  = 'BM'; // WORD bfType;
156 3
        $bitMapHeader .= self::littleEndian2String($bmpSize, 4); // DWORD bfSize;
157 3
        $bitMapHeader .= self::littleEndian2String(0, 2); // WORD bfReserved1;
158 3
        $bitMapHeader .= self::littleEndian2String(0, 2); // WORD bfReserved2;
159 3
        $bitMapHeader .= self::littleEndian2String(54, 4); // DWORD bfOffBits;
160
        // bitMapInfoHeader - [40 bytes] http://msdn.microsoft.com/library/en-us/gdi/bitmaps_1rw2.asp
161 3
        $bitMapInfoHeader  = self::littleEndian2String(40, 4); // DWORD biSize;
162 3
        $bitMapInfoHeader .= self::littleEndian2String($imageX, 4); // LONG biWidth;
163 3
        $bitMapInfoHeader .= self::littleEndian2String($imageY, 4); // LONG biHeight;
164 3
        $bitMapInfoHeader .= self::littleEndian2String(1, 2); // WORD biPlanes;
165 3
        $bitMapInfoHeader .= self::littleEndian2String(24, 2); // WORD biBitCount;
166 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biCompression;
167 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biSizeImage;
168 3
        $bitMapInfoHeader .= self::littleEndian2String(2835, 4); // LONG biXPelsPerMeter;
169 3
        $bitMapInfoHeader .= self::littleEndian2String(2835, 4); // LONG biYPelsPerMeter;
170 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biClrUsed;
171 3
        $bitMapInfoHeader .= self::littleEndian2String(0, 4); // DWORD biClrImportant;
172 3
        $data = $bitMapHeader.$bitMapInfoHeader.$bmp;
173 3
        $this->saveImage($filename, $data);
174 2
        return $data;
175
    }
176
    
177
    /**
178
     * loadBMP
179
     * Create a GD image from BMP file
180
     * @param string $filename
181
     * @return GD object
182
     */
183 1
    public function loadBMP($filename)
184
    {
185
        //Load the image into a string
186 1
        $file = fopen($filename, "rb");
187 1
        $read = fread($file, 10);
188
        //continue at the end of file
189 1
        while (! feof($file) && ($read <> "")) {
190 1
            $read .= fread($file, 1024);
191 1
        }
192 1
        fclose($file);
193 1
        $temp = unpack("H*", $read);
194 1
        $hex = $temp[1];
195 1
        $header = substr($hex, 0, 108);
196
        //Process the header
197
        //Structure: http://www.fastgraph.com/help/bmp_header_format.html
198 1
        if (substr($header, 0, 4) != "424d") {
199
            //is not a BMP file
200
            return false;
201
        }
202
        //Cut it in parts of 2 bytes
203 1
        $headerParts = str_split($header, 2);
204
        //Get the width 4 bytes
205 1
        $width = hexdec($headerParts[19].$headerParts[18]);
206
        //Get the height 4 bytes
207 1
        $height = hexdec($headerParts[23].$headerParts[22]);
208
        // Unset the header params
209 1
        unset($headerParts);
210
        // Define starting X and Y
211 1
        $xPos = 0;
212 1
        $yPos = 1;
213
        //Create new gd image
214 1
        $image = imagecreatetruecolor($width, $height);
215
        //Grab the body from the image
216 1
        $body = substr($hex, 108);
217
        //Calculate if padding at the end-line is needed
218
        //Divided by two to keep overview.
219
        //1 byte = 2 HEX-chars
220 1
        $bodySize = (strlen($body) / 2);
221 1
        $headerSize = ($width * $height);
222
        //Use end-line padding? Only when needed
223 1
        $usePadding = ($bodySize > ($headerSize * 3) + 4);
224
        //Using a for-loop with index-calculation instaid of str_split to avoid large memory consumption
225
        //Calculate the next DWORD-position in the body
226 1
        for ($iCount = 0; $iCount < $bodySize; $iCount += 3) {
227
            //Calculate line-ending and padding
228 1
            if ($xPos >= $width) {
229
                //If padding needed, ignore image-padding
230
                //Shift i to the ending of the current 32-bit-block
231 1
                if ($usePadding) {
232 1
                    $iCount += $width % 4;
233 1
                }
234
                //Reset horizontal position
235 1
                $xPos = 0;
236
                //Raise the height-position (bottom-up)
237 1
                $yPos++;
238
                //Reached the image-height? Break the for-loop
239 1
                if ($yPos > $height) {
240 1
                    break;
241
                }
242 1
            }
243
            //Calculation of the RGB-pixel (defined as BGR in image-data)
244
            //Define $i_pos as absolute position in the body
245 1
            $iPos = $iCount * 2;
246 1
            $red = hexdec($body[$iPos + 4] . $body[$iPos + 5]);
247 1
            $green = hexdec($body[$iPos + 2] . $body[$iPos + 3]);
248 1
            $blue = hexdec($body[$iPos] . $body[$iPos + 1]);
249
            //Calculate and draw the pixel
250 1
            $color = imagecolorallocate($image, $red, $green, $blue);
251 1
            imagesetpixel($image, $xPos, $height-$yPos, $color);
252
            //Raise the horizontal position
253 1
            $xPos++;
254 1
        }
255
        //Unset the body / free the memory
256 1
        unset($body);
257
        //Return GD image-object
258 1
        $this->img = $image;
259 1
        return $image;
260
    }
261
    
262
    /**
263
     * resizeImage
264
     * Resize an image
265
     * NOTE: the width is always set to the multiple of 8 more 
266
     * next, why? printers have a resolution of 8 dots per mm
267
     * 
268
     * @param float $width
269
     * @param float $height
270
     * @throws InvalidArgumentException
271
     */
272 1
    public function resizeImage($width = null, $height = null)
273
    {
274 1
        if ($width == null && $height == null) {
275
            throw new InvalidArgumentException("No dimensions was passed.");
276
        }
277 1
        if ($width != null) {
278 1
            $width = $this->closestMultiple($width);
279 1
            $razao = $width / $this->imgWidth;
280 1
            $height = (int) round($razao * $this->imgHeight);
281 1
        } elseif ($width == null && $height != null) {
282
            $razao = $height / $this->imgHeight;
283
            $width = (int) round($razao * $this->imgWidth);
284
            $width = $this->closestMultiple($width);
285
        }
286 1
        $tempimg = imagecreatetruecolor($width, $height);
287 1
        imagecopyresampled($tempimg, $this->img, 0, 0, 0, 0, $width, $height, $this->imgWidth, $this->imgHeight);
288 1
        $this->img = $tempimg;
289 1
        $this->getDimImage();
290 1
    }
291
    
292
    /**
293
     * Creates a  GD QRCode image
294
     * 
295
     * @param string $dataText
296
     * @param int $width
297
     * @param int $padding
298
     * @param string $errCorretion
299
     */
300 1
    public function imageQRCode(
301
        $dataText = 'NADA NADA NADA NADA NADA NADA NADA NADA NADA NADA NADA NADA',
302
        $width = 200,
303
        $padding = 10,
304
        $errCorretion = 'medium'
305
    ) {
306
        //adjust width for a closest multiple of 8
307 1
        $width = $this->closestMultiple($width, 8);
308 1
        $qrCode = new QrCode();
309 1
        $qrCode->setText($dataText)
310 1
            ->setImageType('png')
311 1
            ->setSize($width)
312 1
            ->setPadding($padding)
313 1
            ->setErrorCorrection($errCorretion)
314 1
            ->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0))
315 1
            ->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0))
316 1
            ->setLabel('')
317 1
            ->setLabelFontSize(8);
318 1
        $this->img = $qrCode->getImage();
319 1
        $this->getDimImage();
320 1
    }
321
    
322
    /**
323
     * Convert image from GD resource 
324
     * into Black and White pixels image
325
     * 
326
     * @return string Representation of bytes image in BW 
327
     */
328 1
    protected function convertPixelBW()
329
    {
330
        // Make a string of 1's and 0's 
331 1
        $this->imgData = str_repeat("\0", $this->imgHeight * $this->imgWidth);
332 1
        for ($y = 0; $y < $this->imgHeight; $y++) {
333 1
            for ($x = 0; $x < $this->imgWidth; $x++) {
334
                //get colors from byte image
335 1
                $cols = imagecolorsforindex($this->img, imagecolorat($this->img, $x, $y));
336
                //convert to greyness color
337 1
                $greyness = (int)(($cols['red'] + $cols['green'] + $cols['blue']) / 3) >> 7; // 1 for white, 0 for black
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
338
                //switch to Black and white
339
                //1 for black, 0 for white, taking into account transparency color
340 1
                $black = (1 - $greyness) >> ($cols['alpha'] >> 6);
341 1
                $this->imgData[$y * $this->imgWidth + $x] = $black;
342 1
            }
343 1
        }
344 1
        return $this->imgData;
345
    }
346
347
    /**
348
     * Output the image in raster (row) format.
349
     * This can result in padding on the right of the image, 
350
     * if its width is not divisible by 8.
351
     * 
352
     * @throws RuntimeException Where the generated data is unsuitable for the printer (indicates a bug or oversized image).
353
     * @return string The image in raster format.
354
     */
355 1
    protected function convertRaster()
356
    {
357 1
        if (! is_null($this->imgRasterData)) {
358
             return $this->imgRasterData;
359
        }
360 1
        if (is_null($this->imgData)) {
361
            $this->convertPixelBW();
362
        }
363
        //get width in Pixels
364 1
        $widthPixels = $this->getWidth();
365
        //get heightin in Pixels
366 1
        $heightPixels = $this->getHeight();
367
        //get width in Bytes
368 1
        $widthBytes = $this->getWidthBytes();
369
        //initialize vars
370 1
        $xCount = $yCount = $bit = $byte = $byteVal = 0;
371
        //create a string for converted bytes
372 1
        $data = str_repeat("\0", $widthBytes * $heightPixels);
373 1
        if (strlen($data) == 0) {
374
            return $data;
375
        }
376
        /* Loop through and convert format */
377
        do {
378 1
            $byteVal |= (int) $this->imgData[$yCount * $widthPixels + $xCount] << (7 - $bit);
379 1
            $xCount++;
380 1
            $bit++;
381 1
            if ($xCount >= $widthPixels) {
382 1
                $xCount = 0;
383 1
                $yCount++;
384 1
                $bit = 8;
385 1
                if($yCount >= $heightPixels) {
386 1
                    $data[$byte] = chr($byteVal);
387 1
                    break;
388
                }
389 1
            }
390 1
            if ($bit >= 8) {
391 1
                $data[$byte] = chr($byteVal);
392 1
                $byteVal = 0;
393 1
                $bit = 0;
394 1
                $byte++;
395 1
            }
396 1
        } while (true);
397 1
         if (strlen($data) != ($this->getWidthBytes() * $this->getHeight())) {
398
             throw new RuntimeException("Bug in " . __FUNCTION__ . ", wrong number of bytes.");
399
         }
400 1
         $this->imgRasterData = $data;
401 1
         return $this->imgRasterData;
402
    }
403
    
404
    /**
405
     * Save safety binary image file
406
     * 
407
     * @param string $filename
408
     * @param string $data
409
     * @return boolean
410
     * @throws InvalidArgumentException
411
     * @throws RuntimeException
412
     */
413 4
    private function saveImage($filename = null, $data = null)
414
    {
415 4
        if (is_null($filename) || is_null($data)) {
416 1
            return false;
417
        }
418 3
        if (is_resource($data)) {
419
            //use GD to save image to file
420 1
            $result = imagepng($data, $filename);
421 1
            if (!$result) {
422
                throw new InvalidArgumentException("Fail to write in $filename.");
423
            }
424 1
            return true;
425
        }
426 2
        $handle = @fopen($filename, 'w');
427 2
        if (!is_resource($handle)) {
428 1
            throw new InvalidArgumentException("Cant open file $filename. Check permissions.");
429
        }
430 1
        $nbytes = fwrite($handle, $data);
431 1
        fclose($handle);
432 1
        if (!$nbytes) {
433
            throw new RuntimeException("Fail to write in $filename.");
434
        }
435 1
        return true;    
436
    }
437
    
438
    /**
439
     * Converts Litte Endian Bytes do String
440
     * 
441
     * @param int $number
442
     * @param int $minbytes
443
     * @return string
444
     */
445 3
    private static function littleEndian2String($number, $minbytes=1)
446
    {
447 3
        $intstring = '';
448 3
        while ($number > 0) {
449 3
            $intstring = $intstring.chr($number & 255);
450 3
            $number >>= 8;
451 3
        }
452 3
        return str_pad($intstring, $minbytes, "\x00", STR_PAD_RIGHT);
453
    }
454
    
455
    /**
456
     * Get pixel colors
457
     * 
458
     * @param resource $img
459
     * @param int $x
460
     * @param int $y
461
     * @return array
462
     */
463 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...
464
    {
465 3
        return imageColorsForIndex($img, imageColorAt($img, $x, $y));
466
    }
467
    
468
    /**
469
     * Ajusta o numero para o multiplo mais proximo de base
470
     * 
471
     * @param float $num
472
     * @param int $num
473
     * @return int
474
     */
475 2
    private function closestMultiple($num = 0, $base = 8)
476
    {
477 2
        $iNum = ceil($num);
478 2
        if (($iNum % $base) === 0) {
479 1
            return $iNum;
480
        }
481 1
        return round($num/$base) * $base;
482
    }    
483
}
484