Image.php 18 KB

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