ImageMedium.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <?php
  2. /**
  3. * @package Grav\Common\Page
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 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 Gregwar\Image\Image;
  18. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  19. use function func_get_args;
  20. use function in_array;
  21. /**
  22. * Class ImageMedium
  23. * @package Grav\Common\Page\Medium
  24. */
  25. class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulateInterface
  26. {
  27. use ImageMediaTrait;
  28. use ImageLoadingTrait;
  29. /**
  30. * @var mixed|string
  31. */
  32. private $saved_image_path;
  33. /**
  34. * Construct.
  35. *
  36. * @param array $items
  37. * @param Blueprint|null $blueprint
  38. */
  39. public function __construct($items = [], Blueprint $blueprint = null)
  40. {
  41. parent::__construct($items, $blueprint);
  42. $config = $this->getGrav()['config'];
  43. $this->thumbnailTypes = ['page', 'media', 'default'];
  44. $this->default_quality = $config->get('system.images.default_image_quality', 85);
  45. $this->def('debug', $config->get('system.images.debug'));
  46. $path = $this->get('filepath');
  47. if (!$path || !file_exists($path) || !filesize($path)) {
  48. return;
  49. }
  50. $this->set('thumbnails.media', $path);
  51. if (!($this->offsetExists('width') && $this->offsetExists('height') && $this->offsetExists('mime'))) {
  52. $image_info = getimagesize($path);
  53. if ($image_info) {
  54. $this->def('width', (int) $image_info[0]);
  55. $this->def('height', (int) $image_info[1]);
  56. $this->def('mime', $image_info['mime']);
  57. }
  58. }
  59. $this->reset();
  60. if ($config->get('system.images.cache_all', false)) {
  61. $this->cache();
  62. }
  63. }
  64. /**
  65. * @return array
  66. */
  67. public function getMeta(): array
  68. {
  69. return [
  70. 'width' => $this->width,
  71. 'height' => $this->height,
  72. ] + parent::getMeta();
  73. }
  74. /**
  75. * Also unset the image on destruct.
  76. */
  77. #[\ReturnTypeWillChange]
  78. public function __destruct()
  79. {
  80. unset($this->image);
  81. }
  82. /**
  83. * Also clone image.
  84. */
  85. #[\ReturnTypeWillChange]
  86. public function __clone()
  87. {
  88. if ($this->image) {
  89. $this->image = clone $this->image;
  90. }
  91. parent::__clone();
  92. }
  93. /**
  94. * Reset image.
  95. *
  96. * @return $this
  97. */
  98. public function reset()
  99. {
  100. parent::reset();
  101. if ($this->image) {
  102. $this->image();
  103. $this->medium_querystring = [];
  104. $this->filter();
  105. $this->clearAlternatives();
  106. }
  107. $this->format = 'guess';
  108. $this->quality = $this->default_quality;
  109. $this->debug_watermarked = false;
  110. $config = $this->getGrav()['config'];
  111. // Set CLS configuration
  112. $this->auto_sizes = $config->get('system.images.cls.auto_sizes', false);
  113. $this->aspect_ratio = $config->get('system.images.cls.aspect_ratio', false);
  114. $this->retina_scale = $config->get('system.images.cls.retina_scale', 1);
  115. return $this;
  116. }
  117. /**
  118. * Add meta file for the medium.
  119. *
  120. * @param string $filepath
  121. * @return $this
  122. */
  123. public function addMetaFile($filepath)
  124. {
  125. parent::addMetaFile($filepath);
  126. // Apply filters in meta file
  127. $this->reset();
  128. return $this;
  129. }
  130. /**
  131. * Return PATH to image.
  132. *
  133. * @param bool $reset
  134. * @return string path to image
  135. */
  136. public function path($reset = true)
  137. {
  138. $output = $this->saveImage();
  139. if ($reset) {
  140. $this->reset();
  141. }
  142. return $output;
  143. }
  144. /**
  145. * Return URL to image.
  146. *
  147. * @param bool $reset
  148. * @return string
  149. */
  150. public function url($reset = true)
  151. {
  152. $grav = $this->getGrav();
  153. /** @var UniformResourceLocator $locator */
  154. $locator = $grav['locator'];
  155. $image_path = (string)($locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true));
  156. $saved_image_path = $this->saved_image_path = $this->saveImage();
  157. $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path) ?: $saved_image_path;
  158. if ($locator->isStream($output)) {
  159. $output = (string)($locator->findResource($output, false) ?: $locator->findResource($output, false, true));
  160. }
  161. if (Utils::startsWith($output, $image_path)) {
  162. $image_dir = $locator->findResource('cache://images', false);
  163. $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
  164. }
  165. if ($reset) {
  166. $this->reset();
  167. }
  168. return trim($grav['base_url'] . '/' . $this->urlQuerystring($output), '\\');
  169. }
  170. /**
  171. * Return srcset string for this Medium and its alternatives.
  172. *
  173. * @param bool $reset
  174. * @return string
  175. */
  176. public function srcset($reset = true)
  177. {
  178. if (empty($this->alternatives)) {
  179. if ($reset) {
  180. $this->reset();
  181. }
  182. return '';
  183. }
  184. $srcset = [];
  185. foreach ($this->alternatives as $ratio => $medium) {
  186. $srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
  187. }
  188. $srcset[] = str_replace(' ', '%20', $this->url($reset)) . ' ' . $this->get('width') . 'w';
  189. return implode(', ', $srcset);
  190. }
  191. /**
  192. * Parsedown element for source display mode
  193. *
  194. * @param array $attributes
  195. * @param bool $reset
  196. * @return array
  197. */
  198. public function sourceParsedownElement(array $attributes, $reset = true)
  199. {
  200. empty($attributes['src']) && $attributes['src'] = $this->url(false);
  201. $srcset = $this->srcset($reset);
  202. if ($srcset) {
  203. empty($attributes['srcset']) && $attributes['srcset'] = $srcset;
  204. $attributes['sizes'] = $this->sizes();
  205. }
  206. if ($this->saved_image_path && $this->auto_sizes) {
  207. if (!array_key_exists('height', $this->attributes) && !array_key_exists('width', $this->attributes)) {
  208. $info = getimagesize($this->saved_image_path);
  209. $width = (int)$info[0];
  210. $height = (int)$info[1];
  211. $scaling_factor = $this->retina_scale > 0 ? $this->retina_scale : 1;
  212. $attributes['width'] = (int)($width / $scaling_factor);
  213. $attributes['height'] = (int)($height / $scaling_factor);
  214. if ($this->aspect_ratio) {
  215. $style = ($attributes['style'] ?? ' ') . "--aspect-ratio: $width/$height;";
  216. $attributes['style'] = trim($style);
  217. }
  218. }
  219. }
  220. return ['name' => 'img', 'attributes' => $attributes];
  221. }
  222. /**
  223. * Turn the current Medium into a Link
  224. *
  225. * @param bool $reset
  226. * @param array $attributes
  227. * @return MediaLinkInterface
  228. */
  229. public function link($reset = true, array $attributes = [])
  230. {
  231. $attributes['href'] = $this->url(false);
  232. $srcset = $this->srcset(false);
  233. if ($srcset) {
  234. $attributes['data-srcset'] = $srcset;
  235. }
  236. return parent::link($reset, $attributes);
  237. }
  238. /**
  239. * Turn the current Medium into a Link with lightbox enabled
  240. *
  241. * @param int $width
  242. * @param int $height
  243. * @param bool $reset
  244. * @return MediaLinkInterface
  245. */
  246. public function lightbox($width = null, $height = null, $reset = true)
  247. {
  248. if ($this->mode !== 'source') {
  249. $this->display('source');
  250. }
  251. if ($width && $height) {
  252. $this->__call('cropResize', [(int) $width, (int) $height]);
  253. }
  254. return parent::lightbox($width, $height, $reset);
  255. }
  256. /**
  257. * @param string $enabled
  258. * @return $this
  259. */
  260. public function autoSizes($enabled = 'true')
  261. {
  262. $this->auto_sizes = $enabled === 'true' ?: false;
  263. return $this;
  264. }
  265. /**
  266. * @param string $enabled
  267. * @return $this
  268. */
  269. public function aspectRatio($enabled = 'true')
  270. {
  271. $this->aspect_ratio = $enabled === 'true' ?: false;
  272. return $this;
  273. }
  274. /**
  275. * @param int $scale
  276. * @return $this
  277. */
  278. public function retinaScale($scale = 1)
  279. {
  280. $this->retina_scale = (int)$scale;
  281. return $this;
  282. }
  283. /**
  284. * @param string|null $image
  285. * @param string|null $position
  286. * @param int|float|null $scale
  287. * @return $this
  288. */
  289. public function watermark($image = null, $position = null, $scale = null)
  290. {
  291. $grav = $this->getGrav();
  292. $locator = $grav['locator'];
  293. $config = $grav['config'];
  294. $args = func_get_args();
  295. $file = $args[0] ?? '1'; // using '1' because of markdown. doing ![](image.jpg?watermark) returns $args[0]='1';
  296. $file = $file === '1' ? $config->get('system.images.watermark.image') : $args[0];
  297. $watermark = $locator->findResource($file);
  298. $watermark = ImageFile::open($watermark);
  299. // Scaling operations
  300. $scale = ($scale ?? $config->get('system.images.watermark.scale', 100)) / 100;
  301. $wwidth = (int) ($this->get('width') * $scale);
  302. $wheight = (int) ($this->get('height') * $scale);
  303. $watermark->resize($wwidth, $wheight);
  304. // Position operations
  305. $position = !empty($args[1]) ? explode('-', $args[1]) : ['center', 'center']; // todo change to config
  306. $positionY = $position[0] ?? $config->get('system.images.watermark.position_y', 'center');
  307. $positionX = $position[1] ?? $config->get('system.images.watermark.position_x', 'center');
  308. switch ($positionY)
  309. {
  310. case 'top':
  311. $positionY = 0;
  312. break;
  313. case 'bottom':
  314. $positionY = (int)$this->get('height')-$wheight;
  315. break;
  316. case 'center':
  317. $positionY = ((int)$this->get('height')/2) - ($wheight/2);
  318. break;
  319. }
  320. switch ($positionX)
  321. {
  322. case 'left':
  323. $positionX = 0;
  324. break;
  325. case 'right':
  326. $positionX = (int) ($this->get('width')-$wwidth);
  327. break;
  328. case 'center':
  329. $positionX = (int) (($this->get('width')/2) - ($wwidth/2));
  330. break;
  331. }
  332. $this->__call('merge', [$watermark,$positionX, $positionY]);
  333. return $this;
  334. }
  335. /**
  336. * Handle this commonly used variant
  337. *
  338. * @return $this
  339. */
  340. public function cropZoom()
  341. {
  342. $this->__call('zoomCrop', func_get_args());
  343. return $this;
  344. }
  345. /**
  346. * Add a frame to image
  347. *
  348. * @return $this
  349. */
  350. public function addFrame(int $border = 10, string $color = '0x000000')
  351. {
  352. if($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??????).
  353. $image = ImageFile::open($this->path());
  354. }
  355. else {
  356. return $this;
  357. }
  358. $dst_width = (int) ($image->width()+2*$border);
  359. $dst_height = (int) ($image->height()+2*$border);
  360. $frame = ImageFile::create($dst_width, $dst_height);
  361. $frame->__call('fill', [$color]);
  362. $this->image = $frame;
  363. $this->__call('merge', [$image, $border, $border]);
  364. $this->saveImage();
  365. return $this;
  366. }
  367. /**
  368. * Forward the call to the image processing method.
  369. *
  370. * @param string $method
  371. * @param mixed $args
  372. * @return $this|mixed
  373. */
  374. #[\ReturnTypeWillChange]
  375. public function __call($method, $args)
  376. {
  377. if (!in_array($method, static::$magic_actions, true)) {
  378. return parent::__call($method, $args);
  379. }
  380. // Always initialize image.
  381. if (!$this->image) {
  382. $this->image();
  383. }
  384. try {
  385. $this->image->{$method}(...$args);
  386. /** @var ImageMediaInterface $medium */
  387. foreach ($this->alternatives as $medium) {
  388. $args_copy = $args;
  389. // regular image: resize 400x400 -> 200x200
  390. // --> @2x: resize 800x800->400x400
  391. if (isset(static::$magic_resize_actions[$method])) {
  392. foreach (static::$magic_resize_actions[$method] as $param) {
  393. if (isset($args_copy[$param])) {
  394. $args_copy[$param] *= $medium->get('ratio');
  395. }
  396. }
  397. }
  398. // Do the same call for alternative media.
  399. $medium->__call($method, $args_copy);
  400. }
  401. } catch (BadFunctionCallException $e) {
  402. }
  403. return $this;
  404. }
  405. }