Added Grav\Framework\Image classes

This commit is contained in:
Matias Griese
2022-02-12 17:08:58 +02:00
parent 46f8b5b17d
commit a9c4d2b770
10 changed files with 2540 additions and 1 deletions

View File

@@ -83,7 +83,8 @@
"ext-memcache": "Needed to support Memcache servers",
"ext-memcached": "Needed to support Memcached servers",
"ext-redis": "Needed to support Redis servers",
"ext-exif": "Needed to use exif data from images."
"ext-exif": "Needed to use exif data from images.",
"ext-gd": "Needed to resize images."
},
"config": {
"apcu-autoloader": true,

View File

@@ -0,0 +1,297 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Image;
/**
* Image adapter interface.
*/
interface ImageAdapterInterface extends ImageInfoInterface, ImageSaveInterface
{
/**
* Returns true if the adapter is enabled.
*
* @return bool
*/
public static function isEnabled(): bool;
/**
* Returns true if the image type is supported by the adapter.
*
* @param string $type
* @return bool
*/
public static function isSupported(string $type): bool;
/**
* Get the raw resource.
*
* @return object|resource
*/
public function getResource();
/**
* Gets the name of the adapter.
*
* @return string
*/
public function getName(): string;
/**
* Resizes the image.
*
* @param int|null $background
* @param int $target_width
* @param int $target_height
* @param int $new_width
* @param int $new_height
* @return $this
*/
public function resize(?int $background, int $target_width, int $target_height, int $new_width, int $new_height);
/**
* Crops the image.
*
* @param int $x The top-left x position of the crop box
* @param int $y The top-left y position of the crop box
* @param int $width The width of the crop box
* @param int $height The height of the crop box
* @return $this
*/
public function crop(int $x, int $y, int $width, int $height);
/**
* Fixes image orientation based on exif data.
*
* @return $this
*/
public function fixOrientation();
/**
* Applies orientation using exif orientation value.
*
* @param int $exif_orientation
* @return $this
*/
public function applyExifOrientation(int $exif_orientation);
/**
* Enables progressive image loading.
*
* @return $this
*/
public function enableProgressive();
/**
* Fills the image background if the image is transparent.
*
* @param int|null $background Image background, null if transparent
* @return $this
*/
public function fillBackground(?int $background = 0xffffff);
/**
* Negates the image.
*
* @return $this
*/
public function negate();
/**
* Changes the brightness of the image.
*
* @param int $brightness Image brightness
* @return $this
*/
public function brightness(int $brightness);
/**
* Changes the contrast of the image.
*
* @param int $contrast Image contrast [-100, 100]
* @return $this
*/
public function contrast(int $contrast);
/**
* Applies a grayscale level effect on the image.
*
* @return $this
*/
public function grayscale();
/**
* Embosses the image.
*
* @return $this
*/
public function emboss();
/**
* Smooths the image.
*
* @param int $p value between [-10, 10]
*
* @return $this
*/
public function smooth(int $p);
/**
* Sharpens the image.
*
* @return $this
*/
public function sharp();
/**
* Applies edge filtering to the image.
*
* @return $this
*/
public function edge();
/**
* Colorizes the image.
*
* @param int $red Value in range [-255, 255]
* @param int $green Value in range [-255, 255]
* @param int $blue Value in range [-255, 255]
* @return $this
*/
public function colorize(int $red, int $green, int $blue);
/**
* Applies sepia toning to the image.
*
* @return $this
*/
public function sepia();
/**
* Merges the image with another image.
*
* @param ImageAdapterInterface $other
* @param int $x
* @param int $y
* @param int $width
* @param int $height
* @return $this
*/
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = 0, int $height = 0);
/**
* Rotates the image.
*
* @param float $angle
* @param int|null $background Image background, null if transparent
* @return $this
*/
public function rotate(float $angle, ?int $background = 0xffffff);
/**
* Fills the image with color.
*
* @param int $color
* @param int $x
* @param int $y
* @return $this
*/
public function fill(int $color = 0xffffff, int $x = 0, int $y = 0);
/**
* Writes text to the image.
*
* @param string $font
* @param string $text
* @param int $x
* @param int $y
* @param float $size
* @param float $angle
* @param int $color
* @param string $align
* @return $this
*/
public function write(string $font, string $text, int $x = 0, int $y = 0, float $size = 12.0, float $angle = 0.0, int $color = 0x000000, string $align = 'left');
/**
* Draws a rectangle.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param int $color
* @param bool $filled
* @return $this
*/
public function rectangle(int $x1, int $y1, int $x2, int $y2, int $color, bool $filled = false);
/**
* Draws a rounded rectangle.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param int $radius
* @param int $color
* @param bool $filled
* @return $this
*/
public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, int $color, bool $filled = false);
/**
* Draws a line.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param int $color
* @return $this
*/
public function line(int $x1, int $y1, int $x2, int $y2, int $color = 0x000000);
/**
* Draws an ellipse.
*
* @param int $cx
* @param int $cy
* @param int $width
* @param int $height
* @param int $color
* @param bool $filled
* @return $this
*/
public function ellipse(int $cx, int $cy, int $width, int $height, int $color = 0x000000, bool $filled = false);
/**
* Draws a circle.
*
* @param int $cx
* @param int $cy
* @param int $r
* @param int $color
* @param bool $filled
* @return $this
*/
public function circle(int $cx, int $cy, int $r, int $color = 0x000000, bool $filled = false);
/**
* Draws a polygon.
*
* @param array $points
* @param int $color
* @param bool $filled
* @return $this
*/
public function polygon(array $points, int $color, bool $filled = false);
/**
* Flips the image.
*
* @param bool $flipVertical
* @param bool $flipHorizontal
* @return $this
*/
public function flip(bool $flipVertical, bool $flipHorizontal);
}

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Image;
/**
* Image information interface.
*/
interface ImageInfoInterface
{
/**
* Get image width.
*
* @return int
*/
public function width(): int;
/**
* Get image height.
*
* @return int
*/
public function height(): int;
}

View File

