SugarSpriteBuilder::loadImage()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20
Metric Value
cc 4
eloc 11
nc 4
nop 3
dl 0
loc 13
ccs 0
cts 13
cp 0
crap 20
rs 9.2
1
<?php
2
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3
/*********************************************************************************
4
 * SugarCRM Community Edition is a customer relationship management program developed by
5
 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
6
7
 * SuiteCRM is an extension to SugarCRM Community Edition developed by Salesagility Ltd.
8
 * Copyright (C) 2011 - 2014 Salesagility Ltd.
9
 *
10
 * This program is free software; you can redistribute it and/or modify it under
11
 * the terms of the GNU Affero General Public License version 3 as published by the
12
 * Free Software Foundation with the addition of the following permission added
13
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
14
 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
15
 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
16
 *
17
 * This program is distributed in the hope that it will be useful, but WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
20
 * details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License along with
23
 * this program; if not, see http://www.gnu.org/licenses or write to the Free
24
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
 * 02110-1301 USA.
26
 *
27
 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
28
 * SW2-130, Cupertino, CA 95014, USA. or at email address [email protected].
29
 *
30
 * The interactive user interfaces in modified source and object code versions
31
 * of this program must display Appropriate Legal Notices, as required under
32
 * Section 5 of the GNU Affero General Public License version 3.
33
 *
34
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
35
 * these Appropriate Legal Notices must retain the display of the "Powered by
36
 * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
37
 * reasonably feasible for  technical reasons, the Appropriate Legal Notices must
38
 * display the words  "Powered by SugarCRM" and "Supercharged by SuiteCRM".
39
 ********************************************************************************/
