PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/modules/Administration/SugarSpriteBuilder.php

https://bitbucket.org/cviolette/sugarcrm
PHP | 643 lines | 379 code | 96 blank | 168 comment | 68 complexity | 95bb50747b7f6903959bb58404c4a586 MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  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-2012 SugarCRM Inc.
  6. *
  7. * This program is free software; you can redistribute it and/or modify it under
  8. * the terms of the GNU Affero General Public License version 3 as published by the
  9. * Free Software Foundation with the addition of the following permission added
  10. * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  11. * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
  12. * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  13. *
  14. * This program is distributed in the hope that it will be useful, but WITHOUT
  15. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  16. * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  17. * details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License along with
  20. * this program; if not, see http://www.gnu.org/licenses or write to the Free
  21. * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  22. * 02110-1301 USA.
  23. *
  24. * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
  25. * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
  26. *
  27. * The interactive user interfaces in modified source and object code versions
  28. * of this program must display Appropriate Legal Notices, as required under
  29. * Section 5 of the GNU Affero General Public License version 3.
  30. *
  31. * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  32. * these Appropriate Legal Notices must retain the display of the "Powered by
  33. * SugarCRM" logo. If the display of the logo is not reasonably feasible for
  34. * technical reasons, the Appropriate Legal Notices must display the words
  35. * "Powered by SugarCRM".
  36. ********************************************************************************/
  37. require_once("include/SugarTheme/cssmin.php");
  38. class SugarSpriteBuilder
  39. {
  40. var $isAvailable = false;
  41. var $silentRun = false;
  42. var $fromSilentUpgrade = false;
  43. var $writeToUpgradeLog = false;
  44. var $debug = false;
  45. var $fileName = 'sprites';
  46. var $cssMinify = true;
  47. // class supported image types
  48. var $supportedTypeMap = array(
  49. IMG_GIF => IMAGETYPE_GIF,
  50. IMG_JPG => IMAGETYPE_JPEG,
  51. IMG_PNG => IMAGETYPE_PNG,
  52. );
  53. // sprite settings
  54. var $pngCompression = 9;
  55. var $pngFilter = PNG_NO_FILTER;
  56. var $maxWidth = 75;
  57. var $maxHeight = 75;
  58. var $rowCnt = 30;
  59. // processed image types
  60. var $imageTypes = array();
  61. // source files
  62. var $spriteSrc = array();
  63. var $spriteRepeat = array();
  64. // sprite resource images
  65. var $spriteImg;
  66. // sprite_config collection
  67. var $sprites_config = array();
  68. public function __construct()
  69. {
  70. // check if we have gd installed
  71. if(function_exists('imagecreatetruecolor'))
  72. {
  73. $this->isAvailable = true;
  74. foreach($this->supportedTypeMap as $gd_bit => $imagetype)
  75. {
  76. if(imagetypes() & $gd_bit) {
  77. // swap gd_bit & imagetype
  78. $this->imageTypes[$imagetype] = $gd_bit;
  79. }
  80. }
  81. }
  82. if(function_exists('logThis') && isset($GLOBALS['path']))
  83. {
  84. $this->writeToUpgradeLog = true;
  85. }
  86. }
  87. /**
  88. * addDirectory
  89. *
  90. * This function is used to create the spriteSrc array
  91. * @param $name String value of the sprite name
  92. * @param $dir String value of the directory associated with the sprite entry
  93. */
  94. public function addDirectory($name, $dir) {
  95. // sprite namespace
  96. if(!array_key_exists($name, $this->spriteSrc))
  97. {
  98. $this->spriteSrc[$name] = array();
  99. }
  100. // add files from directory
  101. $this->spriteSrc[$name][$dir] = $this->getFileList($dir);
  102. }
  103. /**
  104. * getFileList
  105. *
  106. * This method processes files in a directory and adds them to the sprites array
  107. * @param $dir String value of the directory to scan for image files in
  108. */
  109. private function getFileList($dir) {
  110. $list = array();
  111. if(is_dir($dir)) {
  112. if($dh = opendir($dir)) {
  113. // optional sprites_config.php file
  114. $this->loadSpritesConfig($dir);
  115. while (($file = readdir($dh)) !== false)
  116. {
  117. if ($file != "." && $file != ".." && $file != "sprites_config.php")
  118. {
  119. // file info & check supported image format
  120. if($info = $this->getFileInfo($dir, $file)) {
  121. // skip excluded files
  122. if(isset($this->sprites_config[$dir]['exclude']) && array_search($file, $this->sprites_config[$dir]['exclude']) !== false)
  123. {
  124. global $mod_strings;
  125. $msg = string_format($mod_strings['LBL_SPRITES_EXCLUDING_FILE'], array("{$dir}/{$file}"));
  126. $GLOBALS['log']->debug($msg);
  127. $this->logMessage($msg);
  128. } else {
  129. // repeatable sprite ?
  130. $isRepeat = false;
  131. if(isset($this->sprites_config[$dir]['repeat']))
  132. {
  133. foreach($this->sprites_config[$dir]['repeat'] as $repeat)
  134. {
  135. if($info['x'] == $repeat['width'] && $info['y'] == $repeat['height'])
  136. {
  137. $id = md5($repeat['width'].$repeat['height'].$repeat['direction']);
  138. $isRepeat = true;
  139. $this->spriteRepeat['repeat_'.$repeat['direction'].'_'.$id][$dir][$file] = $info;
  140. }
  141. }
  142. }
  143. if(!$isRepeat)
  144. {
  145. $list[$file] = $info;
  146. }
  147. }
  148. } else if(preg_match('/\.(jpg|jpeg|gif|png|bmp|ico)$/i', $file)) {
  149. $GLOBALS['log']->error('Unable to process image file ' . $file);
  150. //$this->logMessage('Unable to process image file ' . $file);
  151. }
  152. }
  153. }
  154. }
  155. closedir($dh);
  156. }
  157. return $list;
  158. }
  159. /**
  160. * loadSpritesConfig
  161. *
  162. * This function is used to load the sprites_config.php file. The sprites_config.php file may be used to add entries
  163. * to the sprites_config member variable which may contain a list of array entries of files/directories to exclude from
  164. * being included into the sprites image.
  165. *
  166. * @param $dir String value of the directory containing the custom sprites_config.php file
  167. */
  168. private function loadSpritesConfig($dir) {
  169. $sprites_config = array();
  170. if(file_exists("$dir/sprites_config.php"))
  171. {
  172. include("$dir/sprites_config.php");
  173. if(count($sprites_config)) {
  174. $this->sprites_config = array_merge($this->sprites_config, $sprites_config);
  175. }
  176. }
  177. }
  178. /**
  179. * getFileInfo
  180. *
  181. * This is a private helper function to return attributes about an image. If the width, height or type of the
  182. * image file cannot be determined, then we do not process the file.
  183. *
  184. * @return array of file info entries containing file information (x, y, type) if image type is supported
  185. */
  186. private function getFileInfo($dir, $file) {
  187. $result = false;
  188. $info = @getimagesize($dir.'/'.$file);
  189. if($info) {
  190. // supported image type ?
  191. if(isset($this->imageTypes[$info[2]]))
  192. {
  193. $w = $info[0];
  194. $h = $info[1];
  195. $surface = $w * $h;
  196. // be sure we have an image size
  197. $addSprite = false;
  198. if($surface)
  199. {
  200. // sprite dimensions
  201. if($w <= $this->maxWidth && $h <= $this->maxHeight)
  202. {
  203. $addSprite = true;
  204. }
  205. }
  206. if($addSprite)
  207. {
  208. $result = array();
  209. $result['x'] = $w;
  210. $result['y'] = $h;
  211. $result['type'] = $info[2];
  212. }
  213. } else {
  214. $msg = "Skipping unsupported image file type ({$info[2]}) for file {$file}";
  215. $GLOBALS['log']->error($msg);
  216. $this->logMessage($msg."\n");
  217. }
  218. }
  219. return $result;
  220. }
  221. /**
  222. * createSprites
  223. *
  224. * This is the public function to allow the sprites to be built.
  225. *
  226. * @return $result boolean value indicating whether or not sprites were created
  227. */
  228. public function createSprites() {
  229. global $mod_strings;
  230. if(!$this->isAvailable)
  231. {
  232. if(!$this->silentRun)
  233. {
  234. $msg = $mod_strings['LBL_SPRITES_NOT_SUPPORTED'];
  235. $GLOBALS['log']->warn($msg);
  236. $this->logMessage($msg);
  237. }
  238. return false;
  239. }
  240. // add repeatable sprites
  241. if(count($this->spriteRepeat))
  242. {
  243. $this->spriteSrc = array_merge($this->spriteSrc, $this->spriteRepeat);
  244. }
  245. foreach($this->spriteSrc as $name => $dirs)
  246. {
  247. if(!$this->silentRun)
  248. {
  249. $msg = string_format($mod_strings['LBL_SPRITES_CREATING_NAMESPACE'], array($name));
  250. $GLOBALS['log']->debug($msg);
  251. $this->logMessage($msg);
  252. }
  253. // setup config for sprite placement algorithm
  254. if(substr($name, 0, 6) == 'repeat')
  255. {
  256. $isRepeat = true;
  257. $type = substr($name, 7, 10) == 'horizontal' ? 'horizontal' : 'vertical';
  258. $config = array(
  259. 'type' => $type,
  260. );
  261. } else {
  262. $isRepeat = false;
  263. $config = array(
  264. 'type' => 'boxed',
  265. 'width' => $this->maxWidth,
  266. 'height' => $this->maxHeight,
  267. 'rowcnt' => $this->rowCnt,
  268. );
  269. }
  270. // use separate class to arrange the images
  271. $sp = new SpritePlacement($dirs, $config);
  272. $sp->processSprites();
  273. //if(! $this->silentRun)
  274. // echo " (size {$sp->width()}x{$sp->height()})<br />";
  275. // we need a target image size
  276. if($sp->width() && $sp->height())
  277. {
  278. // init sprite image
  279. $this->initSpriteImg($sp->width(), $sp->height());
  280. // add sprites based upon determined coordinates
  281. foreach($dirs as $dir => $files)
  282. {
  283. if(!$this->silentRun)
  284. {
  285. $msg = string_format($mod_strings['LBL_SPRITES_PROCESSING_DIR'], array($dir));
  286. $GLOBALS['log']->debug($msg);
  287. $this->logMessage($msg);
  288. }
  289. foreach($files as $file => $info)
  290. {
  291. if($im = $this->loadImage($dir, $file, $info['type']))
  292. {
  293. // coordinates
  294. $dst_x = $sp->spriteMatrix[$dir.'/'.$file]['x'];
  295. $dst_y = $sp->spriteMatrix[$dir.'/'.$file]['y'];
  296. imagecopy($this->spriteImg, $im, $dst_x, $dst_y, 0, 0, $info['x'], $info['y']);
  297. imagedestroy($im);
  298. if(!$this->silentRun)
  299. {
  300. $msg = string_format($mod_strings['LBL_SPRITES_ADDED'], array("{$dir}/{$file}"));
  301. $GLOBALS['log']->debug($msg);
  302. $this->logMessage($msg);
  303. }
  304. }
  305. }
  306. }
  307. // dir & filenames
  308. if($isRepeat)
  309. {
  310. $outputDir = sugar_cached("sprites/Repeatable");
  311. $spriteFileName = "{$name}.png";
  312. $cssFileName = "{$this->fileName}.css";
  313. $metaFileName = "{$this->fileName}.meta.php";
  314. $nameSpace = "Repeatable";
  315. } else {
  316. $outputDir = sugar_cached("sprites/$name");
  317. $spriteFileName = "{$this->fileName}.png";
  318. $cssFileName = "{$this->fileName}.css";
  319. $metaFileName = "{$this->fileName}.meta.php";
  320. $nameSpace = "{$name}";
  321. }
  322. // directory structure
  323. if(!is_dir(sugar_cached("sprites/$nameSpace")))
  324. {
  325. sugar_mkdir(sugar_cached("sprites/$nameSpace"), 0775, true);
  326. }
  327. // save sprite image
  328. imagepng($this->spriteImg, "$outputDir/$spriteFileName", $this->pngCompression, $this->pngFilter);
  329. imagedestroy($this->spriteImg);
  330. /* generate css & metadata */
  331. $head = '';
  332. $body = '';
  333. $metadata = '';
  334. foreach($sp->spriteSrc as $id => $info)
  335. {
  336. // sprite id
  337. $hash_id = md5($id);
  338. // header
  339. $head .= "span.spr_{$hash_id},\n";
  340. // image size
  341. $w = $info['x'];
  342. $h = $info['y'];
  343. // image offset
  344. $offset_x = $sp->spriteMatrix[$id]['x'];
  345. $offset_y = $sp->spriteMatrix[$id]['y'];
  346. // sprite css
  347. $body .= "/* {$id} */
  348. span.spr_{$hash_id} {
  349. width: {$w}px;
  350. height: {$h}px;
  351. background-position: -{$offset_x}px -{$offset_y}px;
  352. }\n";
  353. $metadata .= '$sprites["'.$id.'"] = array ("class"=>"'.$hash_id.'","width"=>"'.$w.'","height"=>"'.$h.'");'."\n";
  354. }
  355. // common css header
  356. require_once('include/utils.php');
  357. $bg_path = getVersionedPath('index.php').'&entryPoint=getImage&imageName='.$spriteFileName.'&spriteNamespace='.$nameSpace;
  358. $head = rtrim($head, "\n,")." {background: url('../../../{$bg_path}'); no-repeat;display:inline-block;}\n";
  359. // append mode for repeatable sprites
  360. $fileMode = $isRepeat ? 'a' : 'w';
  361. // save css
  362. $css_content = "\n/* autogenerated sprites - $name */\n".$head.$body;
  363. if($this->cssMinify)
  364. {
  365. $css_content = cssmin::minify($css_content);
  366. }
  367. $fh = fopen("$outputDir/$cssFileName", $fileMode);
  368. fwrite($fh, $css_content);
  369. fclose($fh);
  370. /* save metadata */
  371. $add_php_tag = (file_exists("$outputDir/$metaFileName") && $isRepeat) ? false : true;
  372. $fh = fopen("$outputDir/$metaFileName", $fileMode);
  373. if($add_php_tag)
  374. {
  375. fwrite($fh, '<?php');
  376. }
  377. fwrite($fh, "\n/* sprites metadata - $name */\n");
  378. fwrite($fh, $metadata."\n");
  379. fclose($fh);
  380. // if width & height
  381. } else {
  382. if(!$this->silentRun)
  383. {
  384. $msg = string_format($mod_strings['LBL_SPRITES_ADDED'], array($name));
  385. $GLOBALS['log']->debug($msg);
  386. $this->logMessage($msg);
  387. }
  388. }
  389. }
  390. return true;
  391. }
  392. /**
  393. * initSpriteImg
  394. *
  395. * @param w int value representing width of sprite
  396. * @param h int value representing height of sprite
  397. * Private function to initialize creating the sprite canvas image
  398. */
  399. private function initSpriteImg($w, $h) {
  400. $this->spriteImg = imagecreatetruecolor($w,$h);
  401. $transparent = imagecolorallocatealpha($this->spriteImg, 0, 0, 0, 127);
  402. imagefill($this->spriteImg, 0, 0, $transparent);
  403. imagealphablending($this->spriteImg, false);
  404. imagesavealpha($this->spriteImg, true);
  405. }
  406. /**
  407. * loadImage
  408. *
  409. * private function to load image resources
  410. *
  411. * @param $dir String value of directory where image is located
  412. * @param $file String value of file
  413. * @param $type String value of the file type (IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)
  414. *
  415. */
  416. private function loadImage($dir, $file, $type) {
  417. $path_file = $dir.'/'.$file;
  418. switch($type) {
  419. case IMAGETYPE_GIF:
  420. return imagecreatefromgif($path_file);
  421. case IMAGETYPE_JPEG:
  422. return imagecreatefromjpeg($path_file);
  423. case IMAGETYPE_PNG:
  424. return imagecreatefrompng($path_file);
  425. default:
  426. return false;
  427. }
  428. }
  429. /**
  430. * private logMessage
  431. *
  432. * This is a private function used to log messages generated from this class. Depending on whether or not
  433. * silentRun or fromSilentUpgrade is set to true/false then it will either output to screen or write to log file
  434. *
  435. * @param $msg String value of message to log into file or echo into output buffer depending on the context
  436. */
  437. private function logMessage($msg)
  438. {
  439. if(!$this->silentRun && !$this->fromSilentUpgrade)
  440. {
  441. echo $msg . '<br />';
  442. } else if ($this->fromSilentUpgrade && $this->writeToUpgradeLog) {
  443. logThis($msg, $GLOBALS['path']);
  444. } else if(!$this->silentRun) {
  445. echo $msg . "\n";
  446. }
  447. }
  448. }
  449. /**
  450. * SpritePlacement
  451. *
  452. */
  453. class SpritePlacement
  454. {
  455. // occupied space
  456. var $spriteMatrix = array();
  457. // minimum surface
  458. var $minSurface = 0;
  459. // sprite src (flattened array)
  460. var $spriteSrc = array();
  461. // placement config array
  462. /*
  463. type = boxed
  464. horizontal
  465. vertical
  466. required params for
  467. type 1 -> width
  468. -> height
  469. -> rowcnt
  470. */
  471. var $config = array();
  472. function __construct($spriteSrc, $config) {
  473. // convert spriteSrc to flat array
  474. foreach($spriteSrc as $dir => $files) {
  475. foreach($files as $file => $info) {
  476. // use full path as identifier
  477. $full_path = $dir.'/'.$file;
  478. $this->spriteSrc[$full_path] = $info;
  479. }
  480. }
  481. $this->config = $config;
  482. }
  483. function processSprites() {
  484. foreach($this->spriteSrc as $id => $info) {
  485. // dimensions
  486. $x = $info['x'];
  487. $y = $info['y'];
  488. // update min surface
  489. $this->minSurface += $x * $y;
  490. // get coordinates where to add this sprite
  491. if($coor = $this->addSprite($x, $y)) {
  492. $this->spriteMatrix[$id] = $coor;
  493. }
  494. }
  495. }
  496. // returns x/y coordinates to fit the sprite
  497. function addSprite($w, $h) {
  498. $result = false;
  499. switch($this->config['type']) {
  500. // boxed
  501. case 'boxed':
  502. $spriteX = $this->config['width'];
  503. $spriteY = $this->config['height'];
  504. $spriteCnt = count($this->spriteMatrix) + 1;
  505. $y = ceil($spriteCnt / $this->config['rowcnt']);
  506. $x = $spriteCnt - (($y - 1) * $this->config['rowcnt']);
  507. $result = array(
  508. 'x' => ($x * $spriteX) + 1 - $spriteX,
  509. 'y' => ($y * $spriteY) + 1 - $spriteY);
  510. break;
  511. // horizontal -> align vertically
  512. case 'horizontal':
  513. $result = array('x' => 1, 'y' => $this->height() + 1);
  514. break;
  515. // vertical -> align horizontally
  516. case 'vertical':
  517. $result = array('x' => $this->width() + 1, 'y' => 1);
  518. break;
  519. default:
  520. $GLOBALS['log']->warn(__CLASS__.": Unknown sprite placement algorithm -> {$this->config['type']}");
  521. break;
  522. }
  523. return $result;
  524. }
  525. // calculate total width
  526. function width() {
  527. return $this->getMaxAxis('x');
  528. }
  529. // calculate total height
  530. function height() {
  531. return $this->getMaxAxis('y');
  532. }
  533. // helper function to get highest axis value
  534. function getMaxAxis($axis) {
  535. $val = 0;
  536. foreach($this->spriteMatrix as $id => $coor) {
  537. $new_val = $coor[$axis] + $this->spriteSrc[$id][$axis] - 1;
  538. if($new_val > $val) {
  539. $val = $new_val;
  540. }
  541. }
  542. return $val;
  543. }
  544. }
  545. ?>