/lib/vendor/claviska/simpleimage/src/claviska/SimpleImage.php

https://github.com/agentejo/cockpit · PHP · 1855 lines · 959 code · 205 blank · 691 comment · 78 complexity · c57076e8412f81cd2368a4ac17a0c0c3 MD5 · raw file

  1. <?php
  2. //
  3. // SimpleImage
  4. //
  5. // A PHP class that makes working with images as simple as possible.
  6. //
  7. // Developed and maintained by Cory LaViska <https://github.com/claviska>.
  8. //
  9. // Copyright A Beautiful Site, LLC.
  10. //
  11. // Source: https://github.com/claviska/SimpleImage
  12. //
  13. // Licensed under the MIT license <http://opensource.org/licenses/MIT>
  14. //
  15. namespace claviska;
  16. class SimpleImage {
  17. const
  18. ERR_FILE_NOT_FOUND = 1,
  19. ERR_FONT_FILE = 2,
  20. ERR_FREETYPE_NOT_ENABLED = 3,
  21. ERR_GD_NOT_ENABLED = 4,
  22. ERR_INVALID_COLOR = 5,
  23. ERR_INVALID_DATA_URI = 6,
  24. ERR_INVALID_IMAGE = 7,
  25. ERR_LIB_NOT_LOADED = 8,
  26. ERR_UNSUPPORTED_FORMAT = 9,
  27. ERR_WEBP_NOT_ENABLED = 10,
  28. ERR_WRITE = 11;
  29. protected $image, $mimeType, $exif;
  30. //////////////////////////////////////////////////////////////////////////////////////////////////
  31. // Magic methods
  32. //////////////////////////////////////////////////////////////////////////////////////////////////
  33. //
  34. // Creates a new SimpleImage object.
  35. //
  36. // $image (string) - An image file or a data URI to load.
  37. //
  38. public function __construct($image = null) {
  39. // Check for the required GD extension
  40. if(extension_loaded('gd')) {
  41. // Ignore JPEG warnings that cause imagecreatefromjpeg() to fail
  42. ini_set('gd.jpeg_ignore_warning', 1);
  43. } else {
  44. throw new \Exception('Required extension GD is not loaded.', self::ERR_GD_NOT_ENABLED);
  45. }
  46. // Load an image through the constructor
  47. if(preg_match('/^data:(.*?);/', $image)) {
  48. $this->fromDataUri($image);
  49. } elseif($image) {
  50. $this->fromFile($image);
  51. }
  52. }
  53. //
  54. // Destroys the image resource
  55. //
  56. public function __destruct() {
  57. if($this->image !== null && get_resource_type($this->image) === 'gd') {
  58. imagedestroy($this->image);
  59. }
  60. }
  61. //////////////////////////////////////////////////////////////////////////////////////////////////
  62. // Loaders
  63. //////////////////////////////////////////////////////////////////////////////////////////////////
  64. //
  65. // Loads an image from a data URI.
  66. //
  67. // $uri* (string) - A data URI.
  68. //
  69. // Returns a SimpleImage object.
  70. //
  71. public function fromDataUri($uri) {
  72. // Basic formatting check
  73. preg_match('/^data:(.*?);/', $uri, $matches);
  74. if(!count($matches)) {
  75. throw new \Exception('Invalid data URI.', self::ERR_INVALID_DATA_URI);
  76. }
  77. // Determine mime type
  78. $this->mimeType = $matches[1];
  79. if(!preg_match('/^image\/(gif|jpeg|png)$/', $this->mimeType)) {
  80. throw new \Exception(
  81. 'Unsupported format: ' . $this->mimeType,
  82. self::ERR_UNSUPPORTED_FORMAT
  83. );
  84. }
  85. // Get image data
  86. $uri = base64_decode(preg_replace('/^data:(.*?);base64,/', '', $uri));
  87. $this->image = imagecreatefromstring($uri);
  88. if(!$this->image) {
  89. throw new \Exception("Invalid image data.", self::ERR_INVALID_IMAGE);
  90. }
  91. return $this;
  92. }
  93. //
  94. // Loads an image from a file.
  95. //
  96. // $file* (string) - The image file to load.
  97. //
  98. // Returns a SimpleImage object.
  99. //
  100. public function fromFile($file) {
  101. // Check if the file exists and is readable. We're using fopen() instead of file_exists()
  102. // because not all URL wrappers support the latter.
  103. $handle = @fopen($file, 'r');
  104. if($handle === false) {
  105. throw new \Exception("File not found: $file", self::ERR_FILE_NOT_FOUND);
  106. }
  107. fclose($handle);
  108. // Get image info
  109. $info = getimagesize($file);
  110. if($info === false) {
  111. throw new \Exception("Invalid image file: $file", self::ERR_INVALID_IMAGE);
  112. }
  113. $this->mimeType = $info['mime'];
  114. // Create image object from file
  115. switch($this->mimeType) {
  116. case 'image/gif':
  117. // Load the gif
  118. $gif = imagecreatefromgif($file);
  119. if($gif) {
  120. // Copy the gif over to a true color image to preserve its transparency. This is a
  121. // workaround to prevent imagepalettetruecolor() from borking transparency.
  122. $width = imagesx($gif);
  123. $height = imagesy($gif);
  124. $this->image = imagecreatetruecolor($width, $height);
  125. $transparentColor = imagecolorallocatealpha($this->image, 0, 0, 0, 127);
  126. imagecolortransparent($this->image, $transparentColor);
  127. imagefill($this->image, 0, 0, $transparentColor);
  128. imagecopy($this->image, $gif, 0, 0, 0, 0, $width, $height);
  129. imagedestroy($gif);
  130. }
  131. break;
  132. case 'image/jpeg':
  133. $this->image = imagecreatefromjpeg($file);
  134. break;
  135. case 'image/png':
  136. $this->image = imagecreatefrompng($file);
  137. break;
  138. case 'image/webp':
  139. $this->image = imagecreatefromwebp($file);
  140. break;
  141. case 'image/bmp':
  142. case 'image/x-ms-bmp':
  143. case 'image/x-windows-bmp':
  144. $this->image = imagecreatefrombmp($file);
  145. break;
  146. }
  147. if(!$this->image) {
  148. throw new \Exception("Unsupported format: " . $this->mimeType, self::ERR_UNSUPPORTED_FORMAT);
  149. }
  150. // Convert pallete images to true color images
  151. imagepalettetotruecolor($this->image);
  152. // Load exif data from JPEG images
  153. if($this->mimeType === 'image/jpeg' && function_exists('exif_read_data')) {
  154. $this->exif = @exif_read_data($file);
  155. }
  156. return $this;
  157. }
  158. //
  159. // Creates a new image.
  160. //
  161. // $width* (int) - The width of the image.
  162. // $height* (int) - The height of the image.
  163. // $color (string|array) - Optional fill color for the new image (default 'transparent').
  164. //
  165. // Returns a SimpleImage object.
  166. //
  167. public function fromNew($width, $height, $color = 'transparent') {
  168. $this->image = imagecreatetruecolor($width, $height);
  169. // Use PNG for dynamically created images because it's lossless and supports transparency
  170. $this->mimeType = 'image/png';
  171. // Fill the image with color
  172. $this->fill($color);
  173. return $this;
  174. }
  175. //
  176. // Creates a new image from a string.
  177. //
  178. // $string* (string) - The raw image data as a string. Example:
  179. //
  180. // $string = file_get_contents('image.jpg');
  181. //
  182. // Returns a SimpleImage object.
  183. //
  184. public function fromString($string) {
  185. return $this->fromFile('data://;base64,' . base64_encode($string));
  186. }
  187. //////////////////////////////////////////////////////////////////////////////////////////////////
  188. // Savers
  189. //////////////////////////////////////////////////////////////////////////////////////////////////
  190. //
  191. // Generates an image.
  192. //
  193. // $mimeType (string) - The image format to output as a mime type (defaults to the original mime
  194. // type).
  195. // $quality (int) - Image quality as a percentage (default 100).
  196. //
  197. // Returns an array containing the image data and mime type.
  198. //
  199. protected function generate($mimeType = null, $quality = 100) {
  200. // Format defaults to the original mime type
  201. $mimeType = $mimeType ?: $this->mimeType;
  202. // Ensure quality is a valid integer
  203. if($quality === null) $quality = 100;
  204. $quality = self::keepWithin((int) $quality, 0, 100);
  205. // Capture output
  206. ob_start();
  207. // Generate the image
  208. switch($mimeType) {
  209. case 'image/gif':
  210. imagesavealpha($this->image, true);
  211. imagegif($this->image, null);
  212. break;
  213. case 'image/jpeg':
  214. imageinterlace($this->image, true);
  215. imagejpeg($this->image, null, $quality);
  216. break;
  217. case 'image/png':
  218. imagesavealpha($this->image, true);
  219. imagepng($this->image, null, round(9 * $quality / 100));
  220. break;
  221. case 'image/webp':
  222. // Not all versions of PHP will have webp support enabled
  223. if(!function_exists('imagewebp')) {
  224. throw new \Exception(
  225. 'WEBP support is not enabled in your version of PHP.',
  226. self::ERR_WEBP_NOT_ENABLED
  227. );
  228. }
  229. imagesavealpha($this->image, true);
  230. imagewebp($this->image, null, $quality);
  231. break;
  232. case 'image/bmp':
  233. case 'image/x-ms-bmp':
  234. case 'image/x-windows-bmp':
  235. imageinterlace($this->image, true);
  236. imagebmp($this->image, null, $quality);
  237. break;
  238. default:
  239. throw new \Exception('Unsupported format: ' . $mimeType, self::ERR_UNSUPPORTED_FORMAT);
  240. }
  241. // Stop capturing
  242. $data = ob_get_contents();
  243. ob_end_clean();
  244. return [
  245. 'data' => $data,
  246. 'mimeType' => $mimeType
  247. ];
  248. }
  249. //
  250. // Generates a data URI.
  251. //
  252. // $mimeType (string) - The image format to output as a mime type (defaults to the original mime
  253. // type).
  254. // $quality (int) - Image quality as a percentage (default 100).
  255. //
  256. // Returns a string containing a data URI.
  257. //
  258. public function toDataUri($mimeType = null, $quality = 100) {
  259. $image = $this->generate($mimeType, $quality);
  260. return 'data:' . $image['mimeType'] . ';base64,' . base64_encode($image['data']);
  261. }
  262. //
  263. // Forces the image to be downloaded to the clients machine. Must be called before any output is
  264. // sent to the screen.
  265. //
  266. // $filename* (string) - The filename (without path) to send to the client (e.g. 'image.jpeg').
  267. // $mimeType (string) - The image format to output as a mime type (defaults to the original mime
  268. // type).
  269. // $quality (int) - Image quality as a percentage (default 100).
  270. //
  271. public function toDownload($filename, $mimeType = null, $quality = 100) {
  272. $image = $this->generate($mimeType, $quality);
  273. // Set download headers
  274. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  275. header('Content-Description: File Transfer');
  276. header('Content-Length: ' . strlen($image['data']));
  277. header('Content-Transfer-Encoding: Binary');
  278. header('Content-Type: application/octet-stream');
  279. header("Content-Disposition: attachment; filename=\"$filename\"");
  280. echo $image['data'];
  281. return $this;
  282. }
  283. //
  284. // Writes the image to a file.
  285. //
  286. // $mimeType (string) - The image format to output as a mime type (defaults to the original mime
  287. // type).
  288. // $quality (int) - Image quality as a percentage (default 100).
  289. //
  290. // Returns a SimpleImage object.
  291. //
  292. public function toFile($file, $mimeType = null, $quality = 100) {
  293. $image = $this->generate($mimeType, $quality);
  294. // Save the image to file
  295. if(!file_put_contents($file, $image['data'])) {
  296. throw new \Exception("Failed to write image to file: $file", self::ERR_WRITE);
  297. }
  298. return $this;
  299. }
  300. //
  301. // Outputs the image to the screen. Must be called before any output is sent to the screen.
  302. //
  303. // $mimeType (string) - The image format to output as a mime type (defaults to the original mime
  304. // type).
  305. // $quality (int) - Image quality as a percentage (default 100).
  306. //
  307. // Returns a SimpleImage object.
  308. //
  309. public function toScreen($mimeType = null, $quality = 100) {
  310. $image = $this->generate($mimeType, $quality);
  311. // Output the image to stdout
  312. header('Content-Type: ' . $image['mimeType']);
  313. echo $image['data'];
  314. return $this;
  315. }
  316. //
  317. // Generates an image string.
  318. //
  319. // $mimeType (string) - The image format to output as a mime type (defaults to the original mime
  320. // type).
  321. // $quality (int) - Image quality as a percentage (default 100).
  322. //
  323. // Returns a SimpleImage object.
  324. //
  325. public function toString($mimeType = null, $quality = 100) {
  326. return $this->generate($mimeType, $quality)['data'];
  327. }
  328. //////////////////////////////////////////////////////////////////////////////////////////////////
  329. // Utilities
  330. //////////////////////////////////////////////////////////////////////////////////////////////////
  331. //
  332. // Ensures a numeric value is always within the min and max range.
  333. //
  334. // $value* (int|float) - A numeric value to test.
  335. // $min* (int|float) - The minimum allowed value.
  336. // $max* (int|float) - The maximum allowed value.
  337. //
  338. // Returns an int|float value.
  339. //
  340. protected static function keepWithin($value, $min, $max) {
  341. if($value < $min) return $min;
  342. if($value > $max) return $max;
  343. return $value;
  344. }
  345. //
  346. // Gets the image's current aspect ratio.
  347. //
  348. // Returns the aspect ratio as a float.
  349. //
  350. public function getAspectRatio() {
  351. return $this->getWidth() / $this->getHeight();
  352. }
  353. //
  354. // Gets the image's exif data.
  355. //
  356. // Returns an array of exif data or null if no data is available.
  357. //
  358. public function getExif() {
  359. return isset($this->exif) ? $this->exif : null;
  360. }
  361. //
  362. // Gets the image's current height.
  363. //
  364. // Returns the height as an integer.
  365. //
  366. public function getHeight() {
  367. return (int) imagesy($this->image);
  368. }
  369. //
  370. // Gets the mime type of the loaded image.
  371. //
  372. // Returns a mime type string.
  373. //
  374. public function getMimeType() {
  375. return $this->mimeType;
  376. }
  377. //
  378. // Gets the image's current orientation.
  379. //
  380. // Returns a string: 'landscape', 'portrait', or 'square'
  381. //
  382. public function getOrientation() {
  383. $width = $this->getWidth();
  384. $height = $this->getHeight();
  385. if($width > $height) return 'landscape';
  386. if($width < $height) return 'portrait';
  387. return 'square';
  388. }
  389. //
  390. // Gets the resolution of the image
  391. //
  392. // Returns the resolution as an array of integers: [96, 96]
  393. //
  394. public function getResolution() {
  395. return imageresolution($this->image);
  396. }
  397. //
  398. // Gets the image's current width.
  399. //
  400. // Returns the width as an integer.
  401. //
  402. public function getWidth() {
  403. return (int) imagesx($this->image);
  404. }
  405. //////////////////////////////////////////////////////////////////////////////////////////////////
  406. // Manipulation
  407. //////////////////////////////////////////////////////////////////////////////////////////////////
  408. //
  409. // Same as PHP's imagecopymerge, but works with transparent images. Used internally for overlay.
  410. //
  411. protected static function imageCopyMergeAlpha($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $pct) {
  412. // Are we merging with transparency?
  413. if($pct < 100) {
  414. // Disable alpha blending and "colorize" the image using a transparent color
  415. imagealphablending($srcIm, false);
  416. imagefilter($srcIm, IMG_FILTER_COLORIZE, 0, 0, 0, 127 * ((100 - $pct) / 100));
  417. }
  418. imagecopy($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH);
  419. return true;
  420. }
  421. //
  422. // Rotates an image so the orientation will be correct based on its exif data. It is safe to call
  423. // this method on images that don't have exif data (no changes will be made).
  424. //
  425. // Returns a SimpleImage object.
  426. //
  427. public function autoOrient() {
  428. $exif = $this->getExif();
  429. if(!$exif || !isset($exif['Orientation'])){
  430. return $this;
  431. }
  432. switch($exif['Orientation']) {
  433. case 1: // Do nothing!
  434. break;
  435. case 2: // Flip horizontally
  436. $this->flip('x');
  437. break;
  438. case 3: // Rotate 180 degrees
  439. $this->rotate(180);
  440. break;
  441. case 4: // Flip vertically
  442. $this->flip('y');
  443. break;
  444. case 5: // Rotate 90 degrees clockwise and flip vertically
  445. $this->flip('y')->rotate(90);
  446. break;
  447. case 6: // Rotate 90 clockwise
  448. $this->rotate(90);
  449. break;
  450. case 7: // Rotate 90 clockwise and flip horizontally
  451. $this->flip('x')->rotate(90);
  452. break;
  453. case 8: // Rotate 90 counterclockwise
  454. $this->rotate(-90);
  455. break;
  456. }
  457. return $this;
  458. }
  459. //
  460. // Proportionally resize the image to fit inside a specific width and height.
  461. //
  462. // $maxWidth* (int) - The maximum width the image can be.
  463. // $maxHeight* (int) - The maximum height the image can be.
  464. //
  465. // Returns a SimpleImage object.
  466. //
  467. public function bestFit($maxWidth, $maxHeight) {
  468. // If the image already fits, there's nothing to do
  469. if($this->getWidth() <= $maxWidth && $this->getHeight() <= $maxHeight) {
  470. return $this;
  471. }
  472. // Calculate max width or height based on orientation
  473. if($this->getOrientation() === 'portrait') {
  474. $height = $maxHeight;
  475. $width = $maxHeight * $this->getAspectRatio();
  476. } else {
  477. $width = $maxWidth;
  478. $height = $maxWidth / $this->getAspectRatio();
  479. }
  480. // Reduce to max width
  481. if($width > $maxWidth) {
  482. $width = $maxWidth;
  483. $height = $width / $this->getAspectRatio();
  484. }
  485. // Reduce to max height
  486. if($height > $maxHeight) {
  487. $height = $maxHeight;
  488. $width = $height * $this->getAspectRatio();
  489. }
  490. return $this->resize($width, $height);
  491. }
  492. //
  493. // Crop the image.
  494. //
  495. // $x1 - Top left x coordinate.
  496. // $y1 - Top left y coordinate.
  497. // $x2 - Bottom right x coordinate.
  498. // $y2 - Bottom right x coordinate.
  499. //
  500. // Returns a SimpleImage object.
  501. //
  502. public function crop($x1, $y1, $x2, $y2) {
  503. // Keep crop within image dimensions
  504. $x1 = self::keepWithin($x1, 0, $this->getWidth());
  505. $x2 = self::keepWithin($x2, 0, $this->getWidth());
  506. $y1 = self::keepWithin($y1, 0, $this->getHeight());
  507. $y2 = self::keepWithin($y2, 0, $this->getHeight());
  508. // Crop it
  509. $this->image = imagecrop($this->image, [
  510. 'x' => min($x1, $x2),
  511. 'y' => min($y1, $y2),
  512. 'width' => abs($x2 - $x1),
  513. 'height' => abs($y2 - $y1)
  514. ]);
  515. return $this;
  516. }
  517. //
  518. // Applies a duotone filter to the image.
  519. //
  520. // $lightColor* (string|array) - The lightest color in the duotone.
  521. // $darkColor* (string|array) - The darkest color in the duotone.
  522. //
  523. // Returns a SimpleImage object.
  524. //
  525. function duotone($lightColor, $darkColor) {
  526. $lightColor = self::normalizeColor($lightColor);
  527. $darkColor = self::normalizeColor($darkColor);
  528. // Calculate averages between light and dark colors
  529. $redAvg = $lightColor['red'] - $darkColor['red'];
  530. $greenAvg = $lightColor['green'] - $darkColor['green'];
  531. $blueAvg = $lightColor['blue'] - $darkColor['blue'];
  532. // Create a matrix of all possible duotone colors based on gray values
  533. $pixels = [];
  534. for($i = 0; $i <= 255; $i++) {
  535. $grayAvg = $i / 255;
  536. $pixels['red'][$i] = $darkColor['red'] + $grayAvg * $redAvg;
  537. $pixels['green'][$i] = $darkColor['green'] + $grayAvg * $greenAvg;
  538. $pixels['blue'][$i] = $darkColor['blue'] + $grayAvg * $blueAvg;
  539. }
  540. // Apply the filter pixel by pixel
  541. for($x = 0; $x < $this->getWidth(); $x++) {
  542. for($y = 0; $y < $this->getHeight(); $y++) {
  543. $rgb = $this->getColorAt($x, $y);
  544. $gray = min(255, round(0.299 * $rgb['red'] + 0.114 * $rgb['blue'] + 0.587 * $rgb['green']));
  545. $this->dot($x, $y, [
  546. 'red' => $pixels['red'][$gray],
  547. 'green' => $pixels['green'][$gray],
  548. 'blue' => $pixels['blue'][$gray]
  549. ]);
  550. }
  551. }
  552. return $this;
  553. }
  554. //
  555. // Proportionally resize the image to a specific height.
  556. //
  557. // **DEPRECATED:** This method was deprecated in version 3.2.2 and will be removed in version 4.0.
  558. // Please use `resize(null, $height)` instead.
  559. //
  560. // $height* (int) - The height to resize the image to.
  561. //
  562. // Returns a SimpleImage object.
  563. //
  564. public function fitToHeight($height) {
  565. return $this->resize(null, $height);
  566. }
  567. //
  568. // Proportionally resize the image to a specific width.
  569. //
  570. // **DEPRECATED:** This method was deprecated in version 3.2.2 and will be removed in version 4.0.
  571. // Please use `resize($width, null)` instead.
  572. //
  573. // $width* (int) - The width to resize the image to.
  574. //
  575. // Returns a SimpleImage object.
  576. //
  577. public function fitToWidth($width) {
  578. return $this->resize($width, null);
  579. }
  580. //
  581. // Flip the image horizontally or vertically.
  582. //
  583. // $direction* (string) - The direction to flip: x|y|both
  584. //
  585. // Returns a SimpleImage object.
  586. //
  587. public function flip($direction) {
  588. switch($direction) {
  589. case 'x':
  590. imageflip($this->image, IMG_FLIP_HORIZONTAL);
  591. break;
  592. case 'y':
  593. imageflip($this->image, IMG_FLIP_VERTICAL);
  594. break;
  595. case 'both':
  596. imageflip($this->image, IMG_FLIP_BOTH);
  597. break;
  598. }
  599. return $this;
  600. }
  601. //
  602. // Reduces the image to a maximum number of colors.
  603. //
  604. // $max* (int) - The maximum number of colors to use.
  605. // $dither (bool) - Whether or not to use a dithering effect (default true).
  606. //
  607. // Returns a SimpleImage object.
  608. //
  609. public function maxColors($max, $dither = true) {
  610. imagetruecolortopalette($this->image, $dither, max(1, $max));
  611. return $this;
  612. }
  613. //
  614. // Place an image on top of the current image.
  615. //
  616. // $overlay* (string|SimpleImage) - The image to overlay. This can be a filename, a data URI, or
  617. // a SimpleImage object.
  618. // $anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left',
  619. // 'top right', 'bottom left', 'bottom right' (default 'center')
  620. // $opacity (float) - The opacity level of the overlay 0-1 (default 1).
  621. // $xOffset (int) - Horizontal offset in pixels (default 0).
  622. // $yOffset (int) - Vertical offset in pixels (default 0).
  623. //
  624. // Returns a SimpleImage object.
  625. //
  626. public function overlay($overlay, $anchor = 'center', $opacity = 1, $xOffset = 0, $yOffset = 0) {
  627. // Load overlay image
  628. if(!($overlay instanceof SimpleImage)) {
  629. $overlay = new SimpleImage($overlay);
  630. }
  631. // Convert opacity
  632. $opacity = self::keepWithin($opacity, 0, 1) * 100;
  633. // Determine placement
  634. switch($anchor) {
  635. case 'top left':
  636. $x = $xOffset;
  637. $y = $yOffset;
  638. break;
  639. case 'top right':
  640. $x = $this->getWidth() - $overlay->getWidth() + $xOffset;
  641. $y = $yOffset;
  642. break;
  643. case 'top':
  644. $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset;
  645. $y = $yOffset;
  646. break;
  647. case 'bottom left':
  648. $x = $xOffset;
  649. $y = $this->getHeight() - $overlay->getHeight() + $yOffset;
  650. break;
  651. case 'bottom right':
  652. $x = $this->getWidth() - $overlay->getWidth() + $xOffset;
  653. $y = $this->getHeight() - $overlay->getHeight() + $yOffset;
  654. break;
  655. case 'bottom':
  656. $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset;
  657. $y = $this->getHeight() - $overlay->getHeight() + $yOffset;
  658. break;
  659. case 'left':
  660. $x = $xOffset;
  661. $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset;
  662. break;
  663. case 'right':
  664. $x = $this->getWidth() - $overlay->getWidth() + $xOffset;
  665. $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset;
  666. break;
  667. default:
  668. $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset;
  669. $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset;
  670. break;
  671. }
  672. // Perform the overlay
  673. self::imageCopyMergeAlpha(
  674. $this->image,
  675. $overlay->image,
  676. $x, $y,
  677. 0, 0,
  678. $overlay->getWidth(),
  679. $overlay->getHeight(),
  680. $opacity
  681. );
  682. return $this;
  683. }
  684. //
  685. // Resize an image to the specified dimensions. If only one dimension is specified, the image will
  686. // be resized proportionally.
  687. //
  688. // $width* (int) - The new image width.
  689. // $height* (int) - The new image height.
  690. //
  691. // Returns a SimpleImage object.
  692. //
  693. public function resize($width = null, $height = null) {
  694. // No dimentions specified
  695. if(!$width && !$height) {
  696. return $this;
  697. }
  698. // Resize to width
  699. if($width && !$height) {
  700. $height = $width / $this->getAspectRatio();
  701. }
  702. // Resize to height
  703. if(!$width && $height) {
  704. $width = $height * $this->getAspectRatio();
  705. }
  706. // If the dimensions are the same, there's no need to resize
  707. if($this->getWidth() === $width && $this->getHeight() === $height) {
  708. return $this;
  709. }
  710. // We can't use imagescale because it doesn't seem to preserve transparency properly. The
  711. // workaround is to create a new truecolor image, allocate a transparent color, and copy the
  712. // image over to it using imagecopyresampled.
  713. $newImage = imagecreatetruecolor($width, $height);
  714. $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127);
  715. imagecolortransparent($newImage, $transparentColor);
  716. imagefill($newImage, 0, 0, $transparentColor);
  717. imagecopyresampled(
  718. $newImage,
  719. $this->image,
  720. 0, 0, 0, 0,
  721. $width,
  722. $height,
  723. $this->getWidth(),
  724. $this->getHeight()
  725. );
  726. // Swap out the new image
  727. $this->image = $newImage;
  728. return $this;
  729. }
  730. //
  731. // Sets an image's resolution, as per https://www.php.net/manual/en/function.imageresolution.php
  732. //
  733. // $res_x* (int) - The horizontal resolution in DPI
  734. // $res_y (int) - The vertical resolution in DPI
  735. //
  736. // Returns a SimpleImage object.
  737. //
  738. public function resolution($res_x, $res_y = null) {
  739. if(is_null($res_y)) {
  740. imageresolution($this->image, $res_x);
  741. } else {
  742. imageresolution($this->image, $res_x, $res_y);
  743. }
  744. return $this;
  745. }
  746. //
  747. // Rotates the image.
  748. //
  749. // $angle* (int) - The angle of rotation (-360 - 360).
  750. // $backgroundColor (string|array) - The background color to use for the uncovered zone area
  751. // after rotation (default 'transparent').
  752. //
  753. // Returns a SimpleImage object.
  754. //
  755. public function rotate($angle, $backgroundColor = 'transparent') {
  756. // Rotate the image on a canvas with the desired background color
  757. $backgroundColor = $this->allocateColor($backgroundColor);
  758. $this->image = imagerotate(
  759. $this->image,
  760. -(self::keepWithin($angle, -360, 360)),
  761. $backgroundColor
  762. );
  763. imagecolortransparent($this->image, imagecolorallocatealpha($this->image, 0, 0, 0, 127));
  764. return $this;
  765. }
  766. //
  767. // Adds text to the image.
  768. //
  769. // $text* (string) - The desired text.
  770. // $options (array) - An array of options.
  771. // - fontFile* (string) - The TrueType (or compatible) font file to use.
  772. // - size (int) - The size of the font in pixels (default 12).
  773. // - color (string|array) - The text color (default black).
  774. // - anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right',
  775. // 'top left', 'top right', 'bottom left', 'bottom right' (default 'center').
  776. // - xOffset (int) - The horizontal offset in pixels (default 0).
  777. // - yOffset (int) - The vertical offset in pixels (default 0).
  778. // - shadow (array) - Text shadow params.
  779. // - x* (int) - Horizontal offset in pixels.
  780. // - y* (int) - Vertical offset in pixels.
  781. // - color* (string|array) - The text shadow color.
  782. // $boundary (array) - If passed, this variable will contain an array with coordinates that
  783. // surround the text: [x1, y1, x2, y2, width, height]. This can be used for calculating the
  784. // text's position after it gets added to the image.
  785. //
  786. // Returns a SimpleImage object.
  787. //
  788. public function text($text, $options, &$boundary = null) {
  789. // Check for freetype support
  790. if(!function_exists('imagettftext')) {
  791. throw new \Exception(
  792. 'Freetype support is not enabled in your version of PHP.',
  793. self::ERR_FREETYPE_NOT_ENABLED
  794. );
  795. }
  796. // Default options
  797. $options = array_merge([
  798. 'fontFile' => null,
  799. 'size' => 12,
  800. 'color' => 'black',
  801. 'anchor' => 'center',
  802. 'xOffset' => 0,
  803. 'yOffset' => 0,
  804. 'shadow' => null
  805. ], $options);
  806. // Extract and normalize options
  807. $fontFile = $options['fontFile'];
  808. $size = ($options['size'] / 96) * 72; // Convert px to pt (72pt per inch, 96px per inch)
  809. $color = $this->allocateColor($options['color']);
  810. $anchor = $options['anchor'];
  811. $xOffset = $options['xOffset'];
  812. $yOffset = $options['yOffset'];
  813. $angle = 0;
  814. // Calculate the bounding box dimensions
  815. //
  816. // Since imagettfbox() returns a bounding box from the text's baseline, we can end up with
  817. // different heights for different strings of the same font size. For example, 'type' will often
  818. // be taller than 'text' because the former has a descending letter.
  819. //
  820. // To compensate for this, we create two bounding boxes: one to measure the cap height and
  821. // another to measure the descender height. Based on that, we can adjust the text vertically
  822. // to appear inside the box with a reasonable amount of consistency.
  823. //
  824. // See: https://github.com/claviska/SimpleImage/issues/165
  825. //
  826. $box = imagettfbbox($size, $angle, $fontFile, $text);
  827. if(!$box) {
  828. throw new \Exception("Unable to load font file: $fontFile", self::ERR_FONT_FILE);
  829. }
  830. $boxWidth = abs($box[6] - $box[2]);
  831. $boxHeight = $options['size'];
  832. // Determine cap height
  833. $box = imagettfbbox($size, $angle, $fontFile, 'X');
  834. $capHeight = abs($box[7] - $box[1]);
  835. // Determine descender height
  836. $box = imagettfbbox($size, $angle, $fontFile, 'X Qgjpqy');
  837. $fullHeight = abs($box[7] - $box[1]);
  838. $descenderHeight = $fullHeight - $capHeight;
  839. // Determine position
  840. switch($anchor) {
  841. case 'top left':
  842. $x = $xOffset;
  843. $y = $yOffset + $boxHeight;
  844. break;
  845. case 'top right':
  846. $x = $this->getWidth() - $boxWidth + $xOffset;
  847. $y = $yOffset + $boxHeight;
  848. break;
  849. case 'top':
  850. $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset;
  851. $y = $yOffset + $boxHeight;
  852. break;
  853. case 'bottom left':
  854. $x = $xOffset;
  855. $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight;
  856. break;
  857. case 'bottom right':
  858. $x = $this->getWidth() - $boxWidth + $xOffset;
  859. $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight;
  860. break;
  861. case 'bottom':
  862. $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset;
  863. $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight;
  864. break;
  865. case 'left':
  866. $x = $xOffset;
  867. $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset;
  868. break;
  869. case 'right';
  870. $x = $this->getWidth() - $boxWidth + $xOffset;
  871. $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset;
  872. break;
  873. default: // center
  874. $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset;
  875. $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset;
  876. break;
  877. }
  878. $x = (int) round($x);
  879. $y = (int) round($y);
  880. // Pass the boundary back by reference
  881. $boundary = [
  882. 'x1' => $x,
  883. 'y1' => $y - $boxHeight, // $y is the baseline, not the top!
  884. 'x2' => $x + $boxWidth,
  885. 'y2' => $y,
  886. 'width' => $boxWidth,
  887. 'height' => $boxHeight
  888. ];
  889. // Text shadow
  890. if(is_array($options['shadow'])) {
  891. imagettftext(
  892. $this->image,
  893. $size,
  894. $angle,
  895. $x + $options['shadow']['x'],
  896. $y + $options['shadow']['y'] - $descenderHeight,
  897. $this->allocateColor($options['shadow']['color']),
  898. $fontFile,
  899. $text
  900. );
  901. }
  902. // Draw the text
  903. imagettftext($this->image, $size, $angle, $x, $y - $descenderHeight, $color, $fontFile, $text);
  904. return $this;
  905. }
  906. //
  907. // Creates a thumbnail image. This function attempts to get the image as close to the provided
  908. // dimensions as possible, then crops the remaining overflow to force the desired size. Useful
  909. // for generating thumbnail images.
  910. //
  911. // $width* (int) - The thumbnail width.
  912. // $height* (int) - The thumbnail height.
  913. // $anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left',
  914. // 'top right', 'bottom left', 'bottom right' (default 'center').
  915. //
  916. // Returns a SimpleImage object.
  917. //
  918. public function thumbnail($width, $height, $anchor = 'center') {
  919. // Determine aspect ratios
  920. $currentRatio = $this->getHeight() / $this->getWidth();
  921. $targetRatio = $height / $width;
  922. // Fit to height/width
  923. if($targetRatio > $currentRatio) {
  924. $this->resize(null, $height);
  925. } else {
  926. $this->resize($width, null);
  927. }
  928. switch($anchor) {
  929. case 'top':
  930. $x1 = floor(($this->getWidth() / 2) - ($width / 2));
  931. $x2 = $width + $x1;
  932. $y1 = 0;
  933. $y2 = $height;
  934. break;
  935. case 'bottom':
  936. $x1 = floor(($this->getWidth() / 2) - ($width / 2));
  937. $x2 = $width + $x1;
  938. $y1 = $this->getHeight() - $height;
  939. $y2 = $this->getHeight();
  940. break;
  941. case 'left':
  942. $x1 = 0;
  943. $x2 = $width;
  944. $y1 = floor(($this->getHeight() / 2) - ($height / 2));
  945. $y2 = $height + $y1;
  946. break;
  947. case 'right':
  948. $x1 = $this->getWidth() - $width;
  949. $x2 = $this->getWidth();
  950. $y1 = floor(($this->getHeight() / 2) - ($height / 2));
  951. $y2 = $height + $y1;
  952. break;
  953. case 'top left':
  954. $x1 = 0;
  955. $x2 = $width;
  956. $y1 = 0;
  957. $y2 = $height;
  958. break;
  959. case 'top right':
  960. $x1 = $this->getWidth() - $width;
  961. $x2 = $this->getWidth();
  962. $y1 = 0;
  963. $y2 = $height;
  964. break;
  965. case 'bottom left':
  966. $x1 = 0;
  967. $x2 = $width;
  968. $y1 = $this->getHeight() - $height;
  969. $y2 = $this->getHeight();
  970. break;
  971. case 'bottom right':
  972. $x1 = $this->getWidth() - $width;
  973. $x2 = $this->getWidth();
  974. $y1 = $this->getHeight() - $height;
  975. $y2 = $this->getHeight();
  976. break;
  977. default:
  978. $x1 = floor(($this->getWidth() / 2) - ($width / 2));
  979. $x2 = $width + $x1;
  980. $y1 = floor(($this->getHeight() / 2) - ($height / 2));
  981. $y2 = $height + $y1;
  982. break;
  983. }
  984. // Return the cropped thumbnail image
  985. return $this->crop($x1, $y1, $x2, $y2);
  986. }
  987. //////////////////////////////////////////////////////////////////////////////////////////////////
  988. // Drawing
  989. //////////////////////////////////////////////////////////////////////////////////////////////////
  990. //
  991. // Draws an arc.
  992. //
  993. // $x* (int) - The x coordinate of the arc's center.
  994. // $y* (int) - The y coordinate of the arc's center.
  995. // $width* (int) - The width of the arc.
  996. // $height* (int) - The height of the arc.
  997. // $start* (int) - The start of the arc in degrees.
  998. // $end* (int) - The end of the arc in degrees.
  999. // $color* (string|array) - The arc color.
  1000. // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1).
  1001. //
  1002. // Returns a SimpleImage object.
  1003. //
  1004. public function arc($x, $y, $width, $height, $start, $end, $color, $thickness = 1) {
  1005. // Allocate the color
  1006. $color = $this->allocateColor($color);
  1007. // Draw an arc
  1008. if($thickness === 'filled') {
  1009. imagesetthickness($this->image, 1);
  1010. imagefilledarc($this->image, $x, $y, $width, $height, $start, $end, $color, IMG_ARC_PIE);
  1011. } else {
  1012. imagesetthickness($this->image, $thickness);
  1013. imagearc($this->image, $x, $y, $width, $height, $start, $end, $color);
  1014. }
  1015. return $this;
  1016. }
  1017. //
  1018. // Draws a border around the image.
  1019. //
  1020. // $color* (string|array) - The border color.
  1021. // $thickness (int) - The thickness of the border (default 1).
  1022. //
  1023. // Returns a SimpleImage object.
  1024. //
  1025. public function border($color, $thickness = 1) {
  1026. $x1 = 0;
  1027. $y1 = 0;
  1028. $x2 = $this->getWidth() - 1;
  1029. $y2 = $this->getHeight() - 1;
  1030. // Draw a border rectangle until it reaches the correct width
  1031. for($i = 0; $i < $thickness; $i++) {
  1032. $this->rectangle($x1++, $y1++, $x2--, $y2--, $color);
  1033. }
  1034. return $this;
  1035. }
  1036. //
  1037. // Draws a single pixel dot.
  1038. //
  1039. // $x* (int) - The x coordinate of the dot.
  1040. // $y* (int) - The y coordinate of the dot.
  1041. // $color* (string|array) - The dot color.
  1042. //
  1043. // Returns a SimpleImage object.
  1044. //
  1045. public function dot($x, $y, $color) {
  1046. $color = $this->allocateColor($color);
  1047. imagesetpixel($this->image, $x, $y, $color);
  1048. return $this;
  1049. }
  1050. //
  1051. // Draws an ellipse.
  1052. //
  1053. // $x* (int) - The x coordinate of the center.
  1054. // $y* (int) - The y coordinate of the center.
  1055. // $width* (int) - The ellipse width.
  1056. // $height* (int) - The ellipse height.
  1057. // $color* (string|array) - The ellipse color.
  1058. // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1).
  1059. //
  1060. // Returns a SimpleImage object.
  1061. //
  1062. public function ellipse($x, $y, $width, $height, $color, $thickness = 1) {
  1063. // Allocate the color
  1064. $color = $this->allocateColor($color);
  1065. // Draw an ellipse
  1066. if($thickness === 'filled') {
  1067. imagesetthickness($this->image, 1);
  1068. imagefilledellipse($this->image, $x, $y, $width, $height, $color);
  1069. } else {
  1070. // imagesetthickness doesn't appear to work with imageellipse, so we work around it.
  1071. imagesetthickness($this->image, 1);
  1072. $i = 0;
  1073. while($i++ < $thickness * 2 - 1) {
  1074. imageellipse($this->image, $x, $y, --$width, $height--, $color);
  1075. }
  1076. }
  1077. return $this;
  1078. }
  1079. //
  1080. // Fills the image with a solid color.
  1081. //
  1082. // $color (string|array) - The fill color.
  1083. //
  1084. // Returns a SimpleImage object.
  1085. //
  1086. public function fill($color) {
  1087. // Draw a filled rectangle over the entire image
  1088. $this->rectangle(0, 0, $this->getWidth(), $this->getHeight(), 'white', 'filled');
  1089. // Now flood it with the appropriate color
  1090. $color = $this->allocateColor($color);
  1091. imagefill($this->image, 0, 0, $color);
  1092. return $this;
  1093. }
  1094. //
  1095. // Draws a line.
  1096. //
  1097. // $x1* (int) - The x coordinate for the first point.
  1098. // $y1* (int) - The y coordinate for the first point.
  1099. // $x2* (int) - The x coordinate for the second point.
  1100. // $y2* (int) - The y coordinate for the second point.
  1101. // $color (string|array) - The line color.
  1102. // $thickness (int) - The line thickness (default 1).
  1103. //
  1104. // Returns a SimpleImage object.
  1105. //
  1106. public function line($x1, $y1, $x2, $y2, $color, $thickness = 1) {
  1107. // Allocate the color
  1108. $color = $this->allocateColor($color);
  1109. // Draw a line
  1110. imagesetthickness($this->image, $thickness);
  1111. imageline($this->image, $x1, $y1, $x2, $y2, $color);
  1112. return $this;
  1113. }
  1114. //
  1115. // Draws a polygon.
  1116. //
  1117. // $vertices* (array) - The polygon's vertices in an array of x/y arrays. Example:
  1118. // [
  1119. // ['x' => x1, 'y' => y1],
  1120. // ['x' => x2, 'y' => y2],
  1121. // ['x' => xN, 'y' => yN]
  1122. // ]
  1123. // $color* (string|array) - The polygon color.
  1124. // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1).
  1125. //
  1126. // Returns a SimpleImage object.
  1127. //
  1128. public function polygon($vertices, $color, $thickness = 1) {
  1129. // Allocate the color
  1130. $color = $this->allocateColor($color);
  1131. // Convert [['x' => x1, 'y' => x1], ['x' => x1, 'y' => y2], ...] to [x1, y1, x2, y2, ...]
  1132. $points = [];
  1133. foreach($vertices as $vals) {
  1134. $points[] = $vals['x'];
  1135. $points[] = $vals['y'];
  1136. }
  1137. // Draw a polygon
  1138. if($thickness === 'filled') {
  1139. imagesetthickness($this->image, 1);
  1140. imagefilledpolygon($this->image, $points, count($vertices), $color);
  1141. } else {
  1142. imagesetthickness($this->image, $thickness);
  1143. imagepolygon($this->image, $points, count($vertices), $color);
  1144. }
  1145. return $this;
  1146. }
  1147. //
  1148. // Draws a rectangle.
  1149. //
  1150. // $x1* (int) - The upper left x coordinate.
  1151. // $y1* (int) - The upper left y coordinate.
  1152. // $x2* (int) - The bottom right x coordinate.
  1153. // $y2* (int) - The bottom right y coordinate.
  1154. // $color* (string|array) - The rectangle color.
  1155. // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1).
  1156. //
  1157. // Returns a SimpleImage object.
  1158. //
  1159. public function rectangle($x1, $y1, $x2, $y2, $color, $thickness = 1) {
  1160. // Allocate the color
  1161. $color = $this->allocateColor($color);
  1162. // Draw a rectangle
  1163. if($thickness === 'filled') {
  1164. imagesetthickness($this->image, 1);
  1165. imagefilledrectangle($this->image, $x1, $y1, $x2, $y2, $color);
  1166. } else {
  1167. imagesetthickness($this->image, $thickness);
  1168. imagerectangle($this->image, $x1, $y1, $x2, $y2, $color);
  1169. }
  1170. return $this;
  1171. }
  1172. //
  1173. // Draws a rounded rectangle.
  1174. //
  1175. // $x1* (int) - The upper left x coordinate.
  1176. // $y1* (int) - The upper left y coordinate.
  1177. // $x2* (int) - The bottom right x coordinate.
  1178. // $y2* (int) - The bottom right y coordinate.
  1179. // $radius* (int) - The border radius in pixels.
  1180. // $color* (string|array) - The rectangle color.
  1181. // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1).
  1182. //
  1183. // Returns a SimpleImage object.
  1184. //
  1185. public function roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $thickness = 1) {
  1186. if($thickness === 'filled') {
  1187. // Draw the filled rectangle without edges
  1188. $this->rectangle($x1 + $radius + 1, $y1, $x2 - $radius - 1, $y2, $color, 'filled');
  1189. $this->rectangle($x1, $y1 + $radius + 1, $x1 + $radius, $y2 - $radius - 1, $color, 'filled');
  1190. $this->rectangle($x2 - $radius, $y1 + $radius + 1, $x2, $y2 - $radius - 1, $color, 'filled');
  1191. // Fill in the edges with arcs
  1192. $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, 'filled');
  1193. $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, 'filled');
  1194. $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, 'filled');
  1195. $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, 'filled');
  1196. } else {
  1197. // Draw the rectangle outline without edges
  1198. $this->line($x1 + $radius, $y1, $x2 - $radius, $y1, $color, $thickness);
  1199. $this->line($x1 + $radius, $y2, $x2 - $radius, $y2, $color, $thickness);
  1200. $this->line($x1, $y1 + $radius, $x1, $y2 - $radius, $color, $thickness);
  1201. $this->line($x2, $y1 + $radius, $x2, $y2 - $radius, $color, $thickness);
  1202. // Fill in the edges with arcs
  1203. $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, $thickness);
  1204. $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, $thickness);
  1205. $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, $thickness);
  1206. $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, $thickness);
  1207. }
  1208. return $this;
  1209. }
  1210. //////////////////////////////////////////////////////////////////////////////////////////////////
  1211. // Filters
  1212. //////////////////////////////////////////////////////////////////////////////////////////////////
  1213. //
  1214. // Applies the blur filter.
  1215. //
  1216. // $type (string) - The blur algorithm to use: 'selective', 'gaussian' (default 'gaussian').
  1217. // $passes (int) - The number of time to apply the filter, enhancing the effect (default 1).
  1218. //
  1219. // Returns a SimpleImage object.
  1220. //
  1221. public function blur($type = 'selective', $passes = 1) {
  1222. $filter = $type === 'gaussian' ? IMG_FILTER_GAUSSIAN_BLUR : IMG_FILTER_SELECTIVE_BLUR;
  1223. for($i = 0; $i < $passes; $i++) {
  1224. imagefilter($this->image, $filter);
  1225. }
  1226. return $this;
  1227. }
  1228. //
  1229. // Applies the brightness filter to brighten the image.
  1230. //
  1231. // $percentage* (int) - Percentage to brighten the image (0 - 100).
  1232. //
  1233. // Returns a SimpleImage object.
  1234. //
  1235. public function brighten($percentage) {
  1236. $percentage = self::keepWithin(255 * $percentage / 100, 0, 255);
  1237. imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $percentage);
  1238. return $this;
  1239. }
  1240. //
  1241. // Applies the colorize filter.
  1242. //
  1243. // $color* (string|array) - The filter color.
  1244. //
  1245. // Returns a SimpleImage object.
  1246. //
  1247. public function colorize($color) {
  1248. $color = self::normalizeColor($color);
  1249. imagefilter(
  1250. $this->image,
  1251. IMG_FILTER_COLORIZE,
  1252. $color['red'],
  1253. $color['green'],
  1254. $color['blue'],
  1255. 127 - ($color['alpha'] * 127)
  1256. );
  1257. return $this;
  1258. }
  1259. //
  1260. // Applies the contrast filter.
  1261. //
  1262. // $percentage* (int) - Percentage to adjust (-100 - 100).
  1263. //
  1264. // Returns a SimpleImage object.
  1265. //
  1266. public function contrast($percentage) {
  1267. imagefilter($this->image, IMG_FILTER_CONTRAST, self::keepWithin($percentage, -100, 100));
  1268. return $this;
  1269. }
  1270. //
  1271. // Applies the brightness filter to darken the image.
  1272. //
  1273. // $percentage* (int) - Percentage to darken the image (0 - 100).
  1274. //
  1275. // Returns a SimpleImage object.
  1276. //
  1277. public function darken($percentage) {
  1278. $percentage = self::keepWithin(255 * $percentage / 100, 0, 255);
  1279. imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -$percentage);
  1280. return $this;
  1281. }
  1282. //
  1283. // Applies the desaturate (grayscale) filter.
  1284. //
  1285. // Returns a SimpleImage object.
  1286. //
  1287. public function desaturate() {
  1288. imagefilter($this->image, IMG_FILTER_GRAYSCALE);
  1289. return $this;
  1290. }
  1291. //
  1292. // Applies the edge detect filter.
  1293. //
  1294. // Returns a SimpleImage object.
  1295. //
  1296. public function edgeDetect() {
  1297. imagefilter($this->image, IMG_FILTER_EDGEDETECT);
  1298. return $this;
  1299. }
  1300. //
  1301. // Applies the emboss filter.
  1302. //
  1303. // Returns a SimpleImage object.
  1304. //
  1305. public function emboss() {
  1306. imagefilter($this->image, IMG_FILTER_EMBOSS);
  1307. return $this;
  1308. }
  1309. //
  1310. // Inverts the image's colors.
  1311. //
  1312. // Returns a SimpleImage object.
  1313. //
  1314. public function invert() {
  1315. imagefilter($this->image, IMG_FILTER_NEGATE);
  1316. return $this;
  1317. }
  1318. //
  1319. // Changes the image's opacity level.
  1320. //
  1321. // $opacity* (float) - The desired opacity level (0 - 1).
  1322. //
  1323. // Returns a SimpleImage object.
  1324. //
  1325. public function opacity($opacity) {
  1326. // Create a transparent image
  1327. $newImage = new SimpleImage();
  1328. $newImage->fromNew($this->getWidth(), $this->getHeight());
  1329. // Copy the current image (with opacity) onto the transparent image
  1330. self::imageCopyMergeAlpha(
  1331. $newImage->image,
  1332. $this->image,
  1333. 0, 0,
  1334. 0, 0,
  1335. $this->getWidth(),
  1336. $this->getHeight(),
  1337. self::keepWithin($opacity, 0, 1) * 100
  1338. );
  1339. return $this;
  1340. }
  1341. //
  1342. // Applies the pixelate filter.
  1343. //
  1344. // $size (int) - The size of the blocks in pixels (default 10).
  1345. //
  1346. // Returns a SimpleImage object.
  1347. //
  1348. public function pixelate($size = 10) {
  1349. imagefilter($this->image, IMG_FILTER_PIXELATE, $size, true);
  1350. return $this;
  1351. }
  1352. //
  1353. // Simulates a sepia effect by desaturating the image and applying a sepia tone.
  1354. //
  1355. // Returns a SimpleImage object.
  1356. //
  1357. public function sepia() {
  1358. imagefilter($this->image, IMG_FILTER_GRAYSCALE);
  1359. imagefilter($this->image, IMG_FILTER_COLORIZE, 70, 35, 0);
  1360. return $this;
  1361. }
  1362. //
  1363. // Sharpens the image.
  1364. //
  1365. // $amount (int) - Sharpening amount (default 50)
  1366. //
  1367. // Returns a SimpleImage object.
  1368. //
  1369. public function sharpen($amount = 50) {
  1370. // Normalize amount
  1371. $amount = max(1, min(100, $amount)) / 100;
  1372. $sharpen = [
  1373. [-1, -1, -1],
  1374. [-1, 8 / $amount, -1],
  1375. [-1, -1, -1],
  1376. ];
  1377. $divisor = array_sum(array_map('array_sum', $sharpen));
  1378. imageconvolution($this->image, $sharpen, $divisor, 0);
  1379. return $this;
  1380. }
  1381. //
  1382. // Applies the mean remove filter to produce a sketch effect.
  1383. //
  1384. // Returns a SimpleImage object.
  1385. //
  1386. public function sketch() {
  1387. imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL);
  1388. return $this;
  1389. }
  1390. //////////////////////////////////////////////////////////////////////////////////////////////////
  1391. // Color utilities
  1392. //////////////////////////////////////////////////////////////////////////////////////////////////
  1393. //
  1394. // Converts a "friendly color" into a color identifier for use with GD's image functions.
  1395. //
  1396. // $image (resource) - The target image.
  1397. // $color (string|array) - The color to allocate.
  1398. //
  1399. // Returns a color identifier.
  1400. //
  1401. protected function allocateColor($color) {
  1402. $color = self::normalizeColor($color);
  1403. // Was this color already allocated?
  1404. $index = imagecolorexactalpha(
  1405. $this->image,
  1406. $color['red'],
  1407. $color['green'],
  1408. $color['blue'],
  1409. 127 - ($color['alpha'] * 127)
  1410. );
  1411. if($index > -1) {
  1412. // Yes, return this color index
  1413. return $index;
  1414. }
  1415. // Allocate a new color index
  1416. return imagecolorallocatealpha(
  1417. $this->image,
  1418. $color['red'],
  1419. $color['green'],
  1420. $color['blue'],
  1421. 127 - ($color['alpha'] * 127)
  1422. );
  1423. }
  1424. //
  1425. // Adjusts a color by increasing/decreasing red/green/blue/alpha values independently.
  1426. //
  1427. // $color* (string|array) - The color to adjust.
  1428. // $red* (int) - Red adjustment (-255 - 255).
  1429. // $green* (int) - Green adjustment (-255 - 255).
  1430. // $blue* (int) - Blue adjustment (-255 - 255).
  1431. // $alpha* (float) - Alpha adjustment (-1 - 1).
  1432. //
  1433. // Returns an RGBA color array.
  1434. //
  1435. public static function adjustColor($color, $red, $green, $blue, $alpha) {
  1436. // Normalize to RGBA
  1437. $color = self::normalizeColor($color);
  1438. // Adjust each channel
  1439. return self::normalizeColor([
  1440. 'red' => $color['red'] + $red,
  1441. 'green' => $color['green'] + $green,
  1442. 'blue' => $color['blue'] + $blue,
  1443. 'alpha' => $color['alpha'] + $alpha
  1444. ]);
  1445. }
  1446. //
  1447. // Darkens a color.
  1448. //
  1449. // $color* (string|array) - The color to darken.
  1450. // $amount* (int) - Amount to darken (0 - 255).
  1451. //
  1452. // Returns an RGBA color array.
  1453. //
  1454. public static function darkenColor($color, $amount) {
  1455. return self::adjustColor($color, -$amount, -$amount, -$amount, 0);
  1456. }
  1457. //
  1458. // Extracts colors from an image like a human would do.™ This method requires the third-party
  1459. // library \League\ColorExtractor. If you're using Composer, it will be installed for you
  1460. // automatically.
  1461. //
  1462. // $count (int) - The max number of colors to extract (default 5).
  1463. // $backgroundColor (string|array) - By default any pixel with alpha value greater than zero will
  1464. // be discarded. This is because transparent colors are not perceived as is. For example, fully
  1465. // transparent black would be seen white on a white background. So if you want to take
  1466. // transparency into account, you have to specify a default background color.
  1467. //
  1468. // Returns an array of RGBA colors arrays.
  1469. //
  1470. public function extractColors($count = 5, $backgroundColor = null) {
  1471. // Check for required library
  1472. if(!class_exists('\League\ColorExtractor\ColorExtractor')) {
  1473. throw new \Exception(
  1474. 'Required library \League\ColorExtractor is missing.',
  1475. self::ERR_LIB_NOT_LOADED
  1476. );
  1477. }
  1478. // Convert background color to an integer value
  1479. if($backgroundColor) {
  1480. $backgroundColor = self::normalizeColor($backgroundColor);
  1481. $backgroundColor = \League\ColorExtractor\Color::fromRgbToInt([
  1482. 'r' => $backgroundColor['red'],
  1483. 'g' => $backgroundColor['green'],
  1484. 'b' => $backgroundColor['blue']
  1485. ]);
  1486. }
  1487. // Extract colors from the image
  1488. $palette = \League\ColorExtractor\Palette::fromGD($this->image, $backgroundColor);
  1489. $extractor = new \League\ColorExtractor\ColorExtractor($palette);
  1490. $colors = $extractor->extract($count);
  1491. // Convert colors to an RGBA color array
  1492. foreach($colors as $key => $value) {
  1493. $colors[$key] = self::normalizeColor(\League\ColorExtractor\Color::fromIntToHex($value));
  1494. }
  1495. return $colors;
  1496. }
  1497. //
  1498. // Gets the RGBA value of a single pixel.
  1499. //
  1500. // $x* (int) - The horizontal position of the pixel.
  1501. // $y* (int) - The vertical position of the pixel.
  1502. //
  1503. // Returns an RGBA color array or false if the x/y position is off the canvas.
  1504. //
  1505. public function getColorAt($x, $y) {
  1506. // Coordinates must be on the canvas
  1507. if($x < 0 || $x > $this->getWidth() || $y < 0 || $y > $this->getHeight()) {
  1508. return false;
  1509. }
  1510. // Get the color of this pixel and convert it to RGBA
  1511. $color = imagecolorat($this->image, $x, $y);
  1512. $rgba = imagecolorsforindex($this->image, $color);
  1513. $rgba['alpha'] = 127 - ($color >> 24) & 0xFF;
  1514. return $rgba;
  1515. }
  1516. //
  1517. // Lightens a color.
  1518. //
  1519. // $color* (string|array) - The color to lighten.
  1520. // $amount* (int) - Amount to darken (0 - 255).
  1521. //
  1522. // Returns an RGBA color array.
  1523. //
  1524. public static function lightenColor($color, $amount) {
  1525. return self::adjustColor($color, $amount, $amount, $amount, 0);
  1526. }
  1527. //
  1528. // Normalizes a hex or array color value to a well-formatted RGBA array.
  1529. //
  1530. // $color* (string|array) - A CSS color name, hex string, or an array [red, green, blue, alpha].
  1531. // You can pipe alpha transparency through hex strings and color names. For example:
  1532. //
  1533. // #fff|0.50 <-- 50% white
  1534. // red|0.25 <-- 25% red
  1535. //
  1536. // Returns an array: [red, green, blue, alpha]
  1537. //
  1538. public static function normalizeColor($color) {
  1539. // 140 CSS color names and hex values
  1540. $cssColors = [
  1541. 'aliceblue' => '#f0f8ff', 'antiquewhite' => '#faebd7', 'aqua' => '#00ffff',
  1542. 'aquamarine' => '#7fffd4', 'azure' => '#f0ffff', 'beige' => '#f5f5dc', 'bisque' => '#ffe4c4',
  1543. 'black' => '#000000', 'blanchedalmond' => '#ffebcd', 'blue' => '#0000ff',
  1544. 'blueviolet' => '#8a2be2', 'brown' => '#a52a2a', 'burlywood' => '#deb887',
  1545. 'cadetblue' => '#5f9ea0', 'chartreuse' => '#7fff00', 'chocolate' => '#d2691e',
  1546. 'coral' => '#ff7f50', 'cornflowerblue' => '#6495ed', 'cornsilk' => '#fff8dc',
  1547. 'crimson' => '#dc143c', 'cyan' => '#00ffff', 'darkblue' => '#00008b', 'darkcyan' => '#008b8b',
  1548. 'darkgoldenrod' => '#b8860b', 'darkgray' => '#a9a9a9', 'darkgrey' => '#a9a9a9',
  1549. 'darkgreen' => '#006400', 'darkkhaki' => '#bdb76b', 'darkmagenta' => '#8b008b',
  1550. 'darkolivegreen' => '#556b2f', 'darkorange' => '#ff8c00', 'darkorchid' => '#9932cc',
  1551. 'darkred' => '#8b0000', 'darksalmon' => '#e9967a', 'darkseagreen' => '#8fbc8f',
  1552. 'darkslateblue' => '#483d8b', 'darkslategray' => '#2f4f4f', 'darkslategrey' => '#2f4f4f',
  1553. 'darkturquoise' => '#00ced1', 'darkviolet' => '#9400d3', 'deeppink' => '#ff1493',
  1554. 'deepskyblue' => '#00bfff', 'dimgray' => '#696969', 'dimgrey' => '#696969',
  1555. 'dodgerblue' => '#1e90ff', 'firebrick' => '#b22222', 'floralwhite' => '#fffaf0',
  1556. 'forestgreen' => '#228b22', 'fuchsia' => '#ff00ff', 'gainsboro' => '#dcdcdc',
  1557. 'ghostwhite' => '#f8f8ff', 'gold' => '#ffd700', 'goldenrod' => '#daa520', 'gray' => '#808080',
  1558. 'grey' => '#808080', 'green' => '#008000', 'greenyellow' => '#adff2f',
  1559. 'honeydew' => '#f0fff0', 'hotpink' => '#ff69b4', 'indianred ' => '#cd5c5c',
  1560. 'indigo ' => '#4b0082', 'ivory' => '#fffff0', 'khaki' => '#f0e68c', 'lavender' => '#e6e6fa',
  1561. 'lavenderblush' => '#fff0f5', 'lawngreen' => '#7cfc00', 'lemonchiffon' => '#fffacd',
  1562. 'lightblue' => '#add8e6', 'lightcoral' => '#f08080', 'lightcyan' => '#e0ffff',
  1563. 'lightgoldenrodyellow' => '#fafad2', 'lightgray' => '#d3d3d3', 'lightgrey' => '#d3d3d3',
  1564. 'lightgreen' => '#90ee90', 'lightpink' => '#ffb6c1', 'lightsalmon' => '#ffa07a',
  1565. 'lightseagreen' => '#20b2aa', 'lightskyblue' => '#87cefa', 'lightslategray' => '#778899',
  1566. 'lightslategrey' => '#778899', 'lightsteelblue' => '#b0c4de', 'lightyellow' => '#ffffe0',
  1567. 'lime' => '#00ff00', 'limegreen' => '#32cd32', 'linen' => '#faf0e6', 'magenta' => '#ff00ff',
  1568. 'maroon' => '#800000', 'mediumaquamarine' => '#66cdaa', 'mediumblue' => '#0000cd',
  1569. 'mediumorchid' => '#ba55d3', 'mediumpurple' => '#9370db', 'mediumseagreen' => '#3cb371',
  1570. 'mediumslateblue' => '#7b68ee', 'mediumspringgreen' => '#00fa9a',
  1571. 'mediumturquoise' => '#48d1cc', 'mediumvioletred' => '#c71585', 'midnightblue' => '#191970',
  1572. 'mintcream' => '#f5fffa', 'mistyrose' => '#ffe4e1', 'moccasin' => '#ffe4b5',
  1573. 'navajowhite' => '#ffdead', 'navy' => '#000080', 'oldlace' => '#fdf5e6', 'olive' => '#808000',
  1574. 'olivedrab' => '#6b8e23', 'orange' => '#ffa500', 'orangered' => '#ff4500',
  1575. 'orchid' => '#da70d6', 'palegoldenrod' => '#eee8aa', 'palegreen' => '#98fb98',
  1576. 'paleturquoise' => '#afeeee', 'palevioletred' => '#db7093', 'papayawhip' => '#ffefd5',
  1577. 'peachpuff' => '#ffdab9', 'peru' => '#cd853f', 'pink' => '#ffc0cb', 'plum' => '#dda0dd',
  1578. 'powderblue' => '#b0e0e6', 'purple' => '#800080', 'rebeccapurple' => '#663399',
  1579. 'red' => '#ff0000', 'rosybrown' => '#bc8f8f', 'royalblue' => '#4169e1',
  1580. 'saddlebrown' => '#8b4513', 'salmon' => '#fa8072', 'sandybrown' => '#f4a460',
  1581. 'seagreen' => '#2e8b57', 'seashell' => '#fff5ee', 'sienna' => '#a0522d',
  1582. 'silver' => '#c0c0c0', 'skyblue' => '#87ceeb', 'slateblue' => '#6a5acd',
  1583. 'slategray' => '#708090', 'slategrey' => '#708090', 'snow' => '#fffafa',
  1584. 'springgreen' => '#00ff7f', 'steelblue' => '#4682b4', 'tan' => '#d2b48c', 'teal' => '#008080',
  1585. 'thistle' => '#d8bfd8', 'tomato' => '#ff6347', 'turquoise' => '#40e0d0',
  1586. 'violet' => '#ee82ee', 'wheat' => '#f5deb3', 'white' => '#ffffff', 'whitesmoke' => '#f5f5f5',
  1587. 'yellow' => '#ffff00', 'yellowgreen' => '#9acd32'
  1588. ];
  1589. // Parse alpha from '#fff|.5' and 'white|.5'
  1590. if(is_string($color) && strstr($color, '|')) {
  1591. $color = explode('|', $color);
  1592. $alpha = (float) $color[1];
  1593. $color = trim($color[0]);
  1594. } else {
  1595. $alpha = 1;
  1596. }
  1597. // Translate CSS color names to hex values
  1598. if(is_string($color) && array_key_exists(strtolower($color), $cssColors)) {
  1599. $color = $cssColors[strtolower($color)];
  1600. }
  1601. // Translate transparent keyword to a transparent color
  1602. if($color === 'transparent') {
  1603. $color = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0];
  1604. }
  1605. // Convert hex values to RGBA
  1606. if(is_string($color)) {
  1607. // Remove #
  1608. $hex = preg_replace('/^#/', '', $color);
  1609. // Support short and standard hex codes
  1610. if(strlen($hex) === 3) {
  1611. list($red, $green, $blue) = [
  1612. $hex[0] . $hex[0],
  1613. $hex[1] . $hex[1],
  1614. $hex[2] . $hex[2]
  1615. ];
  1616. } elseif(strlen($hex) === 6) {
  1617. list($red, $green, $blue) = [
  1618. $hex[0] . $hex[1],
  1619. $hex[2] . $hex[3],
  1620. $hex[4] . $hex[5]
  1621. ];
  1622. } else {
  1623. throw new \Exception("Invalid color value: $color", self::ERR_INVALID_COLOR);
  1624. }
  1625. // Turn color into an array
  1626. $color = [
  1627. 'red' => hexdec($red),
  1628. 'green' => hexdec($green),
  1629. 'blue' => hexdec($blue),
  1630. 'alpha' => $alpha
  1631. ];
  1632. }
  1633. // Enforce color value ranges
  1634. if(is_array($color)) {
  1635. // RGB default to 0
  1636. $color['red'] = isset($color['red']) ? $color['red'] : 0;
  1637. $color['green'] = isset($color['green']) ? $color['green'] : 0;
  1638. $color['blue'] = isset($color['blue']) ? $color['blue'] : 0;
  1639. // Alpha defaults to 1
  1640. $color['alpha'] = isset($color['alpha']) ? $color['alpha'] : 1;
  1641. return [
  1642. 'red' => (int) self::keepWithin((int) $color['red'], 0, 255),
  1643. 'green' => (int) self::keepWithin((int) $color['green'], 0, 255),
  1644. 'blue' => (int) self::keepWithin((int) $color['blue'], 0, 255),
  1645. 'alpha' => self::keepWithin($color['alpha'], 0, 1)
  1646. ];
  1647. }
  1648. throw new \Exception("Invalid color value: $color", self::ERR_INVALID_COLOR);
  1649. }
  1650. }