40
41
42
require_once("include/SugarTheme/cssmin.php");
43
44
class SugarSpriteBuilder
45
{
46
	var $isAvailable = false;
47
	var $silentRun = false;
48
    var $fromSilentUpgrade = false;
49
    var $writeToUpgradeLog = false;
50
51
	var $debug = false;
52
	var $fileName = 'sprites';
53
	var $cssMinify = true;
54
55
	// class supported image types
56
	var $supportedTypeMap = array(
57
		IMG_GIF => IMAGETYPE_GIF,
58
		IMG_JPG => IMAGETYPE_JPEG,
59
		IMG_PNG => IMAGETYPE_PNG,
60
	);
61
62
	// sprite settings
63
	var $pngCompression = 9;
64
	var $pngFilter = PNG_NO_FILTER;
65
	var $maxWidth = 75;
66
	var $maxHeight = 75;
67
	var $rowCnt = 30;
68
69
	// processed image types
70
	var $imageTypes = array();
71
72
	// source files
73
	var $spriteSrc = array();
74
	var $spriteRepeat = array();
75
76
	// sprite resource images
77
	var $spriteImg;
78
79
	// sprite_config collection
80
	var $sprites_config = array();
81
82
83
    public function __construct()
84
    {
85
		// check if we have gd installed
86
		if(function_exists('imagecreatetruecolor'))
87
        {
88
			$this->isAvailable = true;
89
            foreach($this->supportedTypeMap as $gd_bit => $imagetype)
90
            {
91
                if(imagetypes() & $gd_bit) {
92
                    // swap gd_bit & imagetype
93
                    $this->imageTypes[$imagetype] = $gd_bit;
94
                }
95
            }
96
		}
97
98
        if(function_exists('logThis') && isset($GLOBALS['path']))
99
        {
100
            $this->writeToUpgradeLog = true;
101
        }
102
	}
103
104
105
    /**
106
     * addDirectory
107
     *
108
     * This function is used to create the spriteSrc array
109
     * @param $name String value of the sprite name
110
     * @param $dir String value of the directory associated with the sprite entry
111
     */
112
	public function addDirectory($name, $dir) {
113
114
		// sprite namespace
115
		if(!array_key_exists($name, $this->spriteSrc))
116
        {
117
			$this->spriteSrc[$name] = array();
118
		}
119
120
		// add files from directory
121
		$this->spriteSrc[$name][$dir] = $this->getFileList($dir);
122
	}
123
124
	/**
125
     * getFileList
126
     *
127
     * This method processes files in a directory and adds them to the sprites array
128
     * @param $dir String value of the directory to scan for image files in
129
     */
130
	private function getFileList($dir) {
131
		$list = array();
132
		if(is_dir($dir)) {
133
			if($dh = opendir($dir)) {
134
135
				// optional sprites_config.php file
136
				$this->loadSpritesConfig($dir);
137
138
			    while (($file = readdir($dh)) !== false)
139
                {
140
					if ($file != "." && $file != ".." && $file != "sprites_config.php")
141
                    {
142
143
						// file info & check supported image format
144
						if($info = $this->getFileInfo($dir, $file)) {
145
146
							// skip excluded files
147
							if(isset($this->sprites_config[$dir]['exclude']) && array_search($file, $this->sprites_config[$dir]['exclude']) !== false)
148
                            {
149
                                global $mod_strings;
150
                                $msg = string_format($mod_strings['LBL_SPRITES_EXCLUDING_FILE'], array("{$dir}/{$file}"));
151
								$GLOBALS['log']->debug($msg);
152
                                $this->logMessage($msg);
153
							} else {
154
								// repeatable sprite ?
155
								$isRepeat = false;
156
157
								if(isset($this->sprites_config[$dir]['repeat']))
158
                                {
159
									foreach($this->sprites_config[$dir]['repeat'] as $repeat)
160
                                    {
161
										if($info['x'] == $repeat['width'] && $info['y'] == $repeat['height'])
162
                                        {
163
											$id = md5($repeat['width'].$repeat['height'].$repeat['direction']);
164
											$isRepeat = true;
165
											$this->spriteRepeat['repeat_'.$repeat['direction'].'_'.$id][$dir][$file] = $info;
166
										}
167
									}
168
								}
169
170
								if(!$isRepeat)
171
                                {
172
									$list[$file] = $info;
173
                                }
174
							}
175
						} else if(preg_match('/\.(jpg|jpeg|gif|png|bmp|ico)$/i', $file)) {
176
                            $GLOBALS['log']->error('Unable to process image file ' . $file);
177
                            //$this->logMessage('Unable to process image file ' . $file);
178
                        }
179
	   	     		}
180
   		 		}
181
			}
182
		    closedir($dh);
183
		}
184
		return $list;
185
	}
186
187
188
    /**
189
     * loadSpritesConfig
190
     *
191
     * This function is used to load the sprites_config.php file.  The sprites_config.php file may be used to add entries
192
     * to the sprites_config member variable which may contain a list of array entries of files/directories to exclude from
193
     * being included into the sprites image.
194
     *
195
     * @param $dir String value of the directory containing the custom sprites_config.php file
196
     */
197
	private function loadSpritesConfig($dir) {
198
		$sprites_config = array();
199
		if(file_exists("$dir/sprites_config.php"))
200
        {
201
			include("$dir/sprites_config.php");
202
			if(count($sprites_config)) {
203
				$this->sprites_config = array_merge($this->sprites_config, $sprites_config);
204
			}
205
		}
206
	}
207
208
209
	/**
210
     * getFileInfo
211
     *
212
     * This is a private helper function to return attributes about an image.  If the width, height or type of the
213
     * image file cannot be determined, then we do not process the file.
214
     *
215
     * @return array of file info entries containing file information (x, y, type) if image type is supported
216
     */
217
	private function getFileInfo($dir, $file) {
218
		$result = false;
219
		$info = @getimagesize($dir.'/'.$file);
220
		if($info) {
221
222
			// supported image type ?
223
			if(isset($this->imageTypes[$info[2]]))
224
            {
225
				$w = $info[0];
226
				$h = $info[1];
227
				$surface = $w * $h;
228
229
				// be sure we have an image size
230
				$addSprite = false;
231
				if($surface)
232
                {
233
					// sprite dimensions
234
					if($w <= $this->maxWidth && $h <= $this->maxHeight)
235
                    {
236
						$addSprite = true;
237
					}
238
				}
239
240
				if($addSprite)
241
                {
242
					$result = array();
243
					$result['x'] = $w;
244
					$result['y'] = $h;
245
					$result['type'] = $info[2];
246
				}
247
			} else {
248
                $msg = "Skipping unsupported image file type ({$info[2]}) for file {$file}";
249
                $GLOBALS['log']->error($msg);
250
                $this->logMessage($msg."\n");
251
            }
252
		}
253
		return $result;
254
	}
255
256
257
    /**
258
     * createSprites
259
     *
260
     * This is the public function to allow the sprites to be built.
261
     *
262
     * @return $result boolean value indicating whether or not sprites were created
0 ignored issues
show
Documentation introduced by
The doc-type $result could not be parsed: Unknown type name "$result" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
263
     */
264
	public function createSprites() {
265
266
        global $mod_strings;
267
268
		if(!$this->isAvailable)
269
        {
270
			if(!$this->silentRun)
271
            {
272
                $msg = $mod_strings['LBL_SPRITES_NOT_SUPPORTED'];
273
                $GLOBALS['log']->warn($msg);
274
                $this->logMessage($msg);
275
            }
276
			return false;
277
		}
278
279
		// add repeatable sprites
280
		if(count($this->spriteRepeat))
281
        {
282
			$this->spriteSrc = array_merge($this->spriteSrc, $this->spriteRepeat);
283
        }
284
285
		foreach($this->spriteSrc as $name => $dirs)
286
        {
287
			if(!$this->silentRun)
288
            {
289
                $msg = string_format($mod_strings['LBL_SPRITES_CREATING_NAMESPACE'], array($name));
290
                $GLOBALS['log']->debug($msg);
291
				$this->logMessage($msg);
292
            }
293
294
			// setup config for sprite placement algorithm
295
			if(substr($name, 0, 6) == 'repeat')
296
            {
297
				$isRepeat = true;
298
                $type = substr($name, 7, 10) == 'horizontal' ? 'horizontal' : 'vertical';
299
				$config = array(
300
					'type' => $type,
301
				);
302
			} else {
303
				$isRepeat = false;
304
				$config = array(
305
					'type' => 'boxed',
306
					'width' => $this->maxWidth,
307
					'height' => $this->maxHeight,
308
					'rowcnt' => $this->rowCnt,
309
				);
310
			}
311
312
			// use separate class to arrange the images
313
			$sp = new SpritePlacement($dirs, $config);
314
			$sp->processSprites();
315
316
			//if(! $this->silentRun)
317
			//	echo " (size {$sp->width()}x{$sp->height()})<br />";
318
319
			// we need a target image size
320
			if($sp->width() && $sp->height())
321
            {
322
				// init sprite image
323
				$this->initSpriteImg($sp->width(), $sp->height());
324
325
				// add sprites based upon determined coordinates
326
				foreach($dirs as $dir => $files)
327
                {
328
					if(!$this->silentRun)
329
                    {
330
                        $msg = string_format($mod_strings['LBL_SPRITES_PROCESSING_DIR'], array($dir));
331
                        $GLOBALS['log']->debug($msg);
332
                        $this->logMessage($msg);
333
                    }
334
335
					foreach($files as $file => $info)
336
                    {
337
						if($im = $this->loadImage($dir, $file, $info['type']))
338
                        {
339
							// coordinates
340
							$dst_x = $sp->spriteMatrix[$dir.'/'.$file]['x'];
341
							$dst_y = $sp->spriteMatrix[$dir.'/'.$file]['y'];
342
343
							imagecopy($this->spriteImg, $im, $dst_x, $dst_y, 0, 0, $info['x'], $info['y']);
344
							imagedestroy($im);
345
346
							if(!$this->silentRun)
347
                            {
348
                                $msg = string_format($mod_strings['LBL_SPRITES_ADDED'], array("{$dir}/{$file}"));
349
                                $GLOBALS['log']->debug($msg);
350
                                $this->logMessage($msg);
351
                            }
352
						}
353
					}
354
				}
355
356
				// dir & filenames
357
				if($isRepeat)
358
                {
359
					$outputDir = sugar_cached("sprites/Repeatable");
360
					$spriteFileName = "{$name}.png";
361
					$cssFileName = "{$this->fileName}.css";
362
					$metaFileName = "{$this->fileName}.meta.php";
363
					$nameSpace = "Repeatable";
364
				} else {
365
					$outputDir = sugar_cached("sprites/$name");
366
					$spriteFileName = "{$this->fileName}.png";
367
					$cssFileName = "{$this->fileName}.css";
368
					$metaFileName = "{$this->fileName}.meta.php";
369
					$nameSpace = "{$name}";
370
				}
371
372
				// directory structure
373
				if(!is_dir(sugar_cached("sprites/$nameSpace")))
374
                {
375
					sugar_mkdir(sugar_cached("sprites/$nameSpace"), 0775, true);
376
                }
377
378
				// save sprite image
379
				imagepng($this->spriteImg, "$outputDir/$spriteFileName", $this->pngCompression, $this->pngFilter);
380
				imagedestroy($this->spriteImg);
381
382
				/* generate css & metadata */
383
384
				$head = '';
385
				$body = '';
386
				$metadata = '';
387
388
				foreach($sp->spriteSrc as $id => $info)
389
                {
390
					// sprite id
391
					$hash_id = md5($id);
392
393
					// header
394
					$head .= "span.spr_{$hash_id},\n";
395
396
					// image size
397
					$w = $info['x'];
398
					$h = $info['y'];
399
400
					// image offset
401
					$offset_x = $sp->spriteMatrix[$id]['x'];
402
					$offset_y = $sp->spriteMatrix[$id]['y'];
403
404
					// sprite css
405
					$body .= "/* {$id} */
406
span.spr_{$hash_id} {
407
width: {$w}px;
408
height: {$h}px;
409
background-position: -{$offset_x}px -{$offset_y}px;
410
}\n";
411
412
					$metadata .= '$sprites["'.$id.'"] = array ("class"=>"'.$hash_id.'","width"=>"'.$w.'","height"=>"'.$h.'");'."\n";
413
				}
414
415
				// common css header
416
                require_once('include/utils.php');
417
                $bg_path = getVersionedPath('index.php').'&entryPoint=getImage&imageName='.$spriteFileName.'&spriteNamespace='.$nameSpace;
418
				$head = rtrim($head, "\n,")." {background: url('../../../{$bg_path}'); no-repeat;display:inline-block;}\n";
419
420
				// append mode for repeatable sprites
421
                $fileMode = $isRepeat ? 'a' : 'w';
422
423
				// save css
424
				$css_content = "\n/* autogenerated sprites - $name */\n".$head.$body;
425
				if($this->cssMinify)
426
                {
427
					$css_content = cssmin::minify($css_content);
428
                }
429
				$fh = fopen("$outputDir/$cssFileName", $fileMode);
430
				fwrite($fh, $css_content);
431
				fclose($fh);
432
433
				/* save metadata */
434
				$add_php_tag = (file_exists("$outputDir/$metaFileName") && $isRepeat) ? false : true;
435
				$fh = fopen("$outputDir/$metaFileName", $fileMode);
436
				if($add_php_tag)
437
                {
438
					fwrite($fh, '<?php');
439
                }
440
				fwrite($fh, "\n/* sprites metadata - $name */\n");
441
				fwrite($fh, $metadata."\n");
442
				fclose($fh);
443
444
			// if width & height
445
			} else {
446
447
				if(!$this->silentRun)
448
                {
449
                    $msg = string_format($mod_strings['LBL_SPRITES_ADDED'], array($name));
450
                    $GLOBALS['log']->debug($msg);
451
                    $this->logMessage($msg);
452
                }
453
454
			}
455
456
		}
457
		return true;
458
	}
459
460
461
	/**
462
     * initSpriteImg
463
     *
464
     * @param w int value representing width of sprite
465
     * @param h int value representing height of sprite
466
     * Private function to initialize creating the sprite canvas image
467
     */
468
	private function initSpriteImg($w, $h) {
469
		$this->spriteImg = imagecreatetruecolor($w,$h);
470
		$transparent = imagecolorallocatealpha($this->spriteImg, 0, 0, 0, 127);
471
		imagefill($this->spriteImg, 0, 0, $transparent);
472
		imagealphablending($this->spriteImg, false);
473
		imagesavealpha($this->spriteImg, true);
474
	}
475
476
477
	/**
478
     * loadImage
479
     *
480
     * private function to load image resources
481
     *
482
     * @param $dir String value of directory where image is located
483
     * @param $file String value of file
484
     * @param $type String value of the file type (IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)
485
     *
486
     */
487
	private function loadImage($dir, $file, $type) {
488
		$path_file = $dir.'/'.$file;
489
		switch($type) {
490
			case IMAGETYPE_GIF:
491
				return imagecreatefromgif($path_file);
492
			case IMAGETYPE_JPEG:
493
				return imagecreatefromjpeg($path_file);
494
			case IMAGETYPE_PNG:
495
				return imagecreatefrompng($path_file);
496
			default:
497
				return false;
498
		}
499
	}
500
501
    /**
502
     * private logMessage
503
     *
504
     * This is a private function used to log messages generated from this class.  Depending on whether or not
505
     * silentRun or fromSilentUpgrade is set to true/false then it will either output to screen or write to log file
506
     *
507
     * @param $msg String value of message to log into file or echo into output buffer depending on the context
508
     */
509
    private function logMessage($msg)
510
    {
511
        if(!$this->silentRun && !$this->fromSilentUpgrade)
512
        {
513
            echo $msg . '<br />';
514
        } else if ($this->fromSilentUpgrade && $this->writeToUpgradeLog) {
515
            logThis($msg, $GLOBALS['path']);
516
        } else if(!$this->silentRun) {
517
            echo $msg . "\n";
518
        }
519
    }
520
}
521
522
523
/**
524
 * SpritePlacement
525
 *
526
 */
