├── README.md
├── LICENSE
├── composer.json
├── .phpstorm.meta.php
└── src
└── Image.php
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aplus Framework Image Library
4 |
5 | - [Home](https://aplus-framework.com/packages/image)
6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/image/index.html)
7 | - [API Documentation](https://docs.aplus-framework.com/packages/image.html)
8 |
9 | [](https://github.com/aplus-framework/image/actions/workflows/tests.yml)
10 | [](https://coveralls.io/github/aplus-framework/image?branch=master)
11 | [](https://packagist.org/packages/aplus/image)
12 | [](https://aplus-framework.com/sponsor)
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Natan Felles
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aplus/image",
3 | "description": "Aplus Framework Image Library",
4 | "license": "MIT",
5 | "type": "library",
6 | "keywords": [
7 | "image",
8 | "opacity",
9 | "watermark",
10 | "filter",
11 | "flatten",
12 | "gd"
13 | ],
14 | "authors": [
15 | {
16 | "name": "Natan Felles",
17 | "email": "natanfelles@gmail.com",
18 | "homepage": "https://natanfelles.github.io"
19 | }
20 | ],
21 | "homepage": "https://aplus-framework.com/packages/image",
22 | "support": {
23 | "email": "support@aplus-framework.com",
24 | "issues": "https://github.com/aplus-framework/image/issues",
25 | "forum": "https://aplus-framework.com/forum",
26 | "source": "https://github.com/aplus-framework/image",
27 | "docs": "https://docs.aplus-framework.com/guides/libraries/image/"
28 | },
29 | "funding": [
30 | {
31 | "type": "Aplus Sponsor",
32 | "url": "https://aplus-framework.com/sponsor"
33 | }
34 | ],
35 | "require": {
36 | "php": ">=8.3",
37 | "ext-gd": "*",
38 | "ext-json": "*"
39 | },
40 | "require-dev": {
41 | "ext-xdebug": "*",
42 | "aplus/coding-standard": "^2.8",
43 | "ergebnis/composer-normalize": "^2.15",
44 | "jetbrains/phpstorm-attributes": "^1.0",
45 | "phpmd/phpmd": "^2.13",
46 | "phpstan/phpstan": "^1.11",
47 | "phpunit/phpunit": "^10.5"
48 | },
49 | "minimum-stability": "dev",
50 | "prefer-stable": true,
51 | "autoload": {
52 | "psr-4": {
53 | "Framework\\Image\\": "src/"
54 | }
55 | },
56 | "autoload-dev": {
57 | "psr-4": {
58 | "Tests\\Image\\": "tests/"
59 | }
60 | },
61 | "config": {
62 | "allow-plugins": {
63 | "ergebnis/composer-normalize": true
64 | },
65 | "optimize-autoloader": true,
66 | "preferred-install": "dist",
67 | "sort-packages": true
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace PHPSTORM_META;
11 |
12 | registerArgumentsSet(
13 | 'flip_directions',
14 | 'b',
15 | 'both',
16 | 'h',
17 | 'horizontal',
18 | 'v',
19 | 'vertical',
20 | );
21 | registerArgumentsSet(
22 | 'filter_types',
23 | \IMG_FILTER_NEGATE,
24 | \IMG_FILTER_GRAYSCALE,
25 | \IMG_FILTER_BRIGHTNESS,
26 | \IMG_FILTER_CONTRAST,
27 | \IMG_FILTER_COLORIZE,
28 | \IMG_FILTER_EDGEDETECT,
29 | \IMG_FILTER_EMBOSS,
30 | \IMG_FILTER_GAUSSIAN_BLUR,
31 | \IMG_FILTER_SELECTIVE_BLUR,
32 | \IMG_FILTER_MEAN_REMOVAL,
33 | \IMG_FILTER_SMOOTH,
34 | \IMG_FILTER_PIXELATE,
35 | \IMG_FILTER_SCATTER,
36 | );
37 | registerArgumentsSet(
38 | 'filter_arg1',
39 | \IMG_FILTER_BRIGHTNESS,
40 | \IMG_FILTER_CONTRAST,
41 | \IMG_FILTER_COLORIZE,
42 | \IMG_FILTER_SMOOTH,
43 | \IMG_FILTER_PIXELATE,
44 | \IMG_FILTER_SCATTER,
45 | );
46 | registerArgumentsSet(
47 | 'filter_arg2',
48 | \IMG_FILTER_COLORIZE,
49 | \IMG_FILTER_PIXELATE,
50 | \IMG_FILTER_SCATTER,
51 | );
52 | registerArgumentsSet(
53 | 'filter_arg3',
54 | \IMG_FILTER_COLORIZE,
55 | \IMG_FILTER_SCATTER,
56 | );
57 | registerArgumentsSet(
58 | 'filter_arg4',
59 | \IMG_FILTER_COLORIZE,
60 | );
61 | expectedArguments(
62 | \Framework\Image\Image::flip(),
63 | 0,
64 | argumentsSet('flip_directions')
65 | );
66 | expectedArguments(
67 | \Framework\Image\Image::filter(),
68 | 0,
69 | argumentsSet('filter_types')
70 | );
71 | expectedArguments(
72 | \Framework\Image\Image::filter(),
73 | 1,
74 | argumentsSet('filter_arg1')
75 | );
76 | expectedArguments(
77 | \Framework\Image\Image::filter(),
78 | 2,
79 | argumentsSet('filter_arg2')
80 | );
81 | expectedArguments(
82 | \Framework\Image\Image::filter(),
83 | 3,
84 | argumentsSet('filter_arg3')
85 | );
86 | expectedArguments(
87 | \Framework\Image\Image::filter(),
88 | 4,
89 | argumentsSet('filter_arg4')
90 | );
91 |
--------------------------------------------------------------------------------
/src/Image.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Framework\Image;
11 |
12 | use GdImage;
13 | use InvalidArgumentException;
14 | use JetBrains\PhpStorm\ArrayShape;
15 | use JetBrains\PhpStorm\Pure;
16 | use LogicException;
17 | use RuntimeException;
18 |
19 | /**
20 | * Class Image.
21 | *
22 | * @package image
23 | */
24 | class Image implements \JsonSerializable, \Stringable
25 | {
26 | /**
27 | * Path to the image file.
28 | */
29 | protected string $filename;
30 | /**
31 | * Image type. One of IMAGETYPE_* constants.
32 | */
33 | protected int $type;
34 | /**
35 | * MIME type.
36 | */
37 | protected string $mime;
38 | /**
39 | * GdImage instance.
40 | */
41 | protected GdImage $instance;
42 | /**
43 | * The image quality/compression level.
44 | *
45 | * 0 to 9 on PNG, default is 6. 0 to 100 on JPEG, default is 75.
46 | * Null to update to the default when getQuality is called.
47 | *
48 | * @see Image::getQuality()
49 | */
50 | protected ?int $quality = null;
51 |
52 | /**
53 | * Image constructor.
54 | *
55 | * @param string $filename Path to the image file.
56 | * Acceptable types are: GIF, JPEG and PNG
57 | *
58 | * @throws InvalidArgumentException for invalid file
59 | * @throws RuntimeException for unsupported image type of could not get image info
60 | */
61 | public function __construct(string $filename)
62 | {
63 | $realpath = \realpath($filename);
64 | if ($realpath === false || !\is_file($realpath) || !\is_readable($realpath)) {
65 | throw new InvalidArgumentException('File does not exists or is not readable: ' . $filename);
66 | }
67 | $this->filename = $realpath;
68 | $info = \getimagesize($this->filename);
69 | if ($info === false) {
70 | throw new RuntimeException(
71 | 'Could not get image info from the given filename: ' . $this->filename
72 | );
73 | }
74 | if (!(\imagetypes() & $info[2])) {
75 | throw new RuntimeException('Unsupported image type: ' . $info[2]);
76 | }
77 | $this->type = $info[2];
78 | $this->mime = $info['mime'];
79 | $instance = match ($this->type) {
80 | \IMAGETYPE_PNG => \imagecreatefrompng($this->filename),
81 | \IMAGETYPE_JPEG => \imagecreatefromjpeg($this->filename),
82 | \IMAGETYPE_GIF => \imagecreatefromgif($this->filename),
83 | default => throw new RuntimeException('Image type is not acceptable: ' . $this->type),
84 | };
85 | if (!$instance instanceof GdImage) {
86 | throw new RuntimeException(
87 | "Image of type '{$this->type}' does not returned a GdImage instance"
88 | );
89 | }
90 | $this->instance = $instance;
91 | }
92 |
93 | public function __destruct()
94 | {
95 | $this->destroy();
96 | }
97 |
98 | public function __toString() : string
99 | {
100 | return $this->getDataUrl();
101 | }
102 |
103 | /**
104 | * Destroys the GdImage instance.
105 | *
106 | * @return bool
107 | */
108 | public function destroy() : bool
109 | {
110 | return \imagedestroy($this->instance);
111 | }
112 |
113 | /**
114 | * Gets the GdImage instance.
115 | *
116 | * @return GdImage GdImage instance
117 | */
118 | #[Pure]
119 | public function getInstance() : GdImage
120 | {
121 | return $this->instance;
122 | }
123 |
124 | /**
125 | * Sets the GdImage instance.
126 | *
127 | * @param GdImage $instance GdImage instance
128 | *
129 | * @return static
130 | */
131 | public function setInstance(GdImage $instance) : static
132 | {
133 | $this->instance = $instance;
134 | return $this;
135 | }
136 |
137 | /**
138 | * Gets the image quality/compression level.
139 | *
140 | * @return int|null An integer for PNG and JPEG types or null for GIF
141 | */
142 | public function getQuality() : ?int
143 | {
144 | if ($this->quality === null) {
145 | if ($this->type === \IMAGETYPE_PNG) {
146 | $this->quality = 6;
147 | } elseif ($this->type === \IMAGETYPE_JPEG) {
148 | $this->quality = 75;
149 | }
150 | }
151 | return $this->quality;
152 | }
153 |
154 | /**
155 | * Sets the image quality/compression level.
156 | *
157 | * @param int $quality The quality/compression level
158 | *
159 | * @throws LogicException when trying to set a quality value for a GIF image
160 | * @throws InvalidArgumentException if the image type is PNG and the value
161 | * is not between 0 and 9 or if the image type is JPEG and the value is not
162 | * between 0 and 100
163 | *
164 | * @return static
165 | */
166 | public function setQuality(int $quality) : static
167 | {
168 | if ($this->type === \IMAGETYPE_GIF) {
169 | throw new LogicException(
170 | 'GIF images does not receive a quality value'
171 | );
172 | }
173 | if ($this->type === \IMAGETYPE_PNG && ($quality < 0 || $quality > 9)) {
174 | throw new InvalidArgumentException(
175 | 'PNG images must receive a quality value between 0 and 9, ' . $quality . ' given'
176 | );
177 | }
178 | if ($this->type === \IMAGETYPE_JPEG && ($quality < 0 || $quality > 100)) {
179 | throw new InvalidArgumentException(
180 | 'JPEG images must receive a quality value between 0 and 100, ' . $quality . ' given'
181 | );
182 | }
183 | $this->quality = $quality;
184 | return $this;
185 | }
186 |
187 | /**
188 | * Gets the image resolution.
189 | *
190 | * @throws RuntimeException for image could not get resolution
191 | *
192 | * @return array Returns an array containing two keys, horizontal and
193 | * vertical, with integers as values
194 | */
195 | #[ArrayShape(['horizontal' => 'int', 'vertical' => 'int'])]
196 | public function getResolution() : array
197 | {
198 | $resolution = \imageresolution($this->instance);
199 | if ($resolution === false) {
200 | throw new RuntimeException('Image could not to get resolution');
201 | }
202 | return [
203 | 'horizontal' => $resolution[0], // @phpstan-ignore-line
204 | // @phpstan-ignore-next-line
205 | 'vertical' => $resolution[1],
206 | ];
207 | }
208 |
209 | /**
210 | * Sets the image resolution.
211 | *
212 | * @param int $horizontal The horizontal resolution in DPI
213 | * @param int $vertical The vertical resolution in DPI
214 | *
215 | * @throws RuntimeException for image could not to set resolution
216 | *
217 | * @return static
218 | */
219 | public function setResolution(int $horizontal = 96, int $vertical = 96) : static
220 | {
221 | $set = \imageresolution($this->instance, $horizontal, $vertical);
222 | if ($set === false) {
223 | throw new RuntimeException('Image could not to set resolution');
224 | }
225 | return $this;
226 | }
227 |
228 | /**
229 | * Gets the image height.
230 | *
231 | * @return int
232 | */
233 | #[Pure]
234 | public function getHeight() : int
235 | {
236 | return \imagesy($this->instance);
237 | }
238 |
239 | /**
240 | * Gets the image width.
241 | *
242 | * @return int
243 | */
244 | #[Pure]
245 | public function getWidth() : int
246 | {
247 | return \imagesx($this->instance);
248 | }
249 |
250 | /**
251 | * Gets the file extension for image type.
252 | *
253 | * @return false|string a string with the extension corresponding to the
254 | * given image type or false on fail
255 | */
256 | #[Pure]
257 | public function getExtension() : false | string
258 | {
259 | return \image_type_to_extension($this->type);
260 | }
261 |
262 | /**
263 | * Gets the image MIME type.
264 | *
265 | * @return string
266 | */
267 | #[Pure]
268 | public function getMime() : string
269 | {
270 | return $this->mime;
271 | }
272 |
273 | /**
274 | * Saves the image contents to a given filename.
275 | *
276 | * @param string|null $filename Optional filename or null to use the original
277 | *
278 | * @return bool
279 | */
280 | public function save(?string $filename = null) : bool
281 | {
282 | $filename ??= $this->filename;
283 | return match ($this->type) {
284 | \IMAGETYPE_PNG => \imagepng($this->instance, $filename, $this->getQuality()),
285 | \IMAGETYPE_JPEG => \imagejpeg($this->instance, $filename, $this->getQuality()),
286 | \IMAGETYPE_GIF => \imagegif($this->instance, $filename),
287 | default => false,
288 | };
289 | }
290 |
291 | /**
292 | * Sends the image contents to the output buffer.
293 | *
294 | * @return bool
295 | */
296 | public function send() : bool
297 | {
298 | if (\in_array($this->type, [\IMAGETYPE_PNG, \IMAGETYPE_GIF], true)) {
299 | \imagesavealpha($this->instance, true);
300 | }
301 | return match ($this->type) {
302 | \IMAGETYPE_PNG => \imagepng($this->instance, null, $this->getQuality()),
303 | \IMAGETYPE_JPEG => \imagejpeg($this->instance, null, $this->getQuality()),
304 | \IMAGETYPE_GIF => \imagegif($this->instance),
305 | default => false,
306 | };
307 | }
308 |
309 | /**
310 | * Renders the image contents.
311 | *
312 | * @throws RuntimeException for image could not be rendered
313 | *
314 | * @return string The image contents
315 | */
316 | public function render() : string
317 | {
318 | \ob_start();
319 | $status = $this->send();
320 | $contents = \ob_get_clean();
321 | if ($status === false || $contents === false) {
322 | throw new RuntimeException('Image could not be rendered');
323 | }
324 | return $contents;
325 | }
326 |
327 | /**
328 | * Crops the image.
329 | *
330 | * @param int $width Width in pixels
331 | * @param int $height Height in pixels
332 | * @param int $marginLeft Margin left in pixels
333 | * @param int $marginTop Margin top in pixels
334 | *
335 | * @throws RuntimeException for image could not to crop
336 | *
337 | * @return static
338 | */
339 | public function crop(int $width, int $height, int $marginLeft = 0, int $marginTop = 0) : static
340 | {
341 | $crop = \imagecrop($this->instance, [
342 | 'x' => $marginLeft,
343 | 'y' => $marginTop,
344 | 'width' => $width,
345 | 'height' => $height,
346 | ]);
347 | if ($crop === false) {
348 | throw new RuntimeException('Image could not to crop');
349 | }
350 | $this->instance = $crop;
351 | return $this;
352 | }
353 |
354 | /**
355 | * Flips the image.
356 | *
357 | * @param string $direction Allowed values are: h or horizontal. v or vertical. b or both.
358 | *
359 | * @throws InvalidArgumentException for invalid image flip direction
360 | * @throws RuntimeException for image could not to flip
361 | *
362 | * @return static
363 | */
364 | public function flip(string $direction = 'horizontal') : static
365 | {
366 | $direction = match ($direction) {
367 | 'h', 'horizontal' => \IMG_FLIP_HORIZONTAL,
368 | 'v', 'vertical' => \IMG_FLIP_VERTICAL,
369 | 'b', 'both' => \IMG_FLIP_BOTH,
370 | default => throw new InvalidArgumentException('Invalid image flip direction: ' . $direction),
371 | };
372 | $flip = \imageflip($this->instance, $direction);
373 | if ($flip === false) {
374 | throw new RuntimeException('Image could not to flip');
375 | }
376 | return $this;
377 | }
378 |
379 | /**
380 | * Applies a filter to the image.
381 | *
382 | * @param int $type IMG_FILTER_* constants
383 | * @param int ...$arguments Arguments for the filter type
384 | *
385 | * @see https://www.php.net/manual/en/function.imagefilter.php
386 | *
387 | * @throws RuntimeException for image could not apply the filter
388 | *
389 | * @return static
390 | */
391 | public function filter(int $type, int ...$arguments) : static
392 | {
393 | $filtered = \imagefilter($this->instance, $type, ...$arguments);
394 | if ($filtered === false) {
395 | throw new RuntimeException('Image could not apply the filter');
396 | }
397 | return $this;
398 | }
399 |
400 | /**
401 | * Flattens the image.
402 | *
403 | * Replaces transparency with an RGB color.
404 | *
405 | * @param int $red
406 | * @param int $green
407 | * @param int $blue
408 | *
409 | * @throws RuntimeException for could not create a true color image, could
410 | * not allocate a color or image could not to flatten
411 | *
412 | * @return static
413 | */
414 | public function flatten(int $red = 255, int $green = 255, int $blue = 255) : static
415 | {
416 | \imagesavealpha($this->instance, false);
417 | $image = \imagecreatetruecolor($this->getWidth(), $this->getHeight());
418 | if ($image === false) {
419 | throw new RuntimeException('Could not create a true color image');
420 | }
421 | $color = \imagecolorallocate($image, $red, $green, $blue);
422 | if ($color === false) {
423 | throw new RuntimeException('Image could not allocate a color');
424 | }
425 | \imagefilledrectangle(
426 | $image,
427 | 0,
428 | 0,
429 | $this->getWidth(),
430 | $this->getHeight(),
431 | $color
432 | );
433 | $copied = \imagecopy(
434 | $image,
435 | $this->instance,
436 | 0,
437 | 0,
438 | 0,
439 | 0,
440 | $this->getWidth(),
441 | $this->getHeight()
442 | );
443 | if ($copied === false) {
444 | throw new RuntimeException('Image could not to flatten');
445 | }
446 | $this->instance = $image;
447 | return $this;
448 | }
449 |
450 | /**
451 | * Sets the image opacity level.
452 | *
453 | * @param int $opacity Opacity percentage: from 0 to 100
454 | *
455 | * @return static
456 | */
457 | public function opacity(int $opacity = 100) : static
458 | {
459 | if ($opacity < 0 || $opacity > 100) {
460 | throw new InvalidArgumentException(
461 | 'Opacity percentage must be between 0 and 100, ' . $opacity . ' given'
462 | );
463 | }
464 | if ($opacity === 100) {
465 | \imagealphablending($this->instance, true);
466 | return $this;
467 | }
468 | $opacity = (int) \round(\abs(($opacity * 127 / 100) - 127));
469 | \imagelayereffect($this->instance, \IMG_EFFECT_OVERLAY);
470 | $color = \imagecolorallocatealpha($this->instance, 127, 127, 127, $opacity);
471 | if ($color === false) {
472 | throw new RuntimeException('Image could not allocate a color');
473 | }
474 | \imagefilledrectangle(
475 | $this->instance,
476 | 0,
477 | 0,
478 | $this->getWidth(),
479 | $this->getHeight(),
480 | $color
481 | );
482 | \imagesavealpha($this->instance, true);
483 | \imagealphablending($this->instance, false);
484 | return $this;
485 | }
486 |
487 | /**
488 | * Rotates the image with a given angle.
489 | *
490 | * @param float $angle Rotation angle, in degrees. Clockwise direction.
491 | *
492 | * @throws RuntimeException for image could not allocate a color or could not rotate
493 | *
494 | * @return static
495 | */
496 | public function rotate(float $angle) : static
497 | {
498 | if (\in_array($this->type, [\IMAGETYPE_PNG, \IMAGETYPE_GIF], true)) {
499 | \imagealphablending($this->instance, false);
500 | \imagesavealpha($this->instance, true);
501 | $background = \imagecolorallocatealpha($this->instance, 0, 0, 0, 127);
502 | } else {
503 | $background = \imagecolorallocate($this->instance, 255, 255, 255);
504 | }
505 | if ($background === false) {
506 | throw new RuntimeException('Image could not allocate a color');
507 | }
508 | $rotate = \imagerotate($this->instance, -1 * $angle, $background);
509 | if ($rotate === false) {
510 | throw new RuntimeException('Image could not to rotate');
511 | }
512 | $this->instance = $rotate;
513 | return $this;
514 | }
515 |
516 | /**
517 | * Scales the image.
518 | *
519 | * @param int $width Width in pixels
520 | * @param int $height Height in pixels. Use -1 to use a proportional height
521 | * based on the width.
522 | *
523 | * @throws RuntimeException for image could not to scale
524 | *
525 | * @return static
526 | */
527 | public function scale(int $width, int $height = -1) : static
528 | {
529 | $scale = \imagescale($this->instance, $width, $height);
530 | if ($scale === false) {
531 | throw new RuntimeException('Image could not to scale');
532 | }
533 | $this->instance = $scale;
534 | return $this;
535 | }
536 |
537 | /**
538 | * Adds a watermark to the image.
539 | *
540 | * @param Image $watermark The image to use as watermark
541 | * @param int $horizontalPosition Horizontal position
542 | * @param int $verticalPosition Vertical position
543 | *
544 | * @throws RuntimeException for image could not to create watermark
545 | *
546 | * @return static
547 | */
548 | public function watermark(
549 | Image $watermark,
550 | int $horizontalPosition = 0,
551 | int $verticalPosition = 0
552 | ) : static {
553 | if ($horizontalPosition < 0) {
554 | $horizontalPosition = $this->getWidth()
555 | - (-1 * $horizontalPosition + $watermark->getWidth());
556 | }
557 | if ($verticalPosition < 0) {
558 | $verticalPosition = $this->getHeight()
559 | - (-1 * $verticalPosition + $watermark->getHeight());
560 | }
561 | $copied = \imagecopy(
562 | $this->instance,
563 | $watermark->getInstance(),
564 | $horizontalPosition,
565 | $verticalPosition,
566 | 0,
567 | 0,
568 | $watermark->getWidth(),
569 | $watermark->getHeight()
570 | );
571 | if ($copied === false) {
572 | throw new RuntimeException('Image could not to create watermark');
573 | }
574 | return $this;
575 | }
576 |
577 | /**
578 | * Allow embed the image contents in a document.
579 | *
580 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
581 | * @see https://datatracker.ietf.org/doc/html/rfc2397
582 | *
583 | * @return string The image "data" URL
584 | */
585 | public function getDataUrl() : string
586 | {
587 | return 'data:' . $this->getMime() . ';base64,' . \base64_encode($this->render());
588 | }
589 |
590 | /**
591 | * @return string
592 | */
593 | public function jsonSerialize() : string
594 | {
595 | return $this->getDataUrl();
596 | }
597 |
598 | /**
599 | * Indicates if a given filename has an acceptable image type.
600 | *
601 | * @param string $filename
602 | *
603 | * @return bool
604 | */
605 | public static function isAcceptable(string $filename) : bool
606 | {
607 | $filename = \realpath($filename);
608 | if ($filename === false || !\is_file($filename) || !\is_readable($filename)) {
609 | return false;
610 | }
611 | $info = \getimagesize($filename);
612 | if ($info === false) {
613 | return false;
614 | }
615 | return match ($info[2]) {
616 | \IMAGETYPE_PNG, \IMAGETYPE_JPEG, \IMAGETYPE_GIF => true,
617 | default => false,
618 | };
619 | }
620 | }
621 |
--------------------------------------------------------------------------------