@@ -0,0 +1,314 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Image;
use Grav\Framework\Image\Image;
/**
* Image operations interface.
*/
interface ImageOperationsInterface extends ImageInfoInterface
{
/**
* Works as resize() excepts that the layout will be cropped.
*
* @param string|int|null $width New width
* @param string|int|null $height New height
* @param string|int $background Background color
* @return $this
*/
public function cropResize($width = null, $height = null, $background = 0xffffff);
/**
* Resizes the image preserving scale. Can enlarge it.
*
* @param string|int|null $width New width
* @param string|int|null $height New height
* @param string|int $background Background color
* @param bool $crop
* @return $this
*/
public function scaleResize($width = null, $height = null, $background = 0xffffff, bool $crop = false);
/**
* Resizes the image forcing the destination to have exactly the given width and the height.
*
* @param string|int|null $width New width
* @param string|int|null $height New height
* @param string|int $background Background color
* @return $this
*/
public function forceResize($width = null, $height = null, $background = 0xffffff);
/**
* Resizes the image. It will never be enlarged.
*
* @param string|int|null $width New width
* @param string|int|null $height New height
* @param string|int $background Background color
* @param bool $force
* @param bool $rescale
* @param bool $crop
* @return $this
*/
public function resize($width = null, $height = null, $background = 0xffffff, bool $force = false, bool $rescale = false, bool $crop = false);
/**
* Perform a zoom crop of the image to desired width and height.
*
* @param string|int|null $width New width
* @param string|int|null $height New height
* @param string|int $background Background color
* @param string|int $xPosLetter
* @param string|int $yPosLetter
* @return $this
*/
public function zoomCrop($width, $height, $background = 0xffffff, $xPosLetter = 'center', $yPosLetter = 'center');
/**
* Crops the image.
*
* @param int $x the top-left x position of the crop box
* @param int $y the top-left y position of the crop box
* @param int $width the width of the crop box
* @param int $height the height of the crop box
* @return $this
*/
public function crop(int $x, int $y, int $width, int $height);
/**
* Read exif rotation from file and apply it.
*
* @return $this
*/
public function fixOrientation();
/**
* Apply orientation using Exif orientation value.
*
* @param int $exif_orientation
* @return $this
*/
public function applyExifOrientation(int $exif_orientation);
/**
* enable progressive image loading.
*
* @return $this
*/
public function enableProgressive();
/**
* Fills the image background to $bg if the image is transparent.
*
* @param string|int $background background color
* @return $this
*/
public function fillBackground($background = 0xffffff);
/**
* Negates the image.
*
* @return $this
*/
public function negate();
/**
* Changes the brightness of the image.
*
* @param int $brightness the brightness
* @return $this
*/
public function brightness(int $brightness);
/**
* Contrasts the image.
*
* @param int $contrast the contrast [-100, 100]
* @return $this
*/
public function contrast(int $contrast);
/**
* Apply a grayscale level effect on the image.
*
* @return $this
*/
public function grayscale();
/**
* Emboss the image.
*
* @return $this
*/
public function emboss();
/**
* Smooth the image.
*
* @param int $p value between [-10,10]
*
* @return $this
*/
public function smooth(int $p);
/**
* Sharpens the image.
*
* @return $this
*/
public function sharp();
/**
* Edges the image.
*
* @return $this
*/
public function edge();
/**
* Colorize the image.
*
* @param int $red value in range [-255, 255]
* @param int $green value in range [-255, 255]
* @param int $blue value in range [-255, 255]
* @return $this
*/
public function colorize(int $red, int $green, int $blue);
/**
* apply sepia to the image.
*
* @return $this
*/
public function sepia();
/**
* Merge with another image.
*
* @param Image $other
* @param int $x
* @param int $y
* @param int $width
* @param int $height
* @return $this
*/
public function merge(Image $other, int $x = 0, int $y = 0, int $width = 0, int $height = 0);
/**
* Rotate the image.
*
* @param float $angle
* @param string|int $background
* @return $this
*/
public function rotate(float $angle, $background = 0xffffff);
/**
* Fills the image.
*
* @param string|int $color
* @param int $x
* @param int $y
* @return $this
*/
public function fill($color = 0xffffff, int $x = 0, int $y = 0);
/**
* write text to the image.
*
* @param string $font
* @param string $text
* @param int $x
* @param int $y
* @param float $size
* @param float $angle
* @param string|int $color
* @param string $align
* @return $this
*/
public function write(string $font, string $text, int $x = 0, int $y = 0, float $size = 12.0, float $angle = 0.0, $color = 0x000000, string $align = 'left');
/**
* Draws a rectangle.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function rectangle(int $x1, int $y1, int $x2, int $y2, $color, bool $filled = false);
/**
* Draws a rounded rectangle.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param int $radius
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, $color, bool $filled = false);
/**
* Draws a line.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param string|int $color
* @return $this
*/
public function line(int $x1, int $y1, int $x2, int $y2, $color = 0x000000);
/**
* Draws an ellipse.
*
* @param int $cx
* @param int $cy
* @param int $width
* @param int $height
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function ellipse(int $cx, int $cy, int $width, int $height, $color = 0x000000, bool $filled = false);
/**
* Draws a circle.
*
* @param int $cx
* @param int $cy
* @param int $r
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function circle(int $cx, int $cy, int $r, $color = 0x000000, bool $filled = false);
/**
* Draws a polygon.
*
* @param array $points
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function polygon(array $points, $color, bool $filled = false);
/**
* Flips the image.
*
* @param bool $flipVertical
* @param bool $flipHorizontal
* @return $this
*/
public function flip(bool $flipVertical, bool $flipHorizontal);
}

View File

@@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Image;
/**
* Image save interface.
*/
interface ImageSaveInterface
{
/**
* Save the image as a gif.
*
* @param string $file
* @return $this
*/
public function saveGif(string $file);
/**
* Save the image as a png.
*
* @param string $file
* @return $this
*/
public function savePng(string $file);
/**
* Save the image as a Webp.
*
* @param string $file
* @param int $quality
* @return $this
*/
public function saveWebp(string $file, int $quality);
/**
* Save the image as a jpeg.
*
* @param string $file
* @param int $quality
* @return $this
*/
public function saveJpeg(string $file, int $quality);
}

View File

@@ -0,0 +1,65 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Image\Adapter;
use Grav\Framework\Contracts\Image\ImageAdapterInterface;
/**
* Abstract Image Adapter.
*/
abstract class Adapter implements ImageAdapterInterface
{
/** @var int */
protected $orientation = 1;
/**
* {@inheritdoc}
*/
public function fixOrientation()
{
return $this->applyExifOrientation($this->orientation);
}
/**
* {@inheritdoc}
*/
public function applyExifOrientation(int $exif_orientation)
{
switch ($exif_orientation) {
case 1: // do nothing
break;
case 2: // horizontal flip
$this->flip(false, true);
break;
case 3: // 180 rotate left
$this->rotate(180.0);
break;
case 4: // vertical flip
$this->flip(true, false);
break;
case 5: // vertical flip + 90 rotate right
$this->flip(true, false);
$this->rotate(-90.0);
break;
case 6: // 90 rotate right
$this->rotate(-90.0);
break;
case 7: // horizontal flip + 90 rotate right
$this->flip(false, true);
$this->rotate(-90.0);
break;
case 8: // 90 rotate left
$this->rotate(90.0);
break;
}
return $this;
}
}

