PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/wire/core/ImageSizer.php

http://github.com/ryancramerdesign/ProcessWire
PHP | 1959 lines | 1022 code | 253 blank | 684 comment | 354 complexity | 142042332bb4d9ce4d5819f5eeea9889 MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * ProcessWire ImageSizer
  4. *
  5. * ImageSizer handles resizing of a single JPG, GIF, or PNG image using GD2.
  6. *
  7. * ImageSizer class includes ideas adapted from comments found at PHP.net
  8. * in the GD functions documentation.
  9. *
  10. * Code for IPTC, auto rotation and sharpening by Horst Nogajski.
  11. * http://nogajski.de/
  12. *
  13. * Other user contributions as noted.
  14. *
  15. * ProcessWire 2.x
  16. * Copyright (C) 2015 by Ryan Cramer
  17. * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
  18. *
  19. * https://processwire.com
  20. *
  21. */
  22. class ImageSizer extends Wire {
  23. /**
  24. * Filename to be resized
  25. *
  26. */
  27. protected $filename;
  28. /**
  29. * Extension of filename
  30. *
  31. */
  32. protected $extension;
  33. /**
  34. * Type of image
  35. *
  36. */
  37. protected $imageType = null;
  38. /**
  39. * Image quality setting, 1..100
  40. *
  41. */
  42. protected $quality = 90;
  43. /**
  44. * Information about the image (width/height)
  45. *
  46. */
  47. protected $image = array(
  48. 'width' => 0,
  49. 'height' => 0
  50. );
  51. /**
  52. * Allow images to be upscaled / enlarged?
  53. *
  54. */
  55. protected $upscaling = true;
  56. /**
  57. * Directions that cropping may gravitate towards
  58. *
  59. * Beyond those included below, TRUE represents center and FALSE represents no cropping.
  60. *
  61. */
  62. static protected $croppingValues = array(
  63. 'nw' => 'northwest',
  64. 'n' => 'north',
  65. 'ne' => 'northeast',
  66. 'w' => 'west',
  67. 'e' => 'east',
  68. 'sw' => 'southwest',
  69. 's' => 'south',
  70. 'se' => 'southeast',
  71. );
  72. /**
  73. * Allow images to be cropped to achieve necessary dimension? If so, what direction?
  74. *
  75. * Possible values: northwest, north, northeast, west, center, east, southwest, south, southeast
  76. * or TRUE to crop to center, or FALSE to disable cropping.
  77. * Default is: TRUE
  78. *
  79. */
  80. protected $cropping = true;
  81. /**
  82. * This can be populated on a per image basis. It provides cropping first and then resizing, the opposite of the default behave
  83. *
  84. * It needs an array with 4 params: x y w h for the cropping rectangle
  85. *
  86. * Default is: null
  87. *
  88. */
  89. protected $cropExtra = null;
  90. /**
  91. * Was the given image modified?
  92. *
  93. */
  94. protected $modified = false;
  95. /**
  96. * enable auto_rotation according to EXIF-Orientation-Flag
  97. *
  98. */
  99. protected $autoRotation = true;
  100. /**
  101. * default sharpening mode: [ none | soft | medium | strong ]
  102. *
  103. */
  104. protected $sharpening = 'soft';
  105. /**
  106. * Degrees to rotate: -270, -180, -90, 90, 180, 270
  107. *
  108. * @var int
  109. *
  110. */
  111. protected $rotate = 0;
  112. /**
  113. * Flip image: Specify 'v' for vertical or 'h' for horizontal
  114. *
  115. * @var string
  116. *
  117. */
  118. protected $flip = '';
  119. /**
  120. * default gamma correction: 0.5 - 4.0 | -1 to disable gammacorrection, default = 2.0
  121. *
  122. * can be overridden by setting it to $config->imageSizerOptions['defaultGamma']
  123. * or passing it along with image options array
  124. *
  125. */
  126. protected $defaultGamma = 2.0;
  127. /**
  128. * Factor to use when determining if enough memory available for resize.
  129. *
  130. */
  131. protected $memoryCheckFactor = 2.2;
  132. /**
  133. * Other options for 3rd party use
  134. *
  135. */
  136. protected $options = array();
  137. /**
  138. * Options allowed for sharpening
  139. *
  140. */
  141. static protected $sharpeningValues = array(
  142. 0 => 'none', // none
  143. 1 => 'soft',
  144. 2 => 'medium',
  145. 3 => 'strong'
  146. );
  147. /**
  148. * List of valid option Names from config.php (@horst)
  149. *
  150. */
  151. protected $optionNames = array(
  152. 'autoRotation',
  153. 'upscaling',
  154. 'cropping',
  155. 'quality',
  156. 'sharpening',
  157. 'defaultGamma',
  158. 'scale',
  159. 'rotate',
  160. 'flip',
  161. );
  162. /**
  163. * Supported image types (@teppo)
  164. *
  165. */
  166. protected $supportedImageTypes = array(
  167. 'gif' => IMAGETYPE_GIF,
  168. 'jpg' => IMAGETYPE_JPEG,
  169. 'jpeg' => IMAGETYPE_JPEG,
  170. 'png' => IMAGETYPE_PNG,
  171. );
  172. /**
  173. * Indicates how much an image should be sharpened
  174. *
  175. */
  176. protected $usmValue = 100;
  177. /**
  178. * Result of iptcparse(), if available
  179. *
  180. */
  181. protected $iptcRaw = null;
  182. /**
  183. * List of valid IPTC tags (@horst)
  184. *
  185. */
  186. protected $validIptcTags = array(
  187. '005','007','010','012','015','020','022','025','030','035','037','038','040','045','047','050','055','060',
  188. '062','063','065','070','075','080','085','090','092','095','100','101','103','105','110','115','116','118',
  189. '120','121','122','130','131','135','150','199','209','210','211','212','213','214','215','216','217');
  190. /**
  191. * Information about the image from getimagesize (width, height, imagetype, channels, etc.)
  192. *
  193. */
  194. protected $info = null;
  195. /**
  196. * HiDPI scale value (2.0 = hidpi, 1.0 = normal)
  197. *
  198. * @var float
  199. *
  200. */
  201. protected $scale = 1.0;
  202. /**
  203. * Construct the ImageSizer for a single image
  204. *
  205. */
  206. public function __construct($filename, $options = array()) {
  207. // ensures the resize doesn't timeout the request (with at least 30 seconds)
  208. $this->setTimeLimit();
  209. // set the use of UnSharpMask as default, can be overwritten per pageimage options
  210. // or per $config->imageSizerOptions in site/config.php
  211. $this->options['useUSM'] = true;
  212. // filling all options with global custom values from config.php
  213. $options = array_merge($this->wire('config')->imageSizerOptions, $options);
  214. $this->setOptions($options);
  215. $this->loadImageInfo($filename, true);
  216. }
  217. /**
  218. * Load the image information (width/height) using PHP's getimagesize function
  219. *
  220. */
  221. protected function loadImageInfo($filename, $reloadAll = false) {
  222. $this->filename = $filename;
  223. $this->extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
  224. $additionalInfo = array();
  225. $this->info = @getimagesize($this->filename, $additionalInfo);
  226. if($this->info === false) throw new WireException(basename($filename) . " - not a recognized image");
  227. $this->info['channels'] = isset($this->info['channels']) && $this->info['channels'] > 0 && $this->info['channels'] <= 4 ? $this->info['channels'] : 3;
  228. if(function_exists("exif_imagetype")) {
  229. $this->imageType = exif_imagetype($filename);
  230. } else if(isset($info[2])) {
  231. // imagetype (IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)
  232. $this->imageType = $info[2];
  233. } else if(isset($this->supportedImageTypes[$this->extension])) {
  234. $this->imageType = $this->supportedImageTypes[$this->extension];
  235. }
  236. if(!in_array($this->imageType, $this->supportedImageTypes)) {
  237. throw new WireException(basename($filename) . " - not a supported image type");
  238. }
  239. // width, height
  240. $this->setImageInfo($this->info[0], $this->info[1]);
  241. // read metadata if present and if its the first call of the method
  242. if(is_array($additionalInfo) && $reloadAll) {
  243. $appmarker = array();
  244. foreach($additionalInfo as $k => $v) {
  245. $appmarker[$k] = substr($v, 0, strpos($v, null));
  246. }
  247. $this->info['appmarker'] = $appmarker;
  248. if(isset($additionalInfo['APP13'])) {
  249. $iptc = iptcparse($additionalInfo["APP13"]);
  250. if(is_array($iptc)) $this->iptcRaw = $iptc;
  251. }
  252. }
  253. }
  254. /**
  255. * Resize the image proportionally to the given width/height
  256. *
  257. * Note: Some code used in this method is adapted from code found in comments at php.net for the GD functions
  258. *
  259. * @param int $targetWidth Target width in pixels, or 0 for proportional to height
  260. * @param int $targetHeight Target height in pixels, or 0 for proportional to width. Optional-if not specified, 0 is assumed.
  261. * @return bool True if the resize was successful
  262. * @throws WireException when not enough memory to load image
  263. *
  264. */
  265. public function ___resize($targetWidth, $targetHeight = 0) {
  266. if($this->scale !== 1.0) {
  267. // adjust for hidpi
  268. if($targetWidth) $targetWidth = ceil($targetWidth * $this->scale);
  269. if($targetHeight) $targetHeight = ceil($targetHeight * $this->scale);
  270. }
  271. $orientations = null; // @horst
  272. $needRotation = $this->autoRotation !== true ? false : ($this->checkOrientation($orientations) && (!empty($orientations[0]) || !empty($orientations[1])) ? true : false);
  273. $source = $this->filename;
  274. $dest = str_replace("." . $this->extension, "_tmp." . $this->extension, $source);
  275. $image = null;
  276. // check if we can load the sourceimage into ram
  277. if(self::checkMemoryForImage(array($this->info[0], $this->info[1], $this->info['channels'])) === false) {
  278. throw new WireException(basename($source) . " - not enough memory to load");
  279. }
  280. switch($this->imageType) { // @teppo
  281. case IMAGETYPE_GIF: $image = @imagecreatefromgif($source); break;
  282. case IMAGETYPE_PNG: $image = @imagecreatefrompng($source); break;
  283. case IMAGETYPE_JPEG: $image = @imagecreatefromjpeg($source); break;
  284. }
  285. if(!$image) return false;
  286. if($this->imageType != IMAGETYPE_PNG || !$this->hasAlphaChannel()) {
  287. // @horst: linearize gamma to 1.0 - we do not use gamma correction with pngs containing alphachannel, because GD-lib doesn't respect transparency here (is buggy)
  288. $this->gammaCorrection($image, true);
  289. }
  290. if($this->rotate || $needRotation) { // @horst
  291. $degrees = $this->rotate ? $this->rotate : $orientations[0];
  292. $image = $this->imRotate($image, $degrees);
  293. if(abs($degrees) == 90 || abs($degrees) == 270) {
  294. // we have to swap width & height now!
  295. $tmp = array($this->getWidth(), $this->getHeight());
  296. $this->setImageInfo($tmp[1], $tmp[0]);
  297. }
  298. }
  299. if($this->flip || $needRotation) {
  300. $vertical = null;
  301. if($this->flip) {
  302. $vertical = $this->flip == 'v';
  303. } else if($orientations[1] > 0) {
  304. $vertical = $orientations[1] == 2;
  305. }
  306. if(!is_null($vertical)) $image = $this->imFlip($image, $vertical);
  307. }
  308. // if there is requested to crop _before_ resize, we do it here @horst
  309. if(is_array($this->cropExtra)) {
  310. // check if we can load a second copy from sourceimage into ram
  311. if(self::checkMemoryForImage(array($this->info[0], $this->info[1], 3)) === false) {
  312. throw new WireException(basename($source) . " - not enough memory to load a copy for cropExtra");
  313. }
  314. $imageTemp = imagecreatetruecolor(imagesx($image), imagesy($image)); // create an intermediate memory image
  315. $this->prepareImageLayer($imageTemp, $image);
  316. imagecopy($imageTemp, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); // copy our initial image into the intermediate one
  317. imagedestroy($image); // release the initial image
  318. // get crop values and create a new initial image
  319. list($x, $y, $w, $h) = $this->cropExtra;
  320. // check if we can load a cropped version into ram
  321. if(self::checkMemoryForImage(array($w, $h, 3)) === false) {
  322. throw new WireException(basename($source) . " - not enough memory to load a cropped version for cropExtra");
  323. }
  324. $image = imagecreatetruecolor($w, $h);
  325. $this->prepareImageLayer($image, $imageTemp);
  326. imagecopy($image, $imageTemp, 0, 0, $x, $y, $w, $h);
  327. unset($x, $y, $w, $h);
  328. // now release the intermediate image and update settings
  329. imagedestroy($imageTemp);
  330. $this->setImageInfo(imagesx($image), imagesy($image));
  331. // $this->cropping = false; // ?? set this to prevent overhead with the following manipulation ??
  332. }
  333. // here we check for cropping, upscaling, sharpening
  334. // we get all dimensions at first, before any image operation !
  335. list($gdWidth, $gdHeight, $targetWidth, $targetHeight) = $this->getResizeDimensions($targetWidth, $targetHeight);
  336. $x1 = ($gdWidth / 2) - ($targetWidth / 2);
  337. $y1 = ($gdHeight / 2) - ($targetHeight / 2);
  338. $this->getCropDimensions($x1, $y1, $gdWidth, $targetWidth, $gdHeight, $targetHeight);
  339. // now lets check what operations are necessary:
  340. if($gdWidth == $targetWidth && $gdWidth == $this->image['width'] && $gdHeight == $this->image['height'] && $gdHeight == $targetHeight) {
  341. // this is the case if the original size is requested or a greater size but upscaling is set to false
  342. /*
  343. // since we have added support for crop-before-resize, we have to check for this
  344. if(!is_array($this->cropExtra)) {
  345. // the sourceimage is allready the targetimage, we can leave here
  346. @imagedestroy($image);
  347. return true;
  348. }
  349. // we have a cropped_before_resized image and need to save this version,
  350. // so we let pass it through without further manipulation, we just need to copy it into the final memimage called "$thumb"
  351. */
  352. // the current version is allready the desired result, we only may have to apply compression where possible
  353. $this->sharpening = 'none'; // we set sharpening to none
  354. if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) {
  355. throw new WireException(basename($source) . " - not enough memory to copy the final cropExtra");
  356. }
  357. $thumb = imagecreatetruecolor(imagesx($image), imagesy($image)); // create the final memory image
  358. $this->prepareImageLayer($thumb, $image);
  359. imagecopy($thumb, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); // copy our intermediate image into the final one
  360. } else if($gdWidth == $targetWidth && $gdHeight == $targetHeight) {
  361. // this is the case if we scale up or down _without_ cropping
  362. if(self::checkMemoryForImage(array($gdWidth, $gdHeight, 3)) === false) {
  363. throw new WireException(basename($source) . " - not enough memory to resize to the final image");
  364. }
  365. $thumb = imagecreatetruecolor($gdWidth, $gdHeight);
  366. $this->prepareImageLayer($thumb, $image);
  367. imagecopyresampled($thumb, $image, 0, 0, 0, 0, $gdWidth, $gdHeight, $this->image['width'], $this->image['height']);
  368. } else {
  369. // we have to scale up or down and to _crop_
  370. if(self::checkMemoryForImage(array($gdWidth, $gdHeight, 3)) === false) {
  371. throw new WireException(basename($source) . " - not enough memory to resize to the intermediate image");
  372. }
  373. $thumb2 = imagecreatetruecolor($gdWidth, $gdHeight);
  374. $this->prepareImageLayer($thumb2, $image);
  375. imagecopyresampled($thumb2, $image, 0, 0, 0, 0, $gdWidth, $gdHeight, $this->image['width'], $this->image['height']);
  376. if(self::checkMemoryForImage(array($targetWidth, $targetHeight, 3)) === false) {
  377. throw new WireException(basename($source) . " - not enough memory to crop to the final image");
  378. }
  379. $thumb = imagecreatetruecolor($targetWidth, $targetHeight);
  380. $this->prepareImageLayer($thumb, $image);
  381. imagecopyresampled($thumb, $thumb2, 0, 0, $x1, $y1, $targetWidth, $targetHeight, $targetWidth, $targetHeight);
  382. imagedestroy($thumb2);
  383. }
  384. // optionally apply sharpening to the final thumb
  385. if($this->sharpening && $this->sharpening != 'none') { // @horst
  386. if(IMAGETYPE_PNG != $this->imageType || ! $this->hasAlphaChannel()) {
  387. // is needed for the USM sharpening function to calculate the best sharpening params
  388. $this->usmValue = $this->calculateUSMfactor($targetWidth, $targetHeight);
  389. $thumb = $this->imSharpen($thumb, $this->sharpening);
  390. }
  391. }
  392. // write to file
  393. $result = false;
  394. switch($this->imageType) {
  395. case IMAGETYPE_GIF:
  396. // correct gamma from linearized 1.0 back to 2.0
  397. $this->gammaCorrection($thumb, false);
  398. $result = imagegif($thumb, $dest);
  399. break;
  400. case IMAGETYPE_PNG:
  401. if(!$this->hasAlphaChannel()) $this->gammaCorrection($thumb, false);
  402. // always use highest compression level for PNG (9) per @horst
  403. $result = imagepng($thumb, $dest, 9);
  404. break;
  405. case IMAGETYPE_JPEG:
  406. // correct gamma from linearized 1.0 back to 2.0
  407. $this->gammaCorrection($thumb, false);
  408. $result = imagejpeg($thumb, $dest, $this->quality);
  409. break;
  410. }
  411. if(isset($image) && is_resource($image)) @imagedestroy($image); // @horst
  412. if(isset($thumb) && is_resource($thumb)) @imagedestroy($thumb);
  413. if(isset($thumb2) && is_resource($thumb2)) @imagedestroy($thumb2);
  414. if(isset($image)) $image = null;
  415. if(isset($thumb)) $thumb = null;
  416. if(isset($thumb2)) $thumb2 = null;
  417. if($result === false) {
  418. if(is_file($dest)) @unlink($dest);
  419. return false;
  420. }
  421. unlink($source); // $source is equal to $this->filename
  422. rename($dest, $source); // $dest is the intermediate filename ({basename}_tmp{.ext})
  423. // @horst: if we've retrieved IPTC-Metadata from sourcefile, we write it back now
  424. $this->writeBackIptc();
  425. $this->loadImageInfo($this->filename, true);
  426. $this->modified = true;
  427. return true;
  428. }
  429. /**
  430. * Default IPTC Handling: if we've retrieved IPTC-Metadata from sourcefile, we write it into the variation here but we omit custom tags for internal use (@horst)
  431. *
  432. * @param bool $includeCustomTags, default is FALSE
  433. * @return bool
  434. *
  435. */
  436. public function writeBackIptc($includeCustomTags = false) {
  437. if(!$this->iptcRaw) return;
  438. $content = iptcembed($this->iptcPrepareData($includeCustomTags), $this->filename);
  439. if($content === false) return;
  440. $dest = preg_replace('/\.' . $this->extension . '$/', '_tmp.' . $this->extension, $this->filename);
  441. if(strlen($content) == @file_put_contents($dest, $content, LOCK_EX)) {
  442. // on success we replace the file
  443. unlink($this->filename);
  444. rename($dest, $this->filename);
  445. return true;
  446. } else {
  447. // it was created a temp diskfile but not with all data in it
  448. if(file_exists($dest)) @unlink($dest);
  449. return false;
  450. }
  451. }
  452. /**
  453. * Save the width and height of the image
  454. *
  455. */
  456. protected function setImageInfo($width, $height) {
  457. $this->image['width'] = $width;
  458. $this->image['height'] = $height;
  459. }
  460. /**
  461. * Return the image width
  462. *
  463. * @return int
  464. *
  465. */
  466. public function getWidth() { return $this->image['width']; }
  467. /**
  468. * Return the image height
  469. *
  470. * @return int
  471. *
  472. */
  473. public function getHeight() { return $this->image['height']; }
  474. /**
  475. * Return true if it's necessary to perform a resize with the given width/height, or false if not.
  476. *
  477. * @param int $targetWidth
  478. * @param int $targetHeight
  479. * @return bool
  480. * @deprecated no longer in use, left as comment for reference, TBD later
  481. *
  482. protected function isResizeNecessary($targetWidth, $targetHeight) {
  483. $img =& $this->image;
  484. $resize = true;
  485. if( (!$targetWidth || $img['width'] == $targetWidth) &&
  486. (!$targetHeight || $img['height'] == $targetHeight)) {
  487. $resize = false;
  488. } else if(!$this->upscaling && ($targetHeight >= $img['height'] && $targetWidth >= $img['width'])) {
  489. $resize = false;
  490. }
  491. return $resize;
  492. }
  493. */
  494. /**
  495. * Given a target height, return the proportional width for this image
  496. *
  497. */
  498. protected function getProportionalWidth($targetHeight) {
  499. $img =& $this->image;
  500. return ceil(($targetHeight / $img['height']) * $img['width']); // @horst
  501. }
  502. /**
  503. * Given a target width, return the proportional height for this image
  504. *
  505. */
  506. protected function getProportionalHeight($targetWidth) {
  507. $img =& $this->image;
  508. return ceil(($targetWidth / $img['width']) * $img['height']); // @horst
  509. }
  510. /**
  511. * Get an array of the 4 dimensions necessary to perform the resize
  512. *
  513. * Note: Some code used in this method is adapted from code found in comments at php.net for the GD functions
  514. *
  515. * Intended for use by the resize() method
  516. *
  517. * @param int $targetWidth
  518. * @param int $targetHeight
  519. * @return array
  520. *
  521. */
  522. protected function getResizeDimensions($targetWidth, $targetHeight) {
  523. $pWidth = $targetWidth;
  524. $pHeight = $targetHeight;
  525. $img =& $this->image;
  526. if(!$targetHeight) $targetHeight = round(($targetWidth / $img['width']) * $img['height']);
  527. if(!$targetWidth) $targetWidth = round(($targetHeight / $img['height']) * $img['width']);
  528. $originalTargetWidth = $targetWidth;
  529. $originalTargetHeight = $targetHeight;
  530. if($img['width'] < $img['height']) {
  531. $pHeight = $this->getProportionalHeight($targetWidth);
  532. } else {
  533. $pWidth = $this->getProportionalWidth($targetHeight);
  534. }
  535. if($pWidth < $targetWidth) {
  536. // if the proportional width is smaller than specified target width
  537. $pWidth = $targetWidth;
  538. $pHeight = $this->getProportionalHeight($targetWidth);
  539. }
  540. if($pHeight < $targetHeight) {
  541. // if the proportional height is smaller than specified target height
  542. $pHeight = $targetHeight;
  543. $pWidth = $this->getProportionalWidth($targetHeight);
  544. }
  545. if(!$this->upscaling) {
  546. // we are going to shoot for something smaller than the target
  547. while($pWidth > $img['width'] || $pHeight > $img['height']) {
  548. // favor the smallest dimension
  549. if($pWidth > $img['width']) {
  550. $pWidth = $img['width'];
  551. $pHeight = $this->getProportionalHeight($pWidth);
  552. }
  553. if($pHeight > $img['height']) {
  554. $pHeight = $img['height'];
  555. $pWidth = $this->getProportionalWidth($pHeight);
  556. }
  557. if($targetWidth > $pWidth) $targetWidth = $pWidth;
  558. if($targetHeight > $pHeight) $targetHeight = $pHeight;
  559. if(!$this->cropping) {
  560. $targetWidth = $pWidth;
  561. $targetHeight = $pHeight;
  562. }
  563. }
  564. }
  565. if(!$this->cropping) {
  566. // we will make the image smaller so that none of it gets cropped
  567. // this means we'll be adjusting either the targetWidth or targetHeight
  568. // till we have a suitable dimension
  569. if($pHeight > $originalTargetHeight) {
  570. $pHeight = $originalTargetHeight;
  571. $pWidth = $this->getProportionalWidth($pHeight);
  572. $targetWidth = $pWidth;
  573. $targetHeight = $pHeight;
  574. }
  575. if($pWidth > $originalTargetWidth) {
  576. $pWidth = $originalTargetWidth;
  577. $pHeight = $this->getProportionalHeight($pWidth);
  578. $targetWidth = $pWidth;
  579. $targetHeight = $pHeight;
  580. }
  581. }
  582. $r = array( 0 => (int) $pWidth,
  583. 1 => (int) $pHeight,
  584. 2 => (int) $targetWidth,
  585. 3 => (int) $targetHeight
  586. );
  587. return $r;
  588. }
  589. /**
  590. * Was the image modified?
  591. *
  592. * @return bool
  593. *
  594. */
  595. public function isModified() {
  596. return $this->modified;
  597. }
  598. /**
  599. * Given an unknown cropping value, return the validated internal representation of it
  600. *
  601. * @param string|bool|array $cropping
  602. * @return string|bool
  603. *
  604. */
  605. static public function croppingValue($cropping) {
  606. if(is_string($cropping)) {
  607. $cropping = strtolower($cropping);
  608. if(strpos($cropping, ',')) {
  609. $cropping = explode(',', $cropping);
  610. if(strpos($cropping[0], '%') !== false) $cropping[0] = round(min(100, max(0, $cropping[0]))) . '%';
  611. else $cropping[0] = (int) $cropping[0];
  612. if(strpos($cropping[1], '%') !== false) $cropping[1] = round(min(100, max(0, $cropping[1]))) . '%';
  613. else $cropping[1] = (int) $cropping[1];
  614. }
  615. }
  616. if($cropping === true) $cropping = true; // default, crop to center
  617. else if(!$cropping) $cropping = false;
  618. else if(is_array($cropping)) $cropping = $cropping; // already took care of it above
  619. else if(in_array($cropping, self::$croppingValues)) $cropping = array_search($cropping, self::$croppingValues);
  620. else if(array_key_exists($cropping, self::$croppingValues)) $cropping = $cropping;
  621. else $cropping = true; // unknown value or 'center', default to TRUE/center
  622. return $cropping;
  623. }
  624. /**
  625. * Given an unknown cropping value, return the string representation of it
  626. *
  627. * Okay for use in filenames
  628. *
  629. * @param string|bool|array $cropping
  630. * @return string
  631. *
  632. */
  633. static public function croppingValueStr($cropping) {
  634. $cropping = self::croppingValue($cropping);
  635. // crop name if custom center point is specified
  636. if(is_array($cropping)) {
  637. // p = percent, d = pixel dimension
  638. $cropping = (strpos($cropping[0], '%') !== false ? 'p' : 'd') . ((int) $cropping[0]) . 'x' . ((int) $cropping[1]);
  639. }
  640. // if crop is TRUE or FALSE, we don't reflect that in the filename, so make it blank
  641. if(is_bool($cropping)) $cropping = '';
  642. return $cropping;
  643. }
  644. /**
  645. * Turn on/off cropping and/or set cropping direction
  646. *
  647. * @param bool|string|array $cropping Specify one of: northwest, north, northeast, west, center, east, southwest, south, southeast.
  648. * Or a string of: 50%,50% (x and y percentages to crop from)
  649. * Or an array('50%', '50%')
  650. * Or to disable cropping, specify boolean false. To enable cropping with default (center), you may also specify boolean true.
  651. * @return $this
  652. *
  653. */
  654. public function setCropping($cropping = true) {
  655. $this->cropping = self::croppingValue($cropping);
  656. return $this;
  657. }
  658. /**
  659. * Set values for cropExtra rectangle, which enables cropping before resizing
  660. *
  661. * Added by @horst
  662. *
  663. * @param array $value containing 4 params (x y w h) indexed or associative
  664. * @return $this
  665. * @throws WireException when given invalid value
  666. *
  667. */
  668. public function setCropExtra($value) {
  669. $this->cropExtra = null;
  670. if(!is_array($value) || 4 != count($value)) {
  671. throw new WireException('Missing or wrong param Array for ImageSizer-cropExtra!');
  672. }
  673. if(array_keys($value) === range(0, count($value) - 1)) {
  674. // we have a zerobased sequential array, we assume this order: x y w h
  675. list($x, $y, $w, $h) = $value;
  676. } else {
  677. // check for associative array
  678. foreach(array('x','y','w','h') as $v) {
  679. if(isset($value[$v])) $$v = $value[$v];
  680. }
  681. }
  682. foreach(array('x', 'y', 'w', 'h') as $k) {
  683. $v = isset($$k) ? $$k : -1;
  684. if(!is_int($v) || $v < 0) throw new WireException("Missing or wrong param $k for ImageSizer-cropExtra!");
  685. if(('w' == $k || 'h' == $k) && 0 == $v) throw new WireException("Wrong param $k for ImageSizer-cropExtra!");
  686. }
  687. $this->cropExtra = array($x, $y, $w, $h);
  688. return $this;
  689. }
  690. /**
  691. * Set the image quality 1-100, where 100 is highest quality
  692. *
  693. * @param int $n
  694. * @return $this
  695. *
  696. */
  697. public function setQuality($n) {
  698. $n = (int) $n;
  699. if($n < 1) $n = 1;
  700. if($n > 100) $n = 100;
  701. $this->quality = (int) $n;
  702. return $this;
  703. }
  704. /**
  705. * Given an unknown sharpening value, return the string representation of it
  706. *
  707. * Okay for use in filenames. Method added by @horst
  708. *
  709. * @param string|bool $value
  710. * @param bool $short
  711. * @return string
  712. *
  713. */
  714. static public function sharpeningValueStr($value, $short = false) {
  715. $sharpeningValues = self::$sharpeningValues;
  716. if(is_string($value) && in_array(strtolower($value), $sharpeningValues)) {
  717. $ret = strtolower($value);
  718. } else if(is_int($value) && isset($sharpeningValues[$value])) {
  719. $ret = $sharpeningValues[$value];
  720. } else if(is_bool($value)) {
  721. $ret = $value ? "soft" : "none";
  722. } else {
  723. // sharpening is unknown, return empty string
  724. return '';
  725. }
  726. if(!$short) return $ret; // return name
  727. $flip = array_flip($sharpeningValues);
  728. return 's' . $flip[$ret]; // return char s appended with the numbered index
  729. }
  730. /**
  731. * Set sharpening value: blank (for none), soft, medium, or strong
  732. *
  733. * @param mixed $value
  734. * @return $this
  735. * @throws WireException
  736. *
  737. */
  738. public function setSharpening($value) {
  739. if(is_string($value) && in_array(strtolower($value), self::$sharpeningValues)) {
  740. $ret = strtolower($value);
  741. } else if(is_int($value) && isset(self::$sharpeningValues[$value])) {
  742. $ret = self::$sharpeningValues[$value];
  743. } else if(is_bool($value)) {
  744. $ret = $value ? "soft" : "none";
  745. } else {
  746. throw new WireException("Unknown value for sharpening");
  747. }
  748. $this->sharpening = $ret;
  749. return $this;
  750. }
  751. /**
  752. * Turn on/off auto rotation
  753. *
  754. * @param bool value Whether to auto-rotate or not (default = true)
  755. * @return $this
  756. *
  757. */
  758. public function setAutoRotation($value = true) {
  759. $this->autoRotation = $this->getBooleanValue($value);
  760. return $this;
  761. }
  762. /**
  763. * Turn on/off upscaling
  764. *
  765. * @param bool $value Whether to upscale or not (default = true)
  766. * @return $this
  767. *
  768. */
  769. public function setUpscaling($value = true) {
  770. $this->upscaling = $this->getBooleanValue($value);
  771. return $this;
  772. }
  773. /**
  774. * Set default gamma value: 0.5 - 4.0 | -1
  775. *
  776. * @param float|int $value 0.5 to 4.0 or -1 to disable
  777. * @return $this
  778. * @throws WireException when given invalid value
  779. *
  780. */
  781. public function setDefaultGamma($value = 2.0) {
  782. if($value === -1 || ($value >= 0.5 && $value <= 4.0)) {
  783. $this->defaultGamma = $value;
  784. } else {
  785. throw new WireException('Invalid defaultGamma value - must be 0.5 - 4.0 or -1 to disable gammacorrection');
  786. }
  787. return $this;
  788. }
  789. /**
  790. * Set a time limit for manipulating one image (default is 30)
  791. *
  792. * If specified time limit is less than PHP's max_execution_time, then PHP's setting will be used instead.
  793. *
  794. * @param int $value 10 to 60 recommended, default is 30
  795. * @return this
  796. *
  797. */
  798. public function setTimeLimit($value = 30) {
  799. // imagesizer can get invoked from different locations, including those that are inside of loops
  800. // like the wire/modules/Inputfield/InputfieldFile/InputfieldFile.module :: ___renderList() method
  801. $prevLimit = ini_get('max_execution_time');
  802. // if unlimited execution time, no need to introduce one
  803. if(!$prevLimit) return;
  804. // don't override a previously set high time limit, just start over with it
  805. $timeLimit = (int) ($prevLimit > $value ? $prevLimit : $value);
  806. // restart time limit
  807. set_time_limit($timeLimit);
  808. return $this;
  809. }
  810. /**
  811. * Set scale for hidpi (2.0=hidpi, 1.0=normal, or other value if preferred)
  812. *
  813. * @param float $scale
  814. * @return $this
  815. *
  816. */
  817. public function setScale($scale) {
  818. $this->scale = (float) $scale;
  819. return $this;
  820. }
  821. /**
  822. * Enable hidpi mode?
  823. *
  824. * Just a shortcut for calling $this->scale()
  825. *
  826. * @param bool $hidpi True or false (default=true)
  827. * @return this
  828. *
  829. */
  830. public function setHidpi($hidpi = true) {
  831. return $this->setScale($hidpi ? 2.0 : 1.0);
  832. }
  833. /**
  834. * Set rotation degrees
  835. *
  836. * Specify one of: -270, -180, -90, 90, 180, 270
  837. *
  838. * @param $degrees
  839. * @return this
  840. *
  841. */
  842. public function setRotate($degrees) {
  843. $valid = array(-270, -180, -90, 90, 180, 270);
  844. $degrees = (int) $degrees;
  845. if(in_array($degrees, $valid)) $this->rotate = $degrees;
  846. return $this;
  847. }
  848. /**
  849. * Set flip
  850. *
  851. * Specify one of: 'vertical' or 'horizontal', also accepts
  852. * shorter versions like, 'vert', 'horiz', 'v', 'h', etc.
  853. *
  854. * @param $flip
  855. * @return this
  856. *
  857. */
  858. public function setFlip($flip) {
  859. $flip = strtolower(substr($flip, 0, 1));
  860. if($flip == 'v' || $flip == 'h') $this->flip = $flip;
  861. return $this;
  862. }
  863. /**
  864. * Alternative to the above set* functions where you specify all in an array
  865. *
  866. * @param array $options May contain the following (show with default values):
  867. * 'quality' => 90,
  868. * 'cropping' => true,
  869. * 'upscaling' => true,
  870. * 'autoRotation' => true,
  871. * 'sharpening' => 'soft' (none|soft|medium|string)
  872. * 'scale' => 1.0 (use 2.0 for hidpi or 1.0 for normal-default)
  873. * 'hidpi' => false, (alternative to scale, specify true to enable hidpi)
  874. * 'rotate' => 0 (90, 180, 270 or negative versions of those)
  875. * 'flip' => '', (vertical|horizontal)
  876. * @return $this
  877. *
  878. */
  879. public function setOptions(array $options) {
  880. foreach($options as $key => $value) {
  881. switch($key) {
  882. case 'autoRotation': $this->setAutoRotation($value); break;
  883. case 'upscaling': $this->setUpscaling($value); break;
  884. case 'sharpening': $this->setSharpening($value); break;
  885. case 'quality': $this->setQuality($value); break;
  886. case 'cropping': $this->setCropping($value); break;
  887. case 'defaultGamma': $this->setDefaultGamma($value); break;
  888. case 'cropExtra': $this->setCropExtra($value); break;
  889. case 'scale': $this->setScale($value); break;
  890. case 'hidpi': $this->setHidpi($value); break;
  891. case 'rotate': $this->setRotate($value); break;
  892. case 'flip': $this->setFlip($value); break;
  893. default:
  894. // unknown or 3rd party option
  895. $this->options[$key] = $value;
  896. }
  897. }
  898. return $this;
  899. }
  900. /**
  901. * Given a value, convert it to a boolean.
  902. *
  903. * Value can be string representations like: 0, 1 off, on, yes, no, y, n, false, true.
  904. *
  905. * @param bool|int|string $value
  906. * @return bool
  907. *
  908. */
  909. protected function getBooleanValue($value) {
  910. if(in_array(strtolower($value), array('0', 'off', 'false', 'no', 'n', 'none'))) return false;
  911. return ((int) $value) > 0;
  912. }
  913. /**
  914. * Return an array of the current options
  915. *
  916. * @return array
  917. *
  918. */
  919. public function getOptions() {
  920. $options = array(
  921. 'quality' => $this->quality,
  922. 'cropping' => $this->cropping,
  923. 'upscaling' => $this->upscaling,
  924. 'autoRotation' => $this->autoRotation,
  925. 'sharpening' => $this->sharpening,
  926. 'defaultGamma' => $this->defaultGamma,
  927. 'cropExtra' => $this->cropExtra,
  928. 'scale' => $this->scale,
  929. );
  930. $options = array_merge($this->options, $options);
  931. return $options;
  932. }
  933. public function __get($key) {
  934. $keys = array(
  935. 'filename',
  936. 'extension',
  937. 'imageType',
  938. 'image',
  939. 'modified',
  940. 'supportedImageTypes',
  941. 'info',
  942. 'iptcRaw',
  943. 'validIptcTags',
  944. 'cropExtra',
  945. 'options'
  946. );
  947. if(in_array($key, $keys)) return $this->$key;
  948. if(in_array($key, $this->optionNames)) return $this->$key;
  949. if(isset($this->options[$key])) return $this->options[$key];
  950. return null;
  951. }
  952. /**
  953. * Return the filename
  954. *
  955. * @return string
  956. *
  957. */
  958. public function getFilename() {
  959. return $this->filename;
  960. }
  961. /**
  962. * Return the file extension
  963. *
  964. * @return string
  965. *
  966. */
  967. public function getExtension() {
  968. return $this->extension;
  969. }
  970. /**
  971. * Return the image type constant
  972. *
  973. * @return string
  974. *
  975. */
  976. public function getImageType() {
  977. return $this->imageType;
  978. }
  979. /**
  980. * Prepare IPTC data (@horst)
  981. *
  982. * @param bool $includeCustomTags (default=false)
  983. * @return string $iptcNew
  984. *
  985. */
  986. protected function iptcPrepareData($includeCustomTags = false) {
  987. $customTags = array('213','214','215','216','217');
  988. $iptcNew = '';
  989. foreach(array_keys($this->iptcRaw) as $s) {
  990. $tag = substr($s, 2);
  991. if(!$includeCustomTags && in_array($tag, $customTags)) continue;
  992. if(substr($s, 0, 1) == '2' && in_array($tag, $this->validIptcTags) && is_array($this->iptcRaw[$s])) {
  993. foreach($this->iptcRaw[$s] as $row) {
  994. $iptcNew .= $this->iptcMakeTag(2, $tag, $row);
  995. }
  996. }
  997. }
  998. return $iptcNew;
  999. }
  1000. /**
  1001. * Make IPTC tag (@horst)
  1002. *
  1003. * @param string $rec
  1004. * @param string $dat
  1005. * @param string $val
  1006. * @return string
  1007. *
  1008. */
  1009. protected function iptcMakeTag($rec, $dat, $val) {
  1010. $len = strlen($val);
  1011. if($len < 0x8000) {
  1012. return @chr(0x1c) . @chr($rec) . @chr($dat) .
  1013. chr($len >> 8) .
  1014. chr($len & 0xff) .
  1015. $val;
  1016. } else {
  1017. return chr(0x1c) . chr($rec) . chr($dat) .
  1018. chr(0x80) . chr(0x04) .
  1019. chr(($len >> 24) & 0xff) .
  1020. chr(($len >> 16) & 0xff) .
  1021. chr(($len >> 8 ) & 0xff) .
  1022. chr(($len ) & 0xff) .
  1023. $val;
  1024. }
  1025. }
  1026. /**
  1027. * Rotate image (@horst)
  1028. *
  1029. * @param resource $im
  1030. * @param int $degree
  1031. * @return resource
  1032. *
  1033. */
  1034. protected function imRotate($im, $degree) {
  1035. $degree = (is_float($degree) || is_int($degree)) && $degree > -361 && $degree < 361 ? $degree : false;
  1036. if($degree === false) return $im;
  1037. if(in_array($degree, array(-360, 0, 360))) return $im;
  1038. return @imagerotate($im, $degree, imagecolorallocate($im, 0, 0, 0));
  1039. }
  1040. /**
  1041. * Flip image (@horst)
  1042. *
  1043. * @param resource $im
  1044. * @param bool $vertical (default = false)
  1045. * @return resource
  1046. *
  1047. */
  1048. protected function imFlip($im, $vertical = false) {
  1049. $sx = imagesx($im);
  1050. $sy = imagesy($im);
  1051. $im2 = @imagecreatetruecolor($sx, $sy);
  1052. if($vertical === true) {
  1053. @imagecopyresampled($im2, $im, 0, 0, 0, ($sy-1), $sx, $sy, $sx, 0-$sy);
  1054. } else {
  1055. @imagecopyresampled($im2, $im, 0, 0, ($sx-1), 0, $sx, $sy, 0-$sx, $sy);
  1056. }
  1057. return $im2;
  1058. }
  1059. /**
  1060. * Sharpen image (@horst)
  1061. *
  1062. * @param resource $im
  1063. * @param string $mode May be: none | soft | medium | strong
  1064. * @return resource
  1065. *
  1066. */
  1067. protected function imSharpen($im, $mode) {
  1068. // check if we have to use an individual value for "useUSM"
  1069. if(isset($this->options['useUSM'])) {
  1070. $this->useUSM = $this->getBooleanValue($this->options['useUSM']);
  1071. }
  1072. // due to a bug in PHP's bundled GD-Lib with the function imageconvolution in some PHP versions
  1073. // we have to bypass this for those who have to run on this PHP versions
  1074. // see: https://bugs.php.net/bug.php?id=66714
  1075. // and here under GD: http://php.net/ChangeLog-5.php#5.5.11
  1076. $buggyPHP = (version_compare(phpversion(), '5.5.8', '>') && version_compare(phpversion(), '5.5.11', '<')) ? true : false;
  1077. // USM method is used for buggy PHP versions
  1078. // for regular versions it can be omitted per: useUSM = false passes as pageimage option
  1079. // or set in the site/config.php under $config->imageSizerOptions: 'useUSM' => false | true
  1080. if($buggyPHP || $this->useUSM) {
  1081. switch($mode) {
  1082. case 'none':
  1083. return $im;
  1084. break;
  1085. case 'strong':
  1086. $amount=160;
  1087. $radius=1.0;
  1088. $threshold=7;
  1089. break;
  1090. case 'medium':
  1091. $amount=130;
  1092. $radius=0.75;
  1093. $threshold=7;
  1094. break;
  1095. case 'soft':
  1096. default:
  1097. $amount=100;
  1098. $radius=0.5;
  1099. $threshold=7;
  1100. break;
  1101. }
  1102. // calculate the final amount according to the usmValue
  1103. $this->usmValue = $this->usmValue < 0 ? 0 : ($this->usmValue > 100 ? 100 : $this->usmValue);
  1104. if(0 == $this->usmValue) return $im;
  1105. $amount = intval($amount / 100 * $this->usmValue);
  1106. // apply unsharp mask filter
  1107. return $this->UnsharpMask($im, $amount, $radius, $threshold);
  1108. }
  1109. // if we do not use USM, we use our default sharpening method,
  1110. // entirely based on GDs imageconvolution
  1111. switch($mode) {
  1112. case 'none':
  1113. return $im;
  1114. break;
  1115. case 'strong':
  1116. $sharpenMatrix = array(
  1117. array( -1.2, -1, -1.2 ),
  1118. array( -1, 16, -1 ),
  1119. array( -1.2, -1, -1.2 )
  1120. );
  1121. break;
  1122. case 'medium':
  1123. $sharpenMatrix = array(
  1124. array( -1.1, -1, -1.1 ),
  1125. array( -1, 20, -1 ),
  1126. array( -1.1, -1, -1.1 )
  1127. );
  1128. break;
  1129. case 'soft':
  1130. default:
  1131. $sharpenMatrix = array(
  1132. array( -1, -1, -1 ),
  1133. array( -1, 24, -1 ),
  1134. array( -1, -1, -1 )
  1135. );
  1136. break;
  1137. }
  1138. // calculate the sharpen divisor
  1139. $divisor = array_sum(array_map('array_sum', $sharpenMatrix));
  1140. $offset = 0;
  1141. if(!imageconvolution($im, $sharpenMatrix, $divisor, $offset)) return false; // TODO 4 -c errorhandling: Throw WireException?
  1142. return $im;
  1143. }
  1144. /**
  1145. * Check orientation (@horst)
  1146. *
  1147. * @param array
  1148. * @return bool
  1149. *
  1150. */
  1151. protected function checkOrientation(&$correctionArray) {
  1152. // first value is rotation-degree and second value is flip-mode: 0=NONE | 1=HORIZONTAL | 2=VERTICAL
  1153. $corrections = array(
  1154. '1' => array( 0, 0),
  1155. '2' => array( 0, 1),
  1156. '3' => array(180, 0),
  1157. '4' => array( 0, 2),
  1158. '5' => array(270, 1),
  1159. '6' => array(270, 0),
  1160. '7' => array( 90, 1),
  1161. '8' => array( 90, 0)
  1162. );
  1163. if(!function_exists('exif_read_data')) return false;
  1164. $exif = @exif_read_data($this->filename, 'IFD0');
  1165. if(!is_array($exif) || !isset($exif['Orientation']) || !in_array(strval($exif['Orientation']), array_keys($corrections))) return false;
  1166. $correctionArray = $corrections[strval($exif['Orientation'])];
  1167. return true;
  1168. }
  1169. /**
  1170. * Check orientation (@horst)
  1171. *
  1172. * @param mixed $image, pageimage or filename
  1173. * @param mixed $correctionArray, null or array by reference
  1174. * @return bool
  1175. *
  1176. */
  1177. static public function imageIsRotated($image, &$correctionArray = null) {
  1178. if($image instanceof Pageimage) {
  1179. $fn = $image->filename;
  1180. } elseif(is_readable($image)) {
  1181. $fn = $image;
  1182. } else {
  1183. return null;
  1184. }
  1185. // first value is rotation-degree and second value is flip-mode: 0=NONE | 1=HORIZONTAL | 2=VERTICAL
  1186. $corrections = array(
  1187. '1' => array( 0, 0),
  1188. '2' => array( 0, 1),
  1189. '3' => array(180, 0),
  1190. '4' => array( 0, 2),
  1191. '5' => array(270, 1),
  1192. '6' => array(270, 0),
  1193. '7' => array( 90, 1),
  1194. '8' => array( 90, 0)
  1195. );
  1196. if(!function_exists('exif_read_data')) return null;
  1197. $exif = @exif_read_data($fn, 'IFD0');
  1198. if(!is_array($exif) || !isset($exif['Orientation']) || !in_array(strval($exif['Orientation']), array_keys($corrections))) return null;
  1199. $correctionArray = $corrections[strval($exif['Orientation'])];
  1200. return $correctionArray[0] > 0;
  1201. }
  1202. /**
  1203. * Check if GIF-image is animated or not (@horst)
  1204. *
  1205. * @param mixed $image, pageimage or filename
  1206. * @return bool
  1207. *
  1208. */
  1209. static public function imageIsAnimatedGif($image) {
  1210. if($image instanceof Pageimage) {
  1211. $fn = $image->filename;
  1212. } elseif(is_readable($image)) {
  1213. $fn = $image;
  1214. } else {
  1215. return null;
  1216. }
  1217. $info = getimagesize($fn);
  1218. if(IMAGETYPE_GIF != $info[2]) return false;
  1219. if(self::checkMemoryForImage(array($info[0], $info[1]))) {
  1220. return (bool) preg_match('/\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)/s', file_get_contents($fn));
  1221. }
  1222. // we have not enough free memory to load the complete image at once, so we do it in chunks
  1223. if(!($fh = @fopen($fn, 'rb'))) {
  1224. return null;
  1225. }
  1226. $count = 0;
  1227. while(!feof($fh) && $count < 2) {
  1228. $chunk = fread($fh, 1024 * 100); //read 100kb at a time
  1229. $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
  1230. }
  1231. fclose($fh);
  1232. return $count > 1;
  1233. }
  1234. /**
  1235. * Possibility to clean IPTC data, also for original images (@horst)
  1236. *
  1237. * @param mixed $image, pageimage or filename
  1238. * @return mixed, null or bool
  1239. *
  1240. */
  1241. static public function imageResetIPTC($image) {
  1242. if($image instanceof Pageimage) {
  1243. $fn = $image->filename;
  1244. } elseif(is_readable($image)) {
  1245. $fn = $image;
  1246. } else {
  1247. return null;
  1248. }
  1249. $is = new ImageSizer($fn);
  1250. $result = false !== $is->writeBackIptc() ? true : false;
  1251. unset($is);
  1252. return $result;
  1253. }
  1254. /**
  1255. * Check for alphachannel in PNGs
  1256. *
  1257. * This method by Horst, who also credits initial code as coming from the FPDF project:
  1258. * http://www.fpdf.org/
  1259. *
  1260. * @return bool
  1261. *
  1262. */
  1263. protected function hasAlphaChannel() {
  1264. $errors = array();
  1265. $a = array();
  1266. $f = @fopen($this->filename,'rb');
  1267. if($f === false) return false;
  1268. // Check signature
  1269. if(@fread($f,8) != chr(137) .'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) {
  1270. @fclose($f);
  1271. return false;
  1272. }
  1273. // Read header chunk
  1274. @fread($f, 4);
  1275. if(@fread($f, 4) != 'IHDR') {
  1276. @fclose($f);
  1277. return false;
  1278. }
  1279. $a['width'] = $this->freadint($f);
  1280. $a['height'] = $this->freadint($f);
  1281. $a['bits'] = ord(@fread($f, 1));
  1282. $a['alpha'] = false;
  1283. $ct = ord(@fread($f, 1));
  1284. if($ct == 0) {
  1285. $a['channels'] = 1;
  1286. $a['colspace'] = 'DeviceGray';
  1287. } else if($ct == 2) {
  1288. $a['channels'] = 3;
  1289. $a['colspace'] = 'DeviceRGB';
  1290. } else if($ct == 3) {
  1291. $a['channels'] = 1;
  1292. $a['colspace'] = 'Indexed';
  1293. } else {
  1294. $a['channels'] = $ct;
  1295. $a['colspace'] = 'DeviceRGB';
  1296. $a['alpha'] = true; // alphatransparency in 24bit images !
  1297. }
  1298. if($a['alpha']) return true; // early return
  1299. if(ord(@fread($f, 1)) != 0) $errors[] = 'Unknown compression method!';
  1300. if(ord(@fread($f, 1)) != 0) $errors[] = 'Unknown filter method!';
  1301. if(ord(@fread($f, 1)) != 0) $errors[] = 'Interlacing not supported!';
  1302. // Scan chunks looking for palette, transparency and image data
  1303. // http://www.w3.org/TR/2003/REC-PNG-20031110/#table53
  1304. // http://www.libpng.org/pub/png/book/chapter11.html#png.ch11.div.6
  1305. @fread($f, 4);
  1306. $pal = '';
  1307. $trns = '';
  1308. $counter = 0;
  1309. do {
  1310. $n = $this->freadint($f);
  1311. $counter += $n;
  1312. $type = @fread($f, 4);
  1313. if($type == 'PLTE') {
  1314. // Read palette
  1315. $pal = @fread($f, $n);
  1316. @fread($f, 4);
  1317. } else if($type == 'tRNS') {
  1318. // Read transparency info
  1319. $t = @fread($f, $n);
  1320. if($ct == 0) {
  1321. $trns = array(ord(substr($t, 1, 1)));
  1322. } else if($ct == 2) {
  1323. $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1)));
  1324. } else {
  1325. $pos = strpos($t, chr(0));
  1326. if(is_int($pos)) {
  1327. $trns = array($pos);
  1328. }
  1329. }
  1330. @fread($f, 4);
  1331. break;
  1332. } else if($type == 'IEND' || $type == 'IDAT' || $counter >= 2048) {
  1333. break;
  1334. } else {
  1335. fread($f, $n + 4);
  1336. }
  1337. } while($n);
  1338. @fclose($f);
  1339. if($a['colspace'] == 'Indexed' and empty($pal)) $errors[] = 'Missing palette!';
  1340. if(count($errors) > 0) $a['errors'] = $errors;
  1341. if(!empty($trns)) $a['alpha'] = true; // alphatransparency in 8bit images !
  1342. return $a['alpha'];
  1343. }
  1344. /**
  1345. * apply GammaCorrection to an image (@horst)
  1346. *
  1347. * with mode = true it linearizes an image to 1
  1348. * with mode = false it set it back to the originating gamma value
  1349. *
  1350. * @param GD-image-resource $image
  1351. * @param boolean $mode
  1352. *
  1353. */
  1354. protected function gammaCorrection(&$image, $mode) {
  1355. if(-1 == $this->defaultGamma || !is_bool($mode)) return;
  1356. if($mode) {
  1357. // linearizes to 1.0
  1358. if(imagegammacorrect($image, $this->defaultGamma, 1.0)) $this->gammaLinearized = true;
  1359. } else {
  1360. if(!isset($this->gammaLinearized) || !$this->gammaLinearized) return;
  1361. // switch back to original Gamma
  1362. if(imagegammacorrect($image, 1.0, $this->defaultGamma)) unset($this->gammaLinearized);
  1363. }
  1364. }
  1365. /**
  1366. * reads a 4-byte integer from file (@horst)
  1367. *
  1368. * @param filepointer
  1369. * @return mixed
  1370. *
  1371. */
  1372. protected function freadint(&$f) {
  1373. $i = ord(@fread($f, 1)) << 24;
  1374. $i += ord(@fread($f, 1)) << 16;
  1375. $i += ord(@fread($f, 1)) << 8;
  1376. $i += ord(@fread($f, 1));
  1377. return $i;
  1378. }
  1379. /**
  1380. * Set whether the image was modified
  1381. *
  1382. * Public so that other modules/hooks can adjust this property if needed.
  1383. * Not for general API use
  1384. *
  1385. * @param bool $modified
  1386. * @return this
  1387. *
  1388. */
  1389. public function setModified($modified) {
  1390. $this->modified = $modified ? true : false;
  1391. return $this;
  1392. }
  1393. /**
  1394. * Unsharp Mask for PHP - version 2.1.1
  1395. *
  1396. * Unsharp mask algorithm by Torstein Hønsi 2003-07.
  1397. * thoensi_at_netcom_dot_no.
  1398. * Please leave this notice.
  1399. *
  1400. * http://vikjavev.no/computing/ump.php
  1401. *
  1402. */
  1403. protected function unsharpMask($img, $amount, $radius, $threshold) {
  1404. // Attempt to calibrate the parameters to Photoshop:
  1405. if($amount > 500) $amount = 500;
  1406. $amount = $amount * 0.016;
  1407. if($radius > 50) $radius = 50;
  1408. $radius = $radius * 2;
  1409. if($threshold > 255) $threshold = 255;
  1410. $radius = abs(round($radius)); // Only integers make sense.
  1411. if($radius == 0) {
  1412. return $img;
  1413. }
  1414. $w = imagesx($img);
  1415. $h = imagesy($img);
  1416. $imgCanvas = imagecreatetruecolor($w, $h);
  1417. $imgBlur = imagecreatetruecolor($w, $h);
  1418. // due to a bug in PHP's bundled GD-Lib with the function imageconvolution in some PHP versions
  1419. // we have to bypass this for those who have to run on this PHP versions
  1420. // see: https://bugs.php.net/bug.php?id=66714
  1421. // and here under GD: http://php.net/ChangeLog-5.php#5.5.11
  1422. $buggyPHP = (version_compare(phpversion(), '5.5.8', '>') && version_compare(phpversion(), '5.5.11', '<')) ? true : false;
  1423. // Gaussian blur matrix:
  1424. //
  1425. // 1 2 1
  1426. // 2 4 2
  1427. // 1 2 1
  1428. //
  1429. //////////////////////////////////////////////////
  1430. if(function_exists('imageconvolution') && !$buggyPHP) {
  1431. $matrix = array(
  1432. array( 1, 2, 1 ),
  1433. array( 2, 4, 2 ),
  1434. array( 1, 2, 1 )
  1435. );
  1436. imagecopy ($imgBlur, $img, 0, 0, 0, 0, $w, $h);
  1437. imageconvolution($imgBlur, $matrix, 16, 0);
  1438. } else {
  1439. // Move copies of the image around one pixel at the time and merge them with weight
  1440. // according to the matrix. The same matrix is simply repeated for higher radii.
  1441. for ($i = 0; $i < $radius; $i++) {
  1442. imagecopy ($imgBlur, $img, 0, 0, 1, 0, $w - 1, $h); // left
  1443. imagecopymerge ($imgBlur, $img, 1, 0, 0, 0, $w, $h, 50); // right
  1444. imagecopymerge ($imgBlur, $img, 0, 0, 0, 0, $w, $h, 50); // center
  1445. imagecopy ($imgCanvas, $imgBlur, 0, 0, 0, 0, $w, $h);
  1446. imagecopymerge ($imgBlur, $imgCanvas, 0, 0, 0, 1, $w, $h - 1, 33.33333 ); // up
  1447. imagecopymerge ($imgBlur, $imgCanvas, 0, 1, 0, 0, $w, $h, 25); // down
  1448. }
  1449. }
  1450. if($threshold>0) {
  1451. // Calculate the difference between the blurred pixels and the original
  1452. // and set the pixels
  1453. for($x = 0; $x < $w-1; $x++) { // each row
  1454. for($y = 0; $y < $h; $y++) { // each pixel
  1455. $rgbOrig = ImageColorAt($img, $x, $y);
  1456. $rOrig = (($rgbOrig >> 16) & 0xFF);
  1457. $gOrig = (($rgbOrig >> 8) & 0xFF);
  1458. $bOrig = ($rgbOrig & 0xFF);
  1459. $rgbBlur = ImageColorAt($imgBlur, $x, $y);
  1460. $rBlur = (($rgbBlur >> 16) & 0xFF);
  1461. $gBlur = (($rgbBlur >> 8) & 0xFF);
  1462. $bBlur = ($rgbBlur & 0xFF);
  1463. // When the masked pixels differ less from the original
  1464. // than the threshold specifies, they are set to their original value.
  1465. $rNew = (abs($rOrig - $rBlur) >= $threshold)
  1466. ? max(0, min(255, ($amount * ($rOrig - $rBlur)) + $rOrig))
  1467. : $rOrig;
  1468. $gNew = (abs($gOrig - $gBlur) >= $threshold)
  1469. ? max(0, min(255, ($amount * ($gOrig - $gBlur)) + $gOrig))
  1470. : $gOrig;
  1471. $bNew = (abs($bOrig - $bBlur) >= $threshold)
  1472. ? max(0, min(255, ($amount * ($bOrig - $bBlur)) + $bOrig))
  1473. : $bOrig;
  1474. if(($rOrig != $rNew) || ($gOrig != $gNew) || ($bOrig != $bNew)) {
  1475. $pixCol = ImageColorAllocate($img, $rNew, $gNew, $bNew);
  1476. ImageSetPixel($img, $x, $y, $pixCol);
  1477. }
  1478. }
  1479. }
  1480. } else {
  1481. for($x = 0; $x < $w; $x++) { // each row
  1482. for($y = 0; $y < $h; $y++) { // each pixel
  1483. $rgbOrig = ImageColorAt($img, $x, $y);
  1484. $rOrig = (($rgbOrig >> 16) & 0xFF);
  1485. $gOrig = (($rgbOrig >> 8) & 0xFF);
  1486. $bOrig = ($rgbOrig & 0xFF);
  1487. $rgbBlur = ImageColorAt($imgBlur, $x, $y);
  1488. $rBlur = (($rgbBlur >> 16) & 0xFF);
  1489. $gBlur = (($rgbBlur >> 8) & 0xFF);
  1490. $bBlur = ($rgbBlur & 0xFF);
  1491. $rNew = ($amount * ($rOrig - $rBlur)) + $rOrig;
  1492. if($rNew>255) {
  1493. $rNew=255;
  1494. } else if($rNew<0) {
  1495. $rNew=0;
  1496. }
  1497. $gNew = ($amount * ($gOrig - $gBlur)) + $gOrig;
  1498. if($gNew>255) {
  1499. $gNew=255;
  1500. }
  1501. else if($gNew<0) {
  1502. $gNew=0;
  1503. }
  1504. $bNew = ($amount * ($bOrig - $bBlur)) + $bOrig;
  1505. if($bNew>255) {
  1506. $bNew=255;
  1507. }
  1508. else if($bNew<0) {
  1509. $bNew=0;
  1510. }
  1511. $rgbNew = ($rNew << 16) + ($gNew <<8) + $bNew;
  1512. ImageSetPixel($img, $x, $y, $rgbNew);
  1513. }
  1514. }
  1515. }
  1516. imagedestroy($imgCanvas);
  1517. imagedestroy($imgBlur);
  1518. return $img;
  1519. }
  1520. /**
  1521. * return an integer value indicating how much an image should be sharpened
  1522. * according to resizing scalevalue and absolute target dimensions
  1523. *
  1524. * @param mixed $targetWidth width of the targetimage
  1525. * @param mixed $targetHeight height of the targetimage
  1526. * @param mixed $origWidth
  1527. * @param mixed $origHeight
  1528. * @return int
  1529. *
  1530. */
  1531. protected function calculateUSMfactor($targetWidth, $targetHeight, $origWidth = null, $origHeight = null) {
  1532. if(null === $origWidth) $origWidth = $this->getWidth();
  1533. if(null === $origHeight) $origHeight = $this->getHeight();
  1534. $w = ceil($targ

Large files files are truncated, but you can click here to view the full file