LICENSE000064400000002074150731647100005560 0ustar00The MIT License (MIT) Copyright (c) 2013-2024 Oliver Vogel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. composer.json000064400000002314150731647100007272 0ustar00{ "name": "intervention/image", "description": "PHP image manipulation", "homepage": "https://image.intervention.io/", "keywords": [ "image", "gd", "imagick", "watermark", "thumbnail", "resize" ], "license": "MIT", "authors": [ { "name": "Oliver Vogel", "email": "oliver@intervention.io", "homepage": "https://intervention.io/" } ], "require": { "php": "^8.1", "ext-mbstring": "*", "intervention/gif": "^4.2" }, "require-dev": { "phpunit/phpunit": "^10.0", "mockery/mockery": "^1.6", "phpstan/phpstan": "^1", "squizlabs/php_codesniffer": "^3.8", "slevomat/coding-standard": "~8.0" }, "suggest": { "ext-exif": "Recommended to be able to read EXIF data properly." }, "autoload": { "psr-4": { "Intervention\\Image\\": "src" } }, "autoload-dev": { "psr-4": { "Intervention\\Image\\Tests\\": "tests" } }, "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } } } readme.md000064400000004700150731647100006330 0ustar00# Intervention Image ## PHP Image Processing [![Latest Version](https://img.shields.io/packagist/v/intervention/image.svg)](https://packagist.org/packages/intervention/image) [![Build Status](https://github.com/Intervention/image/actions/workflows/run-tests.yml/badge.svg)](https://github.com/Intervention/image/actions) [![Monthly Downloads](https://img.shields.io/packagist/dm/intervention/image.svg)](https://packagist.org/packages/intervention/image/stats) [![Support me on Ko-fi](https://raw.githubusercontent.com/Intervention/image/develop/.github/images/support.svg)](https://ko-fi.com/interventionphp) Intervention Image is a **PHP image processing library** that provides a simple and expressive way to create, edit, and compose images. It features a unified API for the two most popular image manipulation extensions. You can choose between the GD library or Imagick as the base layer for all operations. - Simple interface for common image editing tasks - Interchangeable driver architecture - Support for animated images - Framework-agnostic - PSR-12 compliant ## Installation You can easily install this library using [Composer](https://getcomposer.org). Just request the package with the following command: ```bash composer require intervention/image ``` ## Getting Started Learn the [basics](https://image.intervention.io/v3/basics/instantiation/) on how to use Intervention Image and more with the [official documentation](https://image.intervention.io/v3/). ## Code Examples ```php use Intervention\Image\ImageManager; // create image manager with desired driver $manager = new ImageManager( new Intervention\Image\Drivers\Gd\Driver() ); // open an image file $image = $manager->read('images/example.gif'); // resize image instance $image->resize(height: 300); // insert a watermark $image->place('images/watermark.png'); // encode edited image $encoded = $image->toJpg(); // save encoded image $encoded->save('images/example.jpg'); ``` ## Requirements - PHP >= 8.1 ## Supported Image Libraries - GD Library - Imagick PHP extension ## Security If you discover any security related issues, please email oliver@intervention.io directly. ## Authors This library is developed and maintained by [Oliver Vogel](https://intervention.io) Thanks to the community of [contributors](https://github.com/Intervention/image/graphs/contributors) who have helped to improve this project. ## License Intervention Image is licensed under the [MIT License](LICENSE). src/Analyzers/ColorspaceAnalyzer.php000064400000000274150731647100013623 0ustar00 */ class Collection implements CollectionInterface, IteratorAggregate, Countable { /** * Create new collection object * * @param array $items * @return void */ public function __construct(protected array $items = []) { } /** * Static constructor * * @param array $items * @return self */ public static function create(array $items = []): self { return new self($items); } /** * {@inheritdoc} * * @see CollectionInterface::has() */ public function has(int|string $key): bool { return array_key_exists($key, $this->items); } /** * Returns Iterator * * @return Traversable */ public function getIterator(): Traversable { return new ArrayIterator($this->items); } /** * {@inheritdoc} * * @see CollectionInterface::toArray() */ public function toArray(): array { return $this->items; } /** * Count items in collection * * @return int */ public function count(): int { return count($this->items); } /** * Append new item to collection * * @param mixed $item * @return CollectionInterface */ public function push($item): CollectionInterface { $this->items[] = $item; return $this; } /** * Return first item in collection * * @return mixed */ public function first(): mixed { if ($item = reset($this->items)) { return $item; } return null; } /** * Returns last item in collection * * @return mixed */ public function last(): mixed { if ($item = end($this->items)) { return $item; } return null; } /** * Return item at given position starting at 0 * * @param int $key * @return mixed */ public function getAtPosition(int $key = 0, $default = null): mixed { if ($this->count() == 0) { return $default; } $positions = array_values($this->items); if (!array_key_exists($key, $positions)) { return $default; } return $positions[$key]; } /** * {@inheritdoc} * * @see CollectionInterface::get() */ public function get(int|string $query, $default = null): mixed { if ($this->count() == 0) { return $default; } if (is_int($query) && array_key_exists($query, $this->items)) { return $this->items[$query]; } if (is_string($query) && strpos($query, '.') === false) { return array_key_exists($query, $this->items) ? $this->items[$query] : $default; } $query = explode('.', (string) $query); $result = $default; $items = $this->items; foreach ($query as $key) { if (!is_array($items) || !array_key_exists($key, $items)) { $result = $default; break; } $result = $items[$key]; $items = $result; } return $result; } /** * Map each item of collection by given callback * * @param callable $callback * @return self */ public function map(callable $callback): self { $items = array_map(function ($item) use ($callback) { return $callback($item); }, $this->items); return new self($items); } /** * Run callback on each item of the collection an remove it if it does not return true * * @param callable $callback * @return self */ public function filter(callable $callback): self { $items = array_filter($this->items, function ($item) use ($callback) { return $callback($item); }); return new self($items); } /** * {@inheritdoc} * * @see CollectionInterface::empty() */ public function empty(): CollectionInterface { $this->items = []; return $this; } /** * {@inheritdoc} * * @see CollectionInterface::slice() */ public function slice(int $offset, ?int $length = null): CollectionInterface { $this->items = array_slice($this->items, $offset, $length); return $this; } } src/Colors/AbstractColor.php000064400000004274150731647100012062 0ustar00 */ protected array $channels; /** * {@inheritdoc} * * @see ColorInterface::channels() */ public function channels(): array { return $this->channels; } /** * {@inheritdoc} * * @see ColorInterface::channel() */ public function channel(string $classname): ColorChannelInterface { $channels = array_filter($this->channels(), function (ColorChannelInterface $channel) use ($classname) { return $channel::class == $classname; }); if (count($channels) == 0) { throw new ColorException('Color channel ' . $classname . ' could not be found.'); } return reset($channels); } /** * {@inheritdoc} * * @see ColorInterface::normalize() */ public function normalize(): array { return array_map(function (ColorChannelInterface $channel) { return $channel->normalize(); }, $this->channels()); } /** * {@inheritdoc} * * @see ColorInterface::toArray() */ public function toArray(): array { return array_map(function (ColorChannelInterface $channel) { return $channel->value(); }, $this->channels()); } /** * {@inheritdoc} * * @see ColorInterface::convertTo() */ public function convertTo(string|ColorspaceInterface $colorspace): ColorInterface { $colorspace = match (true) { is_object($colorspace) => $colorspace, default => new $colorspace(), }; return $colorspace->importColor($this); } /** * {@inheritdoc} * * @see ColorInterface::__toString() */ public function __toString(): string { return $this->toString(); } } src/Colors/AbstractColorChannel.php000064400000004134150731647100013346 0ustar00value = $this->validate( match (true) { is_null($value) && is_numeric($normalized) => intval(round($normalized * $this->max())), is_numeric($value) && is_null($normalized) => $value, default => throw new ColorException('Color channels must either have a value or a normalized value') } ); } /** * Alias of value() * * @return int */ public function toInt(): int { return $this->value; } /** * {@inheritdoc} * * @see ColorChannelInterface::value() */ public function value(): int { return $this->value; } /** * {@inheritdoc} * * @see ColorChannelInterface::normalize() */ public function normalize(int $precision = 32): float { return round(($this->value() - $this->min()) / ($this->max() - $this->min()), $precision); } /** * {@inheritdoc} * * @see ColorChannelInterface::validate() */ public function validate(mixed $value): mixed { if ($value < $this->min() || $value > $this->max()) { throw new ColorException('Color channel value must be in range ' . $this->min() . ' to ' . $this->max()); } return $value; } /** * {@inheritdoc} * * @see ColorChannelInterface::toString() */ public function toString(): string { return (string) $this->value(); } /** * {@inheritdoc} * * @see ColorChannelInterface::__toString() */ public function __toString(): string { return $this->toString(); } } src/Colors/Cmyk/Channels/Cyan.php000064400000000463150731647100012644 0ustar00channels = [ new Cyan($c), new Magenta($m), new Yellow($y), new Key($k), ]; } /** * {@inheritdoc} * * @see ColorInterface::create() */ public static function create(mixed $input): ColorInterface { return InputHandler::withDecoders([ Decoders\StringColorDecoder::class, ])->handle($input); } /** * {@inheritdoc} * * @see ColorInterface::colorspace() */ public function colorspace(): ColorspaceInterface { return new Colorspace(); } /** * {@inheritdoc} * * @see ColorInterface::toHex() */ public function toHex(string $prefix = ''): string { return $this->convertTo(RgbColorspace::class)->toHex($prefix); } /** * Return the CMYK cyan channel * * @return ColorChannelInterface */ public function cyan(): ColorChannelInterface { /** @throws void */ return $this->channel(Cyan::class); } /** * Return the CMYK magenta channel * * @return ColorChannelInterface */ public function magenta(): ColorChannelInterface { /** @throws void */ return $this->channel(Magenta::class); } /** * Return the CMYK yellow channel * * @return ColorChannelInterface */ public function yellow(): ColorChannelInterface { /** @throws void */ return $this->channel(Yellow::class); } /** * Return the CMYK key channel * * @return ColorChannelInterface */ public function key(): ColorChannelInterface { /** @throws void */ return $this->channel(Key::class); } /** * {@inheritdoc} * * @see ColorInterface::toString() */ public function toString(): string { return sprintf( 'cmyk(%d%%, %d%%, %d%%, %d%%)', $this->cyan()->value(), $this->magenta()->value(), $this->yellow()->value(), $this->key()->value() ); } /** * {@inheritdoc} * * @see ColorInterface::isGreyscale() */ public function isGreyscale(): bool { return 0 === array_sum([ $this->cyan()->value(), $this->magenta()->value(), $this->yellow()->value(), ]); } /** * {@inheritdoc} * * @see ColorInterface::isTransparent() */ public function isTransparent(): bool { return false; } /** * {@inheritdoc} * * @see ColorInterface::isClear() */ public function isClear(): bool { return false; } } src/Colors/Cmyk/Colorspace.php000064400000004662150731647100012316 0ustar00 */ public static array $channels = [ Channels\Cyan::class, Channels\Magenta::class, Channels\Yellow::class, Channels\Key::class ]; /** * {@inheritdoc} * * @see ColorspaceInterface::createColor() */ public function colorFromNormalized(array $normalized): ColorInterface { $values = array_map(function ($classname, $value_normalized) { return (new $classname(normalized: $value_normalized))->value(); }, self::$channels, $normalized); return new Color(...$values); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ public function importColor(ColorInterface $color): ColorInterface { return match ($color::class) { RgbColor::class => $this->importRgbColor($color), HsvColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)), HslColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)), default => $color, }; } /** * @param ColorInterface $color * @return Color * @throws ColorException */ protected function importRgbColor(ColorInterface $color): CmykColor { if (!($color instanceof RgbColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } $c = (255 - $color->red()->value()) / 255.0 * 100; $m = (255 - $color->green()->value()) / 255.0 * 100; $y = (255 - $color->blue()->value()) / 255.0 * 100; $k = intval(round(min([$c, $m, $y]))); $c = intval(round($c - $k)); $m = intval(round($m - $k)); $y = intval(round($y - $k)); return new CmykColor($c, $m, $y, $k); } } src/Colors/Cmyk/Decoders/StringColorDecoder.php000064400000002372150731647100015503 0ustar00[0-9\.]+%?), ?(?P[0-9\.]+%?), ?(?P[0-9\.]+%?), ?(?P[0-9\.]+%?)\)$/i'; if (preg_match($pattern, $input, $matches) != 1) { throw new DecoderException('Unable to decode input'); } $values = array_map(function ($value) { return intval(round(floatval(trim(str_replace('%', '', $value))))); }, [$matches['c'], $matches['m'], $matches['y'], $matches['k']]); return new Color(...$values); } } src/Colors/Hsl/Channels/Hue.php000064400000000733150731647100012316 0ustar00channels = [ new Hue($h), new Saturation($s), new Luminance($l), ]; } /** * {@inheritdoc} * * @see ColorInterface::colorspace() */ public function colorspace(): ColorspaceInterface { return new Colorspace(); } /** * {@inheritdoc} * * @see ColorInterface::create() */ public static function create(mixed $input): ColorInterface { return InputHandler::withDecoders([ Decoders\StringColorDecoder::class, ])->handle($input); } /** * Return the Hue channel * * @return ColorChannelInterface */ public function hue(): ColorChannelInterface { /** @throws void */ return $this->channel(Hue::class); } /** * Return the Saturation channel * * @return ColorChannelInterface */ public function saturation(): ColorChannelInterface { /** @throws void */ return $this->channel(Saturation::class); } /** * Return the Luminance channel * * @return ColorChannelInterface */ public function luminance(): ColorChannelInterface { /** @throws void */ return $this->channel(Luminance::class); } public function toHex(string $prefix = ''): string { return $this->convertTo(RgbColorspace::class)->toHex($prefix); } /** * {@inheritdoc} * * @see ColorInterface::toString() */ public function toString(): string { return sprintf( 'hsl(%d, %d%%, %d%%)', $this->hue()->value(), $this->saturation()->value(), $this->luminance()->value() ); } /** * {@inheritdoc} * * @see ColorInterface::isGreyscale() */ public function isGreyscale(): bool { return $this->saturation()->value() == 0; } /** * {@inheritdoc} * * @see ColorInterface::isTransparent() */ public function isTransparent(): bool { return false; } /** * {@inheritdoc} * * @see ColorInterface::isClear() */ public function isClear(): bool { return false; } } src/Colors/Hsl/Colorspace.php000064400000010131150731647100012125 0ustar00 */ public static array $channels = [ Channels\Hue::class, Channels\Saturation::class, Channels\Luminance::class ]; /** * {@inheritdoc} * * @see ColorspaceInterface::colorFromNormalized() */ public function colorFromNormalized(array $normalized): ColorInterface { $values = array_map(function ($classname, $value_normalized) { return (new $classname(normalized: $value_normalized))->value(); }, self::$channels, $normalized); return new Color(...$values); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ public function importColor(ColorInterface $color): ColorInterface { return match ($color::class) { CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)), RgbColor::class => $this->importRgbColor($color), HsvColor::class => $this->importHsvColor($color), default => $color, }; } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importRgbColor(ColorInterface $color): ColorInterface { if (!($color instanceof RgbColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } // normalized values of rgb channels $values = array_map(function ($channel) { return $channel->normalize(); }, $color->channels()); // take only RGB $values = array_slice($values, 0, 3); // calculate Luminance $min = min(...$values); $max = max(...$values); $luminance = ($max + $min) / 2; $delta = $max - $min; // calculate saturation $saturation = match (true) { $delta == 0 => 0, default => $delta / (1 - abs(2 * $luminance - 1)), }; // calculate hue list($r, $g, $b) = $values; $hue = match (true) { ($delta == 0) => 0, ($max == $r) => 60 * fmod((($g - $b) / $delta), 6), ($max == $g) => 60 * ((($b - $r) / $delta) + 2), ($max == $b) => 60 * ((($r - $g) / $delta) + 4), default => 0, }; $hue = ($hue + 360) % 360; // normalize hue return new Color( intval(round($hue)), intval(round($saturation * 100)), intval(round($luminance * 100)), ); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importHsvColor(ColorInterface $color): ColorInterface { if (!($color instanceof HsvColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } // normalized values of hsv channels list($h, $s, $v) = array_map(function ($channel) { return $channel->normalize(); }, $color->channels()); // calculate Luminance $luminance = (2 - $s) * $v / 2; // calculate Saturation $saturation = match (true) { $luminance == 0 => $s, $luminance == 1 => 0, $luminance < .5 => $s * $v / ($luminance * 2), default => $s * $v / (2 - $luminance * 2), }; return new Color( intval(round($h * 360)), intval(round($saturation * 100)), intval(round($luminance * 100)), ); } } src/Colors/Hsl/Decoders/StringColorDecoder.php000064400000002470150731647100015325 0ustar00[0-9\.]+), ?(?P[0-9\.]+%?), ?(?P[0-9\.]+%?)?\)$/i'; if (preg_match($pattern, $input, $matches) != 1) { throw new DecoderException('Unable to decode input'); } $values = array_map(function ($value) { return match (strpos($value, '%')) { false => intval(trim($value)), default => intval(trim(str_replace('%', '', $value))), }; }, [$matches['h'], $matches['s'], $matches['l']]); return new Color(...$values); } } src/Colors/Hsv/Channels/Hue.php000064400000000733150731647100012330 0ustar00channels = [ new Hue($h), new Saturation($s), new Value($v), ]; } /** * {@inheritdoc} * * @see ColorInterface::colorspace() */ public function colorspace(): ColorspaceInterface { return new Colorspace(); } /** * {@inheritdoc} * * @see ColorInterface::create() */ public static function create(mixed $input): ColorInterface { return InputHandler::withDecoders([ Decoders\StringColorDecoder::class, ])->handle($input); } /** * Return the Hue channel * * @return ColorChannelInterface */ public function hue(): ColorChannelInterface { /** @throws void */ return $this->channel(Hue::class); } /** * Return the Saturation channel * * @return ColorChannelInterface */ public function saturation(): ColorChannelInterface { /** @throws void */ return $this->channel(Saturation::class); } /** * Return the Value channel * * @return ColorChannelInterface */ public function value(): ColorChannelInterface { /** @throws void */ return $this->channel(Value::class); } public function toHex(string $prefix = ''): string { return $this->convertTo(RgbColorspace::class)->toHex($prefix); } /** * {@inheritdoc} * * @see ColorInterface::toString() */ public function toString(): string { return sprintf( 'hsv(%d, %d%%, %d%%)', $this->hue()->value(), $this->saturation()->value(), $this->value()->value() ); } /** * {@inheritdoc} * * @see ColorInterface::isGreyscale() */ public function isGreyscale(): bool { return $this->saturation()->value() == 0; } /** * {@inheritdoc} * * @see ColorInterface::isTransparent() */ public function isTransparent(): bool { return false; } /** * {@inheritdoc} * * @see ColorInterface::isClear() */ public function isClear(): bool { return false; } } src/Colors/Hsv/Colorspace.php000064400000007154150731647100012152 0ustar00 */ public static array $channels = [ Channels\Hue::class, Channels\Saturation::class, Channels\Value::class ]; /** * {@inheritdoc} * * @see ColorspaceInterface::colorFromNormalized() */ public function colorFromNormalized(array $normalized): ColorInterface { $values = array_map(function ($classname, $value_normalized) { return (new $classname(normalized: $value_normalized))->value(); }, self::$channels, $normalized); return new Color(...$values); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ public function importColor(ColorInterface $color): ColorInterface { return match ($color::class) { CmykColor::class => $this->importRgbColor($color->convertTo(RgbColorspace::class)), RgbColor::class => $this->importRgbColor($color), HslColor::class => $this->importHslColor($color), default => $color, }; } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importRgbColor(ColorInterface $color): ColorInterface { if (!($color instanceof RgbColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } // normalized values of rgb channels $values = array_map(function ($channel) { return $channel->normalize(); }, $color->channels()); // take only RGB $values = array_slice($values, 0, 3); // calculate chroma $min = min(...$values); $max = max(...$values); $chroma = $max - $min; // calculate value $v = 100 * $max; if ($chroma == 0) { // greyscale color return new Color(0, 0, intval(round($v))); } // calculate saturation $s = 100 * ($chroma / $max); // calculate hue list($r, $g, $b) = $values; $h = match (true) { ($r == $min) => 3 - (($g - $b) / $chroma), ($b == $min) => 1 - (($r - $g) / $chroma), default => 5 - (($b - $r) / $chroma), } * 60; return new Color( intval(round($h)), intval(round($s)), intval(round($v)) ); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importHslColor(ColorInterface $color): ColorInterface { if (!($color instanceof HslColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } // normalized values of hsl channels list($h, $s, $l) = array_map(function ($channel) { return $channel->normalize(); }, $color->channels()); $v = $l + $s * min($l, 1 - $l); $s = ($v == 0) ? 0 : 2 * (1 - $l / $v); return $this->colorFromNormalized([$h, $s, $v]); } } src/Colors/Hsv/Decoders/StringColorDecoder.php000064400000002500150731647100015331 0ustar00[0-9\.]+), ?(?P[0-9\.]+%?), ?(?P[0-9\.]+%?)?\)$/i'; if (preg_match($pattern, $input, $matches) != 1) { throw new DecoderException('Unable to decode input'); } $values = array_map(function ($value) { return match (strpos($value, '%')) { false => intval(trim($value)), default => intval(trim(str_replace('%', '', $value))), }; }, [$matches['h'], $matches['s'], $matches['v']]); return new Color(...$values); } } src/Colors/Profile.php000064400000000324150731647100010710 0ustar00normalize(), 6)); } } src/Colors/Rgb/Channels/Blue.php000064400000000157150731647100012450 0ustar00channels = [ new Red($r), new Green($g), new Blue($b), new Alpha($a), ]; } /** * {@inheritdoc} * * @see ColorInterface::colorspace() */ public function colorspace(): ColorspaceInterface { return new Colorspace(); } /** * {@inheritdoc} * * @see ColorInterface::create() */ public static function create(mixed $input): ColorInterface { return InputHandler::withDecoders([ Decoders\HexColorDecoder::class, Decoders\StringColorDecoder::class, Decoders\TransparentColorDecoder::class, Decoders\HtmlColornameDecoder::class, ])->handle($input); } /** * Return the RGB red color channel * * @return ColorChannelInterface */ public function red(): ColorChannelInterface { /** @throws void */ return $this->channel(Red::class); } /** * Return the RGB green color channel * * @return ColorChannelInterface */ public function green(): ColorChannelInterface { /** @throws void */ return $this->channel(Green::class); } /** * Return the RGB blue color channel * * @return ColorChannelInterface */ public function blue(): ColorChannelInterface { /** @throws void */ return $this->channel(Blue::class); } /** * Return the colors alpha channel * * @return ColorChannelInterface */ public function alpha(): ColorChannelInterface { /** @throws void */ return $this->channel(Alpha::class); } /** * {@inheritdoc} * * @see ColorInterface::toHex() */ public function toHex(string $prefix = ''): string { if ($this->isTransparent()) { return sprintf( '%s%02x%02x%02x%02x', $prefix, $this->red()->value(), $this->green()->value(), $this->blue()->value(), $this->alpha()->value() ); } return sprintf( '%s%02x%02x%02x', $prefix, $this->red()->value(), $this->green()->value(), $this->blue()->value() ); } /** * {@inheritdoc} * * @see ColorInterface::toString() */ public function toString(): string { if ($this->isTransparent()) { return sprintf( 'rgba(%d, %d, %d, %.1F)', $this->red()->value(), $this->green()->value(), $this->blue()->value(), $this->alpha()->normalize(), ); } return sprintf( 'rgb(%d, %d, %d)', $this->red()->value(), $this->green()->value(), $this->blue()->value() ); } /** * {@inheritdoc} * * @see ColorInterface::isGreyscale() */ public function isGreyscale(): bool { $values = [$this->red()->value(), $this->green()->value(), $this->blue()->value()]; return count(array_unique($values, SORT_REGULAR)) === 1; } /** * {@inheritdoc} * * @see ColorInterface::isTransparent() */ public function isTransparent(): bool { return $this->alpha()->value() < $this->alpha()->max(); } /** * {@inheritdoc} * * @see ColorInterface::isClear() */ public function isClear(): bool { return $this->alpha()->value() == 0; } } src/Colors/Rgb/Colorspace.php000064400000010760150731647100012121 0ustar00 */ public static array $channels = [ Channels\Red::class, Channels\Green::class, Channels\Blue::class, Channels\Alpha::class ]; /** * {@inheritdoc} * * @see ColorspaceInterface::colorFromNormalized() */ public function colorFromNormalized(array $normalized): ColorInterface { $values = array_map(function ($classname, $value_normalized) { return (new $classname(normalized: $value_normalized))->value(); }, self::$channels, $normalized); return new Color(...$values); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ public function importColor(ColorInterface $color): ColorInterface { return match ($color::class) { CmykColor::class => $this->importCmykColor($color), HsvColor::class => $this->importHsvColor($color), HslColor::class => $this->importHslColor($color), default => $color, }; } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importCmykColor(ColorInterface $color): ColorInterface { if (!($color instanceof CmykColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } return new Color( (int) (255 * (1 - $color->cyan()->normalize()) * (1 - $color->key()->normalize())), (int) (255 * (1 - $color->magenta()->normalize()) * (1 - $color->key()->normalize())), (int) (255 * (1 - $color->yellow()->normalize()) * (1 - $color->key()->normalize())), ); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importHsvColor(ColorInterface $color): ColorInterface { if (!($color instanceof HsvColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } $chroma = $color->value()->normalize() * $color->saturation()->normalize(); $hue = $color->hue()->normalize() * 6; $x = $chroma * (1 - abs(fmod($hue, 2) - 1)); // connect channel values $values = match (true) { $hue < 1 => [$chroma, $x, 0], $hue < 2 => [$x, $chroma, 0], $hue < 3 => [0, $chroma, $x], $hue < 4 => [0, $x, $chroma], $hue < 5 => [$x, 0, $chroma], default => [$chroma, 0, $x], }; // add to each value $values = array_map(function ($value) use ($color, $chroma) { return $value + $color->value()->normalize() - $chroma; }, $values); array_push($values, 1); // append alpha channel value return $this->colorFromNormalized($values); } /** * @param ColorInterface $color * @return ColorInterface * @throws ColorException */ protected function importHslColor(ColorInterface $color): ColorInterface { if (!($color instanceof HslColor)) { throw new ColorException('Unabled to import color of type ' . $color::class . '.'); } // normalized values of hsl channels list($h, $s, $l) = array_map(function ($channel) { return $channel->normalize(); }, $color->channels()); $c = (1 - abs(2 * $l - 1)) * $s; $x = $c * (1 - abs(fmod($h * 6, 2) - 1)); $m = $l - $c / 2; $values = match (true) { $h < 1 / 6 => [$c, $x, 0], $h < 2 / 6 => [$x, $c, 0], $h < 3 / 6 => [0, $c, $x], $h < 4 / 6 => [0, $x, $c], $h < 5 / 6 => [$x, 0, $c], default => [$c, 0, $x], }; $values = array_map(function ($value) use ($m) { return $value + $m; }, $values); array_push($values, 1); // append alpha channel value return $this->colorFromNormalized($values); } } src/Colors/Rgb/Decoders/HexColorDecoder.php000064400000003174150731647100014571 0ustar00[a-f\d]{3}(?:[a-f\d]?|(?:[a-f\d]{3}(?:[a-f\d]{2})?)?)\b)$/i'; if (preg_match($pattern, $input, $matches) != 1) { throw new DecoderException('Unable to decode input'); } $values = str_split($matches['hex']); $values = match (strlen($matches['hex'])) { 3, 4 => str_split($matches['hex']), 6, 8 => str_split($matches['hex'], 2), default => throw new DecoderException('Unable to decode input'), }; $values = array_map(function ($value) { return match (strlen($value)) { 1 => hexdec($value . $value), 2 => hexdec($value), default => throw new DecoderException('Unable to decode input'), }; }, $values); return new Color(...$values); } } src/Colors/Rgb/Decoders/HtmlColornameDecoder.php000064400000013224150731647100015607 0ustar00 */ protected static array $names = [ 'lightsalmon' => '#ffa07a', 'salmon' => '#fa8072', 'darksalmon' => '#e9967a', 'lightcoral' => '#f08080', 'indianred' => '#cd5c5c', 'crimson' => '#dc143c', 'firebrick' => '#b22222', 'red' => '#ff0000', 'darkred' => '#8b0000', 'coral' => '#ff7f50', 'tomato' => '#ff6347', 'orangered' => '#ff4500', 'gold' => '#ffd700', 'orange' => '#ffa500', 'darkorange' => '#ff8c00', 'lightyellow' => '#ffffe0', 'lemonchiffon' => '#fffacd', 'lightgoldenrodyellow' => '#fafad2', 'papayawhip' => '#ffefd5', 'moccasin' => '#ffe4b5', 'peachpuff' => '#ffdab9', 'palegoldenrod' => '#eee8aa', 'khaki' => '#f0e68c', 'darkkhaki' => '#bdb76b', 'yellow' => '#ffff00', 'lawngreen' => '#7cfc00', 'chartreuse' => '#7fff00', 'limegreen' => '#32cd32', 'lime' => '#00ff00', 'forestgreen' => '#228b22', 'green' => '#008000', 'darkgreen' => '#006400', 'greenyellow' => '#adff2f', 'yellowgreen' => '#9acd32', 'springgreen' => '#00ff7f', 'mediumspringgreen' => '#00fa9a', 'lightgreen' => '#90ee90', 'palegreen' => '#98fb98', 'darkseagreen' => '#8fbc8f', 'mediumseagre' => 'en #3cb371', 'seagreen' => '#2e8b57', 'olive' => '#808000', 'darkolivegreen' => '#556b2f', 'olivedrab' => '#6b8e23', 'lightcyan' => '#e0ffff', 'cyan' => '#00ffff', 'aqua' => '#00ffff', 'aquamarine' => '#7fffd4', 'mediumaquamarine' => '#66cdaa', 'paleturquoise' => '#afeeee', 'turquoise' => '#40e0d0', 'mediumturquoise' => '#48d1cc', 'darkturquoise' => '#00ced1', 'lightseagreen' => '#20b2aa', 'cadetblue' => '#5f9ea0', 'darkcyan' => '#008b8b', 'teal' => '#008080', 'powderblue' => '#b0e0e6', 'lightblue' => '#add8e6', 'lightskyblue' => '#87cefa', 'skyblue' => '#87ceeb', 'deepskyblue' => '#00bfff', 'lightsteelblue' => '#b0c4de', 'dodgerblue' => '#1e90ff', 'cornflowerblue' => '#6495ed', 'steelblue' => '#4682b4', 'royalblue' => '#4169e1', 'blue' => '#0000ff', 'mediumblue' => '#0000cd', 'darkblue' => '#00008b', 'navy' => '#000080', 'midnightblue' => '#191970', 'mediumslateblue' => '#7b68ee', 'slateblue' => '#6a5acd', 'darkslateblue' => '#483d8b', 'lavender' => '#e6e6fa', 'thistle' => '#d8bfd8', 'plum' => '#dda0dd', 'violet' => '#ee82ee', 'orchid' => '#da70d6', 'fuchsia' => '#ff00ff', 'magenta' => '#ff00ff', 'mediumorchid' => '#ba55d3', 'mediumpurple' => '#9370db', 'blueviolet' => '#8a2be2', 'darkviolet' => '#9400d3', 'darkorchid' => '#9932cc', 'darkmagenta' => '#8b008b', 'purple' => '#800080', 'indigo' => '#4b0082', 'pink' => '#ffc0cb', 'lightpink' => '#ffb6c1', 'hotpink' => '#ff69b4', 'deeppink' => '#ff1493', 'palevioletred' => '#db7093', 'mediumvioletred' => '#c71585', 'white' => '#ffffff', 'snow' => '#fffafa', 'honeydew' => '#f0fff0', 'mintcream' => '#f5fffa', 'azure' => '#f0ffff', 'aliceblue' => '#f0f8ff', 'ghostwhite' => '#f8f8ff', 'whitesmoke' => '#f5f5f5', 'seashell' => '#fff5ee', 'beige' => '#f5f5dc', 'oldlace' => '#fdf5e6', 'floralwhite' => '#fffaf0', 'ivory' => '#fffff0', 'antiquewhite' => '#faebd7', 'linen' => '#faf0e6', 'lavenderblush' => '#fff0f5', 'mistyrose' => '#ffe4e1', 'gainsboro' => '#dcdcdc', 'lightgray' => '#d3d3d3', 'silver' => '#c0c0c0', 'darkgray' => '#a9a9a9', 'gray' => '#808080', 'dimgray' => '#696969', 'lightslategray' => '#778899', 'slategray' => '#708090', 'darkslategray' => '#2f4f4f', 'black' => '#000000', 'cornsilk' => '#fff8dc', 'blanchedalmond' => '#ffebcd', 'bisque' => '#ffe4c4', 'navajowhite' => '#ffdead', 'wheat' => '#f5deb3', 'burlywood' => '#deb887', 'tan' => '#d2b48c', 'rosybrown' => '#bc8f8f', 'sandybrown' => '#f4a460', 'goldenrod' => '#daa520', 'peru' => '#cd853f', 'chocolate' => '#d2691e', 'saddlebrown' => '#8b4513', 'sienna' => '#a0522d', 'brown' => '#a52a2a', 'maroon' => '#800000', ]; /** * Decode html color names * * @param mixed $input * @return ImageInterface|ColorInterface */ public function decode(mixed $input): ImageInterface|ColorInterface { if (!is_string($input)) { throw new DecoderException('Unable to decode input'); } if (!array_key_exists(strtolower($input), static::$names)) { throw new DecoderException('Unable to decode input'); } return parent::decode(static::$names[strtolower($input)]); } } src/Colors/Rgb/Decoders/StringColorDecoder.php000064400000003405150731647100015310 0ustar00[0-9\.]+%?), ?(?P[0-9\.]+%?), ?(?P[0-9\.]+%?)' . '(?:, ?(?P(?:1)|(?:1\.0*)|(?:0)|(?:0?\.\d+%?)|(?:\d{1,3}%)))?\)$/i'; if (preg_match($pattern, $input, $matches) != 1) { throw new DecoderException('Unable to decode input'); } // rgb values $values = array_map(function ($value) { return match (strpos($value, '%')) { false => intval(trim($value)), default => intval(round(floatval(trim(str_replace('%', '', $value))) / 100 * 255)), }; }, [$matches['r'], $matches['g'], $matches['b']]); // alpha value if (array_key_exists('a', $matches)) { $values[] = match (true) { strpos($matches['a'], '%') => round(intval(trim(str_replace('%', '', $matches['a']))) / 2.55), default => intval(round(floatval(trim($matches['a'])) * 255)), }; } return new Color(...$values); } } src/Colors/Rgb/Decoders/TransparentColorDecoder.php000064400000001351150731647100016341 0ustar00prepareOptions($options) as $name => $value) { if (!property_exists($this, $name)) { throw new InputException('Property ' . $name . ' does not exists for ' . $this::class . '.'); } $this->{$name} = $value; } return $this; } /** * This method makes it possible to call self::setOptions() with a single * array instead of named parameters * * @param array $options * @return array */ private function prepareOptions(array $options): array { if (count($options) === 0) { return $options; } if (count($options) > 1) { return $options; } if (!array_key_exists(0, $options)) { return $options; } if (!is_array($options[0])) { return $options; } return $options[0]; } } src/Decoders/Base64ImageDecoder.php000064400000000271150731647100013055 0ustar00 PHP_MAXPATHLEN) { return false; } try { if (!@is_file($input)) { return false; } } catch (Exception) { return false; } return true; } /** * Extract and return EXIF data from given input which can be binary image * data or a file path. * * @param string $path_or_data * @return CollectionInterface */ protected function extractExifData(string $path_or_data): CollectionInterface { if (!function_exists('exif_read_data')) { return new Collection(); } try { $source = match (true) { $this->isFile($path_or_data) => $path_or_data, // path default => $this->buildFilePointer($path_or_data), // data }; // extract exif data $data = @exif_read_data($source, null, true); if (is_resource($source)) { fclose($source); } } catch (Exception) { $data = []; } return new Collection(is_array($data) ? $data : []); } /** * Determine if given input is base64 encoded data * * @param mixed $input * @return bool */ protected function isValidBase64(mixed $input): bool { if (!is_string($input)) { return false; } return base64_encode(base64_decode($input)) === str_replace(["\n", "\r"], '', $input); } /** * Parse data uri * * @param mixed $input * @return object */ protected function parseDataUri(mixed $input): object { $pattern = "/^data:(?P\w+\/[-+.\w]+)?" . "(?P(;[-\w]+=[-\w]+)*)(?P;base64)?,(?P.*)/"; $result = preg_match($pattern, $input, $matches); return new class ($matches, $result) { /** * @var array */ private array $matches; private int|false $result; /** * @param array $matches * @param int|false $result * @return void */ public function __construct(array $matches, int|false $result) { $this->matches = $matches; $this->result = $result; } public function isValid(): bool { return (bool) $this->result; } public function mediaType(): ?string { if (isset($this->matches['mediatype']) && !empty($this->matches['mediatype'])) { return $this->matches['mediatype']; } return null; } public function hasMediaType(): bool { return !empty($this->mediaType()); } public function isBase64Encoded(): bool { if (isset($this->matches['base64']) && $this->matches['base64'] === ';base64') { return true; } return false; } public function data(): ?string { if (isset($this->matches['data']) && !empty($this->matches['data'])) { return $this->matches['data']; } return null; } }; } } src/Drivers/AbstractDriver.php000064400000006555150731647100012420 0ustar00config = new Config(); $this->checkHealth(); } /** * {@inheritdoc} * * @see DriverInterface::config() */ public function config(): Config { return $this->config; } /** * {@inheritdoc} * * @see DriverInterface::handleInput() */ public function handleInput(mixed $input, array $decoders = []): ImageInterface|ColorInterface { return InputHandler::withDecoders($decoders, $this)->handle($input); } /** * {@inheritdoc} * * @see DriverInterface::specialize() */ public function specialize( ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface $object ): ModifierInterface|AnalyzerInterface|EncoderInterface|DecoderInterface { // return object directly if no specializing is possible if (!($object instanceof SpecializableInterface)) { return $object; } // return directly and only attach driver if object is already specialized if ($object instanceof SpecializedInterface) { $object->setDriver($this); return $object; } // resolve classname for specializable object $driver_namespace = (new ReflectionClass($this))->getNamespaceName(); $object_path = substr($object::class, strlen("Intervention\\Image\\")); $specialized_classname = $driver_namespace . "\\" . $object_path; if (!class_exists($specialized_classname)) { throw new NotSupportedException( "Class '" . $object_path . "' is not supported by " . $this->id() . " driver." ); } // create a driver specialized object with the specializable properties of generic object $specialized = new $specialized_classname(...$object->specializable()); // attach driver return $specialized->setDriver($this); } /** * {@inheritdoc} * * @throws NotSupportedException * @throws DriverException * @see DriverInterface::specializeMultiple() */ public function specializeMultiple(array $objects): array { return array_map(function ($object) { return $this->specialize( match (true) { is_string($object) => new $object(), is_object($object) => $object, } ); }, $objects); } } src/Drivers/AbstractEncoder.php000064400000002343150731647100012533 0ustar00encode($this); } /** * Build new file pointer, run callback with it and return result as encoded image * * @param callable $callback * @param null|string $mediaType * @throws RuntimeException * @return EncodedImage */ protected function createEncodedImage(callable $callback, ?string $mediaType = null): EncodedImage { $pointer = $this->buildFilePointer(); $callback($pointer); return is_string($mediaType) ? new EncodedImage($pointer, $mediaType) : new EncodedImage($pointer); } } src/Drivers/AbstractFontProcessor.php000064400000012657150731647100013773 0ustar00wrapTextBlock(new TextBlock($text), $font); $pivot = $this->buildPivot($lines, $font, $position); $leading = $this->leading($font); $blockWidth = $this->boxSize((string) $lines->longestLine(), $font)->width(); $x = $pivot->x(); $y = $font->hasFilename() ? $pivot->y() + $this->capHeight($font) : $pivot->y(); $xAdjustment = 0; // adjust line positions according to alignment foreach ($lines as $line) { $lineBoxSize = $this->boxSize((string) $line, $font); $lineWidth = $lineBoxSize->width() + $lineBoxSize->pivot()->x(); $xAdjustment = $font->alignment() == 'left' ? 0 : $blockWidth - $lineWidth; $xAdjustment = $font->alignment() == 'right' ? intval(round($xAdjustment)) : $xAdjustment; $xAdjustment = $font->alignment() == 'center' ? intval(round($xAdjustment / 2)) : $xAdjustment; $position = new Point($x + $xAdjustment, $y); $position->rotate($font->angle(), $pivot); $line->setPosition($position); $y += $leading; } return $lines; } /** * {@inheritdoc} * * @see FontProcessorInterface::nativeFontSize() */ public function nativeFontSize(FontInterface $font): float { return $font->size(); } /** * {@inheritdoc} * * @see FontProcessorInterface::typographicalSize() */ public function typographicalSize(FontInterface $font): int { return $this->boxSize('Hy', $font)->height(); } /** * {@inheritdoc} * * @see FontProcessorInterface::capHeight() */ public function capHeight(FontInterface $font): int { return $this->boxSize('T', $font)->height(); } /** * {@inheritdoc} * * @see FontProcessorInterface::leading() */ public function leading(FontInterface $font): int { return intval(round($this->typographicalSize($font) * $font->lineHeight())); } /** * Reformat a text block by wrapping each line before the given maximum width * * @param TextBlock $block * @param FontInterface $font * @throws FontException * @return TextBlock */ protected function wrapTextBlock(TextBlock $block, FontInterface $font): TextBlock { $newLines = []; foreach ($block as $line) { foreach ($this->wrapLine($line, $font) as $newLine) { $newLines[] = $newLine; } } return $block->setLines($newLines); } /** * Check if a line exceeds the given maximum width and wrap it if necessary. * The output will be an array of formatted lines that are all within the * maximum width. * * @param Line $line * @param FontInterface $font * @throws FontException * @return array */ protected function wrapLine(Line $line, FontInterface $font): array { // no wrap width - no wrapping if (is_null($font->wrapWidth())) { return [$line]; } $wrapped = []; $formattedLine = new Line(); foreach ($line as $word) { // calculate width of newly formatted line $lineWidth = $this->boxSize(match ($formattedLine->count()) { 0 => $word, default => (string) $formattedLine . ' ' . $word, }, $font)->width(); // decide if word fits on current line or a new line must be created if ($line->count() === 1 || $lineWidth <= $font->wrapWidth()) { $formattedLine->add($word); } else { if ($formattedLine->count()) { $wrapped[] = $formattedLine; } $formattedLine = new Line($word); } } $wrapped[] = $formattedLine; return $wrapped; } /** * Build pivot point of textblock according to the font settings and based on given position * * @param TextBlock $block * @param FontInterface $font * @param PointInterface $position * @throws FontException * @return PointInterface */ protected function buildPivot(TextBlock $block, FontInterface $font, PointInterface $position): PointInterface { // bounding box $box = new Rectangle( $this->boxSize((string) $block->longestLine(), $font)->width(), $this->leading($font) * ($block->count() - 1) + $this->capHeight($font) ); // set position $box->setPivot($position); // alignment $box->align($font->alignment()); $box->valign($font->valignment()); $box->rotate($font->angle()); return $box->last(); } } src/Drivers/Gd/Analyzers/ColorspaceAnalyzer.php000064400000001133150731647100015566 0ustar00core()->native()); } } src/Drivers/Gd/Analyzers/PixelColorAnalyzer.php000064400000002546150731647100015565 0ustar00colorAt( $image->colorspace(), $image->core()->frame($this->frame_key)->native() ); } /** * @throws GeometryException * @throws ColorException */ protected function colorAt(ColorspaceInterface $colorspace, GdImage $gd): ColorInterface { $index = @imagecolorat($gd, $this->x, $this->y); if ($index === false) { throw new GeometryException( 'The specified position is not in the valid image area.' ); } return $this->driver()->colorProcessor($colorspace)->nativeToColor($index); } } src/Drivers/Gd/Analyzers/PixelColorsAnalyzer.php000064400000001201150731647100015733 0ustar00colorspace(); foreach ($image as $frame) { $colors->push( parent::colorAt($colorspace, $frame->native()) ); } return $colors; } } src/Drivers/Gd/Analyzers/ResolutionAnalyzer.php000064400000001174150731647100015644 0ustar00core()->native())); } } src/Drivers/Gd/Analyzers/WidthAnalyzer.php000064400000001052150731647100014553 0ustar00core()->native()); } } src/Drivers/Gd/Cloner.php000064400000006161150731647100011246 0ustar00width(), $size->height()); // copy resolution to clone $resolution = imageresolution($gd); if (is_array($resolution) && array_key_exists(0, $resolution) && array_key_exists(1, $resolution)) { imageresolution($clone, $resolution[0], $resolution[1]); } // fill with background $processor = new ColorProcessor(); imagefill($clone, 0, 0, $processor->colorToNative($background)); imagealphablending($clone, true); imagesavealpha($clone, true); // set background image as transparent if alpha channel value if color is below .5 // comes into effect when the end format only supports binary transparency (like GIF) if ($background->channel(Alpha::class)->value() < 128) { imagecolortransparent($clone, $processor->colorToNative($background)); } return $clone; } /** * Create a clone of an GdImage that is positioned on the specified background color. * Possible transparent areas are mixed with this color. * * @param GdImage $gd * @param ColorInterface $background * @throws ColorException * @return GdImage */ public static function cloneBlended(GdImage $gd, ColorInterface $background): GdImage { // create empty canvas with same size $clone = static::cloneEmpty($gd, background: $background); // transfer actual image to clone imagecopy($clone, $gd, 0, 0, 0, 0, imagesx($gd), imagesy($gd)); return $clone; } } src/Drivers/Gd/ColorProcessor.php000064400000005624150731647100013005 0ustar00convertTo($this->colorspace); // gd only supports rgb so the channels can be accessed directly $r = $color->channel(Red::class)->value(); $g = $color->channel(Green::class)->value(); $b = $color->channel(Blue::class)->value(); $a = $color->channel(Alpha::class)->value(); // convert alpha value to gd alpha // ([opaque]255-0[transparent]) to ([opaque]0-127[transparent]) $a = (int) $this->convertRange($a, 0, 255, 127, 0); return ($a << 24) + ($r << 16) + ($g << 8) + $b; } /** * {@inheritdoc} * * @see ColorProcessorInterface::nativeToColor() */ public function nativeToColor(mixed $value): ColorInterface { if (!is_int($value)) { throw new ColorException('GD driver can only decode colors in integer format.'); } $a = ($value >> 24) & 0xFF; $r = ($value >> 16) & 0xFF; $g = ($value >> 8) & 0xFF; $b = $value & 0xFF; // convert gd apha integer to intervention alpha integer // ([opaque]0-127[transparent]) to ([opaque]255-0[transparent]) $a = (int) static::convertRange($a, 127, 0, 0, 255); return new Color($r, $g, $b, $a); } /** * Convert input in range (min) to (max) to the corresponding value * in target range (targetMin) to (targetMax). * * @param float|int $input * @param float|int $min * @param float|int $max * @param float|int $targetMin * @param float|int $targetMax * @return float|int */ protected function convertRange( float|int $input, float|int $min, float|int $max, float|int $targetMin, float|int $targetMax ): float|int { return ceil(((($input - $min) * ($targetMax - $targetMin)) / ($max - $min)) + $targetMin); } } src/Drivers/Gd/Core.php000064400000004314150731647100010712 0ustar00push($frame); return $this; } /** * {@inheritdoc} * * @see CoreInterface::native() */ public function native(): mixed { return $this->first()->native(); } /** * {@inheritdoc} * * @see CoreInterface::setNative() */ public function setNative(mixed $native): self { $this->empty()->push(new Frame($native)); return $this; } /** * {@inheritdoc} * * @see CoreInterface::frame() */ public function frame(int $position): FrameInterface { $frame = $this->getAtPosition($position); if (!($frame instanceof FrameInterface)) { throw new AnimationException('Frame #' . $position . ' could not be found in the image.'); } return $frame; } /** * {@inheritdoc} * * @see CoreInterface::loops() */ public function loops(): int { return $this->loops; } /** * {@inheritdoc} * * @see CoreInterface::setLoops() */ public function setLoops(int $loops): self { $this->loops = $loops; return $this; } /** * {@inheritdoc} * * @see CollectionInterface::first() */ public function first(): FrameInterface { return parent::first(); } /** * {@inheritdoc} * * @see CollectionInterface::last() */ public function last(): FrameInterface { return parent::last(); } /** * Clone instance * * @return void */ public function __clone(): void { foreach ($this->items as $key => $frame) { $this->items[$key] = clone $frame; } } } src/Drivers/Gd/Decoders/AbstractDecoder.php000064400000003160150731647100014601 0ustar00isValidBase64($input)) { throw new DecoderException('Unable to decode input'); } return parent::decode(base64_decode($input)); } } src/Drivers/Gd/Decoders/BinaryImageDecoder.php000064400000003670150731647100015233 0ustar00isGifFormat($input)) { true => $this->decodeGif($input), default => $this->decodeBinary($input), }; } /** * Decode image from given binary data * * @param string $input * @throws RuntimeException * @return ImageInterface */ private function decodeBinary(string $input): ImageInterface { $gd = @imagecreatefromstring($input); if ($gd === false) { throw new DecoderException('Unable to decode input'); } // create image instance $image = parent::decode($gd); // get media type $mediaType = $this->getMediaTypeByBinary($input); // extract & set exif data for appropriate formats if (in_array($mediaType->format(), [Format::JPEG, Format::TIFF])) { $image->setExif($this->extractExifData($input)); } // set mediaType on origin $image->origin()->setMediaType($mediaType); // adjust image orientation if ($this->driver()->config()->autoOrientation) { $image->modify(new AlignRotationModifier()); } return $image; } } src/Drivers/Gd/Decoders/DataUriImageDecoder.php000064400000001720150731647100015332 0ustar00parseDataUri($input); if (!$uri->isValid()) { throw new DecoderException('Unable to decode input'); } if ($uri->isBase64Encoded()) { return parent::decode(base64_decode($uri->data())); } return parent::decode(urldecode($uri->data())); } } src/Drivers/Gd/Decoders/EncodedImageObjectDecoder.php000064400000001252150731647100016471 0ustar00toString()); } } src/Drivers/Gd/Decoders/FilePathImageDecoder.php000064400000004006150731647100015475 0ustar00isFile($input)) { throw new DecoderException('Unable to decode input'); } // detect media (mime) type $mediaType = $this->getMediaTypeByFilePath($input); $image = match ($mediaType->format()) { // gif files might be animated and therefore cannot // be handled by the standard GD decoder. Format::GIF => $this->decodeGif($input), default => parent::decode(match ($mediaType->format()) { Format::JPEG => @imagecreatefromjpeg($input), Format::WEBP => @imagecreatefromwebp($input), Format::PNG => @imagecreatefrompng($input), Format::AVIF => @imagecreatefromavif($input), Format::BMP => @imagecreatefrombmp($input), default => throw new DecoderException('Unable to decode input'), }), }; // set file path & mediaType on origin $image->origin()->setFilePath($input); $image->origin()->setMediaType($mediaType); // extract exif for the appropriate formats if ($mediaType->format() === Format::JPEG) { $image->setExif($this->extractExifData($input)); } // adjust image orientation if ($this->driver()->config()->autoOrientation) { $image->modify(new AlignRotationModifier()); } return $image; } } src/Drivers/Gd/Decoders/FilePointerImageDecoder.php000064400000001462150731647100016224 0ustar00driver(), new Core([ new Frame($input) ]) ); } /** * Decode image from given GIF source which can be either a file path or binary data * * Depending on the configuration, this is taken over by the native GD function * or, if animations are required, by our own extended decoder. * * @param mixed $input * @throws RuntimeException * @return ImageInterface */ protected function decodeGif(mixed $input): ImageInterface { // create non-animated image depending on config if (!$this->driver()->config()->decodeAnimation) { $native = match (true) { $this->isGifFormat($input) => @imagecreatefromstring($input), default => @imagecreatefromgif($input), }; if ($native === false) { throw new DecoderException('Unable to decode input.'); } $image = self::decode($native); $image->origin()->setMediaType('image/gif'); return $image; } try { // create empty core $core = new Core(); $gif = GifDecoder::decode($input); $splitter = GifSplitter::create($gif)->split(); $delays = $splitter->getDelays(); // set loops on core if ($loops = $gif->getMainApplicationExtension()?->getLoops()) { $core->setLoops($loops); } // add GDImage instances to core foreach ($splitter->coalesceToResources() as $key => $native) { $core->push( new Frame($native, $delays[$key] / 100) ); } } catch (Exception $e) { throw new DecoderException($e->getMessage(), $e->getCode(), $e); } // create (possibly) animated image $image = new Image($this->driver(), $core); // set media type $image->origin()->setMediaType('image/gif'); return $image; } } src/Drivers/Gd/Decoders/SplFileInfoImageDecoder.php000064400000001350150731647100016152 0ustar00getRealPath()); } } src/Drivers/Gd/Driver.php000064400000007724150731647100011265 0ustar00core->add( $this->driver->handleInput($source)->core()->first()->setDelay($delay) ); return $this; } /** * @throws RuntimeException */ public function __invoke(): ImageInterface { return new Image( $this->driver, $this->core ); } }; $init($animation); return call_user_func($animation); } /** * {@inheritdoc} * * @see DriverInterface::colorProcessor() */ public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface { return new ColorProcessor($colorspace); } /** * {@inheritdoc} * * @see DriverInterface::fontProcessor() */ public function fontProcessor(): FontProcessorInterface { return new FontProcessor(); } /** * {@inheritdoc} * * @see DriverInterface::supports() */ public function supports(string|Format|FileExtension|MediaType $identifier): bool { return match (Format::tryCreate($identifier)) { Format::JPEG => boolval(imagetypes() & IMG_JPEG), Format::WEBP => boolval(imagetypes() & IMG_WEBP), Format::GIF => boolval(imagetypes() & IMG_GIF), Format::PNG => boolval(imagetypes() & IMG_PNG), Format::AVIF => boolval(imagetypes() & IMG_AVIF), Format::BMP => boolval(imagetypes() & IMG_BMP), default => false, }; } } src/Drivers/Gd/Encoders/AvifEncoder.php000064400000001310150731647100013742 0ustar00createEncodedImage(function ($pointer) use ($image) { imageavif($image->core()->native(), $pointer, $this->quality); }, 'image/aviv'); } } src/Drivers/Gd/Encoders/BmpEncoder.php000064400000001271150731647100013601 0ustar00createEncodedImage(function ($pointer) use ($image) { imagebmp($image->core()->native(), $pointer, false); }, 'image/bmp'); } } src/Drivers/Gd/Encoders/GifEncoder.php000064400000003543150731647100013574 0ustar00isAnimated()) { return $this->encodeAnimated($image); } $gd = Cloner::clone($image->core()->native()); return $this->createEncodedImage(function ($pointer) use ($gd) { imageinterlace($gd, $this->interlaced); imagegif($gd, $pointer); }, 'image/gif'); } /** * @throws RuntimeException */ protected function encodeAnimated(ImageInterface $image): EncodedImage { try { $builder = GifBuilder::canvas( $image->width(), $image->height() ); foreach ($image as $frame) { $builder->addFrame( source: $this->encode($frame->toImage($image->driver()))->toFilePointer(), delay: $frame->delay(), interlaced: $this->interlaced ); } $builder->setLoops($image->loops()); return new EncodedImage($builder->encode(), 'image/gif'); } catch (Exception $e) { throw new EncoderException($e->getMessage(), $e->getCode(), $e); } } } src/Drivers/Gd/Encoders/JpegEncoder.php000064400000002024150731647100013745 0ustar00driver()->handleInput( $this->driver()->config()->blendingColor ); $output = Cloner::cloneBlended( $image->core()->native(), background: $blendingColor ); return $this->createEncodedImage(function ($pointer) use ($output) { imageinterlace($output, $this->progressive); imagejpeg($output, $pointer, $this->quality); }, 'image/jpeg'); } } src/Drivers/Gd/Encoders/PngEncoder.php000064400000003030150731647100013602 0ustar00prepareOutput($image); return $this->createEncodedImage(function ($pointer) use ($output) { imageinterlace($output, $this->interlaced); imagepng($output, $pointer, -1); }, 'image/png'); } /** * Prepare given image instance for PNG format output according to encoder settings * * @param ImageInterface $image * @throws RuntimeException * @throws ColorException * @throws AnimationException * @return GdImage */ private function prepareOutput(ImageInterface $image): GdImage { if ($this->indexed) { $output = clone $image; $output->reduceColors(255); return $output->core()->native(); } return Cloner::clone($image->core()->native()); } } src/Drivers/Gd/Encoders/WebpEncoder.php000064400000001435150731647100013762 0ustar00quality === 100 ? IMG_WEBP_LOSSLESS : $this->quality; return $this->createEncodedImage(function ($pointer) use ($image, $quality) { imagewebp($image->core()->native(), $pointer, $quality); }, 'image/webp'); } } src/Drivers/Gd/FontProcessor.php000064400000005242150731647100012631 0ustar00hasFilename()) { // calculate box size from gd font $box = new Rectangle(0, 0); $chars = mb_strlen($text); if ($chars > 0) { $box->setWidth( $chars * $this->gdCharacterWidth((int) $font->filename()) ); $box->setHeight( $this->gdCharacterHeight((int) $font->filename()) ); } return $box; } // calculate box size from ttf font file with angle 0 $box = imageftbbox( size: $this->nativeFontSize($font), angle: 0, font_filename: $font->filename(), string: $text ); // build size from points return new Rectangle( width: intval(abs($box[6] - $box[4])), // difference of upper-left-x and upper-right-x height: intval(abs($box[7] - $box[1])), // difference if upper-left-y and lower-left-y pivot: new Point($box[6], $box[7]), // position of upper-left corner ); } /** * {@inheritdoc} * * @see FontProcessorInterface::nativeFontSize() */ public function nativeFontSize(FontInterface $font): float { return floatval(round($font->size() * .76, 6)); } /** * {@inheritdoc} * * @see FontProcessorInterface::leading() */ public function leading(FontInterface $font): int { return (int) round(parent::leading($font) * .8); } /** * Return width of a single character * * @param int $gdfont * @return int */ protected function gdCharacterWidth(int $gdfont): int { return $gdfont + 4; } /** * Return height of a single character * * @param int $gdfont * @return int */ protected function gdCharacterHeight(int $gdfont): int { return match ($gdfont) { 2, 3 => 14, 4, 5 => 16, default => 8, }; } } src/Drivers/Gd/Frame.php000064400000007245150731647100011062 0ustar00native = $native; return $this; } /** * {@inheritdoc} * * @see FrameInterface::native() */ public function native(): GdImage { return $this->native; } /** * {@inheritdoc} * * @see FrameInterface::size() */ public function size(): SizeInterface { return new Rectangle(imagesx($this->native), imagesy($this->native)); } /** * {@inheritdoc} * * @see FrameInterface::delay() */ public function delay(): float { return $this->delay; } /** * {@inheritdoc} * * @see FrameInterface::setDelay() */ public function setDelay(float $delay): FrameInterface { $this->delay = $delay; return $this; } /** * {@inheritdoc} * * @see FrameInterface::dispose() */ public function dispose(): int { return $this->dispose; } /** * {@inheritdoc} * * @see FrameInterface::setDispose() */ public function setDispose(int $dispose): FrameInterface { $this->dispose = $dispose; return $this; } /** * {@inheritdoc} * * @see FrameInterface::setOffset() */ public function setOffset(int $left, int $top): FrameInterface { $this->offset_left = $left; $this->offset_top = $top; return $this; } /** * {@inheritdoc} * * @see FrameInterface::offsetLeft() */ public function offsetLeft(): int { return $this->offset_left; } /** * {@inheritdoc} * * @see FrameInterface::setOffsetLeft() */ public function setOffsetLeft(int $offset): FrameInterface { $this->offset_left = $offset; return $this; } /** * {@inheritdoc} * * @see FrameInterface::offsetTop() */ public function offsetTop(): int { return $this->offset_top; } /** * {@inheritdoc} * * @see FrameInterface::setOffsetTop() */ public function setOffsetTop(int $offset): FrameInterface { $this->offset_top = $offset; return $this; } /** * This workaround helps cloning GdImages which is currently not possible. * * @throws ColorException * @return void */ public function __clone(): void { $this->native = Cloner::clone($this->native); } } src/Drivers/Gd/Modifiers/AlignRotationModifier.php000064400000003112150731647100016167 0ustar00exif('IFD0.Orientation')) { 2 => $image->flop(), 3 => $image->rotate(180), 4 => $image->rotate(180)->flop(), 5 => $image->rotate(270)->flop(), 6 => $image->rotate(270), 7 => $image->rotate(90)->flop(), 8 => $image->rotate(90), default => $image }; return $this->markAligned($image); } /** * Set exif data of image to top-left orientation, marking the image as * aligned and making sure the rotation correction process is not * performed again. * * @param ImageInterface $image * @return ImageInterface */ private function markAligned(ImageInterface $image): ImageInterface { $exif = $image->exif()->map(function ($item) { if (is_array($item) && array_key_exists('Orientation', $item)) { $item['Orientation'] = 1; return $item; } return $item; }); return $image->setExif($exif); } } src/Drivers/Gd/Modifiers/BlendTransparencyModifier.php000064400000002135150731647100017037 0ustar00driver()->handleInput( $this->color ? $this->color : $this->driver()->config()->blendingColor ); foreach ($image as $frame) { // create new canvas with blending color as background $modified = Cloner::cloneBlended( $frame->native(), background: $color ); // set new gd image $frame->setNative($modified); } return $image; } } src/Drivers/Gd/Modifiers/BlurModifier.php000064400000001314150731647100014323 0ustar00amount; $i++) { imagefilter($frame->native(), IMG_FILTER_GAUSSIAN_BLUR); } } return $image; } } src/Drivers/Gd/Modifiers/BrightnessModifier.php000064400000001267150731647100015536 0ustar00native(), IMG_FILTER_BRIGHTNESS, intval($this->level * 2.55)); } return $image; } } src/Drivers/Gd/Modifiers/ColorizeModifier.php000064400000001536150731647100015213 0ustar00red * 2.55); $green = (int) round($this->green * 2.55); $blue = (int) round($this->blue * 2.55); foreach ($image as $frame) { imagefilter($frame->native(), IMG_FILTER_COLORIZE, $red, $green, $blue); } return $image; } } src/Drivers/Gd/Modifiers/ColorspaceModifier.php000064400000001556150731647100015521 0ustar00targetColorspace(), RgbColorspace::class)) { throw new NotSupportedException( 'Only RGB colorspace is supported by GD driver.' ); } return $image; } } src/Drivers/Gd/Modifiers/ContainModifier.php000064400000005657150731647100015030 0ustar00getCropSize($image); $resize = $this->getResizeSize($image); $background = $this->driver()->handleInput($this->background); $blendingColor = $this->driver()->handleInput( $this->driver()->config()->blendingColor ); foreach ($image as $frame) { $this->modify($frame, $crop, $resize, $background, $blendingColor); } return $image; } /** * @throws ColorException */ protected function modify( FrameInterface $frame, SizeInterface $crop, SizeInterface $resize, ColorInterface $background, ColorInterface $blendingColor ): void { // create new gd image $modified = Cloner::cloneEmpty($frame->native(), $resize, $background); // make image area transparent to keep transparency // even if background-color is set $transparent = imagecolorallocatealpha( $modified, $blendingColor->channel(Red::class)->value(), $blendingColor->channel(Green::class)->value(), $blendingColor->channel(Blue::class)->value(), 127, ); imagealphablending($modified, false); // do not blend / just overwrite imagecolortransparent($modified, $transparent); imagefilledrectangle( $modified, $crop->pivot()->x(), $crop->pivot()->y(), $crop->pivot()->x() + $crop->width() - 1, $crop->pivot()->y() + $crop->height() - 1, $transparent ); // copy image from original with blending alpha imagealphablending($modified, true); imagecopyresampled( $modified, $frame->native(), $crop->pivot()->x(), $crop->pivot()->y(), 0, 0, $crop->width(), $crop->height(), $frame->size()->width(), $frame->size()->height() ); // set new content as resource $frame->setNative($modified); } } src/Drivers/Gd/Modifiers/ContrastModifier.php000064400000001245150731647100015217 0ustar00native(), IMG_FILTER_CONTRAST, ($this->level * -1)); } return $image; } } src/Drivers/Gd/Modifiers/CoverDownModifier.php000064400000000660150731647100015330 0ustar00resizeDown($this->width, $this->height); } } src/Drivers/Gd/Modifiers/CoverModifier.php000064400000003103150731647100014473 0ustar00getCropSize($image); $resize = $this->getResizeSize($crop); foreach ($image as $frame) { $this->modifyFrame($frame, $crop, $resize); } return $image; } /** * @throws ColorException */ protected function modifyFrame(FrameInterface $frame, SizeInterface $crop, SizeInterface $resize): void { // create new image $modified = Cloner::cloneEmpty($frame->native(), $resize); // copy content from resource imagecopyresampled( $modified, $frame->native(), 0, 0, $crop->pivot()->x(), $crop->pivot()->y(), $resize->width(), $resize->height(), $crop->width(), $crop->height() ); // set new content as resource $frame->setNative($modified); } } src/Drivers/Gd/Modifiers/CropModifier.php000064400000010472150731647100014327 0ustar00size(); $crop = $this->crop($image); $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); foreach ($image as $frame) { $this->cropFrame($frame, $originalSize, $crop, $background); } return $image; } /** * @throws ColorException */ protected function cropFrame( FrameInterface $frame, SizeInterface $originalSize, SizeInterface $resizeTo, int $background ): void { // create new image with transparent background $modified = Cloner::cloneEmpty($frame->native(), $resizeTo); // define offset $offset_x = $resizeTo->pivot()->x() + $this->offset_x; $offset_y = $resizeTo->pivot()->y() + $this->offset_y; // define target width & height $targetWidth = min($resizeTo->width(), $originalSize->width()); $targetHeight = min($resizeTo->height(), $originalSize->height()); $targetWidth = $targetWidth < $originalSize->width() ? $targetWidth + $offset_x : $targetWidth; $targetHeight = $targetHeight < $originalSize->height() ? $targetHeight + $offset_y : $targetHeight; // copy content from resource imagecopyresampled( $modified, $frame->native(), $offset_x * -1, $offset_y * -1, 0, 0, $targetWidth, $targetHeight, $targetWidth, $targetHeight ); // don't alpha blend for covering areas imagealphablending($modified, false); // cover the possible newly created areas with background color if ($resizeTo->width() > $originalSize->width() || $this->offset_x > 0) { imagefilledrectangle( $modified, $originalSize->width() + ($this->offset_x * -1) - $resizeTo->pivot()->x(), 0, $resizeTo->width(), $resizeTo->height(), $background ); } // cover the possible newly created areas with background color if ($resizeTo->height() > $originalSize->height() || $this->offset_y > 0) { imagefilledrectangle( $modified, ($this->offset_x * -1) - $resizeTo->pivot()->x(), $originalSize->height() + ($this->offset_y * -1) - $resizeTo->pivot()->y(), ($this->offset_x * -1) + $originalSize->width() - 1 - $resizeTo->pivot()->x(), $resizeTo->height(), $background ); } // cover the possible newly created areas with background color if ((($this->offset_x * -1) - $resizeTo->pivot()->x() - 1) > 0) { imagefilledrectangle( $modified, 0, 0, ($this->offset_x * -1) - $resizeTo->pivot()->x() - 1, $resizeTo->height(), $background ); } // cover the possible newly created areas with background color if ((($this->offset_y * -1) - $resizeTo->pivot()->y() - 1) > 0) { imagefilledrectangle( $modified, ($this->offset_x * -1) - $resizeTo->pivot()->x(), 0, ($this->offset_x * -1) + $originalSize->width() - $resizeTo->pivot()->x() - 1, ($this->offset_y * -1) - $resizeTo->pivot()->y() - 1, $background ); } // set new content as resource $frame->setNative($modified); } } src/Drivers/Gd/Modifiers/DrawBezierModifier.php000064400000022101150731647100015452 0ustar00drawable->count() !== 3 && $this->drawable->count() !== 4) { throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve'); } list($polygon, $polygon_border_segments) = $this->calculateBezierPoints(); if ($this->drawable->hasBackgroundColor() || $this->drawable->hasBorder()) { imagealphablending($frame->native(), true); imageantialias($frame->native(), true); } if ($this->drawable->hasBackgroundColor()) { $background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ); imagesetthickness($frame->native(), 0); imagefilledpolygon( $frame->native(), $polygon, $background_color ); } if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) { $border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ); if ($this->drawable->borderSize() === 1) { imagesetthickness($frame->native(), $this->drawable->borderSize()); for ($i = 0; $i < count($polygon); $i += 2) { if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) { imageline( $frame->native(), $polygon[$i + 0], $polygon[$i + 1], $polygon[$i + 2], $polygon[$i + 3], $border_color ); } } } else { $polygon_border_segments_total = count($polygon_border_segments); for ($i = 0; $i < $polygon_border_segments_total; $i += 1) { imagefilledpolygon( $frame->native(), $polygon_border_segments[$i], $border_color ); } } } } return $image; } /** * Calculate interpolation points for quadratic beziers using the Bernstein polynomial form * * @param float $t * @return array{'x': float, 'y': float} */ private function calculateQuadraticBezierInterpolationPoint(float $t = 0.05): array { $remainder = 1 - $t; $control_point_1_multiplier = $remainder * $remainder; $control_point_2_multiplier = $remainder * $t * 2; $control_point_3_multiplier = $t * $t; $x = ( $this->drawable->first()->x() * $control_point_1_multiplier + $this->drawable->second()->x() * $control_point_2_multiplier + $this->drawable->last()->x() * $control_point_3_multiplier ); $y = ( $this->drawable->first()->y() * $control_point_1_multiplier + $this->drawable->second()->y() * $control_point_2_multiplier + $this->drawable->last()->y() * $control_point_3_multiplier ); return ['x' => $x, 'y' => $y]; } /** * Calculate interpolation points for cubic beziers using the Bernstein polynomial form * * @param float $t * @return array{'x': float, 'y': float} */ private function calculateCubicBezierInterpolationPoint(float $t = 0.05): array { $remainder = 1 - $t; $t_squared = $t * $t; $remainder_squared = $remainder * $remainder; $control_point_1_multiplier = $remainder_squared * $remainder; $control_point_2_multiplier = $remainder_squared * $t * 3; $control_point_3_multiplier = $t_squared * $remainder * 3; $control_point_4_multiplier = $t_squared * $t; $x = ( $this->drawable->first()->x() * $control_point_1_multiplier + $this->drawable->second()->x() * $control_point_2_multiplier + $this->drawable->third()->x() * $control_point_3_multiplier + $this->drawable->last()->x() * $control_point_4_multiplier ); $y = ( $this->drawable->first()->y() * $control_point_1_multiplier + $this->drawable->second()->y() * $control_point_2_multiplier + $this->drawable->third()->y() * $control_point_3_multiplier + $this->drawable->last()->y() * $control_point_4_multiplier ); return ['x' => $x, 'y' => $y]; } /** * Calculate the points needed to draw a quadratic or cubic bezier with optional border/stroke * * @throws GeometryException * @return array{0: array, 1: array} */ private function calculateBezierPoints(): array { if ($this->drawable->count() !== 3 && $this->drawable->count() !== 4) { throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve'); } $polygon = []; $inner_polygon = []; $outer_polygon = []; $polygon_border_segments = []; // define ratio t; equivalent to 5 percent distance along edge $t = (float) 0.05; $polygon[] = $this->drawable->first()->x(); $polygon[] = $this->drawable->first()->y(); for ($i = 0 + $t; $i < 1; $i += $t) { if ($this->drawable->count() === 3) { $ip = $this->calculateQuadraticBezierInterpolationPoint($i); } elseif ($this->drawable->count() === 4) { $ip = $this->calculateCubicBezierInterpolationPoint($i); } $polygon[] = (int) $ip['x']; $polygon[] = (int) $ip['y']; } $polygon[] = $this->drawable->last()->x(); $polygon[] = $this->drawable->last()->y(); if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 1) { // create the border/stroke effect by calculating two new curves with offset positions // from the main polygon and then connecting the inner/outer curves to create separate // 4-point polygon segments $polygon_total_points = count($polygon); $offset = ($this->drawable->borderSize() / 2); for ($i = 0; $i < $polygon_total_points; $i += 2) { if (array_key_exists($i + 2, $polygon) && array_key_exists($i + 3, $polygon)) { $dx = $polygon[$i + 2] - $polygon[$i]; $dy = $polygon[$i + 3] - $polygon[$i + 1]; $dxy_sqrt = ($dx * $dx + $dy * $dy) ** 0.5; // inner polygon $scale = $offset / $dxy_sqrt; $ox = -$dy * $scale; $oy = $dx * $scale; $inner_polygon[] = $ox + $polygon[$i + 0]; $inner_polygon[] = $oy + $polygon[$i + 1]; $inner_polygon[] = $ox + $polygon[$i + 2]; $inner_polygon[] = $oy + $polygon[$i + 3]; // outer polygon $scale = -$offset / $dxy_sqrt; $ox = -$dy * $scale; $oy = $dx * $scale; $outer_polygon[] = $ox + $polygon[$i + 0]; $outer_polygon[] = $oy + $polygon[$i + 1]; $outer_polygon[] = $ox + $polygon[$i + 2]; $outer_polygon[] = $oy + $polygon[$i + 3]; } } $inner_polygon_total_points = count($inner_polygon); for ($i = 0; $i < $inner_polygon_total_points; $i += 2) { if (array_key_exists($i + 2, $inner_polygon) && array_key_exists($i + 3, $inner_polygon)) { $polygon_border_segments[] = [ $inner_polygon[$i + 0], $inner_polygon[$i + 1], $outer_polygon[$i + 0], $outer_polygon[$i + 1], $outer_polygon[$i + 2], $outer_polygon[$i + 3], $inner_polygon[$i + 2], $inner_polygon[$i + 3], ]; } } } return [$polygon, $polygon_border_segments]; } } src/Drivers/Gd/Modifiers/DrawEllipseModifier.php000064400000005362150731647100015641 0ustar00drawable->hasBorder()) { imagealphablending($frame->native(), true); // slightly smaller ellipse to keep 1px bordered edges clean if ($this->drawable->hasBackgroundColor()) { imagefilledellipse( $frame->native(), $this->drawable()->position()->x(), $this->drawable->position()->y(), $this->drawable->width() - 1, $this->drawable->height() - 1, $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ) ); } // gd's imageellipse ignores imagesetthickness // so i use imagearc with 360 degrees instead. imagesetthickness( $frame->native(), $this->drawable->borderSize(), ); imagearc( $frame->native(), $this->drawable()->position()->x(), $this->drawable()->position()->y(), $this->drawable->width(), $this->drawable->height(), 0, 360, $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ) ); } else { imagealphablending($frame->native(), true); imagesetthickness($frame->native(), 0); imagefilledellipse( $frame->native(), $this->drawable()->position()->x(), $this->drawable()->position()->y(), $this->drawable->width(), $this->drawable->height(), $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ) ); } } return $image; } } src/Drivers/Gd/Modifiers/DrawLineModifier.php000064400000002231150731647100015123 0ustar00driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ); foreach ($image as $frame) { imagealphablending($frame->native(), true); imageantialias($frame->native(), true); imagesetthickness($frame->native(), $this->drawable->width()); imageline( $frame->native(), $this->drawable->start()->x(), $this->drawable->start()->y(), $this->drawable->end()->x(), $this->drawable->end()->y(), $color ); } return $image; } } src/Drivers/Gd/Modifiers/DrawPixelModifier.php000064400000001725150731647100015324 0ustar00driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->color) ); foreach ($image as $frame) { imagealphablending($frame->native(), true); imagesetpixel( $frame->native(), $this->position->x(), $this->position->y(), $color ); } return $image; } } src/Drivers/Gd/Modifiers/DrawPolygonModifier.php000064400000003162150731647100015667 0ustar00drawable->hasBackgroundColor()) { imagealphablending($frame->native(), true); imagesetthickness($frame->native(), 0); imagefilledpolygon( $frame->native(), $this->drawable->toArray(), $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ) ); } if ($this->drawable->hasBorder()) { imagealphablending($frame->native(), true); imagesetthickness($frame->native(), $this->drawable->borderSize()); imagepolygon( $frame->native(), $this->drawable->toArray(), $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ) ); } } return $image; } } src/Drivers/Gd/Modifiers/DrawRectangleModifier.php000064400000004024150731647100016142 0ustar00drawable->position(); foreach ($image as $frame) { // draw background if ($this->drawable->hasBackgroundColor()) { imagealphablending($frame->native(), true); imagesetthickness($frame->native(), 0); imagefilledrectangle( $frame->native(), $position->x(), $position->y(), $position->x() + $this->drawable->width(), $position->y() + $this->drawable->height(), $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ) ); } // draw border if ($this->drawable->hasBorder()) { imagealphablending($frame->native(), true); imagesetthickness($frame->native(), $this->drawable->borderSize()); imagerectangle( $frame->native(), $position->x(), $position->y(), $position->x() + $this->drawable->width(), $position->y() + $this->drawable->height(), $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ) ); } } return $image; } } src/Drivers/Gd/Modifiers/FillModifier.php000064400000003344150731647100014312 0ustar00color($image); foreach ($image as $frame) { if ($this->hasPosition()) { $this->floodFillWithColor($frame, $color); } else { $this->fillAllWithColor($frame, $color); } } return $image; } /** * @throws RuntimeException */ private function color(ImageInterface $image): int { return $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->color) ); } private function floodFillWithColor(FrameInterface $frame, int $color): void { imagefill( $frame->native(), $this->position->x(), $this->position->y(), $color ); } private function fillAllWithColor(FrameInterface $frame, int $color): void { imagealphablending($frame->native(), true); imagefilledrectangle( $frame->native(), 0, 0, $frame->size()->width() - 1, $frame->size()->height() - 1, $color ); } } src/Drivers/Gd/Modifiers/FlipModifier.php000064400000001174150731647100014315 0ustar00native(), IMG_FLIP_VERTICAL); } return $image; } } src/Drivers/Gd/Modifiers/FlopModifier.php000064400000001176150731647100014325 0ustar00native(), IMG_FLIP_HORIZONTAL); } return $image; } } src/Drivers/Gd/Modifiers/GammaModifier.php000064400000001206150731647100014441 0ustar00native(), 1, $this->gamma); } return $image; } } src/Drivers/Gd/Modifiers/GreyscaleModifier.php000064400000001225150731647100015336 0ustar00native(), IMG_FILTER_GRAYSCALE); } return $image; } } src/Drivers/Gd/Modifiers/InvertModifier.php000064400000001206150731647100014666 0ustar00native(), IMG_FILTER_NEGATE); } return $image; } } src/Drivers/Gd/Modifiers/PadModifier.php000064400000000203150731647100014117 0ustar00native(), IMG_FILTER_PIXELATE, $this->size, true); } return $image; } } src/Drivers/Gd/Modifiers/PlaceModifier.php000064400000006746150731647100014461 0ustar00driver()->handleInput($this->element); $position = $this->getPosition($image, $watermark); foreach ($image as $frame) { imagealphablending($frame->native(), true); if ($this->opacity === 100) { $this->placeOpaque($frame, $watermark, $position); } else { $this->placeTransparent($frame, $watermark, $position); } } return $image; } /** * Insert watermark with 100% opacity * * @param FrameInterface $frame * @param ImageInterface $watermark * @param PointInterface $position * @throws RuntimeException * @return void */ private function placeOpaque(FrameInterface $frame, ImageInterface $watermark, PointInterface $position): void { imagecopy( $frame->native(), $watermark->core()->native(), $position->x(), $position->y(), 0, 0, $watermark->width(), $watermark->height() ); } /** * Insert watermark transparent with current opacity * * Unfortunately, the original PHP function imagecopymerge does not work reliably. * For example, any transparency of the image to be inserted is not applied correctly. * For this reason, a new GDImage is created into which the original image is inserted * in the first step and the watermark is inserted with 100% opacity in the second * step. This combination is then transferred to the original image again with the * respective opacity. * * Please note: Unfortunately, there is still an edge case, when a transparent image * is placed on a transparent background, the "double" transparent areas appear opaque! * * @param FrameInterface $frame * @param ImageInterface $watermark * @param PointInterface $position * @throws RuntimeException * @return void */ private function placeTransparent(FrameInterface $frame, ImageInterface $watermark, PointInterface $position): void { $cut = imagecreatetruecolor($watermark->width(), $watermark->height()); imagecopy( $cut, $frame->native(), 0, 0, $position->x(), $position->y(), imagesx($cut), imagesy($cut) ); imagecopy( $cut, $watermark->core()->native(), 0, 0, 0, 0, imagesx($cut), imagesy($cut) ); imagecopymerge( $frame->native(), $cut, $position->x(), $position->y(), 0, 0, $watermark->width(), $watermark->height(), $this->opacity ); } } src/Drivers/Gd/Modifiers/ProfileModifier.php000064400000001257150731647100015025 0ustar00limit <= 0) { throw new InputException('Quantization limit must be greater than 0.'); } // no color reduction if the limit is higher than the colors in the img $colorCount = imagecolorstotal($image->core()->native()); if ($colorCount > 0 && $this->limit > $colorCount) { return $image; } $width = $image->width(); $height = $image->height(); $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); $blendingColor = $this->driver()->handleInput( $this->driver()->config()->blendingColor ); foreach ($image as $frame) { // create new image for color quantization $reduced = Cloner::cloneEmpty($frame->native(), background: $blendingColor); // fill with background imagefill($reduced, 0, 0, $background); // set transparency imagecolortransparent($reduced, $background); // copy original image (colors are limited automatically in the copy process) imagecopy($reduced, $frame->native(), 0, 0, 0, 0, $width, $height); // gd library does not support color quantization directly therefore the // colors are decrease by transforming the image to a palette version imagetruecolortopalette($reduced, true, $this->limit); $frame->setNative($reduced); } return $image; } } src/Drivers/Gd/Modifiers/RemoveAnimationModifier.php000064400000001255150731647100016520 0ustar00core()->setNative( $this->chosenFrame($image, $this->position)->native() ); return $image; } } src/Drivers/Gd/Modifiers/ResizeCanvasModifier.php000064400000005330150731647100016016 0ustar00cropSize($image); $background = $this->driver()->handleInput($this->background); foreach ($image as $frame) { $this->modify($frame, $resize, $background); } return $image; } /** * @throws ColorException */ protected function modify( FrameInterface $frame, SizeInterface $resize, ColorInterface $background, ): void { // create new canvas with target size & transparent background color $modified = Cloner::cloneEmpty($frame->native(), $resize, $background); // make image area transparent to keep transparency // even if background-color is set $transparent = imagecolorallocatealpha( $modified, $background->channel(Red::class)->value(), $background->channel(Green::class)->value(), $background->channel(Blue::class)->value(), 127, ); // create transparent area to place the original on top imagealphablending($modified, false); // do not blend / just overwrite imagecolortransparent($modified, $transparent); imagefilledrectangle( $modified, $resize->pivot()->x() * -1, $resize->pivot()->y() * -1, abs($resize->pivot()->x()) + $frame->size()->width() - 1, abs($resize->pivot()->y()) + $frame->size()->height() - 1, $transparent, ); // place original imagecopy( $modified, $frame->native(), $resize->pivot()->x() * -1, $resize->pivot()->y() * -1, 0, 0, $frame->size()->width(), $frame->size()->height(), ); // set new content as resource $frame->setNative($modified); } } src/Drivers/Gd/Modifiers/ResizeCanvasRelativeModifier.php000064400000000626150731647100017515 0ustar00size()->resizeDown($this->width, $this->height); } } src/Drivers/Gd/Modifiers/ResizeModifier.php000064400000004012150731647100014656 0ustar00getAdjustedSize($image); foreach ($image as $frame) { $this->resizeFrame($frame, $resizeTo); } return $image; } /** * @throws ColorException */ private function resizeFrame(FrameInterface $frame, SizeInterface $resizeTo): void { // create empty canvas in target size $modified = Cloner::cloneEmpty($frame->native(), $resizeTo); // copy content from resource imagecopyresampled( $modified, $frame->native(), $resizeTo->pivot()->x(), $resizeTo->pivot()->y(), 0, 0, $resizeTo->width(), $resizeTo->height(), $frame->size()->width(), $frame->size()->height() ); // set new content as resource $frame->setNative($modified); } /** * Return the size the modifier will resize to * * @param ImageInterface $image * @throws RuntimeException * @throws GeometryException * @return SizeInterface */ protected function getAdjustedSize(ImageInterface $image): SizeInterface { return $image->size()->resize($this->width, $this->height); } } src/Drivers/Gd/Modifiers/ResolutionModifier.php000064400000001334150731647100015564 0ustar00x)); $y = intval(round($this->y)); foreach ($image as $frame) { imageresolution($frame->native(), $x, $y); } return $image; } } src/Drivers/Gd/Modifiers/RotateModifier.php000064400000006476150731647100014673 0ustar00driver()->handleInput($this->background); foreach ($image as $frame) { $this->modifyFrame($frame, $background); } return $image; } /** * Apply rotation modification on given frame, given background * color is used for newly create image areas * * @param FrameInterface $frame * @throws ColorException * @param ColorInterface $background * @return void */ protected function modifyFrame(FrameInterface $frame, ColorInterface $background): void { // get transparent color from frame core $transparent = match ($transparent = imagecolortransparent($frame->native())) { -1 => imagecolorallocatealpha( $frame->native(), $background->channel(Red::class)->value(), $background->channel(Green::class)->value(), $background->channel(Blue::class)->value(), 127 ), default => $transparent, }; // rotate original image against transparent background $rotated = imagerotate( $frame->native(), $this->rotationAngle(), $transparent ); // create size from original after rotation $container = (new Rectangle( imagesx($rotated), imagesy($rotated), ))->movePivot('center'); // create size from original and rotate points $cutout = (new Rectangle( imagesx($frame->native()), imagesy($frame->native()), $container->pivot() ))->align('center') ->valign('center') ->rotate($this->rotationAngle() * -1); // create new gd image $modified = Cloner::cloneEmpty($frame->native(), $container, $background); // draw the cutout on new gd image to have a transparent // background where the rotated image will be placed imagealphablending($modified, false); imagefilledpolygon( $modified, $cutout->toArray(), imagecolortransparent($modified) ); // place rotated image on new gd image imagealphablending($modified, true); imagecopy( $modified, $rotated, 0, 0, 0, 0, imagesx($rotated), imagesy($rotated) ); $frame->setNative($modified); } } src/Drivers/Gd/Modifiers/ScaleDownModifier.php000064400000000746150731647100015306 0ustar00size()->scaleDown($this->width, $this->height); } } src/Drivers/Gd/Modifiers/ScaleModifier.php000064400000000604150731647100014447 0ustar00size()->scale($this->width, $this->height); } } src/Drivers/Gd/Modifiers/SharpenModifier.php000064400000002144150731647100015021 0ustar00matrix(); foreach ($image as $frame) { imageconvolution($frame->native(), $matrix, 1, 0); } return $image; } /** * Create matrix to be used by imageconvolution() * * @return array> */ private function matrix(): array { $min = $this->amount >= 10 ? $this->amount * -0.01 : 0; $max = $this->amount * -0.025; $abs = ((4 * $min + 4 * $max) * -1) + 1; return [ [$min, $max, $min], [$max, $abs, $max], [$min, $max, $min] ]; } } src/Drivers/Gd/Modifiers/SliceAnimationModifier.php000064400000001472150731647100016323 0ustar00offset >= $image->count()) { throw new AnimationException('Offset is not in the range of frames.'); } $image->core()->slice($this->offset, $this->length); return $image; } } src/Drivers/Gd/Modifiers/TextModifier.php000064400000010542150731647100014346 0ustar00driver()->fontProcessor(); $lines = $fontProcessor->textBlock($this->text, $this->font, $this->position); // decode text colors $textColor = $this->gdTextColor($image); $strokeColor = $this->gdStrokeColor($image); foreach ($image as $frame) { imagealphablending($frame->native(), true); if ($this->font->hasFilename()) { foreach ($lines as $line) { foreach ($this->strokeOffsets($this->font) as $offset) { imagettftext( $frame->native(), $fontProcessor->nativeFontSize($this->font), $this->font->angle() * -1, $line->position()->x() + $offset->x(), $line->position()->y() + $offset->y(), $strokeColor, $this->font->filename(), (string) $line ); } imagettftext( $frame->native(), $fontProcessor->nativeFontSize($this->font), $this->font->angle() * -1, $line->position()->x(), $line->position()->y(), $textColor, $this->font->filename(), (string) $line ); } } else { foreach ($lines as $line) { foreach ($this->strokeOffsets($this->font) as $offset) { imagestring( $frame->native(), $this->gdFont(), $line->position()->x() + $offset->x(), $line->position()->y() + $offset->y(), (string) $line, $strokeColor ); } imagestring( $frame->native(), $this->gdFont(), $line->position()->x(), $line->position()->y(), (string) $line, $textColor ); } } } return $image; } /** * Decode text color in GD compatible format * * @param ImageInterface $image * @return int * @throws RuntimeException * @throws ColorException */ protected function gdTextColor(ImageInterface $image): int { return $this ->driver() ->colorProcessor($image->colorspace()) ->colorToNative(parent::textColor()); } /** * Decode color for stroke (outline) effect in GD compatible format * * @param ImageInterface $image * @return int * @throws RuntimeException * @throws ColorException */ protected function gdStrokeColor(ImageInterface $image): int { if (!$this->font->hasStrokeEffect()) { return 0; } $color = parent::strokeColor(); if ($color->isTransparent()) { throw new ColorException( 'The stroke color must be fully opaque.' ); } return $this ->driver() ->colorProcessor($image->colorspace()) ->colorToNative($color); } /** * Return GD's internal font size (if no ttf file is set) * * @return int */ private function gdFont(): int { if (is_numeric($this->font->filename())) { return intval($this->font->filename()); } return 1; } } src/Drivers/Gd/Modifiers/TrimModifier.php000064400000005567150731647100014350 0ustar00isAnimated()) { throw new NotSupportedException('Trim modifier cannot be applied to animated images.'); } // apply tolerance with a min. value of .5 because the default tolerance of '0' should // already trim away similar colors which is not the case with imagecropauto. $trimmed = imagecropauto( $image->core()->native(), IMG_CROP_THRESHOLD, max([.5, $this->tolerance / 10]), $this->trimColor($image) ); // if the tolerance is very high, it is possible that no image is left. // imagick returns a 1x1 pixel image in this case. this does the same. if ($trimmed === false) { $trimmed = $this->driver()->createImage(1, 1)->core()->native(); } $image->core()->setNative($trimmed); return $image; } /** * Create an average color from the colors of the four corner points of the given image * * @param ImageInterface $image * @throws RuntimeException * @throws AnimationException * @return int */ private function trimColor(ImageInterface $image): int { // trim color base $red = 0; $green = 0; $blue = 0; // corner coordinates $size = $image->size(); $cornerPoints = [ new Point(0, 0), new Point($size->width() - 1, 0), new Point(0, $size->height() - 1), new Point($size->width() - 1, $size->height() - 1), ]; // create an average color to be used in trim operation foreach ($cornerPoints as $pos) { $cornerColor = imagecolorat($image->core()->native(), $pos->x(), $pos->y()); $rgb = imagecolorsforindex($image->core()->native(), $cornerColor); $red += round(round(($rgb['red'] / 51)) * 51); $green += round(round(($rgb['green'] / 51)) * 51); $blue += round(round(($rgb['blue'] / 51)) * 51); } $red = (int) round($red / 4); $green = (int) round($green / 4); $blue = (int) round($blue / 4); return imagecolorallocate($image->core()->native(), $red, $green, $blue); } } src/Drivers/Imagick/Analyzers/ColorspaceAnalyzer.php000064400000001406150731647100016603 0ustar00core()->native()->getImageColorspace()) { Imagick::COLORSPACE_CMYK => new CmykColorspace(), default => new RgbColorspace(), }; } } src/Drivers/Imagick/Analyzers/HeightAnalyzer.php000064400000000747150731647100015730 0ustar00core()->native()->getImageHeight(); } } src/Drivers/Imagick/Analyzers/PixelColorAnalyzer.php000064400000002101150731647100016562 0ustar00colorAt( $image->colorspace(), $image->core()->frame($this->frame_key)->native() ); } /** * @throws ColorException */ protected function colorAt(ColorspaceInterface $colorspace, Imagick $imagick): ColorInterface { return $this->driver() ->colorProcessor($colorspace) ->nativeToColor( $imagick->getImagePixelColor($this->x, $this->y) ); } } src/Drivers/Imagick/Analyzers/PixelColorsAnalyzer.php000064400000001061150731647100016751 0ustar00colorspace(); foreach ($image as $frame) { $colors->push( parent::colorAt($colorspace, $frame->native()) ); } return $colors; } } src/Drivers/Imagick/Analyzers/ProfileAnalyzer.php000064400000001405150731647100016110 0ustar00core()->native()->getImageProfiles('icc'); if (!array_key_exists('icc', $profiles)) { throw new ColorException('No ICC profile found in image.'); } return new Profile($profiles['icc']); } } src/Drivers/Imagick/Analyzers/ResolutionAnalyzer.php000064400000001061150731647100016651 0ustar00core()->native()->getImageResolution()); } } src/Drivers/Imagick/Analyzers/WidthAnalyzer.php000064400000000742150731647100015572 0ustar00core()->native()->getImageWidth(); } } src/Drivers/Imagick/ColorProcessor.php000064400000002712150731647100014012 0ustar00convertTo($this->colorspace) ); } public function nativeToColor(mixed $native): ColorInterface { return match ($this->colorspace::class) { CmykColorspace::class => $this->colorspace->colorFromNormalized([ $native->getColorValue(Imagick::COLOR_CYAN), $native->getColorValue(Imagick::COLOR_MAGENTA), $native->getColorValue(Imagick::COLOR_YELLOW), $native->getColorValue(Imagick::COLOR_BLACK), ]), default => $this->colorspace->colorFromNormalized([ $native->getColorValue(Imagick::COLOR_RED), $native->getColorValue(Imagick::COLOR_GREEN), $native->getColorValue(Imagick::COLOR_BLUE), $native->getColorValue(Imagick::COLOR_ALPHA), ]), }; } } src/Drivers/Imagick/Core.php000064400000014663150731647100011734 0ustar00 */ class Core implements CoreInterface, Iterator { protected int $iteratorIndex = 0; /** * Create new core instance * * @param Imagick $imagick * @return void */ public function __construct(protected Imagick $imagick) { } /** * {@inheritdoc} * * @see CollectionInterface::has() */ public function has(int|string $key): bool { try { $result = $this->imagick->setIteratorIndex($key); } catch (ImagickException) { return false; } return $result; } /** * {@inheritdoc} * * @see CollectionInterface::push() */ public function push($item): CollectionInterface { return $this->add($item); } /** * {@inheritdoc} * * @see CollectionInterface::get() */ public function get(int|string $key, $default = null): mixed { try { $this->imagick->setIteratorIndex($key); } catch (ImagickException) { return $default; } return new Frame($this->imagick->current()); } /** * {@inheritdoc} * * @see CollectionInterface::getAtPosition() */ public function getAtPosition(int $key = 0, $default = null): mixed { return $this->get($key, $default); } /** * {@inheritdoc} * * @see CollectionInterface::empty() */ public function empty(): CollectionInterface { $this->imagick->clear(); return $this; } /** * {@inheritdoc} * * @see CollectionInterface::slice() */ public function slice(int $offset, ?int $length = null): CollectionInterface { $allowed_indexes = []; $length = is_null($length) ? $this->count() : $length; for ($i = $offset; $i < $offset + $length; $i++) { $allowed_indexes[] = $i; } $sliced = new Imagick(); foreach ($this->imagick as $key => $native) { if (in_array($key, $allowed_indexes)) { $sliced->addImage($native->getImage()); } } $sliced = $sliced->coalesceImages(); $sliced->setImageIterations($this->imagick->getImageIterations()); $this->imagick = $sliced; return $this; } /** * {@inheritdoc} * * @see CoreInterface::add() */ public function add(FrameInterface $frame): CoreInterface { $imagick = $frame->native(); $imagick->setImageDelay( (int) round($frame->delay() * 100) ); $imagick->setImageDispose($frame->dispose()); $size = $frame->size(); $imagick->setImagePage( $size->width(), $size->height(), $frame->offsetLeft(), $frame->offsetTop() ); $this->imagick->addImage($imagick); return $this; } /** * {@inheritdoc} * * @see CoreInterface::count() */ public function count(): int { return $this->imagick->getNumberImages(); } /** * {@inheritdoc} * * @see Iterator::rewind() */ public function current(): mixed { $this->imagick->setIteratorIndex($this->iteratorIndex); return new Frame($this->imagick->current()); } /** * {@inheritdoc} * * @see Iterator::rewind() */ public function next(): void { $this->iteratorIndex = $this->iteratorIndex + 1; } /** * {@inheritdoc} * * @see Iterator::rewind() */ public function key(): mixed { return $this->iteratorIndex; } /** * {@inheritdoc} * * @see Iterator::rewind() */ public function valid(): bool { try { $result = $this->imagick->setIteratorIndex($this->iteratorIndex); } catch (ImagickException) { return false; } return $result; } /** * {@inheritdoc} * * @see Iterator::rewind() */ public function rewind(): void { $this->iteratorIndex = 0; } /** * {@inheritdoc} * * @see CoreInterface::native() */ public function native(): mixed { return $this->imagick; } /** * {@inheritdoc} * * @see CoreInterface::setNative() */ public function setNative(mixed $native): CoreInterface { $this->imagick = $native; return $this; } /** * {@inheritdoc} * * @see CoreInterface::frame() */ public function frame(int $position): FrameInterface { foreach ($this->imagick as $core) { if ($core->getIteratorIndex() == $position) { return new Frame($core); } } throw new AnimationException('Frame #' . $position . ' could not be found in the image.'); } /** * {@inheritdoc} * * @see CoreInterface::loops() */ public function loops(): int { return $this->imagick->getImageIterations(); } /** * {@inheritdoc} * * @see CoreInterface::setLoops() */ public function setLoops(int $loops): CoreInterface { $this->imagick = $this->imagick->coalesceImages(); $this->imagick->setImageIterations($loops); return $this; } /** * {@inheritdoc} * * @see CollectionInterface::first() */ public function first(): FrameInterface { return $this->frame(0); } /** * {@inheritdoc} * * @see CollectableInterface::last() */ public function last(): FrameInterface { return $this->frame($this->count() - 1); } /** * {@inheritdoc} * * @see CollectionInterface::toArray() */ public function toArray(): array { $frames = []; foreach ($this as $frame) { $frames[] = $frame; } return $frames; } /** * Clone instance * * @return void */ public function __clone(): void { $this->imagick = clone $this->imagick; } } src/Drivers/Imagick/Decoders/Base64ImageDecoder.php000064400000001201150731647100016031 0ustar00isValidBase64($input)) { throw new DecoderException('Unable to decode input'); } return parent::decode(base64_decode($input)); } } src/Drivers/Imagick/Decoders/BinaryImageDecoder.php000064400000002350150731647100016237 0ustar00readImageBlob($input); } catch (ImagickException) { throw new DecoderException('Unable to decode input'); } // decode image $image = parent::decode($imagick); // get media type enum from string media type $format = Format::tryCreate($image->origin()->mediaType()); // extract exif data for appropriate formats if (in_array($format, [Format::JPEG, Format::TIFF])) { $image->setExif($this->extractExifData($input)); } return $image; } } src/Drivers/Imagick/Decoders/DataUriImageDecoder.php000064400000001605150731647100016346 0ustar00parseDataUri($input); if (!$uri->isValid()) { throw new DecoderException('Unable to decode input'); } if ($uri->isBase64Encoded()) { return parent::decode(base64_decode($uri->data())); } return parent::decode(urldecode($uri->data())); } } src/Drivers/Imagick/Decoders/EncodedImageObjectDecoder.php000064400000001257150731647100017510 0ustar00toString()); } } src/Drivers/Imagick/Decoders/FilePathImageDecoder.php000064400000002265150731647100016514 0ustar00isFile($input)) { throw new DecoderException('Unable to decode input'); } try { $imagick = new Imagick(); $imagick->readImage($input); } catch (ImagickException) { throw new DecoderException('Unable to decode input'); } // decode image $image = parent::decode($imagick); // set file path on origin $image->origin()->setFilePath($input); // extract exif data for the appropriate formats if (in_array($imagick->getImageFormat(), ['JPEG', 'TIFF', 'TIF'])) { $image->setExif($this->extractExifData($input)); } return $image; } } src/Drivers/Imagick/Decoders/FilePointerImageDecoder.php000064400000001467150731647100017243 0ustar00getImageFormat() != 'JPEG') { $input = $input->coalesceImages(); } // turn images with colorspace 'GRAY' into 'SRGB' to avoid working on // greyscale colorspace images as this results images loosing color // information when placed into this image. if ($input->getImageColorspace() == Imagick::COLORSPACE_GRAY) { $input->setImageColorspace(Imagick::COLORSPACE_SRGB); } // create image object $image = new Image( $this->driver(), new Core($input) ); // discard animation depending on config if (!$this->driver()->config()->decodeAnimation) { $image->modify(new RemoveAnimationModifier()); } // adjust image rotatation if ($this->driver()->config()->autoOrientation) { $image->modify(new AlignRotationModifier()); } // set media type on origin $image->origin()->setMediaType($input->getImageMimeType()); return $image; } } src/Drivers/Imagick/Decoders/SplFileInfoImageDecoder.php000064400000001235150731647100017166 0ustar00getRealPath()); } } src/Drivers/Imagick/Driver.php000064400000007747150731647100012304 0ustar00newImage($width, $height, $background, 'png'); $imagick->setType(Imagick::IMGTYPE_UNDEFINED); $imagick->setImageType(Imagick::IMGTYPE_UNDEFINED); $imagick->setColorspace(Imagick::COLORSPACE_SRGB); $imagick->setImageResolution(96, 96); $imagick->setImageBackgroundColor($background); return new Image($this, new Core($imagick)); } /** * {@inheritdoc} * * @throws RuntimeException * @see DriverInterface::createAnimation() */ public function createAnimation(callable $init): ImageInterface { $imagick = new Imagick(); $imagick->setFormat('gif'); $animation = new class ($this, $imagick) { public function __construct( protected DriverInterface $driver, public Imagick $imagick ) { } /** * @throws RuntimeException */ public function add(mixed $source, float $delay = 1): self { $native = $this->driver->handleInput($source)->core()->native(); $native->setImageDelay(intval(round($delay * 100))); $this->imagick->addImage($native); return $this; } /** * @throws RuntimeException */ public function __invoke(): ImageInterface { return new Image( $this->driver, new Core($this->imagick) ); } }; $init($animation); return call_user_func($animation); } /** * {@inheritdoc} * * @see DriverInterface::colorProcessor() */ public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface { return new ColorProcessor($colorspace); } /** * {@inheritdoc} * * @see DriverInterface::fontProcessor() */ public function fontProcessor(): FontProcessorInterface { return new FontProcessor(); } /** * {@inheritdoc} * * @see DriverInterface::supports() */ public function supports(string|Format|FileExtension|MediaType $identifier): bool { try { $format = Format::create($identifier); } catch (NotSupportedException) { return false; } return count(Imagick::queryFormats($format->name)) >= 1; } } src/Drivers/Imagick/Encoders/AvifEncoder.php000064400000002011150731647100014753 0ustar00core()->native(); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); $imagick->setCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality); return new EncodedImage($imagick->getImagesBlob(), 'image/avif'); } } src/Drivers/Imagick/Encoders/BmpEncoder.php000064400000001613150731647100014613 0ustar00core()->native(); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); return new EncodedImage($imagick->getImagesBlob(), 'image/bmp'); } } src/Drivers/Imagick/Encoders/GifEncoder.php000064400000001774150731647100014612 0ustar00core()->native(); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); if ($this->interlaced) { $imagick->setInterlaceScheme(Imagick::INTERLACE_LINE); } return new EncodedImage($imagick->getImagesBlob(), 'image/gif'); } } src/Drivers/Imagick/Encoders/HeicEncoder.php000064400000001546150731647100014752 0ustar00core()->native(); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality); return new EncodedImage($imagick->getImagesBlob(), 'image/heic'); } } src/Drivers/Imagick/Encoders/Jpeg2000Encoder.php000064400000002173150731647100015266 0ustar00core()->native(); $imagick->setImageBackgroundColor('white'); $imagick->setBackgroundColor('white'); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); $imagick->setCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality); return new EncodedImage($imagick->getImagesBlob(), 'image/jp2'); } } src/Drivers/Imagick/Encoders/JpegEncoder.php000064400000003430150731647100014761 0ustar00driver()->handleInput( $this->driver()->config()->blendingColor ); // resolve blending color because jpeg has no transparency $background = $this->driver() ->colorProcessor($image->colorspace()) ->colorToNative($blendingColor); // set alpha value to 1 because Imagick renders // possible full transparent colors as black $background->setColorValue(Imagick::COLOR_ALPHA, 1); $imagick = $image->core()->native(); $imagick->setImageBackgroundColor($background); $imagick->setBackgroundColor($background); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); $imagick->setCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality); $imagick->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); if ($this->progressive) { $imagick->setInterlaceScheme(Imagick::INTERLACE_PLANE); } return new EncodedImage($imagick->getImagesBlob(), 'image/jpeg'); } } src/Drivers/Imagick/Encoders/PngEncoder.php000064400000002516150731647100014624 0ustar00indexed) { // reduce colors $output = clone $image; $output->reduceColors(256); $output = $output->core()->native(); $output->setFormat('PNG'); $output->setImageFormat('PNG'); } else { $output = clone $image->core()->native(); $output->setFormat('PNG32'); $output->setImageFormat('PNG32'); } $output->setCompression(Imagick::COMPRESSION_ZIP); $output->setImageCompression(Imagick::COMPRESSION_ZIP); if ($this->interlaced) { $output->setInterlaceScheme(Imagick::INTERLACE_LINE); } return new EncodedImage($output->getImagesBlob(), 'image/png'); } } src/Drivers/Imagick/Encoders/TiffEncoder.php000064400000001761150731647100014771 0ustar00core()->native(); $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($imagick->getImageCompression()); $imagick->setImageCompression($imagick->getImageCompression()); $imagick->setCompressionQuality($this->quality); $imagick->setImageCompressionQuality($this->quality); return new EncodedImage($imagick->getImagesBlob(), 'image/tiff'); } } src/Drivers/Imagick/Encoders/WebpEncoder.php000064400000002256150731647100014776 0ustar00core()->native(); $imagick->setImageBackgroundColor(new ImagickPixel('transparent')); if (!$image->isAnimated()) { $imagick = $imagick->mergeImageLayers(Imagick::LAYERMETHOD_MERGE); } $imagick->setFormat($format); $imagick->setImageFormat($format); $imagick->setCompression($compression); $imagick->setImageCompression($compression); $imagick->setImageCompressionQuality($this->quality); return new EncodedImage($imagick->getImagesBlob(), 'image/webp'); } } src/Drivers/Imagick/FontProcessor.php000064400000004017150731647100013642 0ustar00toImagickDraw($font); $dimensions = (new Imagick())->queryFontMetrics($draw, $text); return new Rectangle( intval(round($dimensions['textWidth'])), intval(round($dimensions['ascender'] + $dimensions['descender'])), ); } /** * Imagick::annotateImage() needs an ImagickDraw object - this method takes * the font object as the base and adds an optional passed color to the new * ImagickDraw object. * * @param FontInterface $font * @param null|ImagickPixel $color * @throws FontException * @throws ImagickDrawException * @throws ImagickException * @return ImagickDraw */ public function toImagickDraw(FontInterface $font, ?ImagickPixel $color = null): ImagickDraw { if (!$font->hasFilename()) { throw new FontException('No font file specified.'); } $draw = new ImagickDraw(); $draw->setStrokeAntialias(true); $draw->setTextAntialias(true); $draw->setFont($font->filename()); $draw->setFontSize($this->nativeFontSize($font)); $draw->setTextAlignment(Imagick::ALIGN_LEFT); if ($color) { $draw->setFillColor($color); } return $draw; } } src/Drivers/Imagick/Frame.php000064400000007223150731647100012070 0ustar00native->setImageBackgroundColor($background); $this->native->setBackgroundColor($background); } /** * {@inheritdoc} * * @see DriverInterface::toImage() */ public function toImage(DriverInterface $driver): ImageInterface { return new Image($driver, new Core($this->native())); } /** * {@inheritdoc} * * @see DriverInterface::setNative() */ public function setNative($native): FrameInterface { $this->native = $native; return $this; } /** * {@inheritdoc} * * @see DriverInterface::native() */ public function native(): Imagick { return $this->native; } /** * {@inheritdoc} * * @see DriverInterface::size() */ public function size(): SizeInterface { return new Rectangle( $this->native->getImageWidth(), $this->native->getImageHeight() ); } /** * {@inheritdoc} * * @see DriverInterface::delay() */ public function delay(): float { return $this->native->getImageDelay() / 100; } /** * {@inheritdoc} * * @see DriverInterface::setDelay() */ public function setDelay(float $delay): FrameInterface { $this->native->setImageDelay(intval(round($delay * 100))); return $this; } /** * {@inheritdoc} * * @see DriverInterface::dispose() */ public function dispose(): int { return $this->native->getImageDispose(); } /** * {@inheritdoc} * * @see DriverInterface::setDispose() */ public function setDispose(int $dispose): FrameInterface { $this->native->setImageDispose($dispose); return $this; } /** * {@inheritdoc} * * @see DriverInterface::setOffset() */ public function setOffset(int $left, int $top): FrameInterface { $this->native->setImagePage( $this->native->getImageWidth(), $this->native->getImageHeight(), $left, $top ); return $this; } /** * {@inheritdoc} * * @see DriverInterface::offsetLeft() */ public function offsetLeft(): int { return $this->native->getImagePage()['x']; } /** * {@inheritdoc} * * @see DriverInterface::setOffsetLeft() */ public function setOffsetLeft(int $offset): FrameInterface { return $this->setOffset($offset, $this->offsetTop()); } /** * {@inheritdoc} * * @see DriverInterface::offsetTop() */ public function offsetTop(): int { return $this->native->getImagePage()['y']; } /** * {@inheritdoc} * * @see DriverInterface::setOffsetTop() */ public function setOffsetTop(int $offset): FrameInterface { return $this->setOffset($this->offsetLeft(), $offset); } } src/Drivers/Imagick/Modifiers/AlignRotationModifier.php000064400000003530150731647100017205 0ustar00core()->native()->getImageOrientation()) { case Imagick::ORIENTATION_TOPRIGHT: // 2 $image->core()->native()->flopImage(); break; case Imagick::ORIENTATION_BOTTOMRIGHT: // 3 $image->core()->native()->rotateImage("#000", 180); break; case Imagick::ORIENTATION_BOTTOMLEFT: // 4 $image->core()->native()->rotateImage("#000", 180); $image->core()->native()->flopImage(); break; case Imagick::ORIENTATION_LEFTTOP: // 5 $image->core()->native()->rotateImage("#000", -270); $image->core()->native()->flopImage(); break; case Imagick::ORIENTATION_RIGHTTOP: // 6 $image->core()->native()->rotateImage("#000", -270); break; case Imagick::ORIENTATION_RIGHTBOTTOM: // 7 $image->core()->native()->rotateImage("#000", -90); $image->core()->native()->flopImage(); break; case Imagick::ORIENTATION_LEFTBOTTOM: // 8 $image->core()->native()->rotateImage("#000", -90); break; } // set new orientation in image $image->core()->native()->setImageOrientation(Imagick::ORIENTATION_TOPLEFT); return $image; } } src/Drivers/Imagick/Modifiers/BlendTransparencyModifier.php000064400000002237150731647100020054 0ustar00driver()->handleInput( $this->color ? $this->color : $this->driver()->config()->blendingColor ); // get imagickpixel from color $pixel = $this->driver() ->colorProcessor($image->colorspace()) ->colorToNative($color); // merge transparent areas with the background color foreach ($image as $frame) { $frame->native()->setImageBackgroundColor($pixel); $frame->native()->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE); $frame->native()->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN); } return $image; } } src/Drivers/Imagick/Modifiers/BlurModifier.php000064400000001103150731647100015331 0ustar00native()->blurImage(1 * $this->amount, 0.5 * $this->amount); } return $image; } } src/Drivers/Imagick/Modifiers/BrightnessModifier.php000064400000001125150731647100016541 0ustar00native()->modulateImage(100 + $this->level, 100, 100); } return $image; } } src/Drivers/Imagick/Modifiers/ColorizeModifier.php000064400000002276150731647100016227 0ustar00normalizeLevel($this->red); $green = $this->normalizeLevel($this->green); $blue = $this->normalizeLevel($this->blue); foreach ($image as $frame) { $qrange = $frame->native()->getQuantumRange(); $frame->native()->levelImage(0, $red, $qrange['quantumRangeLong'], Imagick::CHANNEL_RED); $frame->native()->levelImage(0, $green, $qrange['quantumRangeLong'], Imagick::CHANNEL_GREEN); $frame->native()->levelImage(0, $blue, $qrange['quantumRangeLong'], Imagick::CHANNEL_BLUE); } return $image; } private function normalizeLevel(int $level): int { return $level > 0 ? intval(round($level / 5)) : intval(round(($level + 100) / 100)); } } src/Drivers/Imagick/Modifiers/ColorspaceModifier.php000064400000003040150731647100016521 0ustar00 */ protected static array $mapping = [ RgbColorspace::class => Imagick::COLORSPACE_SRGB, CmykColorspace::class => Imagick::COLORSPACE_CMYK, ]; public function apply(ImageInterface $image): ImageInterface { $colorspace = $this->targetColorspace(); $imagick = $image->core()->native(); $imagick->transformImageColorspace( $this->getImagickColorspace($colorspace) ); return $image; } /** * @throws NotSupportedException */ private function getImagickColorspace(ColorspaceInterface $colorspace): int { if (!array_key_exists($colorspace::class, self::$mapping)) { throw new NotSupportedException('Given colorspace is not supported.'); } return self::$mapping[$colorspace::class]; } } src/Drivers/Imagick/Modifiers/ContainModifier.php000064400000005430150731647100016027 0ustar00getCropSize($image); $resize = $this->getResizeSize($image); $transparent = new ImagickPixel('transparent'); $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); foreach ($image as $frame) { $frame->native()->scaleImage( $crop->width(), $crop->height(), ); $frame->native()->setBackgroundColor($transparent); $frame->native()->setImageBackgroundColor($transparent); $frame->native()->extentImage( $resize->width(), $resize->height(), $crop->pivot()->x() * -1, $crop->pivot()->y() * -1 ); if ($resize->width() > $crop->width()) { // fill new emerged background $draw = new ImagickDraw(); $draw->setFillColor($background); $delta = abs($crop->pivot()->x()); if ($delta > 0) { $draw->rectangle( 0, 0, $delta - 1, $resize->height() ); } $draw->rectangle( $crop->width() + $delta, 0, $resize->width(), $resize->height() ); $frame->native()->drawImage($draw); } if ($resize->height() > $crop->height()) { // fill new emerged background $draw = new ImagickDraw(); $draw->setFillColor($background); $delta = abs($crop->pivot()->y()); if ($delta > 0) { $draw->rectangle( 0, 0, $resize->width(), $delta - 1 ); } $draw->rectangle( 0, $crop->height() + $delta, $resize->width(), $resize->height() ); $frame->native()->drawImage($draw); } } return $image; } } src/Drivers/Imagick/Modifiers/ContrastModifier.php000064400000001144150731647100016227 0ustar00native()->sigmoidalContrastImage($this->level > 0, abs($this->level / 4), 0); } return $image; } } src/Drivers/Imagick/Modifiers/CoverDownModifier.php000064400000000665150731647100016347 0ustar00resizeDown($this->width, $this->height); } } src/Drivers/Imagick/Modifiers/CoverModifier.php000064400000001713150731647100015512 0ustar00getCropSize($image); $resize = $this->getResizeSize($crop); foreach ($image as $frame) { $frame->native()->cropImage( $crop->width(), $crop->height(), $crop->pivot()->x(), $crop->pivot()->y() ); $frame->native()->scaleImage( $resize->width(), $resize->height() ); $frame->native()->setImagePage(0, 0, 0, 0); } return $image; } } src/Drivers/Imagick/Modifiers/CropModifier.php000064400000006352150731647100015343 0ustar00size(); $crop = $this->crop($image); $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); $transparent = new ImagickPixel('transparent'); $draw = new ImagickDraw(); $draw->setFillColor($background); foreach ($image as $frame) { $frame->native()->setBackgroundColor($transparent); $frame->native()->setImageBackgroundColor($transparent); // crop image $frame->native()->extentImage( $crop->width(), $crop->height(), $crop->pivot()->x() + $this->offset_x, $crop->pivot()->y() + $this->offset_y ); // repage $frame->native()->setImagePage( $crop->width(), $crop->height(), 0, 0, ); // cover the possible newly created areas with background color if ($crop->width() > $originalSize->width() || $this->offset_x > 0) { $draw->rectangle( $originalSize->width() + ($this->offset_x * -1) - $crop->pivot()->x(), 0, $crop->width(), $crop->height() ); } // cover the possible newly created areas with background color if ($crop->height() > $originalSize->height() || $this->offset_y > 0) { $draw->rectangle( ($this->offset_x * -1) - $crop->pivot()->x(), $originalSize->height() + ($this->offset_y * -1) - $crop->pivot()->y(), ($this->offset_x * -1) + $originalSize->width() - 1 - $crop->pivot()->x(), $crop->height() ); } // cover the possible newly created areas with background color if ((($this->offset_x * -1) - $crop->pivot()->x() - 1) > 0) { $draw->rectangle( 0, 0, ($this->offset_x * -1) - $crop->pivot()->x() - 1, $crop->height() ); } // cover the possible newly created areas with background color if ((($this->offset_y * -1) - $crop->pivot()->y() - 1) > 0) { $draw->rectangle( ($this->offset_x * -1) - $crop->pivot()->x(), 0, ($this->offset_x * -1) + $originalSize->width() - $crop->pivot()->x() - 1, ($this->offset_y * -1) - $crop->pivot()->y() - 1, ); } $frame->native()->drawImage($draw); } return $image; } } src/Drivers/Imagick/Modifiers/DrawBezierModifier.php000064400000005037150731647100016475 0ustar00drawable->count() !== 3 && $this->drawable->count() !== 4) { throw new GeometryException('You must specify either 3 or 4 points to create a bezier curve'); } $drawing = new ImagickDraw(); if ($this->drawable->hasBackgroundColor()) { $background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ); } else { $background_color = 'transparent'; } $drawing->setFillColor($background_color); if ($this->drawable->hasBorder() && $this->drawable->borderSize() > 0) { $border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ); $drawing->setStrokeColor($border_color); $drawing->setStrokeWidth($this->drawable->borderSize()); } $drawing->pathStart(); $drawing->pathMoveToAbsolute( $this->drawable->first()->x(), $this->drawable->first()->y() ); if ($this->drawable->count() === 3) { $drawing->pathCurveToQuadraticBezierAbsolute( $this->drawable->second()->x(), $this->drawable->second()->y(), $this->drawable->last()->x(), $this->drawable->last()->y() ); } elseif ($this->drawable->count() === 4) { $drawing->pathCurveToAbsolute( $this->drawable->second()->x(), $this->drawable->second()->y(), $this->drawable->third()->x(), $this->drawable->third()->y(), $this->drawable->last()->x(), $this->drawable->last()->y() ); } $drawing->pathFinish(); foreach ($image as $frame) { $frame->native()->drawImage($drawing); } return $image; } } src/Drivers/Imagick/Modifiers/DrawEllipseModifier.php000064400000002760150731647100016652 0ustar00driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ); $border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ); foreach ($image as $frame) { $drawing = new ImagickDraw(); $drawing->setFillColor($background_color); if ($this->drawable->hasBorder()) { $drawing->setStrokeWidth($this->drawable->borderSize()); $drawing->setStrokeColor($border_color); } $drawing->ellipse( $this->drawable->position()->x(), $this->drawable->position()->y(), $this->drawable->width() / 2, $this->drawable->height() / 2, 0, 360 ); $frame->native()->drawImage($drawing); } return $image; } } src/Drivers/Imagick/Modifiers/DrawLineModifier.php000064400000002226150731647100016141 0ustar00setStrokeWidth($this->drawable->width()); $drawing->setFillOpacity(0); $drawing->setStrokeColor( $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ) ); $drawing->line( $this->drawable->start()->x(), $this->drawable->start()->y(), $this->drawable->end()->x(), $this->drawable->end()->y(), ); foreach ($image as $frame) { $frame->native()->drawImage($drawing); } return $image; } } src/Drivers/Imagick/Modifiers/DrawPixelModifier.php000064400000001560150731647100016333 0ustar00driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->color) ); $pixel = new ImagickDraw(); $pixel->setFillColor($color); $pixel->point($this->position->x(), $this->position->y()); foreach ($image as $frame) { $frame->native()->drawImage($pixel); } return $image; } } src/Drivers/Imagick/Modifiers/DrawPolygonModifier.php000064400000003437150731647100016706 0ustar00setFillColor(new ImagickPixel('transparent')); // defaults to no backgroundColor if ($this->drawable->hasBackgroundColor()) { $background_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ); $drawing->setFillColor($background_color); } if ($this->drawable->hasBorder()) { $border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ); $drawing->setStrokeColor($border_color); $drawing->setStrokeWidth($this->drawable->borderSize()); } $drawing->polygon($this->points()); foreach ($image as $frame) { $frame->native()->drawImage($drawing); } return $image; } /** * Return points of drawable in processable form for ImagickDraw * * @return array> */ private function points(): array { $points = []; foreach ($this->drawable as $point) { $points[] = ['x' => $point->x(), 'y' => $point->y()]; } return $points; } } src/Drivers/Imagick/Modifiers/DrawRectangleModifier.php000064400000002773150731647100017165 0ustar00driver()->colorProcessor($image->colorspace())->colorToNative( $this->backgroundColor() ); $border_color = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->borderColor() ); $drawing->setFillColor($background_color); if ($this->drawable->hasBorder()) { $drawing->setStrokeColor($border_color); $drawing->setStrokeWidth($this->drawable->borderSize()); } // build rectangle $drawing->rectangle( $this->drawable->position()->x(), $this->drawable->position()->y(), $this->drawable->position()->x() + $this->drawable->width(), $this->drawable->position()->y() + $this->drawable->height() ); foreach ($image as $frame) { $frame->native()->drawImage($drawing); } return $image; } } src/Drivers/Imagick/Modifiers/FillModifier.php000064400000003442150731647100015323 0ustar00driver()->handleInput($this->color); $pixel = $this->driver() ->colorProcessor($image->colorspace()) ->colorToNative($color); foreach ($image as $frame) { if ($this->hasPosition()) { $this->floodFillWithColor($frame, $pixel); } else { $this->fillAllWithColor($frame, $pixel); } } return $image; } private function floodFillWithColor(FrameInterface $frame, ImagickPixel $pixel): void { $target = $frame->native()->getImagePixelColor( $this->position->x(), $this->position->y() ); $frame->native()->floodfillPaintImage( $pixel, 100, $target, $this->position->x(), $this->position->y(), false, Imagick::CHANNEL_ALL ); } private function fillAllWithColor(FrameInterface $frame, ImagickPixel $pixel): void { $draw = new ImagickDraw(); $draw->setFillColor($pixel); $draw->rectangle( 0, 0, $frame->native()->getImageWidth(), $frame->native()->getImageHeight() ); $frame->native()->drawImage($draw); } } src/Drivers/Imagick/Modifiers/FlipModifier.php000064400000001035150731647100015323 0ustar00native()->flipImage(); } return $image; } } src/Drivers/Imagick/Modifiers/FlopModifier.php000064400000001041150731647100015326 0ustar00native()->flopImage(); } return $image; } } src/Drivers/Imagick/Modifiers/GammaModifier.php000064400000001056150731647100015456 0ustar00native()->gammaImage($this->gamma); } return $image; } } src/Drivers/Imagick/Modifiers/GreyscaleModifier.php000064400000001100150731647100016340 0ustar00native()->modulateImage(100, 0, 100); } return $image; } } src/Drivers/Imagick/Modifiers/InvertModifier.php000064400000001054150731647100015701 0ustar00native()->negateImage(false); } return $image; } } src/Drivers/Imagick/Modifiers/PadModifier.php000064400000000210150731647100015127 0ustar00pixelateFrame($frame); } return $image; } protected function pixelateFrame(FrameInterface $frame): void { $size = $frame->size(); $frame->native()->scaleImage( (int) round(max(1, $size->width() / $this->size)), (int) round(max(1, $size->height() / $this->size)) ); $frame->native()->scaleImage($size->width(), $size->height()); } } src/Drivers/Imagick/Modifiers/PlaceModifier.php000064400000002343150731647100015460 0ustar00driver()->handleInput($this->element); $position = $this->getPosition($image, $watermark); // set opacity of watermark if ($this->opacity < 100) { $watermark->core()->native()->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET); $watermark->core()->native()->evaluateImage( Imagick::EVALUATE_DIVIDE, $this->opacity > 0 ? 100 / $this->opacity : 1000, Imagick::CHANNEL_ALPHA, ); } foreach ($image as $frame) { $frame->native()->compositeImage( $watermark->core()->native(), Imagick::COMPOSITE_DEFAULT, $position->x(), $position->y() ); } return $image; } } src/Drivers/Imagick/Modifiers/ProfileModifier.php000064400000001361150731647100016033 0ustar00core()->native(); $result = $imagick->profileImage('icc', (string) $this->profile); if ($result === false) { throw new ColorException('ICC color profile could not be set.'); } return $image; } } src/Drivers/Imagick/Modifiers/ProfileRemovalModifier.php000064400000001376150731647100017367 0ustar00core()->native(); $result = $imagick->profileImage('icc', null); if ($result === false) { throw new ColorException('ICC color profile could not be removed.'); } return $image; } } src/Drivers/Imagick/Modifiers/QuantizeColorsModifier.php000064400000002135150731647100017415 0ustar00limit <= 0) { throw new InputException('Quantization limit must be greater than 0.'); } // no color reduction if the limit is higher than the colors in the img if ($this->limit > $image->core()->native()->getImageColors()) { return $image; } foreach ($image as $frame) { $frame->native()->quantizeImage( $this->limit, $frame->native()->getImageColorspace(), 0, false, false ); } return $image; } } src/Drivers/Imagick/Modifiers/RemoveAnimationModifier.php000064400000001432150731647100017527 0ustar00chosenFrame($image, $this->position); $imagick->addImage($frame->native()->getImage()); // set new imagick to image $image->core()->setNative($imagick); return $image; } } src/Drivers/Imagick/Modifiers/ResizeCanvasModifier.php000064400000005137150731647100017035 0ustar00size(); $resize = $this->cropSize($image); $background = $this->driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); foreach ($image as $frame) { $frame->native()->extentImage( $resize->width(), $resize->height(), $resize->pivot()->x(), $resize->pivot()->y() ); if ($resize->width() > $size->width()) { // fill new emerged background $draw = new ImagickDraw(); $draw->setFillColor($background); $delta_width = abs($resize->pivot()->x()); $delta_height = $resize->pivot()->y() * -1; if ($delta_width > 0) { $draw->rectangle( 0, $delta_height, $delta_width - 1, $delta_height + $size->height() - 1 ); } $draw->rectangle( $size->width() + $delta_width, $delta_height, $resize->width(), $delta_height + $size->height() - 1 ); $frame->native()->drawImage($draw); } if ($resize->height() > $size->height()) { // fill new emerged background $draw = new ImagickDraw(); $draw->setFillColor($background); $delta = abs($resize->pivot()->y()); if ($delta > 0) { $draw->rectangle( 0, 0, $resize->width(), $delta - 1 ); } $draw->rectangle( 0, $size->height() + $delta, $resize->width(), $resize->height() ); $frame->native()->drawImage($draw); } } return $image; } } src/Drivers/Imagick/Modifiers/ResizeCanvasRelativeModifier.php000064400000000633150731647100020525 0ustar00size()->resizeDown($this->width, $this->height); } } src/Drivers/Imagick/Modifiers/ResizeModifier.php000064400000001743150731647100015700 0ustar00getAdjustedSize($image); foreach ($image as $frame) { $frame->native()->scaleImage( $resizeTo->width(), $resizeTo->height() ); } return $image; } /** * @throws RuntimeException */ protected function getAdjustedSize(ImageInterface $image): SizeInterface { return $image->size()->resize($this->width, $this->height); } } src/Drivers/Imagick/Modifiers/ResolutionModifier.php000064400000001102150731647100016567 0ustar00core()->native(); $imagick->setImageResolution($this->x, $this->y); return $image; } } src/Drivers/Imagick/Modifiers/RotateModifier.php000064400000001441150731647100015670 0ustar00driver()->colorProcessor($image->colorspace())->colorToNative( $this->driver()->handleInput($this->background) ); foreach ($image as $frame) { $frame->native()->rotateImage( $background, $this->rotationAngle() * -1 ); } return $image; } } src/Drivers/Imagick/Modifiers/ScaleDownModifier.php000064400000000621150731647100016310 0ustar00size()->scaleDown($this->width, $this->height); } } src/Drivers/Imagick/Modifiers/ScaleModifier.php000064400000000611150731647100015457 0ustar00size()->scale($this->width, $this->height); } } src/Drivers/Imagick/Modifiers/SharpenModifier.php000064400000001115150731647100016030 0ustar00native()->unsharpMaskImage(1, 1, $this->amount / 6.25, 0); } return $image; } } src/Drivers/Imagick/Modifiers/SliceAnimationModifier.php000064400000001354150731647100017334 0ustar00offset >= $image->count()) { throw new AnimationException('Offset is not in the range of frames.'); } $image->core()->slice($this->offset, $this->length); return $image; } } src/Drivers/Imagick/Modifiers/TextModifier.php000064400000011271150731647100015360 0ustar00processor()->textBlock($this->text, $this->font, $this->position); $drawText = $this->imagickDrawText($image, $this->font); $drawStroke = $this->imagickDrawStroke($image, $this->font); foreach ($image as $frame) { foreach ($lines as $line) { foreach ($this->strokeOffsets($this->font) as $offset) { // Draw the stroke outline under the actual text $this->maybeDrawTextline($frame, $line, $drawStroke, $offset); } // Draw the actual text $this->maybeDrawTextline($frame, $line, $drawText); } } return $image; } /** * Create an ImagickDraw object to draw text on the image * * @param ImageInterface $image * @param FontInterface $font * @throws RuntimeException * @throws ColorException * @throws FontException * @throws ImagickDrawException * @throws ImagickException * @return ImagickDraw */ private function imagickDrawText(ImageInterface $image, FontInterface $font): ImagickDraw { $color = $this->driver()->handleInput($font->color()); if ($font->hasStrokeEffect() && $color->isTransparent()) { throw new ColorException( 'The text color must be fully opaque when using the stroke effect.' ); } $color = $this->driver()->colorProcessor($image->colorspace())->colorToNative($color); return $this->processor()->toImagickDraw($font, $color); } /** * Create a ImagickDraw object to draw the outline stroke effect on the Image * * @param ImageInterface $image * @param FontInterface $font * @throws RuntimeException * @throws ColorException * @throws FontException * @throws ImagickDrawException * @throws ImagickException * @return null|ImagickDraw */ private function imagickDrawStroke(ImageInterface $image, FontInterface $font): ?ImagickDraw { if (!$font->hasStrokeEffect()) { return null; } $color = $this->driver()->handleInput($font->strokeColor()); if ($color->isTransparent()) { throw new ColorException( 'The stroke color must be fully opaque.' ); } $color = $this->driver()->colorProcessor($image->colorspace())->colorToNative($color); return $this->processor()->toImagickDraw($font, $color); } /** * Maybe draw given line of text on frame instance depending on given * ImageDraw instance. Optionally move line position by given offset. * * @param FrameInterface $frame * @param Line $textline * @param null|ImagickDraw $draw * @param PointInterface $offset * @return void */ private function maybeDrawTextline( FrameInterface $frame, Line $textline, ?ImagickDraw $draw = null, PointInterface $offset = new Point(), ): void { if ($draw !== null) { $frame->native()->annotateImage( $draw, $textline->position()->x() + $offset->x(), $textline->position()->y() + $offset->y(), $this->font->angle(), (string) $textline ); } } /** * Return imagick font processor * * @throws FontException * @return FontProcessor */ private function processor(): FontProcessor { $processor = $this->driver()->fontProcessor(); if (!($processor instanceof FontProcessor)) { throw new FontException('Font processor does not match the driver.'); } return $processor; } } src/Drivers/Imagick/Modifiers/TrimModifier.php000064400000001476150731647100015355 0ustar00isAnimated()) { throw new NotSupportedException('Trim modifier cannot be applied to animated images.'); } $imagick = $image->core()->native(); $imagick->trimImage(($this->tolerance / 100 * $imagick->getQuantum()) / 1.5); $imagick->setImagePage(0, 0, 0, 0); return $image; } } src/Drivers/Specializable.php000064400000000434150731647100012236 0ustar00analyze($this); } } src/Drivers/SpecializableDecoder.php000064400000001323150731647100013522 0ustar00modify($this); } } src/EncodedImage.php000064400000002060150731647100010352 0ustar00mediaType; } /** * {@inheritdoc} * * @see EncodedImageInterface::mimetype() */ public function mimetype(): string { return $this->mediaType(); } /** * {@inheritdoc} * * @see EncodedImageInterface::toDataUri() */ public function toDataUri(): string { return sprintf('data:%s;base64,%s', $this->mediaType(), base64_encode((string) $this)); } } src/Encoders/AutoEncoder.php000064400000001031150731647100012015 0ustar00encode( $this->encoderByMediaType( $image->origin()->mediaType() ) ); } } src/Encoders/AvifEncoder.php000064400000000562150731647100012002 0ustar00 */ protected array $options = []; /** * Create new encoder instance to encode to format of given file extension * * @param null|string|FileExtension $extension Target file extension for example "png" * @return void */ public function __construct(public null|string|FileExtension $extension = null, mixed ...$options) { $this->options = $options; } /** * {@inheritdoc} * * @see EncoderInterface::encode() */ public function encode(ImageInterface $image): EncodedImageInterface { $extension = is_null($this->extension) ? $image->origin()->fileExtension() : $this->extension; return $image->encode( $this->encoderByFileExtension( $extension ) ); } /** * Create matching encoder for given file extension * * @param null|string|FileExtension $extension * @throws EncoderException * @return EncoderInterface */ protected function encoderByFileExtension(null|string|FileExtension $extension): EncoderInterface { if (empty($extension)) { throw new EncoderException('No encoder found for empty file extension.'); } try { $extension = is_string($extension) ? FileExtension::from($extension) : $extension; } catch (Error) { throw new EncoderException('No encoder found for file extension (' . $extension . ').'); } return $extension->format()->encoder(...$this->options); } } src/Encoders/FilePathEncoder.php000064400000001770150731647100012613 0ustar00encode( $this->encoderByFileExtension( is_null($this->path) ? $image->origin()->fileExtension() : pathinfo($this->path, PATHINFO_EXTENSION) ) ); } } src/Encoders/GifEncoder.php000064400000000551150731647100011620 0ustar00 */ protected array $options = []; /** * Create new encoder instance * * @param null|string|MediaType $mediaType Target media type for example "image/jpeg" * @return void */ public function __construct(public null|string|MediaType $mediaType = null, mixed ...$options) { $this->options = $options; } /** * {@inheritdoc} * * @see EncoderInterface::encode() */ public function encode(ImageInterface $image): EncodedImageInterface { $mediaType = is_null($this->mediaType) ? $image->origin()->mediaType() : $this->mediaType; return $image->encode( $this->encoderByMediaType($mediaType) ); } /** * Return new encoder by given media (MIME) type * * @param string|MediaType $mediaType * @throws EncoderException * @return EncoderInterface */ protected function encoderByMediaType(string|MediaType $mediaType): EncoderInterface { try { $mediaType = is_string($mediaType) ? MediaType::from($mediaType) : $mediaType; } catch (Error) { throw new EncoderException('No encoder found for media type (' . $mediaType . ').'); } return $mediaType->format()->encoder(...$this->options); } } src/Encoders/PngEncoder.php000064400000000643150731647100011641 0ustar00pointer = $this->buildFilePointer($data); } /** * {@inheritdoc} * * @see FileInterface::save() */ public function save(string $filepath): void { $dir = pathinfo($filepath, PATHINFO_DIRNAME); if (!is_dir($dir)) { throw new NotWritableException( "Can't write image to path. Directory does not exist." ); } if (!is_writable($dir)) { throw new NotWritableException( "Can't write image to path. Directory is not writable." ); } if (is_file($filepath) && !is_writable($filepath)) { throw new NotWritableException( "Can't write image. Path ({$filepath}) is not writable." ); } // write data $saved = @file_put_contents($filepath, $this->pointer); if ($saved === false) { throw new NotWritableException( "Can't write image data to path ({$filepath})." ); } } /** * {@inheritdoc} * * @see FilterInterface::toString() */ public function toString(): string { return stream_get_contents($this->pointer, offset: 0); } /** * {@inheritdoc} * * @see FilterInterface::toFilePointer() */ public function toFilePointer() { rewind($this->pointer); return $this->pointer; } /** * {@inheritdoc} * * @see FileInterface::size() */ public function size(): int { $info = fstat($this->pointer); return intval($info['size']); } /** * {@inheritdoc} * * @see FileInterface::__toString() */ public function __toString(): string { return $this->toString(); } } src/FileExtension.php000064400000003301150731647100010621 0ustar00 Format::JPEG, self::WEBP => Format::WEBP, self::GIF => Format::GIF, self::PNG => Format::PNG, self::AVIF => Format::AVIF, self::BMP => Format::BMP, self::TIF, self::TIFF => Format::TIFF, self::JP2, self::J2K, self::JPF, self::JPM, self::JPG2, self::J2C, self::JPC, self::JPX => Format::JP2, self::HEIC, self::HEIF => Format::HEIC, }; } /** * Return media types for the current format * * @return array */ public function mediaTypes(): array { return $this->format()->mediaTypes(); } /** * Return the first found media type for the current format * * @return MediaType */ public function mediaType(): MediaType { return $this->format()->mediaType(); } } src/Format.php000064400000011135150731647100007301 0ustar00format(); } if ($identifier instanceof FileExtension) { return $identifier->format(); } try { $format = MediaType::from(strtolower($identifier))->format(); } catch (Error) { try { $format = FileExtension::from(strtolower($identifier))->format(); } catch (Error) { throw new NotSupportedException('Unable to create format from "' . $identifier . '".'); } } return $format; } /** * Try to create format from given identifier and return null on failure * * @param string|Format|MediaType|FileExtension $identifier * @return Format|null */ public static function tryCreate(string|self|MediaType|FileExtension $identifier): ?self { try { return self::create($identifier); } catch (NotSupportedException) { return null; } } /** * Return the possible media (MIME) types for the current format * * @return array */ public function mediaTypes(): array { return array_filter(MediaType::cases(), function ($mediaType) { return $mediaType->format() === $this; }); } /** * Return the first found media type for the current format * * @return MediaType */ public function mediaType(): MediaType { $types = $this->mediaTypes(); return reset($types); } /** * Return the possible file extension for the current format * * @return array */ public function fileExtensions(): array { return array_filter(FileExtension::cases(), function ($fileExtension) { return $fileExtension->format() === $this; }); } /** * Return the first found file extension for the current format * * @return FileExtension */ public function fileExtension(): FileExtension { $extensions = $this->fileExtensions(); return reset($extensions); } /** * Create an encoder instance with given options that matches the format * * @param mixed $options * @return EncoderInterface */ public function encoder(mixed ...$options): EncoderInterface { // get classname of target encoder from current format $classname = match ($this) { self::AVIF => AvifEncoder::class, self::BMP => BmpEncoder::class, self::GIF => GifEncoder::class, self::HEIC => HeicEncoder::class, self::JP2 => Jpeg2000Encoder::class, self::JPEG => JpegEncoder::class, self::PNG => PngEncoder::class, self::TIFF => TiffEncoder::class, self::WEBP => WebpEncoder::class, }; // get parameters of target encoder $parameters = []; $reflectionClass = new ReflectionClass($classname); if ($constructor = $reflectionClass->getConstructor()) { $parameters = array_map( fn ($parameter) => $parameter->getName(), $constructor->getParameters(), ); } // filter out unavailable options of target encoder $options = array_filter( $options, fn ($key) => in_array($key, $parameters), ARRAY_FILTER_USE_KEY, ); return new $classname(...$options); } } src/Geometry/Bezier.php000064400000011145150731647100011065 0ustar00 * @implements ArrayAccess */ class Bezier implements IteratorAggregate, Countable, ArrayAccess, DrawableInterface { use HasBorder; use HasBackgroundColor; /** * Create new bezier instance * * @param array $points * @param PointInterface $pivot * @return void */ public function __construct( protected array $points = [], protected PointInterface $pivot = new Point() ) { } /** * {@inheritdoc} * * @see DrawableInterface::position() */ public function position(): PointInterface { return $this->pivot; } /** * {@inheritdoc} * * @see DrawableInterface::setPosition() */ public function setPosition(PointInterface $position): DrawableInterface { $this->pivot = $position; return $this; } /** * Implement iteration through all points of bezier * * @return Traversable */ public function getIterator(): Traversable { return new ArrayIterator($this->points); } /** * Return current pivot point * * @return PointInterface */ public function pivot(): PointInterface { return $this->pivot; } /** * Change pivot point to given point * * @param PointInterface $pivot * @return Bezier */ public function setPivot(PointInterface $pivot): self { $this->pivot = $pivot; return $this; } /** * Return first control point of bezier * * @return ?PointInterface */ public function first(): ?PointInterface { if ($point = reset($this->points)) { return $point; } return null; } /** * Return second control point of bezier * * @return ?PointInterface */ public function second(): ?PointInterface { if (array_key_exists(1, $this->points)) { return $this->points[1]; } return null; } /** * Return third control point of bezier * * @return ?PointInterface */ public function third(): ?PointInterface { if (array_key_exists(2, $this->points)) { return $this->points[2]; } return null; } /** * Return last control point of bezier * * @return ?PointInterface */ public function last(): ?PointInterface { if ($point = end($this->points)) { return $point; } return null; } /** * Return bezier's point count * * @return int */ public function count(): int { return count($this->points); } /** * Determine if point exists at given offset * * @param mixed $offset * @return bool */ public function offsetExists($offset): bool { return array_key_exists($offset, $this->points); } /** * Return point at given offset * * @param mixed $offset * @return PointInterface */ public function offsetGet($offset): mixed { return $this->points[$offset]; } /** * Set point at given offset * * @param mixed $offset * @param PointInterface $value * @return void */ public function offsetSet($offset, $value): void { $this->points[$offset] = $value; } /** * Unset offset at given offset * * @param mixed $offset * @return void */ public function offsetUnset($offset): void { unset($this->points[$offset]); } /** * Add given point to bezier * * @param PointInterface $point * @return Bezier */ public function addPoint(PointInterface $point): self { $this->points[] = $point; return $this; } /** * Return array of all x/y values of all points of bezier * * @return array */ public function toArray(): array { $coordinates = []; foreach ($this->points as $point) { $coordinates[] = $point->x(); $coordinates[] = $point->y(); } return $coordinates; } } src/Geometry/Circle.php000064400000002373150731647100011051 0ustar00setWidth($diameter); $this->setHeight($diameter); return $this; } /** * Get diameter of circle * * @return int */ public function diameter(): int { return $this->width(); } /** * Set radius of circle * * @param int $radius * @return Circle */ public function setRadius(int $radius): self { return $this->setDiameter(intval($radius * 2)); } /** * Get radius of circle * * @return int */ public function radius(): int { return intval(round($this->diameter() / 2)); } } src/Geometry/Ellipse.php000064400000004316150731647100011244 0ustar00pivot; } /** * {@inheritdoc} * * @see DrawableInterface::setPosition() */ public function setPosition(PointInterface $position): self { $this->pivot = $position; return $this; } /** * Return pivot point of Ellipse * * @return PointInterface */ public function pivot(): PointInterface { return $this->pivot; } /** * Set size of Ellipse * * @param int $width * @param int $height * @return Ellipse */ public function setSize(int $width, int $height): self { return $this->setWidth($width)->setHeight($height); } /** * Set width of Ellipse * * @param int $width * @return Ellipse */ public function setWidth(int $width): self { $this->width = $width; return $this; } /** * Set height of Ellipse * * @param int $height * @return Ellipse */ public function setHeight(int $height): self { $this->height = $height; return $this; } /** * Get width of Ellipse * * @return int */ public function width(): int { return $this->width; } /** * Get height of Ellipse * * @return int */ public function height(): int { return $this->height; } } src/Geometry/Factories/BezierFactory.php000064400000004151150731647100014333 0ustar00bezier = is_a($init, Bezier::class) ? $init : new Bezier([]); if (is_callable($init)) { $init($this); } } /** * {@inheritdoc} * * @see DrawableFactoryInterface::init() */ public static function init(null|Closure|DrawableInterface $init = null): self { return new self($init); } /** * {@inheritdoc} * * @see DrawableFactoryInterface::create() */ public function create(): DrawableInterface { return $this->bezier; } /** * Add a point to the bezier to be produced * * @param int $x * @param int $y * @return BezierFactory */ public function point(int $x, int $y): self { $this->bezier->addPoint(new Point($x, $y)); return $this; } /** * Set the background color of the bezier to be produced * * @param mixed $color * @return BezierFactory */ public function background(mixed $color): self { $this->bezier->setBackgroundColor($color); return $this; } /** * Set the border color & border size of the bezier to be produced * * @param mixed $color * @param int $size * @return BezierFactory */ public function border(mixed $color, int $size = 1): self { $this->bezier->setBorder($color, $size); return $this; } /** * Produce the bezier * * @return Bezier */ public function __invoke(): Bezier { return $this->bezier; } } src/Geometry/Factories/CircleFactory.php000064400000005073150731647100014320 0ustar00circle = is_a($init, Circle::class) ? $init : new Circle(0); $this->circle->setPosition($pivot); if (is_callable($init)) { $init($this); } } /** * {@inheritdoc} * * @see DrawableFactoryInterface::init() */ public static function init(null|Closure|DrawableInterface $init = null): self { return new self(init: $init); } /** * {@inheritdoc} * * @see DrawableFactoryInterface::create() */ public function create(): DrawableInterface { return $this->circle; } /** * Set the radius of the circle to be produced * * @param int $radius * @return CircleFactory */ public function radius(int $radius): self { $this->circle->setSize($radius * 2, $radius * 2); return $this; } /** * Set the diameter of the circle to be produced * * @param int $diameter * @return CircleFactory */ public function diameter(int $diameter): self { $this->circle->setSize($diameter, $diameter); return $this; } /** * Set the background color of the circle to be produced * * @param mixed $color * @return CircleFactory */ public function background(mixed $color): self { $this->circle->setBackgroundColor($color); return $this; } /** * Set the border color & border size of the ellipse to be produced * * @param mixed $color * @param int $size * @return CircleFactory */ public function border(mixed $color, int $size = 1): self { $this->circle->setBorder($color, $size); return $this; } /** * Produce the circle * * @return Circle */ public function __invoke(): Circle { return $this->circle; } } src/Geometry/Factories/Drawable.php000064400000002372150731647100013307 0ustar00ellipse = is_a($init, Ellipse::class) ? $init : new Ellipse(0, 0); $this->ellipse->setPosition($pivot); if (is_callable($init)) { $init($this); } } /** * {@inheritdoc} * * @see DrawableFactoryInterface::init() */ public static function init(null|Closure|DrawableInterface $init = null): self { return new self(init: $init); } /** * {@inheritdoc} * * @see DrawableFactoryInterface::create() */ public function create(): DrawableInterface { return $this->ellipse; } /** * Set the size of the ellipse to be produced * * @param int $width * @param int $height * @return EllipseFactory */ public function size(int $width, int $height): self { $this->ellipse->setSize($width, $height); return $this; } /** * Set the width of the ellipse to be produced * * @param int $width * @return EllipseFactory */ public function width(int $width): self { $this->ellipse->setWidth($width); return $this; } /** * Set the height of the ellipse to be produced * * @param int $height * @return EllipseFactory */ public function height(int $height): self { $this->ellipse->setHeight($height); return $this; } /** * Set the background color of the ellipse to be produced * * @param mixed $color * @return EllipseFactory */ public function background(mixed $color): self { $this->ellipse->setBackgroundColor($color); return $this; } /** * Set the border color & border size of the ellipse to be produced * * @param mixed $color * @param int $size * @return EllipseFactory */ public function border(mixed $color, int $size = 1): self { $this->ellipse->setBorder($color, $size); return $this; } /** * Produce the ellipse * * @return Ellipse */ public function __invoke(): Ellipse { return $this->ellipse; } } src/Geometry/Factories/LineFactory.php000064400000006062150731647100014005 0ustar00line = is_a($init, Line::class) ? $init : new Line(new Point(), new Point()); if (is_callable($init)) { $init($this); } } /** * {@inheritdoc} * * @see DrawableFactoryInterface::init() */ public static function init(null|Closure|DrawableInterface $init = null): self { return new self($init); } /** * {@inheritdoc} * * @see DrawableFactoryInterface::create() */ public function create(): DrawableInterface { return $this->line; } /** * Set the color of the line to be produced * * @param mixed $color * @return LineFactory */ public function color(mixed $color): self { $this->line->setBackgroundColor($color); $this->line->setBorderColor($color); return $this; } /** * Set the (background) color of the line to be produced * * @param mixed $color * @return LineFactory */ public function background(mixed $color): self { $this->line->setBackgroundColor($color); $this->line->setBorderColor($color); return $this; } /** * Set the border size & border color of the line to be produced * * @param mixed $color * @param int $size * @return LineFactory */ public function border(mixed $color, int $size = 1): self { $this->line->setBackgroundColor($color); $this->line->setBorderColor($color); $this->line->setWidth($size); return $this; } /** * Set the width of the line to be produced * * @param int $size * @return LineFactory */ public function width(int $size): self { $this->line->setWidth($size); return $this; } /** * Set the coordinates of the starting point of the line to be produced * * @param int $x * @param int $y * @return LineFactory */ public function from(int $x, int $y): self { $this->line->setStart(new Point($x, $y)); return $this; } /** * Set the coordinates of the end point of the line to be produced * * @param int $x * @param int $y * @return LineFactory */ public function to(int $x, int $y): self { $this->line->setEnd(new Point($x, $y)); return $this; } /** * Produce the line * * @return Line */ public function __invoke(): Line { return $this->line; } } src/Geometry/Factories/PolygonFactory.php000064400000004200150731647100014535 0ustar00polygon = is_a($init, Polygon::class) ? $init : new Polygon([]); if (is_callable($init)) { $init($this); } } /** * {@inheritdoc} * * @see DrawableFactoryInterface::init() */ public static function init(null|Closure|DrawableInterface $init = null): self { return new self($init); } /** * {@inheritdoc} * * @see DrawableFactoryInterface::create() */ public function create(): DrawableInterface { return $this->polygon; } /** * Add a point to the polygon to be produced * * @param int $x * @param int $y * @return PolygonFactory */ public function point(int $x, int $y): self { $this->polygon->addPoint(new Point($x, $y)); return $this; } /** * Set the background color of the polygon to be produced * * @param mixed $color * @return PolygonFactory */ public function background(mixed $color): self { $this->polygon->setBackgroundColor($color); return $this; } /** * Set the border color & border size of the polygon to be produced * * @param mixed $color * @param int $size * @return PolygonFactory */ public function border(mixed $color, int $size = 1): self { $this->polygon->setBorder($color, $size); return $this; } /** * Produce the polygon * * @return Polygon */ public function __invoke(): Polygon { return $this->polygon; } } src/Geometry/Factories/RectangleFactory.php000064400000005624150731647100015025 0ustar00rectangle = is_a($init, Rectangle::class) ? $init : new Rectangle(0, 0, $pivot); $this->rectangle->setPosition($pivot); if (is_callable($init)) { $init($this); } } /** * {@inheritdoc} * * @see DrawableFactoryInterface::init() */ public static function init(null|Closure|DrawableInterface $init = null): self { return new self(init: $init); } /** * {@inheritdoc} * * @see DrawableFactoryInterface::create() */ public function create(): DrawableInterface { return $this->rectangle; } /** * Set the size of the rectangle to be produced * * @param int $width * @param int $height * @return RectangleFactory */ public function size(int $width, int $height): self { $this->rectangle->setSize($width, $height); return $this; } /** * Set the width of the rectangle to be produced * * @param int $width * @return RectangleFactory */ public function width(int $width): self { $this->rectangle->setWidth($width); return $this; } /** * Set the height of the rectangle to be produced * * @param int $height * @return RectangleFactory */ public function height(int $height): self { $this->rectangle->setHeight($height); return $this; } /** * Set the background color of the rectangle to be produced * * @param mixed $color * @return RectangleFactory */ public function background(mixed $color): self { $this->rectangle->setBackgroundColor($color); return $this; } /** * Set the border color & border size of the rectangle to be produced * * @param mixed $color * @param int $size * @return RectangleFactory */ public function border(mixed $color, int $size = 1): self { $this->rectangle->setBorder($color, $size); return $this; } /** * Produce the rectangle * * @return Rectangle */ public function __invoke(): Rectangle { return $this->rectangle; } } src/Geometry/Line.php000064400000005406150731647100010537 0ustar00start; } /** * {@inheritdoc} * * @see DrawableInterface::setPosition() */ public function setPosition(PointInterface $position): DrawableInterface { $this->start = $position; return $this; } /** * Return line width * * @return int */ public function width(): int { return $this->width; } /** * Set line width * * @param int $width * @return Line */ public function setWidth(int $width): self { $this->width = $width; return $this; } /** * Get starting point of line * * @return PointInterface */ public function start(): PointInterface { return $this->start; } /** * get end point of line * * @return PointInterface */ public function end(): PointInterface { return $this->end; } /** * Set starting point of line * * @param PointInterface $start * @return Line */ public function setStart(PointInterface $start): self { $this->start = $start; return $this; } /** * Set starting point of line by coordinates * * @param int $x * @param int $y * @return Line */ public function from(int $x, int $y): self { $this->start()->setX($x); $this->start()->setY($y); return $this; } /** * Set end point of line by coordinates * * @param int $x * @param int $y * @return Line */ public function to(int $x, int $y): self { $this->end()->setX($x); $this->end()->setY($y); return $this; } /** * Set end point of line * * @param PointInterface $end * @return Line */ public function setEnd(PointInterface $end): self { $this->end = $end; return $this; } } src/Geometry/Pixel.php000064400000001601150731647100010722 0ustar00background = $background; return $this; } /** * {@inheritdoc} * * @see DrawableInterface::backgroundColor() */ public function backgroundColor(): ColorInterface { return $this->background; } } src/Geometry/Point.php000064400000004431150731647100010736 0ustar00x = $x; return $this; } /** * {@inheritdoc} * * @see PointInterface::x() */ public function x(): int { return $this->x; } /** * {@inheritdoc} * * @see PointInterface::setY() */ public function setY(int $y): self { $this->y = $y; return $this; } /** * {@inheritdoc} * * @see PointInterface::y() */ public function y(): int { return $this->y; } /** * {@inheritdoc} * * @see PointInterface::moveX() */ public function moveX(int $value): self { $this->x += $value; return $this; } /** * {@inheritdoc} * * @see PointInterface::moveY() */ public function moveY(int $value): self { $this->y += $value; return $this; } /** * {@inheritdoc} * * @see PointInterface::move() */ public function move(int $x, int $y): self { return $this->moveX($x)->moveY($y); } /** * {@inheritdoc} * * @see PointInterface::setPosition() */ public function setPosition(int $x, int $y): self { $this->setX($x); $this->setY($y); return $this; } /** * {@inheritdoc} * * @see PointInterface::rotate() */ public function rotate(float $angle, PointInterface $pivot): self { $sin = round(sin(deg2rad($angle)), 6); $cos = round(cos(deg2rad($angle)), 6); return $this->setPosition( intval($cos * ($this->x() - $pivot->x()) - $sin * ($this->y() - $pivot->y()) + $pivot->x()), intval($sin * ($this->x() - $pivot->x()) + $cos * ($this->y() - $pivot->y()) + $pivot->y()) ); } } src/Geometry/Polygon.php000064400000023705150731647100011301 0ustar00 * @implements ArrayAccess */ class Polygon implements IteratorAggregate, Countable, ArrayAccess, DrawableInterface { use HasBorder; use HasBackgroundColor; /** * Create new polygon instance * * @param array $points * @param PointInterface $pivot * @return void */ public function __construct( protected array $points = [], protected PointInterface $pivot = new Point() ) { } /** * {@inheritdoc} * * @see DrawableInterface::position() */ public function position(): PointInterface { return $this->pivot; } /** * {@inheritdoc} * * @see DrawableInterface::setPosition() */ public function setPosition(PointInterface $position): self { $this->pivot = $position; return $this; } /** * Implement iteration through all points of polygon * * @return Traversable */ public function getIterator(): Traversable { return new ArrayIterator($this->points); } /** * Return current pivot point * * @return PointInterface */ public function pivot(): PointInterface { return $this->pivot; } /** * Change pivot point to given point * * @param PointInterface $pivot * @return Polygon */ public function setPivot(PointInterface $pivot): self { $this->pivot = $pivot; return $this; } /** * Return first point of polygon * * @return ?PointInterface */ public function first(): ?PointInterface { if ($point = reset($this->points)) { return $point; } return null; } /** * Return last point of polygon * * @return ?PointInterface */ public function last(): ?PointInterface { if ($point = end($this->points)) { return $point; } return null; } /** * Return polygon's point count * * @return int */ public function count(): int { return count($this->points); } /** * Determine if point exists at given offset * * @param mixed $offset * @return bool */ public function offsetExists($offset): bool { return array_key_exists($offset, $this->points); } /** * Return point at given offset * * @param mixed $offset * @return PointInterface */ public function offsetGet($offset): mixed { return $this->points[$offset]; } /** * Set point at given offset * * @param mixed $offset * @param PointInterface $value * @return void */ public function offsetSet($offset, $value): void { $this->points[$offset] = $value; } /** * Unset offset at given offset * * @param mixed $offset * @return void */ public function offsetUnset($offset): void { unset($this->points[$offset]); } /** * Add given point to polygon * * @param PointInterface $point * @return Polygon */ public function addPoint(PointInterface $point): self { $this->points[] = $point; return $this; } /** * Calculate total horizontal span of polygon * * @return int */ public function width(): int { return abs($this->mostLeftPoint()->x() - $this->mostRightPoint()->x()); } /** * Calculate total vertical span of polygon * * @return int */ public function height(): int { return abs($this->mostBottomPoint()->y() - $this->mostTopPoint()->y()); } /** * Return most left point of all points in polygon * * @return PointInterface */ public function mostLeftPoint(): PointInterface { $points = []; foreach ($this->points as $point) { $points[] = $point; } usort($points, function ($a, $b) { if ($a->x() === $b->x()) { return 0; } return $a->x() < $b->x() ? -1 : 1; }); return $points[0]; } /** * Return most right point in polygon * * @return PointInterface */ public function mostRightPoint(): PointInterface { $points = []; foreach ($this->points as $point) { $points[] = $point; } usort($points, function ($a, $b) { if ($a->x() === $b->x()) { return 0; } return $a->x() > $b->x() ? -1 : 1; }); return $points[0]; } /** * Return most top point in polygon * * @return PointInterface */ public function mostTopPoint(): PointInterface { $points = []; foreach ($this->points as $point) { $points[] = $point; } usort($points, function ($a, $b) { if ($a->y() === $b->y()) { return 0; } return $a->y() > $b->y() ? -1 : 1; }); return $points[0]; } /** * Return most bottom point in polygon * * @return PointInterface */ public function mostBottomPoint(): PointInterface { $points = []; foreach ($this->points as $point) { $points[] = $point; } usort($points, function ($a, $b) { if ($a->y() === $b->y()) { return 0; } return $a->y() < $b->y() ? -1 : 1; }); return $points[0]; } /** * Return point in absolute center of the polygon * * @return PointInterface */ public function centerPoint(): PointInterface { return new Point( $this->mostRightPoint()->x() - (intval(round($this->width() / 2))), $this->mostTopPoint()->y() - (intval(round($this->height() / 2))) ); } /** * Align all points of polygon horizontally to given position around pivot point * * @param string $position * @return Polygon */ public function align(string $position): self { switch (strtolower($position)) { case 'center': case 'middle': $diff = $this->centerPoint()->x() - $this->pivot()->x(); break; case 'right': $diff = $this->mostRightPoint()->x() - $this->pivot()->x(); break; default: case 'left': $diff = $this->mostLeftPoint()->x() - $this->pivot()->x(); break; } foreach ($this->points as $point) { $point->setX( intval($point->x() - $diff) ); } return $this; } /** * Align all points of polygon vertically to given position around pivot point * * @param string $position * @return Polygon */ public function valign(string $position): self { switch (strtolower($position)) { case 'center': case 'middle': $diff = $this->centerPoint()->y() - $this->pivot()->y(); break; case 'top': $diff = $this->mostTopPoint()->y() - $this->pivot()->y() - $this->height(); break; default: case 'bottom': $diff = $this->mostBottomPoint()->y() - $this->pivot()->y() + $this->height(); break; } foreach ($this->points as $point) { $point->setY( intval($point->y() - $diff), ); } return $this; } /** * Rotate points of polygon around pivot point with given angle * * @param float $angle * @return Polygon */ public function rotate(float $angle): self { $sin = sin(deg2rad($angle)); $cos = cos(deg2rad($angle)); foreach ($this->points as $point) { // translate point to pivot $point->setX( intval($point->x() - $this->pivot()->x()), ); $point->setY( intval($point->y() - $this->pivot()->y()), ); // rotate point $x = $point->x() * $cos - $point->y() * $sin; $y = $point->x() * $sin + $point->y() * $cos; // translate point back $point->setX( intval($x + $this->pivot()->x()), ); $point->setY( intval($y + $this->pivot()->y()), ); } return $this; } /** * Move all points by given amount on the x-axis * * @param int $amount * @return Polygon */ public function movePointsX(int $amount): self { foreach ($this->points as $point) { $point->moveX($amount); } return $this; } /** * Move all points by given amount on the y-axis * * @param int $amount * @return Polygon */ public function movePointsY(int $amount): self { foreach ($this->points as $point) { $point->moveY($amount); } return $this; } /** * Return array of all x/y values of all points of polygon * * @return array */ public function toArray(): array { $coordinates = []; foreach ($this->points as $point) { $coordinates[] = $point->x(); $coordinates[] = $point->y(); } return $coordinates; } } src/Geometry/Rectangle.php000064400000022015150731647100011547 0ustar00addPoint(new Point($this->pivot->x(), $this->pivot->y())); $this->addPoint(new Point($this->pivot->x() + $width, $this->pivot->y())); $this->addPoint(new Point($this->pivot->x() + $width, $this->pivot->y() - $height)); $this->addPoint(new Point($this->pivot->x(), $this->pivot->y() - $height)); } /** * Set size of rectangle * * @param int $width * @param int $height * @return Rectangle */ public function setSize(int $width, int $height): self { return $this->setWidth($width)->setHeight($height); } /** * Set width of rectangle * * @param int $width * @return Rectangle */ public function setWidth(int $width): self { $this[1]->setX($this[0]->x() + $width); $this[2]->setX($this[3]->x() + $width); return $this; } /** * Set height of rectangle * * @param int $height * @return Rectangle */ public function setHeight(int $height): self { $this[2]->setY($this[1]->y() + $height); $this[3]->setY($this[0]->y() + $height); return $this; } /** * Return pivot point of rectangle * * @return PointInterface */ public function pivot(): PointInterface { return $this->pivot; } /** * Set pivot point of rectangle * * @param PointInterface $pivot * @return Rectangle */ public function setPivot(PointInterface $pivot): self { $this->pivot = $pivot; return $this; } /** * Move pivot to the given position in the rectangle and adjust the new * position by given offset values. * * @param string $position * @param int $offset_x * @param int $offset_y * @return Rectangle */ public function movePivot(string $position, int $offset_x = 0, int $offset_y = 0): self { switch (strtolower($position)) { case 'top': case 'top-center': case 'top-middle': case 'center-top': case 'middle-top': $x = intval(round($this->width() / 2)) + $offset_x; $y = 0 + $offset_y; break; case 'top-right': case 'right-top': $x = $this->width() - $offset_x; $y = 0 + $offset_y; break; case 'left': case 'left-center': case 'left-middle': case 'center-left': case 'middle-left': $x = 0 + $offset_x; $y = intval(round($this->height() / 2)) + $offset_y; break; case 'right': case 'right-center': case 'right-middle': case 'center-right': case 'middle-right': $x = $this->width() - $offset_x; $y = intval(round($this->height() / 2)) + $offset_y; break; case 'bottom-left': case 'left-bottom': $x = 0 + $offset_x; $y = $this->height() - $offset_y; break; case 'bottom': case 'bottom-center': case 'bottom-middle': case 'center-bottom': case 'middle-bottom': $x = intval(round($this->width() / 2)) + $offset_x; $y = $this->height() - $offset_y; break; case 'bottom-right': case 'right-bottom': $x = $this->width() - $offset_x; $y = $this->height() - $offset_y; break; case 'center': case 'middle': case 'center-center': case 'middle-middle': $x = intval(round($this->width() / 2)) + $offset_x; $y = intval(round($this->height() / 2)) + $offset_y; break; default: case 'top-left': case 'left-top': $x = 0 + $offset_x; $y = 0 + $offset_y; break; } $this->pivot->setPosition($x, $y); return $this; } /** * Align pivot relative to given size at given position * * @param SizeInterface $size * @param string $position * @return Rectangle */ public function alignPivotTo(SizeInterface $size, string $position): self { $reference = new self($size->width(), $size->height()); $reference->movePivot($position); $this->movePivot($position)->setPivot( $reference->relativePositionTo($this) ); return $this; } /** * Return relative position to given rectangle * * @param SizeInterface $rectangle * @return PointInterface */ public function relativePositionTo(SizeInterface $rectangle): PointInterface { return new Point( $this->pivot()->x() - $rectangle->pivot()->x(), $this->pivot()->y() - $rectangle->pivot()->y() ); } /** * Return aspect ration of rectangle * * @return float */ public function aspectRatio(): float { return $this->width() / $this->height(); } /** * Determine if rectangle fits into given rectangle * * @param SizeInterface $size * @return bool */ public function fitsInto(SizeInterface $size): bool { if ($this->width() > $size->width()) { return false; } if ($this->height() > $size->height()) { return false; } return true; } /** * Determine if rectangle has landscape format * * @return bool */ public function isLandscape(): bool { return $this->width() > $this->height(); } /** * Determine if rectangle has landscape format * * @return bool */ public function isPortrait(): bool { return $this->width() < $this->height(); } /** * Return most top left point of rectangle * * @return PointInterface */ public function topLeftPoint(): PointInterface { return $this->points[0]; } /** * Return bottom right point of rectangle * * @return PointInterface */ public function bottomRightPoint(): PointInterface { return $this->points[2]; } /** * @see SizeInterface::resize() * * @throws GeometryException */ public function resize(?int $width = null, ?int $height = null): SizeInterface { return $this->resizer($width, $height)->resize($this); } /** * @see SizeInterface::resizeDown() * * @throws GeometryException */ public function resizeDown(?int $width = null, ?int $height = null): SizeInterface { return $this->resizer($width, $height)->resizeDown($this); } /** * @see SizeInterface::scale() * * @throws GeometryException */ public function scale(?int $width = null, ?int $height = null): SizeInterface { return $this->resizer($width, $height)->scale($this); } /** * @see SizeInterface::scaleDown() * * @throws GeometryException */ public function scaleDown(?int $width = null, ?int $height = null): SizeInterface { return $this->resizer($width, $height)->scaleDown($this); } /** * @see SizeInterface::cover() * * @throws GeometryException */ public function cover(int $width, int $height): SizeInterface { return $this->resizer($width, $height)->cover($this); } /** * @see SizeInterface::contain() * * @throws GeometryException */ public function contain(int $width, int $height): SizeInterface { return $this->resizer($width, $height)->contain($this); } /** * @see SizeInterface::containMax() * * @throws GeometryException */ public function containMax(int $width, int $height): SizeInterface { return $this->resizer($width, $height)->containDown($this); } /** * Create resizer instance with given target size * * @param null|int $width * @param null|int $height * @throws GeometryException * @return RectangleResizer */ protected function resizer(?int $width = null, ?int $height = null): RectangleResizer { return new RectangleResizer($width, $height); } } src/Geometry/Tools/RectangleResizer.php000064400000024022150731647100014213 0ustar00width); } /** * Return target width of resizer if available * * @return null|int */ protected function getTargetWidth(): ?int { return $this->hasTargetWidth() ? $this->width : null; } /** * Determine if resize has target height * * @return bool */ protected function hasTargetHeight(): bool { return is_integer($this->height); } /** * Return target width of resizer if available * * @return null|int */ protected function getTargetHeight(): ?int { return $this->hasTargetHeight() ? $this->height : null; } /** * Return target size object * * @throws GeometryException * @return SizeInterface */ protected function getTargetSize(): SizeInterface { if (!$this->hasTargetWidth() || !$this->hasTargetHeight()) { throw new GeometryException('Target size needs width and height.'); } return new Rectangle($this->width, $this->height); } /** * Set target width of resizer * * @param int $width * @return RectangleResizer */ public function toWidth(int $width): self { $this->width = $width; return $this; } /** * Set target height of resizer * * @param int $height * @return RectangleResizer */ public function toHeight(int $height): self { $this->height = $height; return $this; } /** * Set target size to given size object * * @param SizeInterface $size * @return RectangleResizer */ public function toSize(SizeInterface $size): self { $this->width = $size->width(); $this->height = $size->height(); return $this; } /** * Get proportinal width * * @param SizeInterface $size * @return int */ protected function getProportionalWidth(SizeInterface $size): int { if (!$this->hasTargetHeight()) { return $size->width(); } return max([1, (int) round($this->height * $size->aspectRatio())]); } /** * Get proportinal height * * @param SizeInterface $size * @return int */ protected function getProportionalHeight(SizeInterface $size): int { if (!$this->hasTargetWidth()) { return $size->height(); } return max([1, (int) round($this->width / $size->aspectRatio())]); } /** * Resize given size to target size of the resizer * * @param SizeInterface $size * @return SizeInterface */ public function resize(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); if ($width = $this->getTargetWidth()) { $resized->setWidth($width); } if ($height = $this->getTargetHeight()) { $resized->setHeight($height); } return $resized; } /** * Resize given size to target size of the resizer but do not exceed original size * * @param SizeInterface $size * @return SizeInterface */ public function resizeDown(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); if ($width = $this->getTargetWidth()) { $resized->setWidth( min($width, $size->width()) ); } if ($height = $this->getTargetHeight()) { $resized->setHeight( min($height, $size->height()) ); } return $resized; } /** * Resize given size to target size proportinally * * @param SizeInterface $size * @return SizeInterface */ public function scale(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); if ($this->hasTargetWidth() && $this->hasTargetHeight()) { $resized->setWidth(min( $this->getProportionalWidth($size), $this->getTargetWidth() )); $resized->setHeight(min( $this->getProportionalHeight($size), $this->getTargetHeight() )); } elseif ($this->hasTargetWidth()) { $resized->setWidth($this->getTargetWidth()); $resized->setHeight($this->getProportionalHeight($size)); } elseif ($this->hasTargetHeight()) { $resized->setWidth($this->getProportionalWidth($size)); $resized->setHeight($this->getTargetHeight()); } return $resized; } /** * Resize given size to target size proportinally but do not exceed original size * * @param SizeInterface $size * @return SizeInterface */ public function scaleDown(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); if ($this->hasTargetWidth() && $this->hasTargetHeight()) { $resized->setWidth(min( $this->getProportionalWidth($size), $this->getTargetWidth(), $size->width() )); $resized->setHeight(min( $this->getProportionalHeight($size), $this->getTargetHeight(), $size->height() )); } elseif ($this->hasTargetWidth()) { $resized->setWidth(min( $this->getTargetWidth(), $size->width() )); $resized->setHeight(min( $this->getProportionalHeight($size), $size->height() )); } elseif ($this->hasTargetHeight()) { $resized->setWidth(min( $this->getProportionalWidth($size), $size->width() )); $resized->setHeight(min( $this->getTargetHeight(), $size->height() )); } return $resized; } /** * Scale given size to cover target size * * @param SizeInterface $size Size to be resized * @throws GeometryException * @return SizeInterface */ public function cover(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); // auto height $resized->setWidth($this->getTargetWidth()); $resized->setHeight($this->getProportionalHeight($size)); if ($resized->fitsInto($this->getTargetSize())) { // auto width $resized->setWidth($this->getProportionalWidth($size)); $resized->setHeight($this->getTargetHeight()); } return $resized; } /** * Scale given size to contain target size * * @param SizeInterface $size Size to be resized * @throws GeometryException * @return SizeInterface */ public function contain(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); // auto height $resized->setWidth($this->getTargetWidth()); $resized->setHeight($this->getProportionalHeight($size)); if (!$resized->fitsInto($this->getTargetSize())) { // auto width $resized->setWidth($this->getProportionalWidth($size)); $resized->setHeight($this->getTargetHeight()); } return $resized; } /** * Scale given size to contain target size but prevent upsizing * * @param SizeInterface $size Size to be resized * @throws GeometryException * @return SizeInterface */ public function containDown(SizeInterface $size): SizeInterface { $resized = new Rectangle($size->width(), $size->height()); // auto height $resized->setWidth( min($size->width(), $this->getTargetWidth()) ); $resized->setHeight( min($size->height(), $this->getProportionalHeight($size)) ); if (!$resized->fitsInto($this->getTargetSize())) { // auto width $resized->setWidth( min($size->width(), $this->getProportionalWidth($size)) ); $resized->setHeight( min($size->height(), $this->getTargetHeight()) ); } return $resized; } /** * Crop target size out of given size at given position (i.e. move the pivot point) * * @param SizeInterface $size * @param string $position * @return SizeInterface */ public function crop(SizeInterface $size, string $position = 'top-left'): SizeInterface { return $this->resize($size)->alignPivotTo( $size->movePivot($position), $position ); } } src/Geometry/Traits/HasBackgroundColor.php000064400000001413150731647100014622 0ustar00backgroundColor = $color; return $this; } /** * {@inheritdoc} * * @see DrawableInterface::backgroundColor() */ public function backgroundColor(): mixed { return $this->backgroundColor; } /** * {@inheritdoc} * * @see DrawableInterface::hasBackgroundColor() */ public function hasBackgroundColor(): bool { return !empty($this->backgroundColor); } } src/Geometry/Traits/HasBorder.php000064400000002571150731647100012767 0ustar00setBorderSize($size)->setBorderColor($color); } /** * {@inheritdoc} * * @see DrawableInterface::setBorderSize() */ public function setBorderSize(int $size): self { $this->borderSize = $size; return $this; } /** * {@inheritdoc} * * @see DrawableInterface::borderSize() */ public function borderSize(): int { return $this->borderSize; } /** * {@inheritdoc} * * @see DrawableInterface::setBorderColor() */ public function setBorderColor(mixed $color): self { $this->borderColor = $color; return $this; } /** * {@inheritdoc} * * @see DrawableInterface::borderColor() */ public function borderColor(): mixed { return $this->borderColor; } /** * {@inheritdoc} * * @see DrawableInterface::hasBorder() */ public function hasBorder(): bool { return $this->borderSize > 0 && !is_null($this->borderColor); } } src/Image.php000064400000065133150731647100007102 0ustar00origin = new Origin(); } /** * {@inheritdoc} * * @see ImageInterface::driver() */ public function driver(): DriverInterface { return $this->driver; } /** * {@inheritdoc} * * @see ImageInterface::core() */ public function core(): CoreInterface { return $this->core; } /** * {@inheritdoc} * * @see ImageInterface::origin() */ public function origin(): Origin { return $this->origin; } /** * {@inheritdoc} * * @see ImageInterface::setOrigin() */ public function setOrigin(Origin $origin): ImageInterface { $this->origin = $origin; return $this; } /** * {@inheritdoc} * * @see ImageInterface::count() */ public function count(): int { return $this->core->count(); } /** * Implementation of IteratorAggregate * * @return Traversable */ public function getIterator(): Traversable { return $this->core; } /** * {@inheritdoc} * * @see ImageInterface::isAnimated() */ public function isAnimated(): bool { return $this->count() > 1; } /** * {@inheritdoc} * * @see ImageInterface::removeAnimation( */ public function removeAnimation(int|string $position = 0): ImageInterface { return $this->modify(new RemoveAnimationModifier($position)); } /** * {@inheritdoc} * * @see ImageInterface::sliceAnimation() */ public function sliceAnimation(int $offset = 0, ?int $length = null): ImageInterface { return $this->modify(new SliceAnimationModifier($offset, $length)); } /** * {@inheritdoc} * * @see ImageInterface::loops() */ public function loops(): int { return $this->core->loops(); } /** * {@inheritdoc} * * @see ImageInterface::setLoops() */ public function setLoops(int $loops): ImageInterface { $this->core->setLoops($loops); return $this; } /** * {@inheritdoc} * * @see ImageInterface::exif() */ public function exif(?string $query = null): mixed { return is_null($query) ? $this->exif : $this->exif->get($query); } /** * {@inheritdoc} * * @see ImageInterface::setExif() */ public function setExif(CollectionInterface $exif): ImageInterface { $this->exif = $exif; return $this; } /** * {@inheritdoc} * * @see ImageInterface::modify() */ public function modify(ModifierInterface $modifier): ImageInterface { return $this->driver->specialize($modifier)->apply($this); } /** * {@inheritdoc} * * @see ImageInterface::analyze() */ public function analyze(AnalyzerInterface $analyzer): mixed { return $this->driver->specialize($analyzer)->analyze($this); } /** * {@inheritdoc} * * @see ImageInterface::encode() */ public function encode(EncoderInterface $encoder = new AutoEncoder()): EncodedImageInterface { return $this->driver->specialize($encoder)->encode($this); } /** * {@inheritdoc} * * @see ImageInterface::save() */ public function save(?string $path = null, mixed ...$options): ImageInterface { $path = is_null($path) ? $this->origin()->filePath() : $path; if (is_null($path)) { throw new EncoderException('Could not determine file path to save.'); } try { // try to determine encoding format by file extension of the path $encoded = $this->encodeByPath($path, ...$options); } catch (EncoderException) { // fallback to encoding format by media type $encoded = $this->encodeByMediaType(null, ...$options); } $encoded->save($path); return $this; } /** * {@inheritdoc} * * @see ImageInterface::width() */ public function width(): int { return $this->analyze(new WidthAnalyzer()); } /** * {@inheritdoc} * * @see ImageInterface::height() */ public function height(): int { return $this->analyze(new HeightAnalyzer()); } /** * {@inheritdoc} * * @see ImageInterface::size() */ public function size(): SizeInterface { return new Rectangle($this->width(), $this->height()); } /** * {@inheritdoc} * * @see ImageInterface::colorspace() */ public function colorspace(): ColorspaceInterface { return $this->analyze(new ColorspaceAnalyzer()); } /** * {@inheritdoc} * * @see ImageInterface::setColorspace() */ public function setColorspace(string|ColorspaceInterface $colorspace): ImageInterface { return $this->modify(new ColorspaceModifier($colorspace)); } /** * {@inheritdoc} * * @see ImageInterface::resolution() */ public function resolution(): ResolutionInterface { return $this->analyze(new ResolutionAnalyzer()); } /** * {@inheritdoc} * * @see ImageInterface::setResolution() */ public function setResolution(float $x, float $y): ImageInterface { return $this->modify(new ResolutionModifier($x, $y)); } /** * {@inheritdoc} * * @see ImageInterface::pickColor() */ public function pickColor(int $x, int $y, int $frame_key = 0): ColorInterface { return $this->analyze(new PixelColorAnalyzer($x, $y, $frame_key)); } /** * {@inheritdoc} * * @see ImageInterface::pickColors() */ public function pickColors(int $x, int $y): CollectionInterface { return $this->analyze(new PixelColorsAnalyzer($x, $y)); } /** * {@inheritdoc} * * @see ImageInterface::blendingColor() */ public function blendingColor(): ColorInterface { return $this->driver()->handleInput( $this->driver()->config()->blendingColor ); } /** * {@inheritdoc} * * @see ImageInterface::setBlendingColor() */ public function setBlendingColor(mixed $color): ImageInterface { $this->driver()->config()->setOptions( blendingColor: $this->driver()->handleInput($color) ); return $this; } /** * {@inheritdoc} * * @see ImageInterface::blendTransparency() */ public function blendTransparency(mixed $color = null): ImageInterface { return $this->modify(new BlendTransparencyModifier($color)); } /** * {@inheritdoc} * * @see ImageInterface::profile() */ public function profile(): ProfileInterface { return $this->analyze(new ProfileAnalyzer()); } /** * {@inheritdoc} * * @see ImageInterface::setProfile() */ public function setProfile(ProfileInterface $profile): ImageInterface { return $this->modify(new ProfileModifier($profile)); } /** * {@inheritdoc} * * @see ImageInterface::removeProfile() */ public function removeProfile(): ImageInterface { return $this->modify(new ProfileRemovalModifier()); } /** * {@inheritdoc} * * @see ImageInterface::reduceColors() */ public function reduceColors(int $limit, mixed $background = 'transparent'): ImageInterface { return $this->modify(new QuantizeColorsModifier($limit, $background)); } /** * {@inheritdoc} * * @see ImageInterface::sharpen() */ public function sharpen(int $amount = 10): ImageInterface { return $this->modify(new SharpenModifier($amount)); } /** * {@inheritdoc} * * @see ImageInterface::invert() */ public function invert(): ImageInterface { return $this->modify(new InvertModifier()); } /** * {@inheritdoc} * * @see ImageInterface::pixelate() */ public function pixelate(int $size): ImageInterface { return $this->modify(new PixelateModifier($size)); } /** * {@inheritdoc} * * @see ImageInterface::greyscale() */ public function greyscale(): ImageInterface { return $this->modify(new GreyscaleModifier()); } /** * {@inheritdoc} * * @see ImageInterface::brightness() */ public function brightness(int $level): ImageInterface { return $this->modify(new BrightnessModifier($level)); } /** * {@inheritdoc} * * @see ImageInterface::contrast() */ public function contrast(int $level): ImageInterface { return $this->modify(new ContrastModifier($level)); } /** * {@inheritdoc} * * @see ImageInterface::gamma() */ public function gamma(float $gamma): ImageInterface { return $this->modify(new GammaModifier($gamma)); } /** * {@inheritdoc} * * @see ImageInterface::colorize() */ public function colorize(int $red = 0, int $green = 0, int $blue = 0): ImageInterface { return $this->modify(new ColorizeModifier($red, $green, $blue)); } /** * {@inheritdoc} * * @see ImageInterface::flip() */ public function flip(): ImageInterface { return $this->modify(new FlipModifier()); } /** * {@inheritdoc} * * @see ImageInterface::flop() */ public function flop(): ImageInterface { return $this->modify(new FlopModifier()); } /** * {@inheritdoc} * * @see ImageInterface::blur() */ public function blur(int $amount = 5): ImageInterface { return $this->modify(new BlurModifier($amount)); } /** * {@inheritdoc} * * @see ImageInterface::rotate() */ public function rotate(float $angle, mixed $background = 'ffffff'): ImageInterface { return $this->modify(new RotateModifier($angle, $background)); } /** * {@inheritdoc} * * @see ImageInterface::orient() */ public function orient(): ImageInterface { return $this->modify(new AlignRotationModifier()); } /** * {@inheritdoc} * * @see ImageInterface::text() */ public function text(string $text, int $x, int $y, callable|Closure|FontInterface $font): ImageInterface { return $this->modify( new TextModifier( $text, new Point($x, $y), call_user_func(new FontFactory($font)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::resize() */ public function resize(?int $width = null, ?int $height = null): ImageInterface { return $this->modify(new ResizeModifier($width, $height)); } /** * {@inheritdoc} * * @see ImageInterface::resizeDown() */ public function resizeDown(?int $width = null, ?int $height = null): ImageInterface { return $this->modify(new ResizeDownModifier($width, $height)); } /** * {@inheritdoc} * * @see ImageInterface::scale() */ public function scale(?int $width = null, ?int $height = null): ImageInterface { return $this->modify(new ScaleModifier($width, $height)); } /** * {@inheritdoc} * * @see ImageInterface::scaleDown() */ public function scaleDown(?int $width = null, ?int $height = null): ImageInterface { return $this->modify(new ScaleDownModifier($width, $height)); } /** * {@inheritdoc} * * @see ImageInterface::cover() */ public function cover(int $width, int $height, string $position = 'center'): ImageInterface { return $this->modify(new CoverModifier($width, $height, $position)); } /** * {@inheritdoc} * * @see ImageInterface::coverDown() */ public function coverDown(int $width, int $height, string $position = 'center'): ImageInterface { return $this->modify(new CoverDownModifier($width, $height, $position)); } /** * {@inheritdoc} * * @see ImageInterface::resizeCanvas() */ public function resizeCanvas( ?int $width = null, ?int $height = null, mixed $background = 'ffffff', string $position = 'center' ): ImageInterface { return $this->modify(new ResizeCanvasModifier($width, $height, $background, $position)); } /** * {@inheritdoc} * * @see ImageInterface::resizeCanvasRelative() */ public function resizeCanvasRelative( ?int $width = null, ?int $height = null, mixed $background = 'ffffff', string $position = 'center' ): ImageInterface { return $this->modify(new ResizeCanvasRelativeModifier($width, $height, $background, $position)); } /** * {@inheritdoc} * * @see ImageInterface::padDown() */ public function pad( int $width, int $height, mixed $background = 'ffffff', string $position = 'center' ): ImageInterface { return $this->modify(new PadModifier($width, $height, $background, $position)); } /** * {@inheritdoc} * * @see ImageInterface::pad() */ public function contain( int $width, int $height, mixed $background = 'ffffff', string $position = 'center' ): ImageInterface { return $this->modify(new ContainModifier($width, $height, $background, $position)); } /** * {@inheritdoc} * * @see ImageInterface::crop() */ public function crop( int $width, int $height, int $offset_x = 0, int $offset_y = 0, mixed $background = 'ffffff', string $position = 'top-left' ): ImageInterface { return $this->modify(new CropModifier($width, $height, $offset_x, $offset_y, $background, $position)); } /** * {@inheritdoc} * * @see ImageInterface::trim() */ public function trim(int $tolerance = 0): ImageInterface { return $this->modify(new TrimModifier($tolerance)); } /** * {@inheritdoc} * * @see ImageInterface::place() */ public function place( mixed $element, string $position = 'top-left', int $offset_x = 0, int $offset_y = 0, int $opacity = 100 ): ImageInterface { return $this->modify(new PlaceModifier($element, $position, $offset_x, $offset_y, $opacity)); } /** * {@inheritdoc} * * @see ImageInterface::fill() */ public function fill(mixed $color, ?int $x = null, ?int $y = null): ImageInterface { return $this->modify( new FillModifier( $color, is_null($x) || is_null($y) ? null : new Point($x, $y), ), ); } /** * {@inheritdoc} * * @see ImageInterface::drawPixel() */ public function drawPixel(int $x, int $y, mixed $color): ImageInterface { return $this->modify(new DrawPixelModifier(new Point($x, $y), $color)); } /** * {@inheritdoc} * * @see ImageInterface::drawRectangle() */ public function drawRectangle(int $x, int $y, callable|Closure|Rectangle $init): ImageInterface { return $this->modify( new DrawRectangleModifier( call_user_func(new RectangleFactory(new Point($x, $y), $init)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::drawEllipse() */ public function drawEllipse(int $x, int $y, callable|Closure|Ellipse $init): ImageInterface { return $this->modify( new DrawEllipseModifier( call_user_func(new EllipseFactory(new Point($x, $y), $init)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::drawCircle() */ public function drawCircle(int $x, int $y, callable|Closure|Circle $init): ImageInterface { return $this->modify( new DrawEllipseModifier( call_user_func(new CircleFactory(new Point($x, $y), $init)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::drawPolygon() */ public function drawPolygon(callable|Closure|Polygon $init): ImageInterface { return $this->modify( new DrawPolygonModifier( call_user_func(new PolygonFactory($init)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::drawLine() */ public function drawLine(callable|Closure|Line $init): ImageInterface { return $this->modify( new DrawLineModifier( call_user_func(new LineFactory($init)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::drawBezier() */ public function drawBezier(callable|Closure|Bezier $init): ImageInterface { return $this->modify( new DrawBezierModifier( call_user_func(new BezierFactory($init)), ), ); } /** * {@inheritdoc} * * @see ImageInterface::encodeByMediaType() */ public function encodeByMediaType(null|string|MediaType $type = null, mixed ...$options): EncodedImageInterface { return $this->encode(new MediaTypeEncoder($type, ...$options)); } /** * {@inheritdoc} * * @see ImageInterface::encodeByExtension() */ public function encodeByExtension( null|string|FileExtension $extension = null, mixed ...$options ): EncodedImageInterface { return $this->encode(new FileExtensionEncoder($extension, ...$options)); } /** * {@inheritdoc} * * @see ImageInterface::encodeByPath() */ public function encodeByPath(?string $path = null, mixed ...$options): EncodedImageInterface { return $this->encode(new FilePathEncoder($path, ...$options)); } /** * {@inheritdoc} * * @see ImageInterface::toJpeg() */ public function toJpeg(mixed ...$options): EncodedImageInterface { return $this->encode(new JpegEncoder(...$options)); } /** * Alias of self::toJpeg() * * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toJpg(mixed ...$options): EncodedImageInterface { return $this->toJpeg(...$options); } /** * {@inheritdoc} * * @see ImageInterface::toJpeg() */ public function toJpeg2000(mixed ...$options): EncodedImageInterface { return $this->encode(new Jpeg2000Encoder(...$options)); } /** * ALias of self::toJpeg2000() * * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toJp2(mixed ...$options): EncodedImageInterface { return $this->toJpeg2000(...$options); } /** * {@inheritdoc} * * @see ImageInterface::toPng() */ public function toPng(mixed ...$options): EncodedImageInterface { return $this->encode(new PngEncoder(...$options)); } /** * {@inheritdoc} * * @see ImageInterface::toGif() */ public function toGif(mixed ...$options): EncodedImageInterface { return $this->encode(new GifEncoder(...$options)); } /** * {@inheritdoc} * * @see ImageInterface::toWebp() */ public function toWebp(mixed ...$options): EncodedImageInterface { return $this->encode(new WebpEncoder(...$options)); } /** * {@inheritdoc} * * @see ImageInterface::toBitmap() */ public function toBitmap(mixed ...$options): EncodedImageInterface { return $this->encode(new BmpEncoder(...$options)); } /** * Alias if self::toBitmap() * * @throws RuntimeException * @return EncodedImageInterface */ public function toBmp(mixed ...$options): EncodedImageInterface { return $this->toBitmap(...$options); } /** * {@inheritdoc} * * @see ImageInterface::toAvif() */ public function toAvif(mixed ...$options): EncodedImageInterface { return $this->encode(new AvifEncoder(...$options)); } /** * {@inheritdoc} * * @see ImageInterface::toTiff() */ public function toTiff(mixed ...$options): EncodedImageInterface { return $this->encode(new TiffEncoder(...$options)); } /** * Alias of self::toTiff() * * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toTif(mixed ...$options): EncodedImageInterface { return $this->toTiff(...$options); } /** * {@inheritdoc} * * @see ImageInterface::toHeic() */ public function toHeic(mixed ...$options): EncodedImageInterface { return $this->encode(new HeicEncoder(...$options)); } /** * Clone image * * @return void */ public function __clone(): void { $this->driver = clone $this->driver; $this->core = clone $this->core; $this->exif = clone $this->exif; } } src/ImageManager.php000064400000007002150731647100010364 0ustar00driver = $this->resolveDriver($driver, ...$options); } /** * Create image manager with given driver * * @link https://image.intervention.io/v3/basics/image-manager * @param string|DriverInterface $driver * @param mixed $options * @return ImageManager */ public static function withDriver(string|DriverInterface $driver, mixed ...$options): self { return new self(self::resolveDriver($driver, ...$options)); } /** * Create image manager with GD driver * * @link https://image.intervention.io/v3/basics/image-manager#static-gd-driver-constructor * @param mixed $options * @throws DriverException * @return ImageManager */ public static function gd(mixed ...$options): self { return self::withDriver(new GdDriver(), ...$options); } /** * Create image manager with Imagick driver * * @link https://image.intervention.io/v3/basics/image-manager#static-imagick-driver-constructor * @param mixed $options * @throws DriverException * @return ImageManager */ public static function imagick(mixed ...$options): self { return self::withDriver(new ImagickDriver(), ...$options); } /** * {@inheritdoc} * * @see ImageManagerInterface::create() */ public function create(int $width, int $height): ImageInterface { return $this->driver->createImage($width, $height); } /** * {@inheritdoc} * * @see ImageManagerInterface::read() */ public function read(mixed $input, string|array|DecoderInterface $decoders = []): ImageInterface { return $this->driver->handleInput( $input, match (true) { is_string($decoders), is_a($decoders, DecoderInterface::class) => [$decoders], default => $decoders, } ); } /** * {@inheritdoc} * * @see ImageManagerInterface::animate() */ public function animate(callable $init): ImageInterface { return $this->driver->createAnimation($init); } /** * {@inheritdoc} * * @see ImageManagerInterface::driver() */ public function driver(): DriverInterface { return $this->driver; } /** * Return driver object * * @param string|DriverInterface $driver * @param mixed $options * @return DriverInterface */ private static function resolveDriver(string|DriverInterface $driver, mixed ...$options): DriverInterface { $driver = is_string($driver) ? new $driver() : $driver; $driver->config()->setOptions(...$options); return $driver; } } src/InputHandler.php000064400000011152150731647100010445 0ustar00 */ protected array $decoders = [ NativeObjectDecoder::class, ImageObjectDecoder::class, ColorObjectDecoder::class, RgbHexColorDecoder::class, RgbStringColorDecoder::class, CmykStringColorDecoder::class, HsvStringColorDecoder::class, HslStringColorDecoder::class, TransparentColorDecoder::class, HtmlColornameDecoder::class, FilePointerImageDecoder::class, FilePathImageDecoder::class, SplFileInfoImageDecoder::class, BinaryImageDecoder::class, DataUriImageDecoder::class, Base64ImageDecoder::class, EncodedImageObjectDecoder::class, ]; /** * Driver with which the decoder classes are specialized * * @var null|DriverInterface */ protected ?DriverInterface $driver = null; /** * Create new input handler instance with given decoder classnames * * @param array $decoders * @param DriverInterface $driver * @return void */ public function __construct(array $decoders = [], ?DriverInterface $driver = null) { $this->decoders = count($decoders) ? $decoders : $this->decoders; $this->driver = $driver; } /** * Static factory method * * @param array $decoders * @param null|DriverInterface $driver * @return InputHandler */ public static function withDecoders(array $decoders, ?DriverInterface $driver = null): self { return new self($decoders, $driver); } /** * {@inheritdoc} * * @see InputHandlerInterface::handle() */ public function handle($input): ImageInterface|ColorInterface { foreach ($this->decoders as $decoder) { try { // decode with driver specialized decoder return $this->resolve($decoder)->decode($input); } catch (DecoderException | NotSupportedException $e) { // try next decoder } } if (isset($e)) { throw new ($e::class)($e->getMessage()); } throw new DecoderException('Unable to decode input.'); } /** * Resolve the given classname to an decoder object * * @param string|DecoderInterface $decoder * @throws DriverException * @throws NotSupportedException * @return DecoderInterface */ private function resolve(string|DecoderInterface $decoder): DecoderInterface { if (($decoder instanceof DecoderInterface) && empty($this->driver)) { return $decoder; } if (($decoder instanceof DecoderInterface) && !empty($this->driver)) { return $this->driver->specialize($decoder); } if (empty($this->driver)) { return new $decoder(); } return $this->driver->specialize(new $decoder()); } } src/Interfaces/AnalyzerInterface.php000064400000000604150731647100013541 0ustar00 */ interface CollectionInterface extends Traversable { /** * Determine if the collection has item at given key * * @param int|string $key * @return bool */ public function has(int|string $key): bool; /** * Add item to collection * * @param mixed $item * @return CollectionInterface */ public function push($item): self; /** * Return item for given key or return default is key does not exist * * @param int|string $key * @param mixed $default * @return mixed */ public function get(int|string $key, $default = null): mixed; /** * Return item at given numeric position starting at 0 * * @param int $key * @param mixed $default * @return mixed */ public function getAtPosition(int $key = 0, $default = null): mixed; /** * Return first item in collection * * @return mixed */ public function first(): mixed; /** * Return last item in collection * * @return mixed */ public function last(): mixed; /** * Return item count of collection * * @return int */ public function count(): int; /** * Empty collection * * @return CollectionInterface */ public function empty(): self; /** * Transform collection as array * * @return array */ public function toArray(): array; /** * Extract items based on given values and discard the rest. * * @param int $offset * @param null|int $length * @return CollectionInterface */ public function slice(int $offset, ?int $length = 0): self; } src/Interfaces/ColorChannelInterface.php000064400000002766150731647100014336 0ustar00 */ public function toArray(): array; /** * Cast color object to hex encoded web color * * @return string */ public function toHex(string $prefix = ''): string; /** * Return array of all color channels * * @return array */ public function channels(): array; /** * Return array of normalized color channel values * * @return array */ public function normalize(): array; /** * Retrieve the color channel by its classname * * @param string $classname * @throws ColorException * @return ColorChannelInterface */ public function channel(string $classname): ColorChannelInterface; /** * Convert color to given colorspace * * @return ColorInterface */ public function convertTo(string|ColorspaceInterface $colorspace): self; /** * Determine if the current color is gray * * @return bool */ public function isGreyscale(): bool; /** * Determine if the current color is (semi) transparent * * @return bool */ public function isTransparent(): bool; /** * Determine whether the current color is completely transparent * * @return bool */ public function isClear(): bool; } src/Interfaces/ColorProcessorInterface.php000064400000001204150731647100014727 0ustar00 $normalized * @return ColorInterface */ public function colorFromNormalized(array $normalized): ColorInterface; } src/Interfaces/CoreInterface.php000064400000003436150731647100012652 0ustar00 */ public function setNative(mixed $native): self; /** * Count number of frames of animated image core * * @return int */ public function count(): int; /** * Return frame of given position in an animated image * * @param int $position * @throws AnimationException * @return FrameInterface */ public function frame(int $position): FrameInterface; /** * Add new frame to core * * @param FrameInterface $frame * @return CoreInterface */ public function add(FrameInterface $frame): self; /** * Return number of repetitions of an animated image * * @return int */ public function loops(): int; /** * Set the number of repetitions for an animation. Where a * value of 0 means infinite repetition. * * @param int $loops * @return CoreInterface */ public function setLoops(int $loops): self; /** * Get first frame in core * * @throws AnimationException * @return FrameInterface */ public function first(): FrameInterface; /** * Get last frame in core * * @throws AnimationException * @return FrameInterface */ public function last(): FrameInterface; } src/Interfaces/DecoderInterface.php000064400000000632150731647100013322 0ustar00 $objects * @throws NotSupportedException * @throws DriverException * @return array */ public function specializeMultiple(array $objects): array; /** * Create new image instance with the current driver in given dimensions * * @param int $width * @param int $height * @throws RuntimeException * @return ImageInterface */ public function createImage(int $width, int $height): ImageInterface; /** * Create new animated image * * @param callable $init * @throws RuntimeException * @return ImageInterface */ public function createAnimation(callable $init): ImageInterface; /** * Handle given input by decoding it to ImageInterface or ColorInterface * * @param mixed $input * @param array $decoders * @throws RuntimeException * @return ImageInterface|ColorInterface */ public function handleInput(mixed $input, array $decoders = []): ImageInterface|ColorInterface; /** * Return color processor for the given colorspace * * @param ColorspaceInterface $colorspace * @return ColorProcessorInterface */ public function colorProcessor(ColorspaceInterface $colorspace): ColorProcessorInterface; /** * Return font processor of the current driver * * @return FontProcessorInterface */ public function fontProcessor(): FontProcessorInterface; /** * Check whether all requirements for operating the driver are met and * throw exception if the check fails. * * @throws DriverException * @return void */ public function checkHealth(): void; /** * Check if the current driver supports the given format and if the * underlying PHP extension was built with support for the format. * * @param string|Format|FileExtension|MediaType $identifier * @return bool */ public function supports(string|Format|FileExtension|MediaType $identifier): bool; } src/Interfaces/EncodedImageInterface.php000064400000001027150731647100014260 0ustar00 */ interface ImageInterface extends IteratorAggregate, Countable { /** * Return driver of current image * * @return DriverInterface */ public function driver(): DriverInterface; /** * Return core of current image * * @return CoreInterface */ public function core(): CoreInterface; /** * Return the origin of the image * * @return Origin */ public function origin(): Origin; /** * Set the origin of the image * * @param Origin $origin * @return ImageInterface */ public function setOrigin(Origin $origin): self; /** * Return width of current image * * @link https://image.intervention.io/v3/basics/meta-information#reading-the-pixel-width * @throws RuntimeException * @return int */ public function width(): int; /** * Return height of current image * * @link https://image.intervention.io/v3/basics/meta-information#reading-the-pixel-height * @throws RuntimeException * @return int */ public function height(): int; /** * Return size of current image * * @link https://image.intervention.io/v3/basics/meta-information#reading-the-image-size-as-an-object * @throws RuntimeException * @return SizeInterface */ public function size(): SizeInterface; /** * Encode image with given encoder * * @link https://image.intervention.io/v3/basics/image-output#encoding-images * @param EncoderInterface $encoder * @throws RuntimeException * @return EncodedImageInterface */ public function encode(EncoderInterface $encoder = new AutoEncoder()): EncodedImageInterface; /** * Save the image to the specified path in the file system. If no path is * given, the image will be saved at its original location. * * @link https://image.intervention.io/v3/basics/image-output#writing-images-directly * @param null|string $path * @throws RuntimeException * @return ImageInterface */ public function save(?string $path = null, mixed ...$options): self; /** * Apply given modifier to current image * * @link https://image.intervention.io/v3/modifying/custom-modifiers * @param ModifierInterface $modifier * @throws RuntimeException * @return ImageInterface */ public function modify(ModifierInterface $modifier): self; /** * Analyzer current image with given analyzer * * @param AnalyzerInterface $analyzer * @throws RuntimeException * @return mixed */ public function analyze(AnalyzerInterface $analyzer): mixed; /** * Determine if current image is animated * * @link https://image.intervention.io/v3/modifying/animations#check-the-current-image-instance-for-animation * @return bool */ public function isAnimated(): bool; /** * Remove all frames but keep the one at the specified position * * It is possible to specify the position as integer or string values. * With the former, the exact position passed is searched for, while * string values must represent a percentage value between '0%' and '100%' * and the respective frame position is only determined approximately. * * @link https://image.intervention.io/v3/modifying/animations#removing-animation * @param int|string $position * @throws RuntimeException * @return ImageInterface */ public function removeAnimation(int|string $position = 0): self; /** * Extract animation frames based on given values and discard the rest * * @link https://image.intervention.io/v3/modifying/animations#changing-the-animation-frames * @param int $offset * @param null|int $length * @throws RuntimeException * @return ImageInterface */ public function sliceAnimation(int $offset = 0, ?int $length = null): self; /** * Return loop count of animated image * * @link https://image.intervention.io/v3/modifying/animations#reading-the-animation-iteration-count * @return int */ public function loops(): int; /** * Set loop count of animated image * * @link https://image.intervention.io/v3/modifying/animations#changing-the-animation-iteration-count * @param int $loops * @return ImageInterface */ public function setLoops(int $loops): self; /** * Return exif data of current image * * @link https://image.intervention.io/v3/basics/meta-information#exif-information * @return mixed */ public function exif(?string $query = null): mixed; /** * Set exif data for the image object * * @param CollectionInterface $exif * @return ImageInterface */ public function setExif(CollectionInterface $exif): self; /** * Return image resolution/density * * @link https://image.intervention.io/v3/basics/meta-information#image-resolution * @throws RuntimeException * @return ResolutionInterface */ public function resolution(): ResolutionInterface; /** * Set image resolution * * @link https://image.intervention.io/v3/basics/meta-information#image-resolution * @param float $x * @param float $y * @throws RuntimeException * @return ImageInterface */ public function setResolution(float $x, float $y): self; /** * Get the colorspace of the image * * @link https://image.intervention.io/v3/basics/colors#reading-the-colorspace * @throws RuntimeException * @return ColorspaceInterface */ public function colorspace(): ColorspaceInterface; /** * Transform image to given colorspace * * @link https://image.intervention.io/v3/basics/colors#changing-the-colorspace * @param string|ColorspaceInterface $colorspace * @throws RuntimeException * @return ImageInterface */ public function setColorspace(string|ColorspaceInterface $colorspace): self; /** * Return color of pixel at given position on given frame position * * @link https://image.intervention.io/v3/basics/colors#color-information * @param int $x * @param int $y * @param int $frame_key * @throws RuntimeException * @return ColorInterface */ public function pickColor(int $x, int $y, int $frame_key = 0): ColorInterface; /** * Return all colors of pixel at given position for all frames of image * * @link https://image.intervention.io/v3/basics/colors#color-information * @param int $x * @param int $y * @throws RuntimeException * @return CollectionInterface */ public function pickColors(int $x, int $y): CollectionInterface; /** * Return color that is mixed with transparent areas when converting to a format which * does not support transparency. * * @throws RuntimeException * @return ColorInterface */ public function blendingColor(): ColorInterface; /** * Set blending color will have no effect unless image is converted into a format * which does not support transparency. * * @param mixed $color * @throws RuntimeException * @return ImageInterface */ public function setBlendingColor(mixed $color): self; /** * Replace transparent areas of the image with given color * * @param mixed $color * @throws RuntimeException * @return ImageInterface */ public function blendTransparency(mixed $color = null): self; /** * Retrieve ICC color profile of image * * @link https://image.intervention.io/v3/basics/colors#color-profiles * @throws RuntimeException * @return ProfileInterface */ public function profile(): ProfileInterface; /** * Set given icc color profile to image * * @link https://image.intervention.io/v3/basics/colors#color-profiles * @param ProfileInterface $profile * @throws RuntimeException * @return ImageInterface */ public function setProfile(ProfileInterface $profile): self; /** * Remove ICC color profile from the current image * * @link https://image.intervention.io/v3/basics/colors#color-profiles * @throws RuntimeException * @return ImageInterface */ public function removeProfile(): self; /** * Apply color quantization to the current image * * @link https://image.intervention.io/v3/modifying/effects#reduce-colors * @param int $limit * @param mixed $background * @throws RuntimeException * @return ImageInterface */ public function reduceColors(int $limit, mixed $background = 'transparent'): self; /** * Sharpen the current image with given strength * * @link https://image.intervention.io/v3/modifying/effects#sharpening-effect * @param int $amount * @throws RuntimeException * @return ImageInterface */ public function sharpen(int $amount = 10): self; /** * Turn image into a greyscale version * * @link https://image.intervention.io/v3/modifying/effects#convert-image-to-a-greyscale-version * @throws RuntimeException * @return ImageInterface */ public function greyscale(): self; /** * Adjust brightness of the current image * * @link https://image.intervention.io/v3/modifying/effects#changing-the-brightness * @param int $level * @throws RuntimeException * @return ImageInterface */ public function brightness(int $level): self; /** * Adjust color contrast of the current image * * @link https://image.intervention.io/v3/modifying/effects#changing-the-contrast * @param int $level * @throws RuntimeException * @return ImageInterface */ public function contrast(int $level): self; /** * Apply gamma correction on the current image * * @link https://image.intervention.io/v3/modifying/effects#gamma-correction * @param float $gamma * @throws RuntimeException * @return ImageInterface */ public function gamma(float $gamma): self; /** * Adjust the intensity of the RGB color channels * * @link https://image.intervention.io/v3/modifying/effects#color-correction * @param int $red * @param int $green * @param int $blue * @throws RuntimeException * @return ImageInterface */ public function colorize(int $red = 0, int $green = 0, int $blue = 0): self; /** * Mirror the current image horizontally * * @link https://image.intervention.io/v3/modifying/effects#mirror-image-vertically * @throws RuntimeException * @return ImageInterface */ public function flip(): self; /** * Mirror the current image vertically * * @link https://image.intervention.io/v3/modifying/effects#mirror-image-horizontally * @throws RuntimeException * @return ImageInterface */ public function flop(): self; /** * Blur current image by given strength * * @link https://image.intervention.io/v3/modifying/effects#blur-effect * @param int $amount * @throws RuntimeException * @return ImageInterface */ public function blur(int $amount = 5): self; /** * Invert the colors of the current image * * @link https://image.intervention.io/v3/modifying/effects#invert-colors * @throws RuntimeException * @return ImageInterface */ public function invert(): self; /** * Apply pixelation filter effect on current image * * @link https://image.intervention.io/v3/modifying/effects#pixelation-effect * @param int $size * @throws RuntimeException * @return ImageInterface */ public function pixelate(int $size): self; /** * Rotate current image by given angle * * @link https://image.intervention.io/v3/modifying/effects#image-rotation * @param float $angle * @param string $background * @throws RuntimeException * @return ImageInterface */ public function rotate(float $angle, mixed $background = 'ffffff'): self; /** * Rotate the image to be upright according to exif information * * @link https://image.intervention.io/v3/modifying/effects#image-orientation-according-to-exif-data * @throws RuntimeException * @return ImageInterface */ public function orient(): self; /** * Draw text on image * * @link https://image.intervention.io/v3/modifying/text-fonts * @param string $text * @param int $x * @param int $y * @param callable|Closure|FontInterface $font * @throws RuntimeException * @return ImageInterface */ public function text(string $text, int $x, int $y, callable|Closure|FontInterface $font): self; /** * Resize image to the given width and/or height * * @link https://image.intervention.io/v3/modifying/resizing#simple-image-resizing * @param null|int $width * @param null|int $height * @throws RuntimeException * @return ImageInterface */ public function resize(?int $width = null, ?int $height = null): self; /** * Resize image to the given width and/or height without exceeding the original dimensions * * @link https://image.intervention.io/v3/modifying/resizing#resizing-without-exceeding-the-original-size * @param null|int $width * @param null|int $height * @throws RuntimeException * @return ImageInterface */ public function resizeDown(?int $width = null, ?int $height = null): self; /** * Resize image to the given width and/or height and keep the original aspect ratio * * @link https://image.intervention.io/v3/modifying/resizing#scaling-images * @param null|int $width * @param null|int $height * @throws RuntimeException * @return ImageInterface */ public function scale(?int $width = null, ?int $height = null): self; /** * Resize image to the given width and/or height, keep the original aspect ratio * and do not exceed the original image width or height * * @link https://image.intervention.io/v3/modifying/resizing#scaling-images-but-do-not-exceed-the-original-size * @param null|int $width * @param null|int $height * @throws RuntimeException * @return ImageInterface */ public function scaleDown(?int $width = null, ?int $height = null): self; /** * Takes the given dimensions and scales it to the largest possible size matching * the original size. Then this size is positioned on the original and cut out * before being resized to the desired size from the arguments * * @link https://image.intervention.io/v3/modifying/resizing#fitted-image-resizing * @param int $width * @param int $height * @param string $position * @throws RuntimeException * @return ImageInterface */ public function cover(int $width, int $height, string $position = 'center'): self; /** * Same as cover() but do not exceed the original image size * * @link https://image.intervention.io/v3/modifying/resizing#fitted-resizing-without-exceeding-the-original-size * @param int $width * @param int $height * @param string $position * @throws RuntimeException * @return ImageInterface */ public function coverDown(int $width, int $height, string $position = 'center'): self; /** * Resize the boundaries of the current image to given width and height. * An anchor position can be defined to determine where the original image * is fixed. A background color can be passed to define the color of the * new emerging areas. * * @link https://image.intervention.io/v3/modifying/resizing#resize-image-canvas * @param null|int $width * @param null|int $height * @param string $position * @param mixed $background * @throws RuntimeException * @return ImageInterface */ public function resizeCanvas( ?int $width = null, ?int $height = null, mixed $background = 'ffffff', string $position = 'center' ): self; /** * Resize canvas in the same way as resizeCanvas() but takes relative values * for the width and height, which will be added or subtracted to the * original image size. * * @link https://image.intervention.io/v3/modifying/resizing#resize-image-boundaries-relative-to-the-original * @param null|int $width * @param null|int $height * @param string $position * @param mixed $background * @throws RuntimeException * @return ImageInterface */ public function resizeCanvasRelative( ?int $width = null, ?int $height = null, mixed $background = 'ffffff', string $position = 'center' ): self; /** * Padded resizing means that the original image is scaled until it fits the * defined target size with unchanged aspect ratio. The original image is * not scaled up but only down. * * Compared to the cover() method, this method does not create cropped areas, * but possibly new empty areas on the sides of the result image. These are * filled with the specified background color. * * @link https://image.intervention.io/v3/modifying/resizing#padded-image-resizing * @param int $width * @param int $height * @param string $background * @param string $position * @throws RuntimeException * @return ImageInterface */ public function pad( int $width, int $height, mixed $background = 'ffffff', string $position = 'center' ): self; /** * This method does the same as pad(), but the original image is also scaled * up if the target size exceeds the original size. * * @link https://image.intervention.io/v3/modifying/resizing#padded-resizing-with-upscaling * @param int $width * @param int $height * @param string $background * @param string $position * @throws RuntimeException * @return ImageInterface */ public function contain( int $width, int $height, mixed $background = 'ffffff', string $position = 'center' ): self; /** * Cut out a rectangular part of the current image with given width and * height at a given position. Define optional x,y offset coordinates * to move the cutout by the given amount of pixels. * * @link https://image.intervention.io/v3/modifying/resizing#crop-image * @param int $width * @param int $height * @param int $offset_x * @param int $offset_y * @param mixed $background * @param string $position * @throws RuntimeException * @return ImageInterface */ public function crop( int $width, int $height, int $offset_x = 0, int $offset_y = 0, mixed $background = 'ffffff', string $position = 'top-left' ): self; /** * Trim the image by removing border areas of similar color within a the given tolerance * * @param int $tolerance * @throws RuntimeException * @throws AnimationException * @return ImageInterface */ public function trim(int $tolerance = 0): self; /** * Place another image into the current image instance * * @link https://image.intervention.io/v3/modifying/inserting * @param mixed $element * @param string $position * @param int $offset_x * @param int $offset_y * @param int $opacity * @throws RuntimeException * @return ImageInterface */ public function place( mixed $element, string $position = 'top-left', int $offset_x = 0, int $offset_y = 0, int $opacity = 100 ): self; /** * Fill image with given color * * If an optional position is specified for the filling process ln the form * of x and y coordinates, the process is executed as flood fill. This means * that the color at the specified position is taken as a reference and all * adjacent pixels are also filled with the filling color. * * If no coordinates are specified, the entire image area is filled. * * @link https://image.intervention.io/v3/modifying/drawing#fill-images-with-color * @param mixed $color * @param null|int $x * @param null|int $y * @throws RuntimeException * @return ImageInterface */ public function fill(mixed $color, ?int $x = null, ?int $y = null): self; /** * Draw a single pixel at given position defined by the coordinates x and y in a given color. * * @link https://image.intervention.io/v3/modifying/drawing#drawing-a-pixel * @param int $x * @param int $y * @param mixed $color * @throws RuntimeException * @return ImageInterface */ public function drawPixel(int $x, int $y, mixed $color): self; /** * Draw a rectangle on the current image * * @link https://image.intervention.io/v3/modifying/drawing#drawing-a-rectangle * @param int $x * @param int $y * @param callable|Closure|Rectangle $init * @throws RuntimeException * @return ImageInterface */ public function drawRectangle(int $x, int $y, callable|Closure|Rectangle $init): self; /** * Draw ellipse on the current image * * @link https://image.intervention.io/v3/modifying/drawing#drawing-ellipses * @param int $x * @param int $y * @param callable|Closure|Ellipse $init * @throws RuntimeException * @return ImageInterface */ public function drawEllipse(int $x, int $y, callable|Closure|Ellipse $init): self; /** * Draw circle on the current image * * @link https://image.intervention.io/v3/modifying/drawing#drawing-a-circle * @param int $x * @param int $y * @param callable|Closure|Circle $init * @throws RuntimeException * @return ImageInterface */ public function drawCircle(int $x, int $y, callable|Closure|Circle $init): self; /** * Draw a polygon on the current image * * @link https://image.intervention.io/v3/modifying/drawing#drawing-a-polygon * @param callable|Closure|Polygon $init * @throws RuntimeException * @return ImageInterface */ public function drawPolygon(callable|Closure|Polygon $init): self; /** * Draw a line on the current image * * @link https://image.intervention.io/v3/modifying/drawing#drawing-a-line * @param callable|Closure|Line $init * @throws RuntimeException * @return ImageInterface */ public function drawLine(callable|Closure|Line $init): self; /** * Draw a bezier curve on the current image * * @link https://image.intervention.io/v3/modifying/drawing#draw-bezier-curves * @param callable|Closure|Bezier $init * @throws RuntimeException * @return ImageInterface */ public function drawBezier(callable|Closure|Bezier $init): self; /** * Encode image to given media (mime) type. If no type is given the image * will be encoded to the format of the originally read image. * * @link https://image.intervention.io/v3/basics/image-output#encode-images-by-media-mime-type * @param null|string|MediaType $type * @throws RuntimeException * @return EncodedImageInterface */ public function encodeByMediaType(null|string|MediaType $type = null, mixed ...$options): EncodedImageInterface; /** * Encode the image into the format represented by the given extension. If no * extension is given the image will be encoded to the format of the * originally read image. * * @link https://image.intervention.io/v3/basics/image-output#encode-images-by-file-extension * @param null|string|FileExtension $extension * @throws RuntimeException * @return EncodedImageInterface */ public function encodeByExtension( null|string|FileExtension $extension = null, mixed ...$options ): EncodedImageInterface; /** * Encode the image into the format represented by the given extension of * the given file path extension is given the image will be encoded to * the format of the originally read image. * * @link https://image.intervention.io/v3/basics/image-output#encode-images-by-file-path * @param null|string $path * @throws RuntimeException * @return EncodedImageInterface */ public function encodeByPath(?string $path = null, mixed ...$options): EncodedImageInterface; /** * Encode image to JPEG format * * @link https://image.intervention.io/v3/basics/image-output#encoding-jpeg-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toJpeg(mixed ...$options): EncodedImageInterface; /** * Encode image to Jpeg2000 format * * @link https://image.intervention.io/v3/basics/image-output#encoding-jpeg-2000-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toJpeg2000(mixed ...$options): EncodedImageInterface; /** * Encode image to Webp format * * @link https://image.intervention.io/v3/basics/image-output#encoding-webp-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toWebp(mixed ...$options): EncodedImageInterface; /** * Encode image to PNG format * * @link https://image.intervention.io/v3/basics/image-output#encoding-png-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toPng(mixed ...$options): EncodedImageInterface; /** * Encode image to GIF format * * @link https://image.intervention.io/v3/basics/image-output#encoding-gif-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toGif(mixed ...$options): EncodedImageInterface; /** * Encode image to Bitmap format * * @link https://image.intervention.io/v3/basics/image-output#encoding-windows-bitmap-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toBitmap(mixed ...$options): EncodedImageInterface; /** * Encode image to AVIF format * * @link https://image.intervention.io/v3/basics/image-output#encoding-av1-image-file-format-avif * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toAvif(mixed ...$options): EncodedImageInterface; /** * Encode image to TIFF format * * @link https://image.intervention.io/v3/basics/image-output#encoding-tiff-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toTiff(mixed ...$options): EncodedImageInterface; /** * Encode image to HEIC format * * @link https://image.intervention.io/v3/basics/image-output#encoding-heic-format * @param mixed $options * @throws RuntimeException * @return EncodedImageInterface */ public function toHeic(mixed ...$options): EncodedImageInterface; } src/Interfaces/ImageManagerInterface.php000064400000004446150731647100014301 0ustar00|DecoderInterface $decoders * @throws RuntimeException * @return ImageInterface */ public function read(mixed $input, string|array|DecoderInterface $decoders = []): ImageInterface; /** * Create new animated image by given callback * * @link https://image.intervention.io/v3/basics/instantiation#creating-animations * @param callable $init * @throws RuntimeException * @return ImageInterface */ public function animate(callable $init): ImageInterface; /** * Return currently used driver * * @return DriverInterface */ public function driver(): DriverInterface; } src/Interfaces/InputHandlerInterface.php000064400000000666150731647100014361 0ustar00 */ public function specializable(): array; /** * Set the driver for which the object is specialized * * @param DriverInterface $driver * @throws DriverException * @return SpecializableInterface */ public function setDriver(DriverInterface $driver): self; /** * Return the driver for which the object was specialized * * @return DriverInterface */ public function driver(): DriverInterface; } src/Interfaces/SpecializedInterface.php000064400000000156150731647100014212 0ustar00 Format::JPEG, self::IMAGE_WEBP, self::IMAGE_X_WEBP => Format::WEBP, self::IMAGE_GIF => Format::GIF, self::IMAGE_PNG, self::IMAGE_X_PNG => Format::PNG, self::IMAGE_AVIF, self::IMAGE_X_AVIF => Format::AVIF, self::IMAGE_BMP, self::IMAGE_MS_BMP, self::IMAGE_X_BITMAP, self::IMAGE_X_BMP, self::IMAGE_X_MS_BMP, self::IMAGE_X_XBITMAP, self::IMAGE_X_WINDOWS_BMP, self::IMAGE_X_WIN_BITMAP => Format::BMP, self::IMAGE_TIFF => Format::TIFF, self::IMAGE_JP2, self::IMAGE_JPX, self::IMAGE_JPM => Format::JP2, self::IMAGE_HEIF, self::IMAGE_HEIC, self::IMAGE_X_HEIC => Format::HEIC, }; } /** * Return the possible file extension for the current media type * * @return array */ public function fileExtensions(): array { return $this->format()->fileExtensions(); } /** * Return the first file extension for the current media type * * @return FileExtension */ public function fileExtension(): FileExtension { return $this->format()->fileExtension(); } } src/ModifierStack.php000064400000002062150731647100010574 0ustar00 $modifiers * @return void */ public function __construct(protected array $modifiers) { } /** * Apply all modifiers in stack to the given image * * @param ImageInterface $image * @return ImageInterface */ public function apply(ImageInterface $image): ImageInterface { foreach ($this->modifiers as $modifier) { $modifier->apply($image); } return $image; } /** * Append new modifier to the stack * * @param ModifierInterface $modifier * @return ModifierStack */ public function push(ModifierInterface $modifier): self { $this->modifiers[] = $modifier; return $this; } } src/Modifiers/AbstractDrawModifier.php000064400000002357150731647100014040 0ustar00driver()->handleInput($this->drawable()->backgroundColor()); } catch (DecoderException) { return $this->driver()->handleInput('transparent'); } return $color; } /** * @throws RuntimeException */ public function borderColor(): ColorInterface { try { $color = $this->driver()->handleInput($this->drawable()->borderColor()); } catch (DecoderException) { return $this->driver()->handleInput('transparent'); } return $color; } } src/Modifiers/AlignRotationModifier.php000064400000000277150731647100014230 0ustar00target)) { return $this->target; } if (in_array($this->target, ['rgb', 'RGB', RgbColorspace::class])) { return new RgbColorspace(); } if (in_array($this->target, ['cmyk', 'CMYK', CmykColorspace::class])) { return new CmykColorspace(); } throw new NotSupportedException('Given colorspace is not supported.'); } } src/Modifiers/ContainModifier.php000064400000002104150731647100013040 0ustar00size() ->contain( $this->width, $this->height ) ->alignPivotTo( $this->getResizeSize($image), $this->position ); } public function getResizeSize(ImageInterface $image): SizeInterface { return new Rectangle($this->width, $this->height); } } src/Modifiers/ContrastModifier.php000064400000000537150731647100013252 0ustar00size(); $crop = new Rectangle($this->width, $this->height); return $crop->contain( $imagesize->width(), $imagesize->height() )->alignPivotTo($imagesize, $this->position); } /** * @throws RuntimeException */ public function getResizeSize(SizeInterface $size): SizeInterface { return $size->resize($this->width, $this->height); } } src/Modifiers/CropModifier.php000064400000002272150731647100012356 0ustar00width, $this->height); $crop->align($this->position); return $crop->alignPivotTo( $image->size(), $this->position ); } } src/Modifiers/DrawBezierModifier.php000064400000001113150731647100013502 0ustar00drawable; } } src/Modifiers/DrawEllipseModifier.php000064400000000611150731647100013661 0ustar00drawable; } } src/Modifiers/DrawLineModifier.php000064400000001103150731647100013150 0ustar00drawable; } } src/Modifiers/DrawPixelModifier.php000064400000000760150731647100013352 0ustar00drawable; } } src/Modifiers/DrawRectangleModifier.php000064400000001127150731647100014173 0ustar00drawable; } } src/Modifiers/FillModifier.php000064400000000701150731647100012334 0ustar00position); } } src/Modifiers/FlipModifier.php000064400000000266150731647100012346 0ustar00size() ->containMax( $this->width, $this->height ) ->alignPivotTo( $this->getResizeSize($image), $this->position ); } } src/Modifiers/PixelateModifier.php000064400000000370150731647100013223 0ustar00size()->movePivot( $this->position, $this->offset_x, $this->offset_y ); $watermark_size = $watermark->size()->movePivot( $this->position ); return $image_size->relativePositionTo($watermark_size); } } src/Modifiers/ProfileModifier.php000064400000000473150731647100013054 0ustar00core()->frame($position); } if (preg_match("/^(?P[0-9]{1,3})%$/", $position, $matches) != 1) { throw new InputException( 'Position must be either integer or a percent value as string.' ); } $total = count($image); $position = intval(round($total / 100 * intval($matches['percent']))); $position = $position == $total ? $position - 1 : $position; return $image->core()->frame($position); } } src/Modifiers/ResizeCanvasModifier.php000064400000003202150731647100014042 0ustar00 new Rectangle( is_null($this->width) ? $image->width() : $image->width() + $this->width, is_null($this->height) ? $image->height() : $image->height() + $this->height, ), default => new Rectangle( is_null($this->width) ? $image->width() : $this->width, is_null($this->height) ? $image->height() : $this->height, ), }; return $size->alignPivotTo($image->size(), $this->position); } } src/Modifiers/ResizeCanvasRelativeModifier.php000064400000000216150731647100015540 0ustar00angle, 360); } } src/Modifiers/ScaleDownModifier.php000064400000000174150731647100013331 0ustar00driver()->handleInput($this->font->color()); if ($this->font->hasStrokeEffect() && $color->isTransparent()) { throw new ColorException( 'The text color must be fully opaque when using the stroke effect.' ); } return $color; } /** * Decode outline stroke color * * @throws RuntimeException * @throws ColorException * @return ColorInterface */ protected function strokeColor(): ColorInterface { $color = $this->driver()->handleInput($this->font->strokeColor()); if ($color->isTransparent()) { throw new ColorException( 'The stroke color must be fully opaque.' ); } return $color; } /** * Return array of offset points to draw text stroke effect below the actual text * * @param FontInterface $font * @return array */ protected function strokeOffsets(FontInterface $font): array { $offsets = []; if ($font->strokeWidth() <= 0) { return $offsets; } for ($x = $font->strokeWidth() * -1; $x <= $font->strokeWidth(); $x++) { for ($y = $font->strokeWidth() * -1; $y <= $font->strokeWidth(); $y++) { $offsets[] = new Point($x, $y); } } return $offsets; } } src/Modifiers/TrimModifier.php000064400000000547150731647100012371 0ustar00mediaType; } /** * Alias of self::mediaType() */ public function mimetype(): string { return $this->mediaType(); } /** * Set media type of current instance * * @param string|MediaType $type * @return Origin */ public function setMediaType(string|MediaType $type): self { $this->mediaType = match (true) { is_string($type) => $type, default => $type->value, }; return $this; } /** * Return file path of origin * * @return null|string */ public function filePath(): ?string { return $this->filePath; } /** * Set file path for origin * * @param string $path * @return Origin */ public function setFilePath(string $path): self { $this->filePath = $path; return $this; } /** * Return file extension if origin was created from file path * * @return null|string */ public function fileExtension(): ?string { return empty($this->filePath) ? null : pathinfo($this->filePath, PATHINFO_EXTENSION); } } src/Resolution.php000064400000005410150731647100010213 0ustar00x; } /** * {@inheritdoc} * * @see ResolutionInterface::setX() */ public function setX(float $x): self { $this->x = $x; return $this; } /** * {@inheritdoc} * * @see ResolutionInterface::y() */ public function y(): float { return $this->y; } /** * {@inheritdoc} * * @see ResolutionInterface::setY() */ public function setY(float $y): self { $this->y = $y; return $this; } /** * {@inheritdoc} * * @see ResolutionInterface::setPerUnit() */ protected function setPerUnit(int $per_unit): self { $this->per_unit = $per_unit; return $this; } /** * {@inheritdoc} * * @see ResolutionInterface::unit() */ public function unit(): string { return match ($this->per_unit) { self::PER_CM => 'dpcm', default => 'dpi', }; } /** * {@inheritdoc} * * @see ResolutionInterface::perInch() */ public function perInch(): self { return match ($this->per_unit) { self::PER_CM => $this ->setPerUnit(self::PER_INCH) ->setX($this->x * (1 / 2.54)) ->setY($this->y * (1 / 2.54)), default => $this }; } /** * {@inheritdoc} * * @see ResolutionInterface::perCm() */ public function perCm(): self { return match ($this->per_unit) { self::PER_INCH => $this ->setPerUnit(self::PER_CM) ->setX($this->x / (1 / 2.54)) ->setY($this->y / (1 / 2.54)), default => $this, }; } /** * {@inheritdoc} * * @see ResolutionInterface::toString() */ public function toString(): string { return sprintf("%1\$.2f x %2\$.2f %3\$s", $this->x, $this->y, $this->unit()); } /** * {@inheritdoc} * * @see ResolutionInterface::__toString() */ public function __toString(): string { return $this->toString(); } } src/Traits/CanBeDriverSpecialized.php000064400000003747150731647100013632 0ustar00getConstructor()) { foreach ($constructor->getParameters() as $parameter) { $specializable[$parameter->getName()] = $this->{$parameter->getName()}; } } return $specializable; } /** * {@inheritdoc} * * @see SpecializableInterface::driver() */ public function driver(): DriverInterface { return $this->driver; } /** * {@inheritdoc} * * @see SpecializableInterface::setDriver() */ public function setDriver(DriverInterface $driver): SpecializableInterface { if (!$this->belongsToDriver($driver)) { throw new DriverException( "Class '" . $this::class . "' can not be used with " . $driver->id() . " driver." ); } $this->driver = $driver; return $this; } /** * Determine if the given object belongs to the driver's namespace * * @param object $object * @return bool */ protected function belongsToDriver(object $object): bool { $driverId = function (object $object): string|bool { $id = substr($object::class, 27); return strstr($id, "\\", true); }; return $driverId($this) === $driverId($object); } } src/Traits/CanBuildFilePointer.php000064400000001766150731647100013152 0ustar00filename = $filename; } /** * {@inheritdoc} * * @see FontInterface::setSize() */ public function setSize(float $size): FontInterface { $this->size = $size; return $this; } /** * {@inheritdoc} * * @see FontInterface::size() */ public function size(): float { return $this->size; } /** * {@inheritdoc} * * @see FontInterface::setAngle() */ public function setAngle(float $angle): FontInterface { $this->angle = $angle; return $this; } /** * {@inheritdoc} * * @see FontInterface::angle() */ public function angle(): float { return $this->angle; } /** * {@inheritdoc} * * @throws FontException * @see FontInterface::setFilename() */ public function setFilename(string $filename): FontInterface { if (!file_exists($filename)) { throw new FontException('Font file ' . $filename . ' does not exist.'); } $this->filename = $filename; return $this; } /** * {@inheritdoc} * * @see FontInterface::filename() */ public function filename(): ?string { return $this->filename; } /** * {@inheritdoc} * * @see FontInterface::hasFilename() */ public function hasFilename(): bool { return !is_null($this->filename) && is_file($this->filename); } /** * {@inheritdoc} * * @see FontInterface::setColor() */ public function setColor(mixed $color): FontInterface { $this->color = $color; return $this; } /** * {@inheritdoc} * * @see FontInterface::color() */ public function color(): mixed { return $this->color; } /** * {@inheritdoc} * * @see FontInterface::setStrokeColor() */ public function setStrokeColor(mixed $color): FontInterface { $this->strokeColor = $color; return $this; } /** * {@inheritdoc} * * @see FontInterface::strokeColor() */ public function strokeColor(): mixed { return $this->strokeColor; } /** * {@inheritdoc} * * @see FontInterface::setStrokeWidth() */ public function setStrokeWidth(int $width): FontInterface { if (!in_array($width, range(0, 10))) { throw new FontException( 'The stroke width must be in the range from 0 to 10.' ); } $this->strokeWidth = $width; return $this; } /** * {@inheritdoc} * * @see FontInterface::strokeWidth() */ public function strokeWidth(): int { return $this->strokeWidth; } /** * {@inheritdoc} * * @see FontInterface::hasStrokeEffect() */ public function hasStrokeEffect(): bool { return $this->strokeWidth > 0; } /** * {@inheritdoc} * * @see FontInterface::alignment() */ public function alignment(): string { return $this->alignment; } /** * {@inheritdoc} * * @see FontInterface::setAlignment() */ public function setAlignment(string $value): FontInterface { $this->alignment = $value; return $this; } /** * {@inheritdoc} * * @see FontInterface::valignment() */ public function valignment(): string { return $this->valignment; } /** * {@inheritdoc} * * @see FontInterface::setValignment() */ public function setValignment(string $value): FontInterface { $this->valignment = $value; return $this; } /** * {@inheritdoc} * * @see FontInterface::setLineHeight() */ public function setLineHeight(float $height): FontInterface { $this->lineHeight = $height; return $this; } /** * {@inheritdoc} * * @see FontInterface::lineHeight() */ public function lineHeight(): float { return $this->lineHeight; } /** * {@inheritdoc} * * @see FontInterface::setWrapWidth() */ public function setWrapWidth(?int $width): FontInterface { $this->wrapWidth = $width; return $this; } /** * {@inheritdoc} * * @see FontInterface::wrapWidth() */ public function wrapWidth(): ?int { return $this->wrapWidth; } } src/Typography/FontFactory.php000064400000006432150731647100012461 0ustar00font = is_a($init, FontInterface::class) ? $init : new Font(); if (is_callable($init)) { $init($this); } } /** * Build font * * @return FontInterface */ public function __invoke(): FontInterface { return $this->font; } /** * Set the filename of the font to be built * * @param string $value * @return FontFactory */ public function filename(string $value): self { $this->font->setFilename($value); return $this; } /** * {@inheritdoc} * * @see self::filename() */ public function file(string $value): self { return $this->filename($value); } /** * Set outline stroke effect for the font to be built * * @param mixed $color * @param int $width * @throws FontException * @return FontFactory */ public function stroke(mixed $color, int $width = 1): self { $this->font->setStrokeWidth($width); $this->font->setStrokeColor($color); return $this; } /** * Set color for the font to be built * * @param mixed $value * @return FontFactory */ public function color(mixed $value): self { $this->font->setColor($value); return $this; } /** * Set the size for the font to be built * * @param float $value * @return FontFactory */ public function size(float $value): self { $this->font->setSize($value); return $this; } /** * Set the horizontal alignment of the font to be built * * @param string $value * @return FontFactory */ public function align(string $value): self { $this->font->setAlignment($value); return $this; } /** * Set the vertical alignment of the font to be built * * @param string $value * @return FontFactory */ public function valign(string $value): self { $this->font->setValignment($value); return $this; } /** * Set the line height of the font to be built * * @param float $value * @return FontFactory */ public function lineHeight(float $value): self { $this->font->setLineHeight($value); return $this; } /** * Set the rotation angle of the font to be built * * @param float $value * @return FontFactory */ public function angle(float $value): self { $this->font->setAngle($value); return $this; } /** * Set the maximum width of the text block to be built * * @param int $width * @return FontFactory */ public function wrap(int $width): self { $this->font->setWrapWidth($width); return $this; } } src/Typography/Line.php000064400000004345150731647100011113 0ustar00 */ class Line implements IteratorAggregate, Countable { /** * Segments (usually individual words including punctuation marks) of the line * * @var array */ protected array $segments = []; /** * Create new text line object with given text & position * * @param string $text * @param PointInterface $position * @return void */ public function __construct( ?string $text = null, protected PointInterface $position = new Point() ) { if (is_string($text)) { $this->segments = explode(" ", $text); } } /** * Add word to current line * * @param string $word * @return Line */ public function add(string $word): self { $this->segments[] = $word; return $this; } /** * Returns Iterator * * @return Traversable */ public function getIterator(): Traversable { return new ArrayIterator($this->segments); } /** * Get Position of line * * @return PointInterface */ public function position(): PointInterface { return $this->position; } /** * Set position of current line * * @param PointInterface $point * @return Line */ public function setPosition(PointInterface $point): self { $this->position = $point; return $this; } /** * Count segments (individual words including punctuation marks) of line * * @return int */ public function count(): int { return count($this->segments); } /** * Count characters of line * * @return int */ public function length(): int { return mb_strlen((string) $this); } /** * Cast line to string * * @return string */ public function __toString(): string { return implode(" ", $this->segments); } } src/Typography/TextBlock.php000064400000002752150731647100012123 0ustar00push(new Line($line)); } } /** * Return array of lines in text block * * @return array */ public function lines(): array { return $this->items; } /** * Set lines of the text block * * @param array $lines * @return self */ public function setLines(array $lines): self { $this->items = $lines; return $this; } /** * Get line by given key * * @param mixed $key * @return null|Line */ public function line($key): ?Line { if (!array_key_exists($key, $this->lines())) { return null; } return $this->lines()[$key]; } /** * Return line with most characters of text block * * @return Line */ public function longestLine(): Line { $lines = $this->lines(); usort($lines, function (Line $a, Line $b) { if ($a->length() === $b->length()) { return 0; } return $a->length() > $b->length() ? -1 : 1; }); return $lines[0]; } }