ImageMedium.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <?php
  2. /**
  3. * @package Grav\Common\Page
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Page\Medium;
  9. use BadFunctionCallException;
  10. use Grav\Common\Data\Blueprint;
  11. use Grav\Common\Media\Interfaces\ImageManipulateInterface;
  12. use Grav\Common\Media\Interfaces\ImageMediaInterface;
  13. use Grav\Common\Media\Interfaces\MediaLinkInterface;
  14. use Grav\Common\Media\Traits\ImageLoadingTrait;
  15. use Grav\Common\Media\Traits\ImageMediaTrait;
  16. use Grav\Common\Utils;
  17. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  18. use function func_get_args;
  19. use function in_array;
  20. /**
  21. * Class ImageMedium
  22. * @package Grav\Common\Page\Medium
  23. */
  24. class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulateInterface
  25. {
  26. use ImageMediaTrait;
  27. use ImageLoadingTrait;
  28. /**
  29. * @var mixed|string
  30. */
  31. private $saved_image_path;
  32. /**
  33. * Construct.
  34. *
  35. * @param array $items
  36. * @param Blueprint|null $blueprint
  37. */
  38. public function __construct($items = [], Blueprint $blueprint = null)
  39. {
  40. parent::__construct($items, $blueprint);
  41. $config = $this->getGrav()['config'];
  42. $this->thumbnailTypes = ['page', 'media', 'default'];
  43. $this->default_quality = $config->get('system.images.default_image_quality', 85);
  44. $this->def('debug', $config->get('system.images.debug'));
  45. $path = $this->get('filepath');
  46. if (!$path || !file_exists($path) || !filesize($path)) {
  47. return;
  48. }
  49. $this->set('thumbnails.media', $path);
  50. if (!($this->offsetExists('width') && $this->offsetExists('height') && $this->offsetExists('mime'))) {
  51. $image_info = getimagesize($path);
  52. if ($image_info) {
  53. $this->def('width', $image_info[0]);
  54. $this->def('height', $image_info[1]);
  55. $this->def('mime', $image_info['mime']);
  56. }
  57. }
  58. $this->reset();
  59. if ($config->get('system.images.cache_all', false)) {
  60. $this->cache();
  61. }
  62. }
  63. /**
  64. * @return array
  65. */
  66. public function getMeta(): array
  67. {
  68. return [
  69. 'width' => $this->width,
  70. 'height' => $this->height,
  71. ] + parent::getMeta();
  72. }
  73. /**
  74. * Also unset the image on destruct.
  75. */
  76. public function __destruct()
  77. {
  78. unset($this->image);
  79. }
  80. /**
  81. * Also clone image.
  82. */
  83. public function __clone()
  84. {
  85. if ($this->image) {
  86. $this->image = clone $this->image;
  87. }
  88. parent::__clone();
  89. }
  90. /**
  91. * Reset image.
  92. *
  93. * @return $this
  94. */
  95. public function reset()
  96. {
  97. parent::reset();
  98. if ($this->image) {
  99. $this->image();
  100. $this->medium_querystring = [];
  101. $this->filter();
  102. $this->clearAlternatives();
  103. }
  104. $this->format = 'guess';
  105. $this->quality = $this->default_quality;
  106. $this->debug_watermarked = false;
  107. $config = $this->getGrav()['config'];
  108. // Set CLS configuration
  109. $this->auto_sizes = $config->get('system.images.cls.auto_sizes', false);
  110. $this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false);
  111. $this->retina_scale = $config->get('system.images.cls.retina_scale', 1);
  112. return $this;
  113. }
  114. /**
  115. * Add meta file for the medium.
  116. *
  117. * @param string $filepath
  118. * @return $this
  119. */
  120. public function addMetaFile($filepath)
  121. {
  122. parent::addMetaFile($filepath);
  123. // Apply filters in meta file
  124. $this->reset();
  125. return $this;
  126. }
  127. /**
  128. * Return PATH to image.
  129. *
  130. * @param bool $reset
  131. * @return string path to image
  132. */
  133. public function path($reset = true)
  134. {
  135. $output = $this->saveImage();
  136. if ($reset) {
  137. $this->reset();
  138. }
  139. return $output;
  140. }
  141. /**
  142. * Return URL to image.
  143. *
  144. * @param bool $reset
  145. * @return string
  146. */
  147. public function url($reset = true)
  148. {
  149. $grav = $this->getGrav();
  150. /** @var UniformResourceLocator $locator */
  151. $locator = $grav['locator'];
  152. $image_path = (string)($locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true));
  153. $saved_image_path = $this->saved_image_path = $this->saveImage();
  154. $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path) ?: $saved_image_path;
  155. if ($locator->isStream($output)) {
  156. $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true));
  157. }
  158. if (Utils::startsWith($output, $image_path)) {
  159. $image_dir = $locator->findResource('cache://images', false);
  160. $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
  161. }
  162. if ($reset) {
  163. $this->reset();
  164. }
  165. return trim($grav['base_url'] . '/' . $this->urlQuerystring($output), '\\');
  166. }
  167. /**
  168. * Return srcset string for this Medium and its alternatives.
  169. *
  170. * @param bool $reset
  171. * @return string
  172. */
  173. public function srcset($reset = true)
  174. {
  175. if (empty($this->alternatives)) {
  176. if ($reset) {
  177. $this->reset();
  178. }
  179. return '';
  180. }
  181. $srcset = [];
  182. foreach ($this->alternatives as $ratio => $medium) {
  183. $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
  184. }
  185. $srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w';
  186. return implode(', ', $srcset);
  187. }
  188. /**
  189. * Parsedown element for source display mode
  190. *
  191. * @param array $attributes
  192. * @param bool $reset
  193. * @return array
  194. */
  195. public function sourceParsedownElement(array $attributes, $reset = true)
  196. {
  197. empty($attributes['src']) && $attributes['src'] = $this->url(false);
  198. $srcset = $this->srcset($reset);
  199. if ($srcset) {
  200. empty($attributes['srcset']) && $attributes['srcset'] = $srcset;
  201. $attributes['sizes'] = $this->sizes();
  202. }
  203. if ($this->saved_image_path && $this->auto_sizes) {
  204. if (!array_key_exists('height', $this->attributes) && !array_key_exists('width', $this->attributes)) {
  205. $info = getimagesize($this->saved_image_path);
  206. $width = intval($info[0]);
  207. $height = intval($info[1]);
  208. $scaling_factor = $this->retina_scale > 0 ? $this->retina_scale : 1;
  209. $attributes['width'] = intval($width / $scaling_factor);
  210. $attributes['height'] = intval($height / $scaling_factor);
  211. if ($this->aspect_ratio) {
  212. $style = ($attributes['style'] ?? ' ') . "--aspect-ratio: $width/$height;";
  213. $attributes['style'] = trim($style);
  214. }
  215. }
  216. }
  217. return ['name' => 'img', 'attributes' => $attributes];
  218. }
  219. /**
  220. * Turn the current Medium into a Link
  221. *
  222. * @param bool $reset
  223. * @param array $attributes
  224. * @return MediaLinkInterface
  225. */
  226. public function link($reset = true, array $attributes = [])
  227. {
  228. $attributes['href'] = $this->url(false);
  229. $srcset = $this->srcset(false);
  230. if ($srcset) {
  231. $attributes['data-srcset'] = $srcset;
  232. }
  233. return parent::link($reset, $attributes);
  234. }
  235. /**
  236. * Turn the current Medium into a Link with lightbox enabled
  237. *
  238. * @param int $width
  239. * @param int $height
  240. * @param bool $reset
  241. * @return MediaLinkInterface
  242. */
  243. public function lightbox($width = null, $height = null, $reset = true)
  244. {
  245. if ($this->mode !== 'source') {
  246. $this->display('source');
  247. }
  248. if ($width && $height) {
  249. $this->__call('cropResize', [$width, $height]);
  250. }
  251. return parent::lightbox($width, $height, $reset);
  252. }
  253. public function autoSizes($enabled = 'true')
  254. {
  255. $enabled = $enabled === 'true' ?: false;
  256. $this->auto_sizes = $enabled;
  257. return $this;
  258. }
  259. public function aspectRatio($enabled = 'true')
  260. {
  261. $enabled = $enabled === 'true' ?: false;
  262. $this->aspect_ratio = $enabled;
  263. return $this;
  264. }
  265. public function retinaScale($scale = 1)
  266. {
  267. $this->retina_scale = intval($scale);
  268. return $this;
  269. }
  270. /**
  271. * Handle this commonly used variant
  272. *
  273. * @return $this
  274. */
  275. public function cropZoom()
  276. {
  277. $this->__call('zoomCrop', func_get_args());
  278. return $this;
  279. }
  280. /**
  281. * Add a frame to image
  282. *
  283. * @return $this
  284. */
  285. public function addFrame(int $border = 10, string $color = '0x000000')
  286. {
  287. if(is_int(intval($border)) && $border>0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????).
  288. $image = ImageFile::open($this->path());
  289. }
  290. else {
  291. return $this;
  292. }
  293. $dst_width = $image->width()+2*$border;
  294. $dst_height = $image->height()+2*$border;
  295. $frame = ImageFile::create($dst_width, $dst_height);
  296. $frame->__call('fill', [$color]);
  297. $this->image = $frame;
  298. $this->__call('merge', [$image, $border, $border]);
  299. $this->saveImage();
  300. return $this;
  301. }
  302. /**
  303. * Forward the call to the image processing method.
  304. *
  305. * @param string $method
  306. * @param mixed $args
  307. * @return $this|mixed
  308. */
  309. public function __call($method, $args)
  310. {
  311. if (!in_array($method, static::$magic_actions, true)) {
  312. return parent::__call($method, $args);
  313. }
  314. // Always initialize image.
  315. if (!$this->image) {
  316. $this->image();
  317. }
  318. try {
  319. $this->image->{$method}(...$args);
  320. /** @var ImageMediaInterface $medium */
  321. foreach ($this->alternatives as $medium) {
  322. $args_copy = $args;
  323. // regular image: resize 400x400 -> 200x200
  324. // --> @2x: resize 800x800->400x400
  325. if (isset(static::$magic_resize_actions[$method])) {
  326. foreach (static::$magic_resize_actions[$method] as $param) {
  327. if (isset($args_copy[$param])) {
  328. $args_copy[$param] *= $medium->get('ratio');
  329. }
  330. }
  331. }
  332. // Do the same call for alternative media.
  333. $medium->__call($method, $args_copy);
  334. }
  335. } catch (BadFunctionCallException $e) {
  336. }
  337. return $this;
  338. }
  339. }