Image.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. <?php
  2. namespace Gregwar\Image;
  3. use Gregwar\Cache\CacheInterface;
  4. use Gregwar\Image\Adapter\AdapterInterface;
  5. use Gregwar\Image\Exceptions\GenerationError;
  6. /**
  7. * Images handling class.
  8. *
  9. * @author Gregwar <g.passault@gmail.com>
  10. *
  11. * @method Image saveGif($file)
  12. * @method Image savePng($file)
  13. * @method Image saveJpeg($file, $quality)
  14. * @method Image resize($width = null, $height = null, $background = 'transparent', $force = false, $rescale = false, $crop = false)
  15. * @method Image forceResize($width = null, $height = null, $background = 'transparent')
  16. * @method Image scaleResize($width = null, $height = null, $background = 'transparent', $crop = false)
  17. * @method Image cropResize($width = null, $height = null, $background=0xffffff)
  18. * @method Image scale($width = null, $height = null, $background=0xffffff, $crop = false)
  19. * @method Image ($width = null, $height = null, $background = 0xffffff, $force = false, $rescale = false, $crop = false)
  20. * @method Image crop($x, $y, $width, $height)
  21. * @method Image enableProgressive()
  22. * @method Image force($width = null, $height = null, $background = 0xffffff)
  23. * @method Image zoomCrop($width, $height, $background = 0xffffff, $xPos, $yPos)
  24. * @method Image fillBackground($background = 0xffffff)
  25. * @method Image negate()
  26. * @method Image brightness($brightness)
  27. * @method Image contrast($contrast)
  28. * @method Image grayscale()
  29. * @method Image emboss()
  30. * @method Image smooth($p)
  31. * @method Image sharp()
  32. * @method Image edge()
  33. * @method Image colorize($red, $green, $blue)
  34. * @method Image sepia()
  35. * @method Image merge(Image $other, $x = 0, $y = 0, $width = null, $height = null)
  36. * @method Image rotate($angle, $background = 0xffffff)
  37. * @method Image fill($color = 0xffffff, $x = 0, $y = 0)
  38. * @method Image write($font, $text, $x = 0, $y = 0, $size = 12, $angle = 0, $color = 0x000000, $align = 'left')
  39. * @method Image rectangle($x1, $y1, $x2, $y2, $color, $filled = false)
  40. * @method Image roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $filled = false)
  41. * @method Image line($x1, $y1, $x2, $y2, $color = 0x000000)
  42. * @method Image ellipse($cx, $cy, $width, $height, $color = 0x000000, $filled = false)
  43. * @method Image circle($cx, $cy, $r, $color = 0x000000, $filled = false)
  44. * @method Image polygon(array $points, $color, $filled = false)
  45. * @method Image flip($flipVertical, $flipHorizontal)
  46. */
  47. class Image
  48. {
  49. /**
  50. * Directory to use for file caching.
  51. */
  52. protected $cacheDir = 'cache/images';
  53. /**
  54. * Directory cache mode.
  55. */
  56. protected $cacheMode = null;
  57. /**
  58. * Internal adapter.
  59. *
  60. * @var AdapterInterface
  61. */
  62. protected $adapter = null;
  63. /**
  64. * Pretty name for the image.
  65. */
  66. protected $prettyName = '';
  67. protected $prettyPrefix;
  68. /**
  69. * Transformations hash.
  70. */
  71. protected $hash = null;
  72. /**
  73. * The image source.
  74. */
  75. protected $source = null;
  76. /**
  77. * Force image caching, even if there is no operation applied.
  78. */
  79. protected $forceCache = true;
  80. /**
  81. * Supported types.
  82. */
  83. public static $types = array(
  84. 'jpg' => 'jpeg',
  85. 'jpeg' => 'jpeg',
  86. 'png' => 'png',
  87. 'gif' => 'gif',
  88. );
  89. /**
  90. * Fallback image.
  91. */
  92. protected $fallback;
  93. /**
  94. * Use fallback image.
  95. */
  96. protected $useFallbackImage = true;
  97. /**
  98. * Cache system.
  99. *
  100. * @var \Gregwar\Cache\CacheInterface
  101. */
  102. protected $cache;
  103. /**
  104. * Get the cache system.
  105. *
  106. * @return \Gregwar\Cache\CacheInterface
  107. */
  108. public function getCacheSystem()
  109. {
  110. if (is_null($this->cache)) {
  111. $this->cache = new \Gregwar\Cache\Cache();
  112. $this->cache->setCacheDirectory($this->cacheDir);
  113. }
  114. return $this->cache;
  115. }
  116. /**
  117. * Set the cache system.
  118. *
  119. * @param \Gregwar\Cache\CacheInterface $cache
  120. */
  121. public function setCacheSystem(CacheInterface $cache)
  122. {
  123. $this->cache = $cache;
  124. }
  125. /**
  126. * Change the caching directory.
  127. */
  128. public function setCacheDir($cacheDir)
  129. {
  130. $this->getCacheSystem()->setCacheDirectory($cacheDir);
  131. return $this;
  132. }
  133. /**
  134. * @param int $dirMode
  135. */
  136. public function setCacheDirMode($dirMode)
  137. {
  138. $this->cache->setDirectoryMode($dirMode);
  139. }
  140. /**
  141. * Enable or disable to force cache even if the file is unchanged.
  142. */
  143. public function setForceCache($forceCache = true)
  144. {
  145. $this->forceCache = $forceCache;
  146. return $this;
  147. }
  148. /**
  149. * The actual cache dir.
  150. */
  151. public function setActualCacheDir($actualCacheDir)
  152. {
  153. $this->getCacheSystem()->setActualCacheDirectory($actualCacheDir);
  154. return $this;
  155. }
  156. /**
  157. * Sets the pretty name of the image.
  158. */
  159. public function setPrettyName($name, $prefix = true)
  160. {
  161. if (empty($name)) {
  162. return $this;
  163. }
  164. $this->prettyName = $this->urlize($name);
  165. $this->prettyPrefix = $prefix;
  166. return $this;
  167. }
  168. /**
  169. * Urlizes the prettyName.
  170. */
  171. protected function urlize($name)
  172. {
  173. $transliterator = '\Behat\Transliterator\Transliterator';
  174. if (class_exists($transliterator)) {
  175. $name = $transliterator::transliterate($name);
  176. $name = $transliterator::urlize($name);
  177. } else {
  178. $name = strtolower($name);
  179. $name = str_replace(' ', '-', $name);
  180. $name = preg_replace('/([^a-z0-9\-]+)/m', '', $name);
  181. }
  182. return $name;
  183. }
  184. /**
  185. * Operations array.
  186. */
  187. protected $operations = array();
  188. public function __construct($originalFile = null, $width = null, $height = null)
  189. {
  190. $this->setFallback(null);
  191. if ($originalFile) {
  192. $this->source = new Source\File($originalFile);
  193. } else {
  194. $this->source = new Source\Create($width, $height);
  195. }
  196. }
  197. /**
  198. * Sets the image data.
  199. */
  200. public function setData($data)
  201. {
  202. $this->source = new Source\Data($data);
  203. }
  204. /**
  205. * Sets the resource.
  206. */
  207. public function setResource($resource)
  208. {
  209. $this->source = new Source\Resource($resource);
  210. }
  211. /**
  212. * Use the fallback image or not.
  213. */
  214. public function useFallback($useFallbackImage = true)
  215. {
  216. $this->useFallbackImage = $useFallbackImage;
  217. return $this;
  218. }
  219. /**
  220. * Sets the fallback image to use.
  221. */
  222. public function setFallback($fallback = null)
  223. {
  224. if ($fallback === null) {
  225. $this->fallback = __DIR__.'/images/error.jpg';
  226. } else {
  227. $this->fallback = $fallback;
  228. }
  229. return $this;
  230. }
  231. /**
  232. * Gets the fallack image path.
  233. */
  234. public function getFallback()
  235. {
  236. return $this->fallback;
  237. }
  238. /**
  239. * Gets the fallback into the cache dir.
  240. */
  241. public function getCacheFallback()
  242. {
  243. $fallback = $this->fallback;
  244. return $this->getCacheSystem()->getOrCreateFile('fallback.jpg', array(), function ($target) use ($fallback) {
  245. copy($fallback, $target);
  246. });
  247. }
  248. /**
  249. * @return AdapterInterface
  250. */
  251. public function getAdapter()
  252. {
  253. if (null === $this->adapter) {
  254. // Defaults to GD
  255. $this->setAdapter('gd');
  256. }
  257. return $this->adapter;
  258. }
  259. public function setAdapter($adapter)
  260. {
  261. if ($adapter instanceof Adapter\Adapter) {
  262. $this->adapter = $adapter;
  263. } else {
  264. if (is_string($adapter)) {
  265. $adapter = strtolower($adapter);
  266. switch ($adapter) {
  267. case 'gd':
  268. $this->adapter = new Adapter\GD();
  269. break;
  270. case 'imagemagick':
  271. case 'imagick':
  272. $this->adapter = new Adapter\Imagick();
  273. break;
  274. default:
  275. throw new \Exception('Unknown adapter: '.$adapter);
  276. break;
  277. }
  278. } else {
  279. throw new \Exception('Unable to load the given adapter (not string or Adapter)');
  280. }
  281. }
  282. $this->adapter->setSource($this->source);
  283. }
  284. /**
  285. * Get the file path.
  286. *
  287. * @return mixed a string with the filen name, null if the image
  288. * does not depends on a file
  289. */
  290. public function getFilePath()
  291. {
  292. if ($this->source instanceof Source\File) {
  293. return $this->source->getFile();
  294. } else {
  295. return;
  296. }
  297. }
  298. /**
  299. * Defines the file only after instantiation.
  300. *
  301. * @param string $originalFile the file path
  302. */
  303. public function fromFile($originalFile)
  304. {
  305. $this->source = new Source\File($originalFile);
  306. return $this;
  307. }
  308. /**
  309. * Tells if the image is correct.
  310. */
  311. public function correct()
  312. {
  313. return $this->source->correct();
  314. }
  315. /**
  316. * Guess the file type.
  317. */
  318. public function guessType()
  319. {
  320. return $this->source->guessType();
  321. }
  322. /**
  323. * Adds an operation.
  324. */
  325. protected function addOperation($method, $args)
  326. {
  327. $this->operations[] = array($method, $args);
  328. }
  329. /**
  330. * Generic function.
  331. */
  332. public function __call($methodName, $args)
  333. {
  334. $adapter = $this->getAdapter();
  335. $reflection = new \ReflectionClass(get_class($adapter));
  336. if ($reflection->hasMethod($methodName)) {
  337. $method = $reflection->getMethod($methodName);
  338. if ($method->getNumberOfRequiredParameters() > count($args)) {
  339. throw new \InvalidArgumentException('Not enough arguments given for '.$methodName);
  340. }
  341. $this->addOperation($methodName, $args);
  342. return $this;
  343. }
  344. throw new \BadFunctionCallException('Invalid method: '.$methodName);
  345. }
  346. /**
  347. * Serialization of operations.
  348. */
  349. public function serializeOperations()
  350. {
  351. $datas = array();
  352. foreach ($this->operations as $operation) {
  353. $method = $operation[0];
  354. $args = $operation[1];
  355. foreach ($args as &$arg) {
  356. if ($arg instanceof self) {
  357. $arg = $arg->getHash();
  358. }
  359. }
  360. $datas[] = array($method, $args);
  361. }
  362. return serialize($datas);
  363. }
  364. /**
  365. * Generates the hash.
  366. */
  367. public function generateHash($type = 'guess', $quality = 80)
  368. {
  369. $inputInfos = $this->source->getInfos();
  370. $datas = array(
  371. $inputInfos,
  372. $this->serializeOperations(),
  373. $type,
  374. $quality,
  375. );
  376. $this->hash = sha1(serialize($datas));
  377. }
  378. /**
  379. * Gets the hash.
  380. */
  381. public function getHash($type = 'guess', $quality = 80)
  382. {
  383. if (null === $this->hash) {
  384. $this->generateHash($type, $quality);
  385. }
  386. return $this->hash;
  387. }
  388. /**
  389. * Gets the cache file name and generate it if it does not exists.
  390. * Note that if it exists, all the image computation process will
  391. * not be done.
  392. *
  393. * @param string $type the image type
  394. * @param int $quality the quality (for JPEG)
  395. */
  396. public function cacheFile($type = 'jpg', $quality = 80, $actual = false)
  397. {
  398. if ($type == 'guess') {
  399. $type = $this->guessType();
  400. }
  401. if (!count($this->operations) && $type == $this->guessType() && !$this->forceCache) {
  402. return $this->getFilename($this->getFilePath());
  403. }
  404. // Computes the hash
  405. $this->hash = $this->getHash($type, $quality);
  406. // Generates the cache file
  407. $cacheFile = '';
  408. if (!$this->prettyName || $this->prettyPrefix) {
  409. $cacheFile .= $this->hash;
  410. }
  411. if ($this->prettyPrefix) {
  412. $cacheFile .= '-';
  413. }
  414. if ($this->prettyName) {
  415. $cacheFile .= $this->prettyName;
  416. }
  417. $cacheFile .= '.'.$type;
  418. // If the files does not exists, save it
  419. $image = $this;
  420. // Target file should be younger than all the current image
  421. // dependencies
  422. $conditions = array(
  423. 'younger-than' => $this->getDependencies(),
  424. );
  425. // The generating function
  426. $generate = function ($target) use ($image, $type, $quality) {
  427. $result = $image->save($target, $type, $quality);
  428. if ($result != $target) {
  429. throw new GenerationError($result);
  430. }
  431. };
  432. // Asking the cache for the cacheFile
  433. try {
  434. $file = $this->getCacheSystem()->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
  435. } catch (GenerationError $e) {
  436. $file = $e->getNewFile();
  437. }
  438. // Nulling the resource
  439. $this->getAdapter()->setSource(new Source\File($file));
  440. $this->getAdapter()->deinit();
  441. if ($actual) {
  442. return $file;
  443. } else {
  444. return $this->getFilename($file);
  445. }
  446. }
  447. /**
  448. * Get cache data (to render the image).
  449. *
  450. * @param string $type the image type
  451. * @param int $quality the quality (for JPEG)
  452. */
  453. public function cacheData($type = 'jpg', $quality = 80)
  454. {
  455. return file_get_contents($this->cacheFile($type, $quality));
  456. }
  457. /**
  458. * Hook to helps to extends and enhance this class.
  459. */
  460. protected function getFilename($filename)
  461. {
  462. return $filename;
  463. }
  464. /**
  465. * Generates and output a jpeg cached file.
  466. */
  467. public function jpeg($quality = 80)
  468. {
  469. return $this->cacheFile('jpg', $quality);
  470. }
  471. /**
  472. * Generates and output a gif cached file.
  473. */
  474. public function gif()
  475. {
  476. return $this->cacheFile('gif');
  477. }
  478. /**
  479. * Generates and output a png cached file.
  480. */
  481. public function png()
  482. {
  483. return $this->cacheFile('png');
  484. }
  485. /**
  486. * Generates and output an image using the same type as input.
  487. */
  488. public function guess($quality = 80)
  489. {
  490. return $this->cacheFile('guess', $quality);
  491. }
  492. /**
  493. * Get all the files that this image depends on.
  494. *
  495. * @return string[] this is an array of strings containing all the files that the
  496. * current Image depends on
  497. */
  498. public function getDependencies()
  499. {
  500. $dependencies = array();
  501. $file = $this->getFilePath();
  502. if ($file) {
  503. $dependencies[] = $file;
  504. }
  505. foreach ($this->operations as $operation) {
  506. foreach ($operation[1] as $argument) {
  507. if ($argument instanceof self) {
  508. $dependencies = array_merge($dependencies, $argument->getDependencies());
  509. }
  510. }
  511. }
  512. return $dependencies;
  513. }
  514. /**
  515. * Applies the operations.
  516. */
  517. public function applyOperations()
  518. {
  519. // Renders the effects
  520. foreach ($this->operations as $operation) {
  521. call_user_func_array(array($this->adapter, $operation[0]), $operation[1]);
  522. }
  523. }
  524. /**
  525. * Initialize the adapter.
  526. */
  527. public function init()
  528. {
  529. $this->getAdapter()->init();
  530. }
  531. /**
  532. * Save the file to a given output.
  533. */
  534. public function save($file, $type = 'guess', $quality = 80)
  535. {
  536. if ($file) {
  537. $directory = dirname($file);
  538. if (!is_dir($directory)) {
  539. @mkdir($directory, 0777, true);
  540. }
  541. }
  542. if (is_int($type)) {
  543. $quality = $type;
  544. $type = 'jpeg';
  545. }
  546. if ($type == 'guess') {
  547. $type = $this->guessType();
  548. }
  549. if (!isset(self::$types[$type])) {
  550. throw new \InvalidArgumentException('Given type ('.$type.') is not valid');
  551. }
  552. $type = self::$types[$type];
  553. try {
  554. $this->init();
  555. $this->applyOperations();
  556. $success = false;
  557. if (null == $file) {
  558. ob_start();
  559. }
  560. if ($type == 'jpeg') {
  561. $success = $this->getAdapter()->saveJpeg($file, $quality);
  562. }
  563. if ($type == 'gif') {
  564. $success = $this->getAdapter()->saveGif($file);
  565. }
  566. if ($type == 'png') {
  567. $success = $this->getAdapter()->savePng($file);
  568. }
  569. if (!$success) {
  570. return false;
  571. }
  572. return null === $file ? ob_get_clean() : $file;
  573. } catch (\Exception $e) {
  574. if ($this->useFallbackImage) {
  575. return null === $file ? file_get_contents($this->fallback) : $this->getCacheFallback();
  576. } else {
  577. throw $e;
  578. }
  579. }
  580. }
  581. /**
  582. * Get the contents of the image.
  583. */
  584. public function get($type = 'guess', $quality = 80)
  585. {
  586. return $this->save(null, $type, $quality);
  587. }
  588. /* Image API */
  589. /**
  590. * Image width.
  591. */
  592. public function width()
  593. {
  594. return $this->getAdapter()->width();
  595. }
  596. /**
  597. * Image height.
  598. */
  599. public function height()
  600. {
  601. return $this->getAdapter()->height();
  602. }
  603. /**
  604. * Tostring defaults to jpeg.
  605. */
  606. public function __toString()
  607. {
  608. return $this->guess();
  609. }
  610. /**
  611. * Returning basic html code for this image.
  612. */
  613. public function html($title = '', $type = 'jpg', $quality = 80)
  614. {
  615. return '<img title="'.$title.'" src="'.$this->cacheFile($type, $quality).'" />';
  616. }
  617. /**
  618. * Returns the Base64 inlinable representation.
  619. */
  620. public function inline($type = 'jpg', $quality = 80)
  621. {
  622. $mime = $type;
  623. if ($mime == 'jpg') {
  624. $mime = 'jpeg';
  625. }
  626. return 'data:image/'.$mime.';base64,'.base64_encode(file_get_contents($this->cacheFile($type, $quality, true)));
  627. }
  628. /**
  629. * Creates an instance, usefull for one-line chaining.
  630. */
  631. public static function open($file = '')
  632. {
  633. return new static($file);
  634. }
  635. /**
  636. * Creates an instance of a new resource.
  637. */
  638. public static function create($width, $height)
  639. {
  640. return new static(null, $width, $height);
  641. }
  642. /**
  643. * Creates an instance of image from its data.
  644. */
  645. public static function fromData($data)
  646. {
  647. $image = new static();
  648. $image->setData($data);
  649. return $image;
  650. }
  651. /**
  652. * Creates an instance of image from resource.
  653. */
  654. public static function fromResource($resource)
  655. {
  656. $image = new static();
  657. $image->setResource($resource);
  658. return $image;
  659. }
  660. }