Image.php 19 KB

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