PageRenderTime 2227ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Nya/Image.php

https://github.com/sasatomislav/resizer
PHP | 690 lines | 402 code | 98 blank | 190 comment | 67 complexity | bb3a65b08d0aab6d29a9b605b8a5bd5e MD5 | raw file
  1. <?php
  2. /**
  3. * Image manipulation class
  4. * Uses GD
  5. *
  6. * @author Saša Tomislav Mataić <sasa.tomislav [ AT ] mataic.com>
  7. */
  8. class Nya_Image
  9. {
  10. /**
  11. * If image was modified, this attribute holds
  12. * the resulting image resource to be saved
  13. * @var resource
  14. */
  15. public $_image = null;
  16. /**
  17. * Where applicable used for quality setting
  18. * Currently not implemented
  19. * @var int
  20. */
  21. private $_quality = 100;
  22. /**
  23. * "High quality" flag
  24. * TRUE: imagecopyresampled is used (slower, higher quality)
  25. * FALSE: imagecopyresized is used (faster, lower quality)
  26. *
  27. * @var bool
  28. */
  29. private $_hq = true;
  30. /**
  31. * Filesystem directory where image file resides
  32. * @var string
  33. */
  34. private $_path = null;
  35. /**
  36. * Image file name
  37. * @var string
  38. */
  39. private $_name = null;
  40. /**
  41. * Full file system path of image file
  42. * @var string
  43. */
  44. private $_filePath = null;
  45. /**
  46. * Used when image is saved by different name
  47. * @var string
  48. */
  49. private $_originalName = null;
  50. /**
  51. * Result of getimagesize();
  52. *
  53. * @var array
  54. */
  55. private $_size = array();
  56. /**
  57. * Image aspect ratio
  58. * @var float
  59. */
  60. private $_aspectRatio = null;
  61. /**
  62. * Revert cache
  63. */
  64. private $_revert = array();
  65. /**
  66. * Image types - used for determining functions for saving
  67. * and modifying images of different types
  68. *
  69. * @var array
  70. */
  71. protected $_imageTypes = array(
  72. 'image/pjpeg' => array(
  73. 'extension' => 'jpg',
  74. 'createFunction' => 'imagecreatefromjpeg',
  75. 'saveFunction' => 'imagejpeg'
  76. ),
  77. 'image/jpeg' => array(
  78. 'extension' => 'jpg',
  79. 'createFunction' => 'imagecreatefromjpeg',
  80. 'saveFunction' => 'imagejpeg'
  81. ),
  82. 'image/gif' => array(
  83. 'extension' => 'gif',
  84. 'createFunction' => 'imagecreatefromgif',
  85. 'saveFunction' => 'imagegif'
  86. ),
  87. 'image/x-png' => array(
  88. 'extension' => 'png',
  89. 'createFunction' => 'imagecreatefrompng',
  90. 'saveFunction' => 'imagepng'
  91. ),
  92. 'image/png' => array(
  93. 'extension' => 'png',
  94. 'createFunction' => 'imagecreatefrompng',
  95. 'saveFunction' => 'imagepng'
  96. ),
  97. );
  98. /**
  99. * Create image object, extract image data -
  100. * dimensions,
  101. * mime type,
  102. * aspect ratio
  103. *
  104. * @param string $filePath
  105. */
  106. public function __construct ($filePath = null, $originalName = null)
  107. {
  108. if (is_array($filePath)) {
  109. if (0 != $filePath['error']) {
  110. // TODO: translate
  111. throw new Exception('Invalid image uploaded');
  112. }
  113. $originalName = $filePath['name'];
  114. $filePath = $filePath['tmp_name'];
  115. }
  116. if (null !== $filePath && is_file($filePath)) {
  117. $this->_size = $this->_revert['size'] = getimagesize($filePath);
  118. if (!$this->valid())
  119. {
  120. // TODO: translate
  121. throw new Exception('File "' . $filePath . '" is not of valid image type');
  122. }
  123. $this->_originalName = $this->_revert['originalName'] = $originalName;
  124. $this->_filePath = $this->_revert['filePath'] = $filePath;
  125. $this->_path = $this->_revert['path'] = dirname($filePath);
  126. $this->_name = $this->_revert['name'] = substr($filePath, strlen($this->_path) + 1);
  127. $this->_aspectRatio = $this->_revert['aspectRatio'] = $this->_size[0] / $this->_size[1];
  128. } else {
  129. // TODO: translate
  130. throw new Exception('File "' . $filePath . '" is not of valid image type');
  131. }
  132. }
  133. /**
  134. * Revert image to original without the need to reload file
  135. * @return Nya_Image provides fluent interface
  136. */
  137. public function revert()
  138. {
  139. $this->_image = null;
  140. $this->_size = $this->_revert['size'];
  141. $this->_originalName = $this->_revert['originalName'];
  142. $this->_filePath = $this->_revert['filePath'];
  143. $this->_path = $this->_revert['path'];
  144. $this->_name = $this->_revert['name'];
  145. $this->_aspectRatio = $this->_revert['aspectRatio'];
  146. return $this;
  147. }
  148. /**
  149. * Checks if loaded image is valid image file
  150. * @return bool
  151. */
  152. private function valid()
  153. {
  154. if (isset($this->_imageTypes[$this->_size['mime']])) {
  155. return true;
  156. } else {
  157. return false;
  158. }
  159. }
  160. /**
  161. * Shorthand function for set/getHighQuality
  162. * @param $hq
  163. * @return bool use "high quality" for image transformations
  164. */
  165. public function hq($hq = null)
  166. {
  167. if (null === $hq) {
  168. return $this->getHighQuality();
  169. } else {
  170. return $this->setHighQuality($hq);
  171. }
  172. }
  173. /**
  174. * $this->_hq attribute setter
  175. *
  176. * @param $highQuality
  177. * @return Nya_Image
  178. */
  179. public function setHighQuality($highQuality = null)
  180. {
  181. if (null === $highQuality) {
  182. return $this;
  183. }
  184. if (true == $highQuality) {
  185. $this->_hq = true;
  186. } else {
  187. $this->_hq = false;
  188. }
  189. return $this;
  190. }
  191. /**
  192. * $this->_hq attribute getter
  193. *
  194. * @return bool
  195. */
  196. public function getHighQuality()
  197. {
  198. return $this->_hq;
  199. }
  200. /**
  201. * Resize image with distorting - resize to $newWidth and $newHeight
  202. * @param $newWidth
  203. * @param $newHeight
  204. * @return Nya_Image provides fluent interface
  205. */
  206. public function resize($newWidth = null, $newHeight = null)
  207. {
  208. if (null === $newWidth or 0 >= $newWidth) {
  209. // TODO: translate
  210. throw new Exception('Invalid dimensions given for ' . __METHOD__);
  211. }
  212. // if only width given, height is the same - make a square image
  213. $newWidth = (int) $newWidth;
  214. $newHeight = (null === $newHeight or 0 >= $newHeight) ? $newWidth : (int) $newHeight;
  215. $width = $this->_size[0];
  216. $height = $this->_size[1];
  217. $createImageFunction = $this->_imageTypes[$this->_size['mime']]['createFunction'];
  218. $saveImageFunction = $this->_imageTypes[$this->_size['mime']]['saveFunction'];
  219. $thumb = imagecreatetruecolor($newWidth, $newHeight);
  220. $source = $createImageFunction($this->_path . DIRECTORY_SEPARATOR . $this->_name);
  221. $resizeFunctionName = ($this->_hq) ? 'imagecopyresampled' : 'imagecopyresized';
  222. if (!$resizeFunctionName($thumb, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height)) {
  223. // TODO: translate
  224. throw new Exception('Failed resizing image: "' . $filePath . '"');
  225. } else {
  226. $this->_image = $thumb;
  227. $this->_size[0] = $newWidth;
  228. $this->_size[1] = $newHeight;
  229. $this->_size[3] = 'width="' . $newWidth . '" height="' . $newHeight . '"';
  230. }
  231. return $this;
  232. }
  233. /**
  234. * Resize image to a percentage of original size
  235. * @param $perc int percent to resize image
  236. * @return Nya_Image provides fluent interface
  237. */
  238. public function resizePercentage($perc = null)
  239. {
  240. if (null === $perc || !is_numeric($perc)) {
  241. throw new Exception('Invalid dimensions given for ' . __METHOD__);
  242. }
  243. $perc = intval(abs($perc));
  244. $dimensions = $this->size();
  245. $nWidth = floor($dimensions[0] / (100/$perc));
  246. return $this->resizeProportional($nWidth);
  247. }
  248. /**
  249. * Resize to certain size, preserving the aspect ratio
  250. * @param $newWidth string [optional] width to target
  251. * @param $newHeight string [optional] height to target
  252. * @return Nya_Image
  253. * @throws Exception if both width and hight are provided, but resulting in not matching aspect ratio
  254. */
  255. public function resizeProportional($newWidth = null, $newHeight = null)
  256. {
  257. if (null !== $newWidth && null !== $newHeight) {
  258. $aspectRatio = $newWidth / $newHeight;
  259. if ($aspectRatio !== $this->_aspectRatio) {
  260. // TODO: translate
  261. throw new Exception('Unable to proportionally resize image, invalid width and height given. Both given - invalid aspect ratio of destination image.');
  262. }
  263. }
  264. $createImageFunction = $this->_imageTypes[$this->_size['mime']]['createFunction'];
  265. $saveImageFunction = $this->_imageTypes[$this->_size['mime']]['saveFunction'];
  266. // do we have already changed image?
  267. if (null !== $this->_image) {
  268. $source = $this->_image;
  269. } else {
  270. $source = $createImageFunction($this->_path . DIRECTORY_SEPARATOR . $this->_name);
  271. }
  272. // determine new dimensions
  273. if (null !== $newWidth) {
  274. $newHeight = (int) ($newWidth / $this->_aspectRatio);
  275. } else {
  276. $newWidth = (int) ($newHeight * $this->_aspectRatio);
  277. }
  278. $resizedImage = imagecreatetruecolor($newWidth, $newHeight);
  279. $resizeFunctionName = ($this->_hq) ? 'imagecopyresampled' : 'imagecopyresized';
  280. if (!$resizeFunctionName($resizedImage, $source, 0, 0, 0, 0, $newWidth, $newHeight, $this->_size[0], $this->_size[1])) {
  281. // TODO: translate
  282. throw new Exception('Failed resizing image.');
  283. } else {
  284. $this->_image = $resizedImage;
  285. $this->_size[0] = $newWidth;
  286. $this->_size[1] = $newHeight;
  287. $this->_size[3] = 'width="' . $newWidth . '" height="' . $newHeight . '"';
  288. }
  289. return $this;
  290. }
  291. /**
  292. * Shrink image if source width or height exceeds
  293. * destination dimensions set by parameters
  294. *
  295. * @param int $maxWidth maximum width in pixels
  296. * @param int $maxHeight maximum height in pixels
  297. * @return Nya_Image
  298. */
  299. public function resizeTo($maxWidth = null, $maxHeight = null)
  300. {
  301. if ((null === $maxWidth or 0 >= $maxWidth) && (null === $maxHeight or 0 >= $maxHeight) ) {
  302. throw new Exception('Invalid dimensions given for ' . __METHOD__);
  303. }
  304. if ($this->_size[0] > $maxWidth) {
  305. $this->resizeProportional($maxWidth);
  306. }
  307. if ($this->_size[1] > $maxHeight) {
  308. $this->resizeProportional(null, $maxHeight);
  309. }
  310. return $this;
  311. }
  312. /**
  313. * Alias of $this->resizeTo()
  314. * @param $width int maximum width in pixels
  315. * @param $height int maximum height in pixels
  316. */
  317. public function shrinkTo($width = null, $height = null)
  318. {
  319. return $this->resizeTo($width, $height);
  320. }
  321. /**
  322. * Resize image to given dimensions with $newWidth & $newHeight
  323. * If destination image has different aspect ratio from source image - crop
  324. * Cropping is performed if destination image is wider or narrower than source
  325. * If destination image is wider - full source width is used, and height is cropped from center
  326. * If destination image is narrower - full source height is used, and width is cropped from center
  327. *
  328. * @param $newWidth
  329. * @param $newHeight
  330. * @return Nya_Image
  331. */
  332. public function cropResize($newWidth = null, $newHeight = null)
  333. {
  334. if (null === $newWidth or 0 >= $newWidth) {
  335. throw new Exception('Invalid dimensions given for ' . __METHOD__);
  336. }
  337. // if only width given, height is the same - square image
  338. $newWidth = (int) $newWidth;
  339. $newHeight = (null === $newHeight or 0 >= $newHeight) ? $newWidth : (int) $newHeight;
  340. $createImageFunction = $this->_imageTypes[$this->_size['mime']]['createFunction'];
  341. $saveImageFunction = $this->_imageTypes[$this->_size['mime']]['saveFunction'];
  342. $thumb = imagecreatetruecolor($newWidth, $newHeight);
  343. $source = $createImageFunction($this->_path . DIRECTORY_SEPARATOR . $this->_name);
  344. $newAspectRatio = $newWidth / $newHeight;
  345. // calculate size and position of image portion to crop and resize
  346. if ($newAspectRatio == $this->_aspectRatio) {
  347. // aspect ratio is the same - use whole image in resizing
  348. $width = $this->_size[0];
  349. $height = $this->_size[1];
  350. $src_x = 0;
  351. $src_y = 0;
  352. } elseif ($newAspectRatio < $this->_aspectRatio) {
  353. // new image is narrower than original - use whole height, calculate width (newWidth = originalHeight * newAspectRatio)
  354. $width = (int) ($this->_size[1] * $newAspectRatio);
  355. $height = $this->_size[1];
  356. $src_x = (int) (($this->_size[0] - $width) / 2);
  357. $src_y = 0;
  358. } else {
  359. // new image is wider than original - use whole width, calculate height (newHeight = originalWidth / newAspectRatio)
  360. $width = $this->_size[0];
  361. $height = (int) ($this->_size[0] / $newAspectRatio);
  362. $src_x = 0;
  363. $src_y = (int) (($this->_size[1] - $height) / 2);
  364. }
  365. $resizeFunctionName = ($this->_hq) ? 'imagecopyresampled' : 'imagecopyresized';
  366. if (!$resizeFunctionName($thumb, $source, 0, 0, $src_x, $src_y, $newWidth, $newHeight, $width, $height)) {
  367. // TODO: translate
  368. throw new Exception('Failed resizing image for: "' . $source . '"');
  369. } else {
  370. $this->_image = $thumb;
  371. $this->_size[0] = $newWidth;
  372. $this->_size[1] = $newHeight;
  373. $this->_size[3] = 'width="' . $newWidth . '" height="' . $newHeight . '"';
  374. }
  375. return $this;
  376. }
  377. /**
  378. * Image is cropped from center with no resizing to
  379. * destination dimensions given by $width & $height.
  380. *
  381. * @param $width
  382. * @param $height
  383. * @return Nya_Image
  384. */
  385. public function cropCenter($newWidth, $newHeight)
  386. {
  387. if (null === $newWidth or 0 >= $newWidth) {
  388. throw new Exception('Invalid dimensions given for ' . __METHOD__);
  389. }
  390. // if only width given, height is the same - square image
  391. $width = (int) $newWidth;
  392. $height = (null === $newHeight or 0 >= $newHeight) ? $newWidth : (int) $newHeight;
  393. $oWidth = $this->_size[0];
  394. $oHeight = $this->_size[1];
  395. $centerY = ceil($oWidth / 2);
  396. $centerX = ceil($oHeight / 2);
  397. $src_x = $centerY - ceil($width / 2);
  398. $src_y = $centerX - ceil($height / 2);
  399. $createImageFunction = $this->_imageTypes[$this->_size['mime']]['createFunction'];
  400. $saveImageFunction = $this->_imageTypes[$this->_size['mime']]['saveFunction'];
  401. $dest = imagecreatetruecolor($width, $height);
  402. $source = $createImageFunction($this->_path . DIRECTORY_SEPARATOR . $this->_name);
  403. $resizeFunctionName = ($this->_hq) ? 'imagecopyresampled' : 'imagecopyresized';
  404. if (!$resizeFunctionName($dest, $source, 0, 0, $src_x, $src_y, $width, $height, $width, $height)) {
  405. throw new Exception('Failed center cropping image for: "' . $source . '"');
  406. } else {
  407. $this->_image = $dest;
  408. $this->_size[0] = $width;
  409. $this->_size[1] = $height;
  410. $this->_size[3] = 'width="' . $width . '" height="' . $height . '"';
  411. }
  412. return $this;
  413. }
  414. /**
  415. * getimagesize() return values
  416. * @return array
  417. */
  418. public function size()
  419. {
  420. return $this->_size;
  421. }
  422. /**
  423. * Returns aspect ratio
  424. */
  425. public function aspectRatio()
  426. {
  427. return $this->_aspectRatio;
  428. }
  429. public function name($newName = null)
  430. {
  431. if (null === $newName) {
  432. if (null === $this->_originalName) {
  433. return $this->_name;
  434. } else {
  435. return $this->_originalName;
  436. }
  437. } else {
  438. $this->_originalName = $newName;
  439. return $this;
  440. }
  441. }
  442. public function path()
  443. {
  444. return $this->_path;
  445. }
  446. public function filePath()
  447. {
  448. return $this->_filePath;
  449. }
  450. /**
  451. * Returns image width in pixels
  452. */
  453. public function width()
  454. {
  455. return $this->_size[0];
  456. }
  457. /**
  458. * Returns image height in pixels
  459. */
  460. public function height()
  461. {
  462. return $this->_size[1];
  463. }
  464. public function crop($x1 = null, $y1 = null, $x2 = null, $y2 = null)
  465. {
  466. throw new Exception('Not implemented');
  467. }
  468. public function save()
  469. {
  470. // if image wasn't modified - don't do anything, otherwise save modified version
  471. if (null === $this->_image) {
  472. } else {
  473. $saveImageFunction = $this->_imageTypes[$this->_size['mime']]['saveFunction'];
  474. if (!$saveImageFunction($this->_image, $this->_filePath)) {
  475. // TODO: translate
  476. throw new Exception("Unable to save modified image '$this->_filePath' as '$this->_filePath' in " . __METHOD__);
  477. }
  478. }
  479. }
  480. public function saveAs($path = null, $name = null, $overWrite = false)
  481. {
  482. if (null === $path) {
  483. // save on same path, different name
  484. $path = $this->_path;
  485. } elseif (!is_dir($path)) {
  486. // TODO: translate
  487. throw new Exception('Invalid file path for saving image given: ' . $path);
  488. }
  489. // file name with what we'll save file
  490. if (null !== $name) {
  491. $fileName = $name;
  492. } elseif (null === $this->_originalName) {
  493. $fileName = $this->_name;
  494. } else {
  495. $fileName = $this->_originalName;
  496. }
  497. $source = $this->_path . DIRECTORY_SEPARATOR . $this->_name;
  498. if (!$overWrite) {
  499. // check if file exists with same name, append number to end of name
  500. $i = 1;
  501. $baseName = $fileName;
  502. while (file_exists($path . DIRECTORY_SEPARATOR . $fileName)) {
  503. $fileNameArray = explode('.', $baseName);
  504. $fileName = $fileNameArray[0] . $i . '.' . $fileNameArray[1];
  505. $i++;
  506. }
  507. }
  508. $destination = $path . DIRECTORY_SEPARATOR . $fileName;
  509. // update name if changed
  510. $this->name($fileName);
  511. // if image wasn't modified - copy file, otherwise save modified version
  512. if (null === $this->_image) {
  513. if (!copy($source, $destination)) {
  514. // TODO: translate
  515. throw new Exception("Unable to copy '$source' to '$destination' in " . __METHOD__);
  516. }
  517. } else {
  518. $saveImageFunction = $this->_imageTypes[$this->_size['mime']]['saveFunction'];
  519. if (!$saveImageFunction($this->_image, $destination)) {
  520. // TODO: translate
  521. throw new Exception("Unable to save modified image '$source' as '$destination' in " . __METHOD__);
  522. }
  523. }
  524. // file is in new location - update path
  525. $this->_path = $path;
  526. return $this;
  527. }
  528. /**
  529. * Unset image instance of resized file
  530. */
  531. public function destroy()
  532. {
  533. if (null !== $this->_image) {
  534. imagedestroy($this->_image);
  535. }
  536. return $this;
  537. }
  538. public function info()
  539. {
  540. $filePath = (null === $this->_originalName) ? $this->_name : $this->_originalName;
  541. $filePath = $this->_path . DIRECTORY_SEPARATOR . $filePath;
  542. return array($filePath, $this->_size[3]);
  543. }
  544. public function extension()
  545. {
  546. return $this->_imageTypes[$this->_size['mime']]['extension'];
  547. }
  548. /**
  549. * Check memory needed for image manipulation
  550. * @param $targetWidth
  551. * @param $targetHeight
  552. */
  553. public function isResizable($targetWidth, $targetHeight) {
  554. $memoryLimit = (int) ini_get('memory_limit');
  555. $memoryUsage = (int) ceil(memory_get_usage() / 1024 / 1024);
  556. $tweak = 1.8;
  557. $memNeededSource = ( $this->_size[0] * $this->_size[1] * $tweak * $this->_size['channels']) / 1024 / 1024;
  558. $memNeededTarget = ( $targetWidth * $targetHeight * $tweak * $this->_size['channels']) / 1024 / 1024;
  559. $memNeededSum = ceil($memNeededSource + $memNeededTarget);
  560. $memToSet = $memoryUsage + $memNeededSum;
  561. if ($memToSet < $memoryLimit) {
  562. return true;
  563. } elseif ($memToSet > 128) {
  564. return false;
  565. } {
  566. $iniMem = $memToSet . 'M';
  567. ini_set('memory_limit', $iniMem);
  568. if (ini_get('memory_limit') == $iniMem) {
  569. return true;
  570. }
  571. return false;
  572. }
  573. }
  574. public function __get ($name = null)
  575. {
  576. if (isset($this->$name)) {
  577. return $this->$name;
  578. } else {
  579. // TODO: translate
  580. throw new Exception('Class member "' . $name . '" unrecognized in ' . __CLASS__);
  581. }
  582. }
  583. public function __set ($name = null, $value = null)
  584. {
  585. if ($name === 'path' && null !== $value) {
  586. $this->_path = $value;
  587. } else {
  588. // TODO: translate
  589. throw new Exception('Cannot set ' . $name . 'Object of type "' . __CLASS__ . '" permits only "path" member to be set!');
  590. }
  591. }
  592. }