View File

@@ -0,0 +1,816 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Image\Adapter;
use Grav\Framework\Contracts\Image\ImageAdapterInterface;
use InvalidArgumentException;
use RuntimeException;
use UnexpectedValueException;
/**
* GD Image adapter.
*/
class GdAdapter extends Adapter
{
/** @var array<string,int> */
public static $types = [
'jpeg' => \IMG_JPG,
'jpg' => \IMG_JPG,
'gif' => \IMG_GIF,
'png' => \IMG_PNG,
'webp' => \IMG_WEBP
];
/** @var \GdImage|resource */
protected $resource;
/**
* {@inheritdoc}
*/
public static function isEnabled(): bool
{
return extension_loaded('gd') && function_exists('gd_info');
}
/**
* {@inheritdoc}
*/
public static function isSupported(string $type): bool
{
$test = self::$types[$type] ?? 0;
return (bool)(imagetypes() & $test);
}
/**
* @param \GdImage|resource $resource
*/
public function __construct($resource)
{
if (PHP_VERSION_ID > 80000) {
if (!$resource instanceof \GdImage) {
throw new InvalidArgumentException('Resource has to be GD Image');
}
} elseif (!is_resource($resource)) {
throw new InvalidArgumentException('Resource has to be GD Image');
}
$this->resource = $resource;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'GD';
}
/**
* @return \GdImage|resource
*/
public function getResource()
{
return $this->resource;
}
/**
* {@inheritdoc}
*/
public function fillBackground(?int $background = 0xffffff)
{
$w = $this->width();
$h = $this->height();
$n = imagecreatetruecolor($w, $h);
if (!$n) {
throw new RuntimeException('Image background color fill failed');
}
imagefill($n, 0, 0, $this->allocateColor($background));
imagecopyresampled($n, $this->resource, 0, 0, 0, 0, $w, $h, $w, $h);
imagedestroy($this->resource);
$this->resource = $n;
return $this;
}
/**
* {@inheritdoc}
*/
public function resize(?int $background, int $target_width, int $target_height, int $new_width, int $new_height)
{
$width = $this->width();
$height = $this->height();
$n = imagecreatetruecolor($target_width, $target_height);
if (!$n) {
throw new RuntimeException('Failed to resize image: image creation failed');
}
if ($background !== null) {
imagefill($n, 0, 0, $this->allocateColor($background));
} else {
imagealphablending($n, false);
$color = $this->allocateColor(null);
imagefill($n, 0, 0, $color);
imagesavealpha($n, true);
}
imagecopyresampled($n, $this->resource, ($target_width - $new_width) / 2, ($target_height - $new_height) / 2, 0, 0, $new_width, $new_height, $width, $height);
imagedestroy($this->resource);
$this->resource = $n;
return $this;
}
/**
* {@inheritdoc}
*/
public function crop(int $x, int $y, int $width, int $height)
{
$destination = imagecreatetruecolor($width, $height);
if (!$destination) {
throw new RuntimeException('Image crop failed');
}
imagealphablending($destination, false);
imagesavealpha($destination, true);
imagecopy($destination, $this->resource, 0, 0, $x, $y, $this->width(), $this->height());
imagedestroy($this->resource);
$this->resource = $destination;
return $this;
}
/**
* {@inheritdoc}
*/
public function negate()
{
imagefilter($this->resource, IMG_FILTER_NEGATE);
return $this;
}
/**
* {@inheritdoc}
*/
public function brightness($brightness)
{
imagefilter($this->resource, IMG_FILTER_BRIGHTNESS, $brightness);
return $this;
}
/**
* {@inheritdoc}
*/
public function contrast($contrast)
{
imagefilter($this->resource, IMG_FILTER_CONTRAST, $contrast);
return $this;
}
/**
* {@inheritdoc}
*/
public function grayscale()
{
imagefilter($this->resource, IMG_FILTER_GRAYSCALE);
return $this;
}
/**
* {@inheritdoc}
*/
public function emboss()
{
imagefilter($this->resource, IMG_FILTER_EMBOSS);
return $this;
}
/**
* {@inheritdoc}
*/
public function smooth(int $p)
{
imagefilter($this->resource, IMG_FILTER_SMOOTH, $p);
return $this;
}
/**
* {@inheritdoc}
*/
public function sharp()
{
imagefilter($this->resource, IMG_FILTER_MEAN_REMOVAL);
return $this;
}
/**
* {@inheritdoc}
*/
public function edge()
{
imagefilter($this->resource, IMG_FILTER_EDGEDETECT);
return $this;
}
/**
* {@inheritdoc}
*/
public function colorize(int $red, int $green, int $blue)
{
imagefilter($this->resource, IMG_FILTER_COLORIZE, $red, $green, $blue);
return $this;
}
/**
* {@inheritdoc}
*/
public function sepia()
{
imagefilter($this->resource, IMG_FILTER_GRAYSCALE);
imagefilter($this->resource, IMG_FILTER_COLORIZE, 100, 50, 0);
return $this;
}
/**
* {@inheritdoc}
*/
public function gaussianBlur(int $blurFactor = 1)
{
if ($blurFactor < 1) {
return $this;
}
$originalWidth = $this->width();
$originalHeight = $this->height();
$smallestWidth = (int)ceil($originalWidth * (0.5 ** $blurFactor));
$smallestHeight = (int)ceil($originalHeight * (0.5 ** $blurFactor));
// for the first run, the previous image is the original input
$prevImage = $this->resource;
$prevWidth = $originalWidth;
$prevHeight = $originalHeight;
// scale way down and gradually scale back up, blurring all the way
for ($i = 0; $i < $blurFactor; ++$i) {
// determine dimensions of next image
$nextWidth = (int)($smallestWidth * (2 ** $i));
$nextHeight = (int)($smallestHeight * (2 ** $i));
// resize previous image to next size
$nextImage = imagecreatetruecolor($nextWidth, $nextHeight);
if (!$nextImage) {
throw new RuntimeException('Image gaussian blur failed');
}
imagecopyresized($nextImage, $prevImage, 0, 0, 0, 0,
$nextWidth, $nextHeight, $prevWidth, $prevHeight);
// apply blur filter
imagefilter($nextImage, IMG_FILTER_GAUSSIAN_BLUR);
// now the new image becomes the previous image for the next step
$prevImage = $nextImage;
$prevWidth = $nextWidth;
$prevHeight = $nextHeight;
}
// scale back to original size and blur one more time
imagecopyresized($this->resource, $nextImage,
0, 0, 0, 0, $originalWidth, $originalHeight, $nextWidth, $nextHeight);
imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR);
// clean up
imagedestroy($prevImage);
return $this;
}
/**
* {@inheritdoc}
*
* @param GdAdapter $other
*/
public function merge(ImageAdapterInterface $other, int $x = 0, int $y = 0, int $width = null, int $height = null)
{
if (!$other instanceof self) {
throw new InvalidArgumentException('Image to be merged needs to be instance of GdAdapter');
}
imagealphablending($this->resource, true);
if (null === $width) {
$width = $other->width();
}
if (null === $height) {
$height = $other->height();
}
imagecopyresampled($this->resource, $other->getResource(), $x, $y, 0, 0, $width, $height, $width, $height);
return $this;
}
/**
* {@inheritdoc}
*/
public function rotate(float $angle, ?int $background = 0xffffff)
{
$resource = imagerotate($this->resource, $angle, $this->allocateColor($background));
if (!$resource) {
throw new RuntimeException('Image rotate failed');
}
$this->resource = $resource;
imagealphablending($this->resource, true);
imagesavealpha($this->resource, true);
return $this;
}
/**
* {@inheritdoc}
*/
public function fill(int $color = 0xffffff, int $x = 0, int $y = 0)
{
imagealphablending($this->resource, false);
imagefill($this->resource, $x, $y, $this->allocateColor($color));
return $this;
}
/**
* {@inheritdoc}
*/
public function write(string $font, string $text, int $x = 0, int $y = 0, float $size = 12.0, float $angle = 0.0, int $color = 0x000000, string $align = 'left')
{
imagealphablending($this->resource, true);
if ($align !== 'left') {
$sim_size = $this->getTTFBox($font, $text, $size, $angle);
if ($align === 'center') {
$x -= $sim_size['width'] / 2;
}
if ($align === 'right') {
$x -= $sim_size['width'];
}
}
imagettftext($this->resource, $size, $angle, $x, $y, $this->allocateColor($color), $font, $text);
return $this;
}
/**
* {@inheritdoc}
*/
public function rectangle(int $x1, int $y1, int $x2, int $y2, int $color, bool $filled = false)
{
$c = $this->allocateColor($color);
if ($filled) {
imagefilledrectangle($this->resource, $x1, $y1, $x2, $y2, $c);
} else {
imagerectangle($this->resource, $x1, $y1, $x2, $y2, $c);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, int $color, bool $filled = false)
{
$c = $this->allocateColor($color);
if ($filled) {
imagefilledrectangle($this->resource, $x1 + $radius, $y1, $x2 - $radius, $y2, $c);
imagefilledrectangle($this->resource, $x1, $y1 + $radius, $x1 + $radius - 1, $y2 - $radius, $c);
imagefilledrectangle($this->resource, $x2 - $radius + 1, $y1 + $radius, $x2, $y2 - $radius, $c);
imagefilledarc($this->resource, $x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $c, IMG_ARC_PIE);
imagefilledarc($this->resource, $x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $c, IMG_ARC_PIE);
imagefilledarc($this->resource, $x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $c, IMG_ARC_PIE);
imagefilledarc($this->resource, $x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $c, IMG_ARC_PIE);
} else {
imageline($this->resource, $x1 + $radius, $y1, $x2 - $radius, $y1, $c);
imageline($this->resource, $x1 + $radius, $y2, $x2 - $radius, $y2, $c);
imageline($this->resource, $x1, $y1 + $radius, $x1, $y2 - $radius, $c);
imageline($this->resource, $x2, $y1 + $radius, $x2, $y2 - $radius, $c);
imagearc($this->resource, $x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $c);
imagearc($this->resource, $x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $c);
imagearc($this->resource, $x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $c);
imagearc($this->resource, $x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $c);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function line(int $x1, int $y1, int $x2, int $y2, $color = 0x000000)
{
imageline($this->resource, $x1, $y1, $x2, $y2, $this->allocateColor($color));
return $this;
}
/**
* {@inheritdoc}
*/
public function ellipse(int $cx, int $cy, int $width, int $height, $color = 0x000000, bool $filled = false)
{
$c = $this->allocateColor($color);
if ($filled) {
imagefilledellipse($this->resource, $cx, $cy, $width, $height, $c);
} else {
imageellipse($this->resource, $cx, $cy, $width, $height, $c);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function circle(int $cx, int $cy, int $r, $color = 0x000000, bool $filled = false)
{
return $this->ellipse($cx, $cy, $r, $r, $this->allocateColor($color), $filled);
}
/**
* {@inheritdoc}
*/
public function polygon(array $points, $color, bool $filled = false)
{
$num = (int)(count($points) / 2);
$c = $this->allocateColor($color);
if ($filled) {
imagefilledpolygon($this->resource, $points, $num, $c);
} else {
imagepolygon($this->resource, $points, $num, $c);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function flip(bool $flipVertical, bool $flipHorizontal)
{
if (!$flipVertical && !$flipHorizontal) {
return $this;
}
if (function_exists('imageflip')) {
if ($flipVertical && $flipHorizontal) {
$flipMode = \IMG_FLIP_BOTH;
} elseif ($flipVertical && !$flipHorizontal) {
$flipMode = \IMG_FLIP_VERTICAL;
} elseif (!$flipVertical && $flipHorizontal) {
$flipMode = \IMG_FLIP_HORIZONTAL;
}
if (isset($flipMode)) {
imageflip($this->resource, $flipMode);
}
} else {
$width = $this->width();
$height = $this->height();
$src_x = 0;
$src_y = 0;
$src_width = $width;
$src_height = $height;
if ($flipVertical) {
$src_y = $height - 1;
$src_height = -$height;
}
if ($flipHorizontal) {
$src_x = $width - 1;
$src_width = -$width;
}
$imgdest = imagecreatetruecolor($width, $height);
if (!$imgdest) {
throw new RuntimeException('Image flip failed');
}
imagealphablending($imgdest, false);
imagesavealpha($imgdest, true);
if (imagecopyresampled($imgdest, $this->resource, 0, 0, $src_x, $src_y, $width, $height, $src_width, $src_height)) {
imagedestroy($this->resource);
$this->resource = $imgdest;
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function width(): int
{
return imagesx($this->resource);
}
/**
* {@inheritdoc}
*/
public function height(): int
{
return imagesy($this->resource);
}
/**
* {@inheritdoc}
*/
public function saveGif(string $filepath)
{
$transColor = imagecolorallocatealpha($this->resource, 255, 255, 255, 127);
if (!$transColor) {
throw new RuntimeException('Image save failed');
}
imagecolortransparent($this->resource, $transColor);
imagegif($this->resource, $filepath);
return $this;
}
/**
* {@inheritdoc}
*/
public function savePng(string $filepath)
{
imagepng($this->resource, $filepath);
return $this;
}
/**
* {@inheritdoc}
*/
public function saveWebp(string $filepath, int $quality)
{
imagewebp($this->resource, $filepath, $quality);
return $this;
}
/**
* {@inheritdoc}
*/
public function saveJpeg(string $filepath, int $quality)
{
imagejpeg($this->resource, $filepath, $quality);
return $this;
}
/**
* {@inheritdoc}
*/
public function enableProgressive()
{
imageinterlace($this->resource, true);
return $this;
}
/**
* Create empty image.
*
* @param int $width
* @param int $height
* @return void
*/
protected function createImage(int $width, int $height): void
{
$this->resource = imagecreatetruecolor($width, $height) ?: null;
}
/**
* Create image from a string.
*
* @param string $data
* @return void
*/
protected function createImageFromString(string $data): void
{
$this->resource = @imagecreatefromstring($data) ?: null;
}
/**
* Converts the image to true color.
*
* @return void
*/
protected function convertToTrueColor(): void
{
if (!imageistruecolor($this->resource)) {
imagepalettetotruecolor($this->resource);
}
imagesavealpha($this->resource, true);
}
/**
* Try to open the file using jpeg.
*
* @param string $filepath
* @return void
*/
protected function openJpeg(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefromjpeg($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Try to open the file using gif.
*
* @param string $filepath
* @return void
*/
protected function openGif(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefromgif($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Try to open the file using PNG.
*
* @param string $filepath
* @return void
*/
protected function openPng(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefrompng($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Try to open the file using WEBP.
*
* @param string $filepath
* @return void
*/
protected function openWebp(string $filepath): void
{
if (file_exists($filepath) && filesize($filepath)) {
$this->resource = @imagecreatefromwebp($filepath) ?: null;
} else {
$this->resource = null;
}
}
/**
* Get color in x, y.
*
* @param int $x
* @param int $y
* @return int|false
*/
protected function getColor(int $x, int $y)
{
return imagecolorat($this->resource, $x, $y);
}
/**
* Load image resource.
*
* @param \GdImage|resource $resource
*/
protected function loadResource($resource): void
{
$this->resource = $resource;
imagesavealpha($this->resource, true);
}
/**
* Load file.
*
* @param string $filepath
* @param string $type
* @return void
* @throws UnexpectedValueException
*/
protected function loadFile(string $filepath, string $type): void
{
if (!static::isSupported($type)) {
throw new UnexpectedValueException('Type ' . $type . ' is not supported by GD');
}
switch ($type) {
case 'jpeg':
$this->openJpeg($filepath);
break;
case 'gif':
$this->openGif($filepath);
break;
case 'png':
$this->openPng($filepath);
break;
case 'webp':
$this->openWebp($filepath);
break;
default:
throw new UnexpectedValueException('Unable to open file (' . $filepath . ')');
}
if (null === $this->getResource()) {
throw new UnexpectedValueException('Unable to open file (' . $filepath . ')');
}
$this->convertToTrueColor();
}
/**
* Give the bounding box of a text using TrueType fonts.
*
* @param string $font
* @param string $text
* @param float $size
* @param float $angle
* @return array
*/
protected function getTTFBox(string $font, string $text, float $size, float $angle = 0): array
{
$box = imagettfbbox($size, $angle, $font, $text);
if (false === $box) {
throw new RuntimeException('Failed to allocate room for text');
}
return [
'width' => abs($box[2] - $box[0]),
'height' => abs($box[3] - $box[5]),
];
}
/**
* Allocate color for the image.
*
* @param int|null $color
* @return int
*/
protected function allocateColor(?int $color): int
{
$colorRGBA = $color ?? 0x7fffffff;
$b = ($colorRGBA) & 0xff;
$colorRGBA >>= 8;
$g = ($colorRGBA) & 0xff;
$colorRGBA >>= 8;
$r = ($colorRGBA) & 0xff;
$colorRGBA >>= 8;
$a = ($colorRGBA) & 0xff;
$c = imagecolorallocatealpha($this->resource, $r, $g, $b, $a);
if (false !== $c && $color === null) {
imagecolortransparent($this->resource, $c);
}
if (false === $c) {
throw new RuntimeException('Failed to allocate color');
}
return $c;
}
}

View File

@@ -0,0 +1,79 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Image;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Image\ImageOperationsInterface;
use Grav\Framework\Image\Traits\ImageOperationsTrait;
use RuntimeException;
/**
* Image class.
*/
class Image implements ImageOperationsInterface
{
use ImageOperationsTrait;
use Serializable;
/** @var string */
protected $filepath;
/** @var array */
protected $info;
/**
* @param string $filepath
* @param array $info
*/
public function __construct(string $filepath, array $info)
{
$this->filepath = $filepath;
$this->width = $info['width'] ?? 0;
$this->height = $info['height'] ?? 0;
$this->orientation = isset($info['exif']['Orientation']) ? (int)$info['exif']['Orientation'] : null;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'image' => 1,
'filepath' => $this->filepath,
'info' => $this->info,
'width' => $this->width,
'height' => $this->height,
'orientation' => $this->orientation,
'operations' => $this->operations,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$image = $data['image'] ?? null;
if ($image !== 1) {
throw new RuntimeException('Cannot unserialize image: Version mismatch');
}
$this->filepath = $data['filepath'];
$this->info = $data['info'];
$this->width = $data['width'];
$this->height = $data['height'];
$this->orientation = $data['orientation'];
$this->operations = $data['operations'];
}
/**
* Generates the hash for the image.
*
* @return string
*/
public function generateHash(): string
{
return sha1(serialize($this));
}
}

View File

@@ -0,0 +1,225 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Image;
use InvalidArgumentException;
/**
* Image color class.
*/
class ImageColor
{
/**
* All HTML color names.
*
* @var int[]
*/
private static $colors = [
'aliceblue' => 0xf0f8ff,
'antiquewhite' => 0xfaebd7,
'aqua' => 0x00ffff,
'aquamarine' => 0x7fffd4,
'azure' => 0xf0ffff,
'beige' => 0xf5f5dc,
'bisque' => 0xffe4c4,
'black' => 0x000000,
'blanchedalmond' => 0xffebcd,
'blue' => 0x0000ff,
'blueviolet' => 0x8a2be2,
'brown' => 0xa52a2a,
'burlywood' => 0xdeb887,
'cadetblue' => 0x5f9ea0,
'chartreuse' => 0x7fff00,
'chocolate' => 0xd2691e,
'coral' => 0xff7f50,
'cornflowerblue' => 0x6495ed,
'cornsilk' => 0xfff8dc,
'crimson' => 0xdc143c,
'cyan' => 0x00ffff,
'darkblue' => 0x00008b,
'darkcyan' => 0x008b8b,
'darkgoldenrod' => 0xb8860b,
'darkgray' => 0xa9a9a9,
'darkgreen' => 0x006400,
'darkgrey' => 0xa9a9a9,
'darkkhaki' => 0xbdb76b,
'darkmagenta' => 0x8b008b,
'darkolivegreen' => 0x556b2f,
'darkorange' => 0xff8c00,
'darkorchid' => 0x9932cc,
'darkred' => 0x8b0000,
'darksalmon' => 0xe9967a,
'darkseagreen' => 0x8fbc8f,
'darkslateblue' => 0x483d8b,
'darkslategray' => 0x2f4f4f,
'darkslategrey' => 0x2f4f4f,
'darkturquoise' => 0x00ced1,
'darkviolet' => 0x9400d3,
'deeppink' => 0xff1493,
'deepskyblue' => 0x00bfff,
'dimgray' => 0x696969,
'dimgrey' => 0x696969,
'dodgerblue' => 0x1e90ff,
'firebrick' => 0xb22222,
'floralwhite' => 0xfffaf0,
'forestgreen' => 0x228b22,
'fuchsia' => 0xff00ff,
'gainsboro' => 0xdcdcdc,
'ghostwhite' => 0xf8f8ff,
'goldenrod' => 0xdaa520,
'gold' => 0xffd700,
'gray' => 0x808080,
'green' => 0x008000,
'greenyellow' => 0xadff2f,
'grey' => 0x808080,
'honeydew' => 0xf0fff0,
'hotpink' => 0xff69b4,
'indianred' => 0xcd5c5c,
'indigo' => 0x4b0082,
'ivory' => 0xfffff0,
'khaki' => 0xf0e68c,
'lavenderblush' => 0xfff0f5,
'lavender' => 0xe6e6fa,
'lawngreen' => 0x7cfc00,
'lemonchiffon' => 0xfffacd,
'lightblue' => 0xadd8e6,
'lightcoral' => 0xf08080,
'lightcyan' => 0xe0ffff,
'lightgoldenrodyellow' => 0xfafad2,
'lightgray' => 0xd3d3d3,
'lightgreen' => 0x90ee90,
'lightgrey' => 0xd3d3d3,
'lightpink' => 0xffb6c1,
'lightsalmon' => 0xffa07a,
'lightseagreen' => 0x20b2aa,
'lightskyblue' => 0x87cefa,
'lightslategray' => 0x778899,
'lightslategrey' => 0x778899,
'lightsteelblue' => 0xb0c4de,
'lightyellow' => 0xffffe0,
'lime' => 0x00ff00,
'limegreen' => 0x32cd32,
'linen' => 0xfaf0e6,
'magenta' => 0xff00ff,
'maroon' => 0x800000,
'mediumaquamarine' => 0x66cdaa,
'mediumblue' => 0x0000cd,
'mediumorchid' => 0xba55d3,
'mediumpurple' => 0x9370db,
'mediumseagreen' => 0x3cb371,
'mediumslateblue' => 0x7b68ee,
'mediumspringgreen' => 0x00fa9a,
'mediumturquoise' => 0x48d1cc,
'mediumvioletred' => 0xc71585,
'midnightblue' => 0x191970,
'mintcream' => 0xf5fffa,
'mistyrose' => 0xffe4e1,
'moccasin' => 0xffe4b5,
'navajowhite' => 0xffdead,
'navy' => 0x000080,
'oldlace' => 0xfdf5e6,
'olive' => 0x808000,
'olivedrab' => 0x6b8e23,
'orange' => 0xffa500,
'orangered' => 0xff4500,
'orchid' => 0xda70d6,
'palegoldenrod' => 0xeee8aa,
'palegreen' => 0x98fb98,
'paleturquoise' => 0xafeeee,
'palevioletred' => 0xdb7093,
'papayawhip' => 0xffefd5,
'peachpuff' => 0xffdab9,
'peru' => 0xcd853f,
'pink' => 0xffc0cb,
'plum' => 0xdda0dd,
'powderblue' => 0xb0e0e6,
'purple' => 0x800080,
'rebeccapurple' => 0x663399,
'red' => 0xff0000,
'rosybrown' => 0xbc8f8f,
'royalblue' => 0x4169e1,
'saddlebrown' => 0x8b4513,
'salmon' => 0xfa8072,
'sandybrown' => 0xf4a460,
'seagreen' => 0x2e8b57,
'seashell' => 0xfff5ee,
'sienna' => 0xa0522d,
'silver' => 0xc0c0c0,
'skyblue' => 0x87ceeb,
'slateblue' => 0x6a5acd,
'slategray' => 0x708090,
'slategrey' => 0x708090,
'snow' => 0xfffafa,
'springgreen' => 0x00ff7f,
'steelblue' => 0x4682b4,
'tan' => 0xd2b48c,
'teal' => 0x008080,
'thistle' => 0xd8bfd8,
'tomato' => 0xff6347,
'turquoise' => 0x40e0d0,
'violet' => 0xee82ee,
'wheat' => 0xf5deb3,
'white' => 0xffffff,
'whitesmoke' => 0xf5f5f5,
'yellow' => 0xffff00,
'yellowgreen' => 0x9acd32
];
/**
* Parse color and return integer value of it. Transparent will be converted to null.
*
* @param string|int|null $color
* @return int|null
* @throws InvalidArgumentException
*/
public static function parse($color): ?int
{
// Direct color representation (0xff0000 or null).
if (null === $color || is_int($color)) {
return $color;
}
if (is_string($color)) {
$color = strtolower($color);
// Transparent becomes null.
if ($color === 'transparent') {
return null;
}
// Normalize string.
$color = str_replace(' ', '', $color);
// Lookup named colors.
if (isset(self::$colors[$color])) {
return self::$colors[$color];
}
// Color string ('ff0000', '#ff0000' or '0xfff').
if (preg_match('/^(#|0x|)([0-9a-f]{3,6})/u', $color, $matches)) {
$col = $matches[2];
if (strlen($col) === 3) {
$r = '';
for ($i = 0; $i < 3; ++$i) {
$r .= $col[$i] . $col[$i];
}
$col = $r;
}
return (int)hexdec($col);
}
// Colors like 'rgb(255, 0, 0)'
if (preg_match('/^rgb\((\d+),(\d+),(\d+)\)/', $color, $matches)) {
[,$r,$g,$b] = $matches;
if ($r >= 0 && $r <= 0xff && $g >= 0 && $g <= 0xff && $b >= 0 && $b <= 0xff) {
return ($r << 16) | ($g << 8) | $b;
}
}
}
throw new InvalidArgumentException('Invalid color: ' . $color);
}
}

View File

@@ -0,0 +1,676 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Image\Traits;
use Grav\Framework\Image\Image;
use Grav\Framework\Image\ImageColor;
/**
* Trait to record image operations.
*/
trait ImageOperationsTrait
{
/** @var int */
protected $width = 0;
/** @var int */
protected $height = 0;
/** @var int|null */
protected $orientation = null;
/** @var array */
protected $operations = [];
/**
* Image width.
*
* @return int
*/
public function width(): int
{
return $this->width;
}
/**
* Image height.
*
* @return int
*/
public function height(): int
{
return $this->height;
}
/**
* Works as resize() excepts that the layout will be cropped.
*
* @param string|int|null $width the width
* @param string|int|null $height he height
* @param string|int $background the background
* @return $this
*/
public function cropResize($width = null, $height = null, $background = 0xffffff)
{
return $this->resize($width, $height, $background, false, false, true);
}
/**
* Resize the image preserving scale. Can enlarge it.
*
* @param string|int|null $width the width
* @param string|int|null $height the height
* @param string|int $background the background
* @param bool $crop
* @return $this
*/
public function scaleResize($width = null, $height = null, $background = 0xffffff, bool $crop = false)
{
return $this->resize($width, $height, $background, false, true, $crop);
}
/**
* Resizes the image forcing the destination to have exactly the given width and the height.
*
* @param string|int|null $width the width
* @param string|int|null $height the height
* @param string|int $background the background
* @return $this
*/
public function forceResize($width = null, $height = null, $background = 0xffffff)
{
return $this->resize($width, $height, $background, true);
}
/**
* Resizes the image. It will never be enlarged.
*
* @param string|int|null $width the width
* @param string|int|null $height the height
* @param string|int $background the background
* @param bool $force
* @param bool $rescale
* @param bool $crop
* @return $this
*/
public function resize($width = null, $height = null, $background = 0xffffff, bool $force = false, bool $rescale = false, bool $crop = false)
{
[$width, $height] = $this->getSize($width, $height);
if ($width < 0 || $height < 0) {
return $this;
}
$bg = ImageColor::parse($background);
$current_width = $this->width();
$current_height = $this->height();
$new_width = 0;
$new_height = 0;
$scale = 1.0;
if (!$rescale && (!$force || $crop)) {
if ($width !== 0 && $current_width > $width) {
$scale = $current_width / $width;
}
if ($height !== 0 && $current_height > $height && $current_height / $height > $scale) {
$scale = $current_height / $height;
}
} else {
if ($width !== 0) {
$scale = $current_width / $width;
$new_width = $width;
}
if ($height !== 0) {
if ($width !== 0 && $rescale) {
$scale = max($scale, $current_height / $height);
} else {
$scale = $current_height / $height;
}
$new_height = $height;
}
}
if (!$force || $width === 0 || $rescale) {
$new_width = (int)round($current_width / $scale);
}
if (!$force || $height === 0 || $rescale) {
$new_height = (int)round($current_height / $scale);
}
if ($width === 0 || $crop) {
$width = $new_width;
}
if ($height === 0 || $crop) {
$height = $new_height;
}
$this->operations[] = ['resize', [$bg, $width, $height, $new_width, $new_height]];
// Update image size.
$this->width = $width;
$this->height = $height;
return $this;
}
/**
* Perform a zoom crop of the image to desired width and height.
*
* @param string|int|null $width Desired width
* @param string|int|null $height Desired height
* @param string|int $background
* @param string|int $xPosLetter
* @param string|int $yPosLetter
* @return $this
*/
public function zoomCrop($width, $height, $background = 0xffffff, $xPosLetter = 'center', $yPosLetter = 'center')
{
[$width, $height] = $this->getSize($width, $height);
if ($width <= 0 || $height <= 0) {
return $this;
}
$bg = ImageColor::parse($background);
$originalWidth = $this->width();
$originalHeight = $this->height();
// Calculate the different ratios
$originalRatio = $originalWidth / $originalHeight;
$newRatio = $width / $height;
// Compare ratios
if ($originalRatio > $newRatio) {
// Original image is wider
$newHeight = $height;
$newWidth = (int)($height * $originalRatio);
} else {
// Equal width or smaller
$newHeight = (int)($width / $originalRatio);
$newWidth = $width;
}
// Perform resize
$this->resize($newWidth, $newHeight, $bg, true);
// Define x position
switch ($xPosLetter) {
case 'L':
case 'left':
$xPos = 0;
break;
case 'R':
case 'right':
$xPos = $newWidth - $width;
break;
case 'C':
case 'center':
$xPos = (int)(($newWidth - $width) / 2);
break;
default:
$factorW = $newWidth / $originalWidth;
$xPos = (int)((int)$xPosLetter * $factorW);
// If the desired cropping position goes beyond the width then
// set the crop to be within the correct bounds.
if ($xPos + $width > $newWidth) {
$xPos = $newWidth - $width;
}
}
// Define y position
switch ($yPosLetter) {
case 'T':
case 'top':
$yPos = 0;
break;
case 'B':
case 'bottom':
$yPos = $newHeight - $height;
break;
case 'C':
case 'center':
$yPos = (int)(($newHeight - $height) / 2);
break;
default:
$factorH = $newHeight / $originalHeight;
$yPos = (int)((int)$yPosLetter * $factorH);
// If the desired cropping position goes beyond the height then
// set the crop to be within the correct bounds.
if ($yPos + $height > $newHeight) {
$yPos = $newHeight - $height;
}
}
// Crop image to reach desired size
$this->crop($xPos, $yPos, $width, $height);
return $this;
}
/**
* Crops the image.
*
* @param int $x the top-left x position of the crop box
* @param int $y the top-left y position of the crop box
* @param int $width the width of the crop box
* @param int $height the height of the crop box
* @return $this
*/
public function crop(int $x, int $y, int $width, int $height)
{
$this->operations[] = ['crop', [$x, $y, $width, $height]];
// Update image size.
$this->width = $width;
$this->height = $height;
return $this;
}
/**
* Read exif rotation from file and apply it.
*
* @return $this
*/
public function fixOrientation()
{
if (null !== $this->orientation) {
return $this->applyExifOrientation($this->orientation);
}
$this->operations[] = ['fixOrientation', []];
return $this;
}
/**
* Apply orientation using Exif orientation value.
*
* @param int $exif_orientation
* @return $this
*/
public function applyExifOrientation(int $exif_orientation)
{
$this->operations[] = ['applyExifOrientation', [$exif_orientation]];
return $this;
}
/**
* enable progressive image loading.
*
* @return $this
*/
public function enableProgressive()
{
$this->operations[] = ['enableProgressive', []];
return $this;
}
/**
* Fills the image background to $bg if the image is transparent.
*
* @param string|int $background background color
* @return $this
*/
public function fillBackground($background = 0xffffff)
{
$bg = ImageColor::parse($background);
$this->operations[] = ['fillBackground', [$bg]];
return $this;
}
/**
* Negates the image.
*
* @return $this
*/
public function negate()
{
$this->operations[] = ['negate', []];
return $this;
}
/**
* Changes the brightness of the image.
*
* @param int $brightness the brightness
* @return $this
*/
public function brightness(int $brightness)
{
$this->operations[] = ['brightness', [$brightness]];
return $this;
}
/**
* Contrasts the image.
*
* @param int $contrast the contrast [-100, 100]
* @return $this
*/
public function contrast(int $contrast)
{
$this->operations[] = ['contrast', [$contrast]];
return $this;
}
/**
* Apply a grayscale level effect on the image.
*
* @return $this
*/
public function grayscale()
{
$this->operations[] = ['grayscale', []];
return $this;
}
/**
* Emboss the image.
*
* @return $this
*/
public function emboss()
{
$this->operations[] = ['emboss', []];
return $this;
}
/**
* Smooth the image.
*
* @param int $p value between [-10,10]
*
* @return $this
*/
public function smooth(int $p)
{
$this->operations[] = ['smooth', [$p]];
return $this;
}
/**
* Sharpens the image.
*
* @return $this
*/
public function sharp()
{
$this->operations[] = ['sharp', []];
return $this;
}
/**
* Edges the image.
*
* @return $this
*/
public function edge()
{
$this->operations[] = ['edge', []];
return $this;
}
/**
* Colorize the image.
*
* @param int $red value in range [-255, 255]
* @param int $green value in range [-255, 255]
* @param int $blue value in range [-255, 255]
* @return $this
*/
public function colorize(int $red, int $green, int $blue)
{
$this->operations[] = ['colorize', [$red, $green, $blue]];
return $this;
}
/**
* apply sepia to the image.
*
* @return $this
*/
public function sepia()
{
$this->operations[] = ['sepia', []];
return $this;
}
/**
* Merge with another image.
*
* @param Image $other
* @param int $x
* @param int $y
* @param int $width
* @param int $height
* @return $this
*/
public function merge(Image $other, int $x = 0, int $y = 0, int $width = 0, int $height = 0)
{
$serialized = $other->__serialize();
$this->operations[] = ['merge', [$serialized, $x, $y, $width, $height]];
return $this;
}
/**
* Rotate the image.
*
* @param float $angle
* @param string|int $background
* @return $this
*/
public function rotate(float $angle, $background = 0xffffff)
{
$bg = ImageColor::parse($background);
$this->operations[] = ['rotate', [$angle, $bg]];
// FIXME: Image size may change?
return $this;
}
/**
* Fills the image.
*
* @param string|int $color
* @param int $x
* @param int $y
* @return $this
*/
public function fill($color = 0xffffff, int $x = 0, int $y = 0)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['fill', [$c, $x, $y]];
return $this;
}
/**
* write text to the image.
*
* @param string $font
* @param string $text
* @param int $x
* @param int $y
* @param float $size
* @param float $angle
* @param string|int $color
* @param string $align
* @return $this
*/
public function write(string $font, string $text, int $x = 0, int $y = 0, float $size = 12.0, float $angle = 0.0, $color = 0x000000, string $align = 'left')
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['write', [$font, $text, $x, $y, $size, $angle, $c, $align]];
return $this;
}
/**
* Draws a rectangle.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function rectangle(int $x1, int $y1, int $x2, int $y2, $color, bool $filled = false)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['rectangle', [$x1, $y1, $x2, $y2, $c, $filled]];
return $this;
}
/**
* Draws a rounded rectangle.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param int $radius
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function roundedRectangle(int $x1, int $y1, int $x2, int $y2, int $radius, $color, bool $filled = false)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['roundedRectangle', [$x1, $y1, $x2, $y2, $radius, $c, $filled]];
return $this;
}
/**
* Draws a line.
*
* @param int $x1
* @param int $y1
* @param int $x2
* @param int $y2
* @param string|int $color
* @return $this
*/
public function line(int $x1, int $y1, int $x2, int $y2, $color = 0x000000)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['line', [$x1, $y1, $x2, $y2, $c]];
return $this;
}
/**
* Draws an ellipse.
*
* @param int $cx
* @param int $cy
* @param int $width
* @param int $height
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function ellipse(int $cx, int $cy, int $width, int $height, $color = 0x000000, bool $filled = false)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['ellipse', [$cx, $cy, $width, $height, $c, $filled]];
return $this;
}
/**
* Draws a circle.
*
* @param int $cx
* @param int $cy
* @param int $r
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function circle(int $cx, int $cy, int $r, $color = 0x000000, bool $filled = false)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['circle', [$cx, $cy, $r, $c, $filled]];
return $this;
}
/**
* Draws a polygon.
*
* @param array $points
* @param string|int $color
* @param bool $filled
* @return $this
*/
public function polygon(array $points, $color, bool $filled = false)
{
$c = (int)ImageColor::parse($color);
$this->operations[] = ['polygon', [$points, $c, $filled]];
return $this;
}
/**
* Flips the image.
*
* @param bool $flipVertical
* @param bool $flipHorizontal
* @return $this
*/
public function flip(bool $flipVertical, bool $flipHorizontal)
{
$this->operations[] = ['flip', [$flipVertical, $flipHorizontal]];
return $this;
}
/**
* @param string|float|int|null $width
* @param string|float|int|null $height
* @return int[]
*/
protected function getSize($width, $height): array
{
if ($height === null && is_string($width) && preg_match('#^(.+)%$#mUs', $width, $matches)) {
$width = round($this->width() * ((float)$matches[1] / 100.0));
$height = round($this->height() * ((float)$matches[1] / 100.0));
}
return [(int)$width, (int)$height];
}
}