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

/app/parsers/slir/slir.class.php

http://github.com/kolber/stacey
PHP | 1508 lines | 736 code | 155 blank | 617 comment | 105 complexity | f08ba20f1e1fb8116f54944363bcd0ba MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. * Class definition file for SLIR (Smart Lencioni Image Resizer)
  4. *
  5. * This file is part of SLIR (Smart Lencioni Image Resizer).
  6. *
  7. * SLIR is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SLIR is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with SLIR. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. * @copyright Copyright © 2010, Joe Lencioni
  21. * @license http://opensource.org/licenses/gpl-3.0.html GNU General Public
  22. * License version 3 (GPLv3)
  23. * @since 2.0
  24. * @package SLIR
  25. */
  26. /* $Id: slir.class.php 129 2010-12-22 19:43:06Z joe.lencioni $ */
  27. /**
  28. * SLIR (Smart Lencioni Image Resizer)
  29. * Resizes images, intelligently sharpens, crops based on width:height ratios,
  30. * color fills transparent GIFs and PNGs, and caches variations for optimal
  31. * performance.
  32. *
  33. * I love to hear when my work is being used, so if you decide to use this,
  34. * feel encouraged to send me an email. I would appreciate it if you would
  35. * include a link on your site back to Shifting Pixel (either the SLIR page or
  36. * shiftingpixel.com), but don’t worry about including a big link on each page
  37. * if you don’t want to–one will do just nicely. Feel free to contact me to
  38. * discuss any specifics (joe@shiftingpixel.com).
  39. *
  40. * REQUIREMENTS:
  41. * - PHP 5.1.0+
  42. * - GD
  43. *
  44. * RECOMMENDED:
  45. * - mod_rewrite
  46. *
  47. * USAGE:
  48. * To use, place an img tag with the src pointing to the path of SLIR (typically
  49. * "/slir/") followed by the parameters, followed by the path to the source
  50. * image to resize. All parameters follow the pattern of a one-letter code and
  51. * then the parameter value:
  52. * - Maximum width = w
  53. * - Maximum height = h
  54. * - Crop ratio = c
  55. * - Quality = q
  56. * - Background fill color = b
  57. * - Progressive = p
  58. *
  59. * Note: filenames that include special characters must be URL-encoded (e.g.
  60. * plus sign, +, should be encoded as %2B) in order for SLIR to recognize them
  61. * properly. This can be accomplished by passing your filenames through PHP's
  62. * rawurlencode() or urlencode() function.
  63. *
  64. * EXAMPLES:
  65. *
  66. * Resizing a JPEG to a max width of 100 pixels and a max height of 100 pixels:
  67. * <code><img src="/slir/w100-h100/path/to/image.jpg" alt="Don't forget your alt
  68. * text" /></code>
  69. *
  70. * Resizing and cropping a JPEG into a square:
  71. * <code><img src="/slir/w100-h100-c1:1/path/to/image.jpg" alt="Don't forget
  72. * your alt text" /></code>
  73. *
  74. * Resizing a JPEG without interlacing (for use in Flash):
  75. * <code><img src="/slir/w100-p0/path/to/image.jpg" alt="Don't forget your alt
  76. * text" /></code>
  77. *
  78. * Matting a PNG with #990000:
  79. * <code><img src="/slir/b900/path/to/image.png" alt="Don't forget your alt
  80. * text" /></code>
  81. *
  82. * Without mod_rewrite (not recommended)
  83. * <code><img src="/slir/?w=100&amp;h=100&amp;c=1:1&amp;i=/path/to/image.jpg"
  84. * alt="Don't forget your alt text" /></code>
  85. *
  86. * @author Joe Lencioni <joe@shiftingpixel.com>
  87. * $Date: 2010-12-22 13:43:06 -0600 (Wed, 22 Dec 2010) $
  88. * @version $Revision: 129 $
  89. * @package SLIR
  90. *
  91. * @uses PEL
  92. *
  93. * @todo lock files when writing?
  94. * @todo Prevent SLIR from calling itself
  95. * @todo Percentage resizing?
  96. * @todo Animated GIF resizing?
  97. * @todo Seam carving?
  98. * @todo Crop zoom?
  99. * @todo Crop offsets?
  100. * @todo Remote image fetching?
  101. * @todo Alternative support for ImageMagick?
  102. * @todo Prevent files in cache from being read directly?
  103. * @todo split directory initialization into a separate
  104. * install/upgrade script with friendly error messages, an opportunity to give a
  105. * tip, and a button that tells me they are using it on their site if they like
  106. * @todo document new code
  107. * @todo clean up new code
  108. */
  109. class SLIR
  110. {
  111. /**
  112. * @since 2.0
  113. * @var string
  114. */
  115. const VERSION = '2.0b4';
  116. /**
  117. * @since 2.0
  118. * @var string
  119. */
  120. const CROP_CLASS_CENTERED = 'centered';
  121. /**
  122. * @since 2.0
  123. * @var string
  124. */
  125. const CROP_CLASS_TOP_CENTERED = 'topcentered';
  126. /**
  127. * @since 2.0
  128. * @var string
  129. */
  130. const CROP_CLASS_SMART = 'smart';
  131. /**
  132. * @since 2.0
  133. * @var string
  134. */
  135. const CROP_CLASS_FACE = 'face';
  136. /**
  137. * Request object
  138. *
  139. * @since 2.0
  140. * @uses SLIRRequest
  141. * @var object
  142. */
  143. private $request;
  144. /**
  145. * Source image object
  146. *
  147. * @since 2.0
  148. * @uses SLIRImage
  149. * @var object
  150. */
  151. private $source;
  152. /**
  153. * Rendered image object
  154. *
  155. * @since 2.0
  156. * @uses SLIRImage
  157. * @var object
  158. */
  159. private $rendered;
  160. /**
  161. * Whether or not the cache has already been initialized
  162. *
  163. * @since 2.0
  164. * @var boolean
  165. */
  166. private $isCacheInitialized = FALSE;
  167. /**
  168. * The magic starts here
  169. *
  170. * @since 2.0
  171. */
  172. final public function __construct()
  173. {
  174. // This helps prevent unnecessary warnings (which messes up images)
  175. // on servers that are set to display E_STRICT errors.
  176. $this->disableStrictErrorReporting();
  177. // Prevents ob_start('ob_gzhandler') in auto_prepend files from messing
  178. // up SLIR's output.
  179. $this->escapeOutputBuffering();
  180. $this->getConfig();
  181. $this->initializeGarbageCollection();
  182. $this->request = new SLIRRequest();
  183. // Check the cache based on the request URI
  184. if (SLIRConfig::$useRequestCache === TRUE && $this->isRequestCached())
  185. {
  186. $this->serveRequestCachedImage();
  187. }
  188. // Set up our error handler after the request cache to help keep
  189. // everything humming along nicely
  190. require 'slirexception.class.php';
  191. set_error_handler(array('SLIRException', 'error'));
  192. // Set all parameters for resizing
  193. $this->setParameters();
  194. // See if there is anything we actually need to do
  195. if ($this->isSourceImageDesired())
  196. {
  197. $this->serveSourceImage();
  198. }
  199. // Determine rendered dimensions
  200. $this->setRenderedProperties();
  201. // Check the cache based on the properties of the rendered image
  202. if (!$this->isRenderedCached() || !$this->serveRenderedCachedImage())
  203. {
  204. // Image is not cached in any way, so we need to render the image,
  205. // cache it, and serve it up to the client
  206. $this->render();
  207. $this->serveRenderedImage();
  208. } // if
  209. }
  210. /**
  211. * Disables E_STRICT error reporting
  212. *
  213. * @since 2.0
  214. * @return integer
  215. */
  216. private function disableStrictErrorReporting()
  217. {
  218. return error_reporting(error_reporting() & ~E_STRICT);
  219. }
  220. /**
  221. * Escapes from output buffering.
  222. *
  223. * @since 2.0
  224. * @return void
  225. */
  226. private function escapeOutputBuffering()
  227. {
  228. while ($level = ob_get_level())
  229. {
  230. ob_end_clean();
  231. if ($level == ob_get_level()) // On some setups, ob_get_level() will return a 1 instead of a 0 when there are no more buffers
  232. {
  233. return;
  234. }
  235. }
  236. }
  237. /**
  238. * Determines if the garbage collector should run for this request.
  239. *
  240. * @since 2.0
  241. * @return boolean
  242. */
  243. private function garbageCollectionShouldRun()
  244. {
  245. if (rand(1, SLIRConfig::$garbageCollectDivisor) <= SLIRConfig::$garbageCollectProbability)
  246. {
  247. return TRUE;
  248. }
  249. else
  250. {
  251. return FALSE;
  252. }
  253. }
  254. /**
  255. * Checks to see if the garbage collector should be initialized, and if it should, initializes it.
  256. *
  257. * @since 2.0
  258. * @return void
  259. */
  260. private function initializeGarbageCollection()
  261. {
  262. if ($this->garbageCollectionShouldRun())
  263. {
  264. // Register this as a shutdown function so the additional processing time
  265. // will not affect the speed of the request
  266. register_shutdown_function(array($this, 'collectGarbage'));
  267. }
  268. }
  269. /**
  270. * Deletes stale files from a directory.
  271. *
  272. * Used by the garbage collector to keep the cache directories from overflowing.
  273. *
  274. * @param string $path Directory to delete stale files from
  275. */
  276. private function deleteStaleFilesFromDirectory($path, $useAccessedTime = TRUE)
  277. {
  278. $now = time();
  279. $dir = new DirectoryIterator($path);
  280. if ($useAccessedTime === TRUE)
  281. {
  282. $function = 'getATime';
  283. }
  284. else
  285. {
  286. $function = 'getCTime';
  287. }
  288. foreach ($dir as $file)
  289. {
  290. if (!$file->isDot() && ($now - $file->$function()) > SLIRConfig::$garbageCollectFileCacheMaxLifetime)
  291. {
  292. unlink($file->getPathName());
  293. }
  294. }
  295. }
  296. /**
  297. * Garbage collector
  298. *
  299. * Clears out old files from the cache
  300. *
  301. * @since 2.0
  302. * @return void
  303. */
  304. public function collectGarbage()
  305. {
  306. $this->deleteStaleFilesFromDirectory($this->getRequestCacheDir(), FALSE);
  307. $this->deleteStaleFilesFromDirectory($this->getRenderedCacheDir());
  308. }
  309. /**
  310. * Includes the configuration file.
  311. *
  312. * If the configuration file cannot be included, this will throw an error that will hopefully explain what needs to be done.
  313. *
  314. * @since 2.0
  315. * @return void
  316. */
  317. private function getConfig()
  318. {
  319. if (file_exists(self::configFilename()))
  320. {
  321. require self::configFilename();
  322. }
  323. else if (file_exists('slirconfig-sample.class.php'))
  324. {
  325. if (copy('slirconfig-sample.class.php', self::configFilename()))
  326. {
  327. require self::configFilename();
  328. }
  329. else
  330. {
  331. throw new SLIRException('Could not load configuration file. '
  332. . 'Please copy "slirconfig-sample.class.php" to '
  333. . '"' . self::configFilename() . '".');
  334. }
  335. }
  336. else
  337. {
  338. throw new SLIRException('Could not find "' . self::configFilename() . '" or '
  339. . '"slirconfig-sample.class.php"');
  340. } // if
  341. }
  342. /**
  343. * Returns the configuration filename. Allows the developer to specify an alternate configuration file.
  344. *
  345. * @since 2.0
  346. * @return string
  347. */
  348. private function configFilename()
  349. {
  350. if (defined('SLIR_CONFIG_FILENAME'))
  351. {
  352. return SLIR_CONFIG_FILENAME;
  353. }
  354. else
  355. {
  356. return 'slirconfig.class.php';
  357. }
  358. }
  359. /**
  360. * Sets up parameters for image resizing
  361. *
  362. * @since 2.0
  363. * @return void
  364. */
  365. private function setParameters()
  366. {
  367. $this->source = new SLIRImage();
  368. $this->source->path = $this->request->path;
  369. // If either a max width or max height are not specified or larger than
  370. // the source image we default to the dimension of the source image so
  371. // they do not become constraints on our resized image.
  372. if (!$this->request->width || $this->request->width > $this->source->width)
  373. {
  374. $this->request->width = $this->source->width;
  375. }
  376. if (!$this->request->height || $this->request->height > $this->source->height)
  377. {
  378. $this->request->height = $this->source->height;
  379. }
  380. }
  381. /**
  382. * Allocates memory for the request.
  383. *
  384. * Tries to dynamically guess how much memory will be needed for the request based on the dimensions of the source image.
  385. *
  386. * @since 2.0
  387. * @return void
  388. */
  389. private function allocateMemory()
  390. {
  391. // Multiply width * height * 5 bytes
  392. $estimatedMemory = $this->source->width * $this->source->height * 5;
  393. // Convert memory to Megabytes and add 15 in order to allow some slack
  394. $estimatedMemory = round(($estimatedMemory / 1024) / 1024, 0) + 15;
  395. $v = ini_set('memory_limit', min($estimatedMemory, SLIRConfig::$maxMemoryToAllocate) . 'M');
  396. }
  397. /**
  398. * Renders requested changes to the image
  399. *
  400. * @since 2.0
  401. * @return void
  402. */
  403. private function render()
  404. {
  405. $this->allocateMemory();
  406. $this->source->createImageFromFile();
  407. $this->rendered->createBlankImage();
  408. $this->rendered->background($this->isBackgroundFillOn());
  409. $this->copySourceToRendered();
  410. $this->rendered->setPath($this->source->path, FALSE);
  411. $this->source->destroyImage();
  412. $this->rendered->crop($this->isBackgroundFillOn());
  413. $this->rendered->sharpen($this->calculateSharpnessFactor());
  414. $this->rendered->interlace();
  415. }
  416. /**
  417. * Copies the source image to the rendered image, resizing (resampling) it if resizing is requested
  418. *
  419. * @since 2.0
  420. * @return void
  421. */
  422. private function copySourceToRendered()
  423. {
  424. // Resample the original image into the resized canvas we set up earlier
  425. if ($this->source->width != $this->rendered->width || $this->source->height != $this->rendered->height)
  426. {
  427. ImageCopyResampled(
  428. $this->rendered->image,
  429. $this->source->image,
  430. 0,
  431. 0,
  432. 0,
  433. 0,
  434. $this->rendered->width,
  435. $this->rendered->height,
  436. $this->source->width,
  437. $this->source->height
  438. );
  439. }
  440. else // No resizing is needed, so make a clean copy
  441. {
  442. ImageCopy(
  443. $this->rendered->image,
  444. $this->source->image,
  445. 0,
  446. 0,
  447. 0,
  448. 0,
  449. $this->source->width,
  450. $this->source->height
  451. );
  452. } // if
  453. }
  454. /**
  455. * Calculates how much to sharpen the image based on the difference in dimensions of the source image and the rendered image
  456. *
  457. * @since 2.0
  458. * @return integer Sharpness factor
  459. */
  460. private function calculateSharpnessFactor()
  461. {
  462. return $this->calculateASharpnessFactor($this->source->area(), $this->rendered->area());
  463. }
  464. /**
  465. * Calculates sharpness factor to be used to sharpen an image based on the
  466. * area of the source image and the area of the destination image
  467. *
  468. * @since 2.0
  469. * @author Ryan Rud
  470. * @link http://adryrun.com
  471. *
  472. * @param integer $sourceArea Area of source image
  473. * @param integer $destinationArea Area of destination image
  474. * @return integer Sharpness factor
  475. */
  476. private function calculateASharpnessFactor($sourceArea, $destinationArea)
  477. {
  478. $final = sqrt($destinationArea) * (750.0 / sqrt($sourceArea));
  479. $a = 52;
  480. $b = -0.27810650887573124;
  481. $c = .00047337278106508946;
  482. $result = $a + $b * $final + $c * $final * $final;
  483. return max(round($result), 0);
  484. }
  485. /**
  486. * Copies IPTC data from the source image to the cached file
  487. *
  488. * @since 2.0
  489. * @param string $cacheFilePath
  490. * @return boolean
  491. */
  492. private function copyIPTC($cacheFilePath)
  493. {
  494. $data = '';
  495. $iptc = $this->source->iptc;
  496. // Originating program
  497. $iptc['2#065'] = array('Smart Lencioni Image Resizer');
  498. // Program version
  499. $iptc['2#070'] = array(SLIR::VERSION);
  500. foreach($iptc as $tag => $iptcData)
  501. {
  502. $tag = substr($tag, 2);
  503. $data .= $this->makeIPTCTag(2, $tag, $iptcData[0]);
  504. }
  505. // Embed the IPTC data
  506. return iptcembed($data, $cacheFilePath);
  507. }
  508. /**
  509. * @since 2.0
  510. * @author Thies C. Arntzen
  511. */
  512. final function makeIPTCTag($rec, $data, $value)
  513. {
  514. $length = strlen($value);
  515. $retval = chr(0x1C) . chr($rec) . chr($data);
  516. if ($length < 0x8000)
  517. {
  518. $retval .= chr($length >> 8) . chr($length & 0xFF);
  519. }
  520. else
  521. {
  522. $retval .= chr(0x80) .
  523. chr(0x04) .
  524. chr(($length >> 24) & 0xFF) .
  525. chr(($length >> 16) & 0xFF) .
  526. chr(($length >> 8) & 0xFF) .
  527. chr($length & 0xFF);
  528. }
  529. return $retval . $value;
  530. }
  531. /**
  532. * Checks parameters against the image's attributes and determines whether
  533. * anything needs to be changed or if we simply need to serve up the source
  534. * image
  535. *
  536. * @since 2.0
  537. * @return boolean
  538. * @todo Add check for JPEGs and progressiveness
  539. */
  540. private function isSourceImageDesired()
  541. {
  542. if ($this->isWidthDifferent()
  543. || $this->isHeightDifferent()
  544. || $this->isBackgroundFillOn()
  545. || $this->isQualityOn()
  546. || $this->isCroppingNeeded()
  547. )
  548. {
  549. return FALSE;
  550. }
  551. else
  552. {
  553. return TRUE;
  554. }
  555. }
  556. /**
  557. * Determines if the requested width is different than the width of the source image
  558. *
  559. * @since 2.0
  560. * @return boolean
  561. */
  562. private function isWidthDifferent()
  563. {
  564. if ($this->request->width !== NULL && $this->request->width < $this->source->width)
  565. {
  566. return TRUE;
  567. }
  568. else
  569. {
  570. return FALSE;
  571. }
  572. }
  573. /**
  574. * Determines if the requested height is different than the height of the source image
  575. *
  576. * @since 2.0
  577. * @return boolean
  578. */
  579. private function isHeightDifferent()
  580. {
  581. if ($this->request->height !== NULL && $this->request->height < $this->source->height)
  582. {
  583. return TRUE;
  584. }
  585. else
  586. {
  587. return FALSE;
  588. }
  589. }
  590. /**
  591. * Determines if a background fill has been requested and if the image is able to have transparency (not for JPEG files)
  592. *
  593. * @since 2.0
  594. * @return boolean
  595. */
  596. private function isBackgroundFillOn()
  597. {
  598. if ($this->request->isBackground() && $this->source->isAbleToHaveTransparency())
  599. {
  600. return TRUE;
  601. }
  602. else
  603. {
  604. return FALSE;
  605. }
  606. }
  607. /**
  608. * Determines if the user included image quality in the request
  609. *
  610. * @since 2.0
  611. * @return boolean
  612. */
  613. private function isQualityOn()
  614. {
  615. return $this->request->isQuality();
  616. }
  617. /**
  618. * Determines if the image should be cropped based on the requested crop ratio and the width:height ratio of the source image
  619. *
  620. * @since 2.0
  621. * @return boolean
  622. */
  623. private function isCroppingNeeded()
  624. {
  625. if ($this->request->isCropping() && $this->request->cropRatio['ratio'] != $this->source->ratio())
  626. {
  627. return TRUE;
  628. }
  629. else
  630. {
  631. return FALSE;
  632. }
  633. }
  634. /**
  635. * Computes and sets properties of the rendered image, such as the actual
  636. * width, height, and quality
  637. *
  638. * @since 2.0
  639. */
  640. private function setRenderedProperties()
  641. {
  642. $this->rendered = new SLIRImage();
  643. // Set default properties of the rendered image
  644. $this->rendered->width = $this->source->width;
  645. $this->rendered->height = $this->source->height;
  646. // Cropping
  647. /*
  648. To determine the width and height of the rendered image, the following
  649. should occur.
  650. If cropping an image is required, we need to:
  651. 1. Compute the dimensions of the source image after cropping before
  652. resizing.
  653. 2. Compute the dimensions of the resized image before cropping. One of
  654. these dimensions may be greater than maxWidth or maxHeight because
  655. they are based on the dimensions of the final rendered image, which
  656. will be cropped to fit within the specified maximum dimensions.
  657. 3. Compute the dimensions of the resized image after cropping. These
  658. must both be less than or equal to maxWidth and maxHeight.
  659. 4. Then when rendering, the image needs to be resized, crop offsets
  660. need to be computed based on the desired method (smart or centered),
  661. and the image needs to be cropped to the specified dimensions.
  662. If cropping an image is not required, we need to compute the dimensions
  663. of the image without cropping. These must both be less than or equal to
  664. maxWidth and maxHeight.
  665. */
  666. if ($this->isCroppingNeeded())
  667. {
  668. // Determine the dimensions of the source image after cropping and
  669. // before resizing
  670. if ($this->request->cropRatio['ratio'] > $this->source->ratio())
  671. {
  672. // Image is too tall so we will crop the top and bottom
  673. $this->source->cropHeight = $this->source->width / $this->request->cropRatio['ratio'];
  674. $this->source->cropWidth = $this->source->width;
  675. }
  676. else
  677. {
  678. // Image is too wide so we will crop off the left and right sides
  679. $this->source->cropWidth = $this->source->height * $this->request->cropRatio['ratio'];
  680. $this->source->cropHeight = $this->source->height;
  681. } // if
  682. $this->source->cropper = $this->request->cropper;
  683. $this->rendered->cropper = $this->source->cropper;
  684. } // if
  685. if ($this->shouldResizeBasedOnWidth())
  686. {
  687. $this->rendered->height = round($this->resizeWidthFactor() * $this->source->height);
  688. $this->rendered->width = round($this->resizeWidthFactor() * $this->source->width);
  689. // Determine dimensions after cropping
  690. if ($this->isCroppingNeeded())
  691. {
  692. $this->rendered->cropHeight = round($this->resizeWidthFactor() * $this->source->cropHeight);
  693. $this->rendered->cropWidth = round($this->resizeWidthFactor() * $this->source->cropWidth);
  694. } // if
  695. }
  696. else if ($this->shouldResizeBasedOnHeight())
  697. {
  698. $this->rendered->width = round($this->resizeHeightFactor() * $this->source->width);
  699. $this->rendered->height = round($this->resizeHeightFactor() * $this->source->height);
  700. // Determine dimensions after cropping
  701. if ($this->isCroppingNeeded())
  702. {
  703. $this->rendered->cropHeight = round($this->resizeHeightFactor() * $this->source->cropHeight);
  704. $this->rendered->cropWidth = round($this->resizeHeightFactor() * $this->source->cropWidth);
  705. } // if
  706. }
  707. else if ($this->isCroppingNeeded()) // No resizing is needed but we still need to crop
  708. {
  709. $ratio = ($this->resizeUncroppedWidthFactor() > $this->resizeUncroppedHeightFactor())
  710. ? $this->resizeUncroppedWidthFactor() : $this->resizeUncroppedHeightFactor();
  711. $this->rendered->width = round($ratio * $this->source->width);
  712. $this->rendered->height = round($ratio * $this->source->height);
  713. $this->rendered->cropWidth = round($ratio * $this->source->cropWidth);
  714. $this->rendered->cropHeight = round($ratio * $this->source->cropHeight);
  715. } // if
  716. // Determine the quality of the output image
  717. $this->rendered->quality = ($this->request->quality !== NULL)
  718. ? $this->request->quality : SLIRConfig::$defaultQuality;
  719. // Set up the appropriate image handling parameters based on the original
  720. // image's mime type
  721. // @todo some of this code should be moved to the SLIRImage class
  722. $this->rendered->mime = $this->source->mime;
  723. if ($this->source->isGIF())
  724. {
  725. // We need to convert GIFs to PNGs
  726. $this->rendered->mime = 'image/png';
  727. $this->rendered->progressive = FALSE;
  728. // We are converting the GIF to a PNG, and PNG needs a
  729. // compression level of 0 (no compression) through 9
  730. $this->rendered->quality = round(10 - ($this->rendered->quality / 10));
  731. }
  732. else if ($this->source->isPNG())
  733. {
  734. $this->rendered->progressive = FALSE;
  735. // PNG needs a compression level of 0 (no compression) through 9
  736. $this->rendered->quality = round(10 - ($this->rendered->quality / 10));
  737. }
  738. else if ($this->source->isJPEG())
  739. {
  740. $this->rendered->progressive = ($this->request->progressive !== NULL)
  741. ? $this->request->progressive : SLIRConfig::$defaultProgressiveJPEG;
  742. $this->rendered->background = NULL;
  743. }
  744. else
  745. {
  746. throw new SLIRException("Unable to determine type of source image");
  747. } // if
  748. if ($this->isBackgroundFillOn())
  749. {
  750. $this->rendered->background = $this->request->background;
  751. }
  752. }
  753. /**
  754. * Detemrines if the image should be resized based on its width (i.e. the width is the constraining dimension for this request)
  755. *
  756. * @since 2.0
  757. * @return boolean
  758. */
  759. private function shouldResizeBasedOnWidth()
  760. {
  761. if (floor($this->resizeWidthFactor() * $this->source->height) <= $this->request->height)
  762. {
  763. return TRUE;
  764. }
  765. else
  766. {
  767. return FALSE;
  768. }
  769. }
  770. /**
  771. * Detemrines if the image should be resized based on its height (i.e. the height is the constraining dimension for this request)
  772. * @since 2.0
  773. * @return boolean
  774. */
  775. private function shouldResizeBasedOnHeight()
  776. {
  777. if (floor($this->resizeHeightFactor() * $this->source->width) <= $this->request->width)
  778. {
  779. return TRUE;
  780. }
  781. else
  782. {
  783. return FALSE;
  784. }
  785. }
  786. /**
  787. * @since 2.0
  788. * @return float
  789. */
  790. private function resizeWidthFactor()
  791. {
  792. if ($this->source->cropWidth !== NULL)
  793. {
  794. return $this->resizeCroppedWidthFactor();
  795. }
  796. else
  797. {
  798. return $this->resizeUncroppedWidthFactor();
  799. }
  800. }
  801. /**
  802. * @since 2.0
  803. * @return float
  804. */
  805. private function resizeUncroppedWidthFactor()
  806. {
  807. return $this->request->width / $this->source->width;
  808. }
  809. /**
  810. * @since 2.0
  811. * @return float
  812. */
  813. private function resizeCroppedWidthFactor()
  814. {
  815. return $this->request->width / $this->source->cropWidth;
  816. }
  817. /**
  818. * @since 2.0
  819. * @return float
  820. */
  821. private function resizeHeightFactor()
  822. {
  823. if ($this->source->cropHeight !== NULL)
  824. {
  825. return $this->resizeCroppedHeightFactor();
  826. }
  827. else
  828. {
  829. return $this->resizeUncroppedHeightFactor();
  830. }
  831. }
  832. /**
  833. * @since 2.0
  834. * @return float
  835. */
  836. private function resizeUncroppedHeightFactor()
  837. {
  838. return $this->request->height / $this->source->height;
  839. }
  840. /**
  841. * @since 2.0
  842. * @return float
  843. */
  844. private function resizeCroppedHeightFactor()
  845. {
  846. return $this->request->height / $this->source->cropHeight;
  847. }
  848. /**
  849. * Determines if the rendered file is in the rendered cache
  850. *
  851. * @since 2.0
  852. * @return boolean
  853. */
  854. private function isRenderedCached()
  855. {
  856. return $this->isCached($this->renderedCacheFilePath());
  857. }
  858. /**
  859. * Determines if the request is symlinked to the rendered file
  860. *
  861. * @since 2.0
  862. * @return boolean
  863. */
  864. private function isRequestCached()
  865. {
  866. return $this->isCached($this->requestCacheFilePath());
  867. }
  868. /**
  869. * Determines if a given file exists in the cache
  870. *
  871. * @since 2.0
  872. * @param string $cacheFilePath
  873. * @return boolean
  874. */
  875. private function isCached($cacheFilePath)
  876. {
  877. if (!file_exists($cacheFilePath))
  878. {
  879. return FALSE;
  880. }
  881. $cacheModified = filemtime($cacheFilePath);
  882. if (!$cacheModified)
  883. {
  884. return FALSE;
  885. }
  886. $imageModified = filectime($this->request->fullPath());
  887. if ($imageModified >= $cacheModified)
  888. {
  889. return FALSE;
  890. }
  891. else
  892. {
  893. return TRUE;
  894. }
  895. }
  896. /**
  897. * @since 2.0
  898. * @return string
  899. */
  900. private function getRenderedCacheDir()
  901. {
  902. return SLIRConfig::$cacheDir . '/rendered';
  903. }
  904. /**
  905. * @since 2.0
  906. * @return string
  907. */
  908. private function renderedCacheFilePath()
  909. {
  910. return $this->getRenderedCacheDir() . $this->renderedCacheFilename();
  911. }
  912. /**
  913. * @since 2.0
  914. * @return string
  915. */
  916. private function renderedCacheFilename()
  917. {
  918. return '/' . md5($this->request->fullPath() . serialize($this->rendered->cacheParameters()));
  919. }
  920. /**
  921. * @since 2.0
  922. * @return string
  923. */
  924. private function requestCacheFilename()
  925. {
  926. return '/' . md5($_SERVER['HTTP_HOST'] . '/' . $this->requestURI() . '/' . SLIRConfig::$defaultCropper);
  927. }
  928. /**
  929. * @since 2.0
  930. * @return string
  931. */
  932. private function requestURI()
  933. {
  934. if (SLIRConfig::$forceQueryString === TRUE)
  935. {
  936. return $_SERVER['SCRIPT_NAME'] . '?' . http_build_query($_GET);
  937. }
  938. else
  939. {
  940. return $_SERVER['REQUEST_URI'];
  941. }
  942. }
  943. /**
  944. * @since 2.0
  945. * @return string
  946. */
  947. private function getRequestCacheDir()
  948. {
  949. return SLIRConfig::$cacheDir . '/request';
  950. }
  951. /**
  952. * @since 2.0
  953. * @return string
  954. */
  955. private function requestCacheFilePath()
  956. {
  957. return $this->getRequestCacheDir() . $this->requestCacheFilename();
  958. }
  959. /**
  960. * Write an image to the cache
  961. *
  962. * @since 2.0
  963. * @param string $imageData
  964. * @return boolean
  965. */
  966. private function cache()
  967. {
  968. $this->cacheRendered();
  969. if (SLIRConfig::$useRequestCache === TRUE)
  970. {
  971. return $this->cacheRequest($this->rendered->data, TRUE);
  972. }
  973. else
  974. {
  975. return TRUE;
  976. }
  977. }
  978. /**
  979. * Write an image to the cache based on the properties of the rendered image
  980. *
  981. * @since 2.0
  982. * @return boolean
  983. */
  984. private function cacheRendered()
  985. {
  986. $this->rendered->data = $this->cacheFile(
  987. $this->renderedCacheFilePath(),
  988. $this->rendered->data,
  989. TRUE
  990. );
  991. return TRUE;
  992. }
  993. /**
  994. * Write an image to the cache based on the request URI
  995. *
  996. * @since 2.0
  997. * @param string $imageData
  998. * @param boolean $copyEXIF
  999. * @return string
  1000. */
  1001. private function cacheRequest($imageData, $copyEXIF = TRUE)
  1002. {
  1003. return $this->cacheFile(
  1004. $this->requestCacheFilePath(),
  1005. $imageData,
  1006. $copyEXIF,
  1007. $this->renderedCacheFilePath()
  1008. );
  1009. }
  1010. /**
  1011. * Write an image to the cache based on the properties of the rendered image
  1012. *
  1013. * @since 2.0
  1014. * @param string $cacheFilePath
  1015. * @param string $imageData
  1016. * @param boolean $copyEXIF
  1017. * @param string $symlinkToPath
  1018. * @return string|boolean
  1019. */
  1020. private function cacheFile($cacheFilePath, $imageData, $copyEXIF = TRUE, $symlinkToPath = NULL)
  1021. {
  1022. $this->initializeCache();
  1023. // Try to create just a symlink to minimize disk space
  1024. if ($symlinkToPath && function_exists('symlink') && (file_exists($cacheFilePath) || symlink('../'.$symlinkToPath, $cacheFilePath)))
  1025. {
  1026. return TRUE;
  1027. }
  1028. // Create the file
  1029. if (!file_put_contents($cacheFilePath, $imageData))
  1030. {
  1031. return FALSE;
  1032. }
  1033. if (SLIRConfig::$copyEXIF == TRUE && $copyEXIF && $this->source->isJPEG())
  1034. {
  1035. // Copy IPTC data
  1036. if (isset($this->source->iptc) && !$this->copyIPTC($cacheFilePath))
  1037. {
  1038. return FALSE;
  1039. }
  1040. // Copy EXIF data
  1041. $imageData = $this->copyEXIF($cacheFilePath);
  1042. } // if
  1043. return $imageData;
  1044. }
  1045. /**
  1046. * Copy the source image's EXIF information to the new file in the cache
  1047. *
  1048. * @since 2.0
  1049. * @uses PEL
  1050. * @param string $cacheFilePath
  1051. * @return mixed string contents of image on success, FALSE on failure
  1052. */
  1053. private function copyEXIF($cacheFilePath)
  1054. {
  1055. // Make sure to suppress strict warning thrown by PEL
  1056. @require_once dirname(__FILE__) . '/pel-0.9.2/src/PelJpeg.php';
  1057. $jpeg = new PelJpeg($this->source->fullPath());
  1058. $exif = $jpeg->getExif();
  1059. if ($exif)
  1060. {
  1061. $jpeg = new PelJpeg($cacheFilePath);
  1062. $jpeg->setExif($exif);
  1063. $imageData = $jpeg->getBytes();
  1064. if (!file_put_contents($cacheFilePath, $imageData))
  1065. {
  1066. return FALSE;
  1067. }
  1068. return $imageData;
  1069. } // if
  1070. return file_get_contents($cacheFilePath);
  1071. }
  1072. /**
  1073. * Makes sure the cache directory exists, is readable, and is writable
  1074. *
  1075. * @since 2.0
  1076. * @return boolean
  1077. */
  1078. private function initializeCache()
  1079. {
  1080. if ($this->isCacheInitialized)
  1081. {
  1082. return TRUE;
  1083. }
  1084. $this->initializeDirectory(SLIRConfig::$cacheDir);
  1085. $this->initializeDirectory(SLIRConfig::$cacheDir . '/rendered', FALSE);
  1086. $this->initializeDirectory(SLIRConfig::$cacheDir . '/request', FALSE);
  1087. $this->isCacheInitialized = TRUE;
  1088. return TRUE;
  1089. }
  1090. /**
  1091. * @since 2.0
  1092. * @param string $path Directory to initialize
  1093. * @param boolean $verifyReadWriteability
  1094. * @return boolean
  1095. */
  1096. private function initializeDirectory($path, $verifyReadWriteability = TRUE, $test = FALSE)
  1097. {
  1098. if (!file_exists($path))
  1099. {
  1100. if (!@mkdir($path, 0755, TRUE))
  1101. {
  1102. header('HTTP/1.1 500 Internal Server Error');
  1103. throw new SLIRException("Directory ($path) does not exist and was unable to be created. Please create the directory.");
  1104. }
  1105. }
  1106. if (!$verifyReadWriteability)
  1107. return TRUE;
  1108. // Make sure we can read and write the cache directory
  1109. if (!is_readable($path))
  1110. {
  1111. header('HTTP/1.1 500 Internal Server Error');
  1112. throw new SLIRException("Directory ($path) is not readable");
  1113. }
  1114. else if (!is_writable($path))
  1115. {
  1116. header('HTTP/1.1 500 Internal Server Error');
  1117. throw new SLIRException("Directory ($path) is not writable");
  1118. }
  1119. return TRUE;
  1120. }
  1121. /**
  1122. * Serves the unmodified source image
  1123. *
  1124. * @since 2.0
  1125. * @return void
  1126. */
  1127. private function serveSourceImage()
  1128. {
  1129. $this->serveFile(
  1130. $this->source->fullPath(),
  1131. NULL,
  1132. NULL,
  1133. NULL,
  1134. $this->source->mime,
  1135. 'source'
  1136. );
  1137. exit();
  1138. }
  1139. /**
  1140. * Serves the image from the cache based on the properties of the rendered
  1141. * image
  1142. *
  1143. * @since 2.0
  1144. * @return void
  1145. */
  1146. private function serveRenderedCachedImage()
  1147. {
  1148. return $this->serveCachedImage($this->renderedCacheFilePath(), 'rendered');
  1149. }
  1150. /**
  1151. * Serves the image from the cache based on the request URI
  1152. *
  1153. * @since 2.0
  1154. * @return void
  1155. */
  1156. private function serveRequestCachedImage()
  1157. {
  1158. return $this->serveCachedImage($this->requestCacheFilePath(), 'request');
  1159. }
  1160. /**
  1161. * Serves the image from the cache
  1162. *
  1163. * @since 2.0
  1164. * @param string $cacheFilePath
  1165. * @param string $cacheType Can be 'request' or 'image'
  1166. * @return void
  1167. */
  1168. private function serveCachedImage($cacheFilePath, $cacheType)
  1169. {
  1170. // Serve the image
  1171. $data = $this->serveFile(
  1172. $cacheFilePath,
  1173. NULL,
  1174. NULL,
  1175. NULL,
  1176. NULL,
  1177. "$cacheType cache"
  1178. );
  1179. // If we are serving from the rendered cache, create a symlink in the
  1180. // request cache to the rendered file
  1181. if ($cacheType != 'request')
  1182. {
  1183. $this->cacheRequest($data, FALSE);
  1184. }
  1185. exit();
  1186. }
  1187. /**
  1188. * Determines the mime type of an image
  1189. *
  1190. * @since 2.0
  1191. * @param string $path
  1192. * @return string
  1193. */
  1194. private function mimeType($path)
  1195. {
  1196. $info = getimagesize($path);
  1197. return $info['mime'];
  1198. }
  1199. /**
  1200. * Serves the rendered image
  1201. *
  1202. * @since 2.0
  1203. * @return void
  1204. */
  1205. private function serveRenderedImage()
  1206. {
  1207. // Cache the image
  1208. $this->cache();
  1209. // Serve the file
  1210. $this->serveFile(
  1211. NULL,
  1212. $this->rendered->data,
  1213. gmdate('U'),
  1214. $this->rendered->fileSize(),
  1215. $this->rendered->mime,
  1216. 'rendered'
  1217. );
  1218. // Clean up memory
  1219. $this->rendered->destroyImage();
  1220. exit();
  1221. }
  1222. /**
  1223. * Serves a file
  1224. *
  1225. * @since 2.0
  1226. * @param string $imagePath Path to file to serve
  1227. * @param string $data Data of file to serve
  1228. * @param integer $lastModified Timestamp of when the file was last modified
  1229. * @param string $mimeType
  1230. * @param string $SLIRheader
  1231. * @return string Image data
  1232. */
  1233. private function serveFile($imagePath, $data, $lastModified, $length, $mimeType, $SLIRHeader)
  1234. {
  1235. if ($imagePath != NULL)
  1236. {
  1237. if ($lastModified == NULL)
  1238. {
  1239. $lastModified = filemtime($imagePath);
  1240. }
  1241. if ($length == NULL)
  1242. {
  1243. $length = filesize($imagePath);
  1244. }
  1245. if ($mimeType == NULL)
  1246. {
  1247. $mimeType = $this->mimeType($imagePath);
  1248. }
  1249. }
  1250. else if ($length == NULL)
  1251. {
  1252. $length = strlen($data);
  1253. } // if
  1254. // Serve the headers
  1255. $this->serveHeaders(
  1256. $this->lastModified($lastModified),
  1257. $mimeType,
  1258. $length,
  1259. $SLIRHeader
  1260. );
  1261. // Read the image data into memory if we need to
  1262. if ($data == NULL)
  1263. {
  1264. $data = file_get_contents($imagePath);
  1265. }
  1266. // Send the image to the browser in bite-sized chunks
  1267. $chunkSize = 1024 * 8;
  1268. $fp = fopen('php://memory', 'r+b');
  1269. fwrite($fp, $data);
  1270. rewind($fp);
  1271. while (!feof($fp))
  1272. {
  1273. echo fread($fp, $chunkSize);
  1274. flush();
  1275. } // while
  1276. fclose($fp);
  1277. return $data;
  1278. }
  1279. /**
  1280. * Serves headers for file for optimal browser caching
  1281. *
  1282. * @since 2.0
  1283. * @param string $lastModified Time when file was last modified in 'D, d M Y H:i:s' format
  1284. * @param string $mimeType
  1285. * @param integer $fileSize
  1286. * @param string $SLIRHeader
  1287. * @return void
  1288. */
  1289. private function serveHeaders($lastModified, $mimeType, $fileSize, $SLIRHeader)
  1290. {
  1291. header("Last-Modified: $lastModified");
  1292. header("Content-Type: $mimeType");
  1293. header("Content-Length: $fileSize");
  1294. // Lets us easily know whether the image was rendered from scratch,
  1295. // from the cache, or served directly from the source image
  1296. header("Content-SLIR: $SLIRHeader");
  1297. // Keep in browser cache how long?
  1298. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + SLIRConfig::$browserCacheTTL) . ' GMT');
  1299. // Public in the Cache-Control lets proxies know that it is okay to
  1300. // cache this content. If this is being served over HTTPS, there may be
  1301. // sensitive content and therefore should probably not be cached by
  1302. // proxy servers.
  1303. header('Cache-Control: max-age=' . SLIRConfig::$browserCacheTTL . ', public');
  1304. $this->doConditionalGet($lastModified);
  1305. // The "Connection: close" header allows us to serve the file and let
  1306. // the browser finish processing the script so we can do extra work
  1307. // without making the user wait. This header must come last or the file
  1308. // size will not properly work for images in the browser's cache
  1309. //header('Connection: close');
  1310. }
  1311. /**
  1312. * Converts a UNIX timestamp into the format needed for the Last-Modified
  1313. * header
  1314. *
  1315. * @since 2.0
  1316. * @param integer $timestamp
  1317. * @return string
  1318. */
  1319. private function lastModified($timestamp)
  1320. {
  1321. return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
  1322. }
  1323. /**
  1324. * Checks the to see if the file is different than the browser's cache
  1325. *
  1326. * @since 2.0
  1327. * @param string $lastModified
  1328. * @return void
  1329. */
  1330. private function doConditionalGet($lastModified)
  1331. {
  1332. $ifModifiedSince = (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) ?
  1333. stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) :
  1334. FALSE;
  1335. if (!$ifModifiedSince || $ifModifiedSince != $lastModified)
  1336. {
  1337. return;
  1338. }
  1339. // Nothing has changed since their last request - serve a 304 and exit
  1340. header('HTTP/1.1 304 Not Modified');
  1341. // Serve a "Connection: close" header here in case there are any
  1342. // shutdown functions that have been registered with
  1343. // register_shutdown_function()
  1344. header('Connection: close');
  1345. exit();
  1346. }
  1347. } // class SLIR
  1348. // old pond
  1349. // a frog jumps
  1350. // the sound of water
  1351. // —Matsuo Basho