527
class SpritePlacement
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
528
{
529
530
	// occupied space
531
	var $spriteMatrix = array();
532
533
	// minimum surface
534
	var $minSurface = 0;
535
536
	// sprite src (flattened array)
537
	var $spriteSrc = array();
538
539
	// placement config array
540
	/*
541
		type = 	boxed
542
				horizontal
543
				vertical
544
545
		required params for
546
		type 1 	-> width
547
				-> height
548
				-> rowcnt
549
550
	*/
551
	var $config = array();
552
553
	function __construct($spriteSrc, $config) {
554
555
		// convert spriteSrc to flat array
556
		foreach($spriteSrc as $dir => $files) {
557
			foreach($files as $file => $info) {
558
				// use full path as identifier
559
				$full_path = $dir.'/'.$file;
560
				$this->spriteSrc[$full_path] = $info;
561
			}
562
		}
563
564
		$this->config = $config;
565
	}
566
567
	function processSprites() {
568
569
		foreach($this->spriteSrc as $id => $info) {
570
571
			// dimensions
572
			$x = $info['x'];
573
			$y = $info['y'];
574
575
			// update min surface
576
			$this->minSurface += $x * $y;
577
578
			// get coordinates where to add this sprite
579
			if($coor = $this->addSprite($x, $y)) {
580
				$this->spriteMatrix[$id] = $coor;
581
			}
582
		}
583
	}
584
585
	// returns x/y coordinates to fit the sprite
586
	function addSprite($w, $h) {
587
		$result = false;
588
589
		switch($this->config['type']) {
590
591
			// boxed
592
			case 'boxed':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
593
594
				$spriteX = $this->config['width'];
595
				$spriteY = $this->config['height'];
596
				$spriteCnt = count($this->spriteMatrix) + 1;
597
				$y = ceil($spriteCnt / $this->config['rowcnt']);
598
				$x = $spriteCnt - (($y - 1) * $this->config['rowcnt']);
599
				$result = array(
600
					'x' => ($x * $spriteX) + 1 - $spriteX,
601
					'y' => ($y * $spriteY) + 1 - $spriteY);
602
603
				break;
604
605
			// horizontal -> align vertically
606
			case 'horizontal':
607
				$result = array('x' => 1, 'y' => $this->height() + 1);
608
				break;
609
610
			// vertical -> align horizontally
611
			case 'vertical':
612
				$result = array('x' => $this->width() + 1, 'y' => 1);
613
				break;
614
615
			default:
616
				$GLOBALS['log']->warn(__CLASS__.": Unknown sprite placement algorithm -> {$this->config['type']}");
617
				break;
618
		}
619
620
		return $result;
621
	}
622
623
	// calculate total width
624
	function width() {
625
		return $this->getMaxAxis('x');
626
	}
627
628
	// calculate total height
629
	function height() {
630
		return $this->getMaxAxis('y');
631
	}
632
633
	// helper function to get highest axis value
634
	function getMaxAxis($axis) {
635
		$val = 0;
636
		foreach($this->spriteMatrix as $id => $coor) {
637
			$new_val = $coor[$axis] + $this->spriteSrc[$id][$axis] - 1;
638
			if($new_val > $val) {
639
				$val = $new_val;
640
			}
641
		}
642
		return $val;
643
	}
644
}
645
646
?>
647