<?php declare(strict_types=1); namespace Image; use BadMethodCallException; use GdFont; use GdImage; use Image\Exception\MyGdImageException; use InvalidArgumentException; /** * @brief Class to use GD functions in an object-oriented way. * * Each GD function imageXYZ($resource, ...) is mapped to $this->XYZ(...) * through __call() as $resource is a property of GDImage * * @see https://www.php.net/manual/fr/ref.image.php * * @author Jérôme Cutrona * * @method static GdFont|false loadfont(string $filename) * @method bool setstyle(array $style) * @method bool istruecolor() * @method bool truecolortopalette(bool $dither, int $num_colors) * @method bool palettetotruecolor() * @method bool colormatch(MyGdImage $image2) * @method bool setthickness(int $thickness) * @method bool filledellipse(int $center_x, int $center_y, int $width, int $height, int $color) * @method bool filledarc(int $center_x, int $center_y, int $width, int $height, int $start_angle, int $end_angle, int $color, int $style) * @method bool alphablending(bool $enable) * @method bool savealpha(bool $enable) * @method bool layereffect(int $effect) * @method int|false colorallocatealpha(int $red, int $green, int $blue, int $alpha) * @method int colorresolvealpha(int $red, int $green, int $blue, int $alpha) * @method int colorclosestalpha(int $red, int $green, int $blue, int $alpha) * @method int colorexactalpha(int $red, int $green, int $blue, int $alpha) * @method bool copyresampled(MyGdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $dst_width, int $dst_height, int $src_width, int $src_height) * @method MyGdImage|false rotate(float $angle, int $background_color, bool $ignore_transparent = false) * @method bool settile(MyGdImage $tile) * @method bool setbrush(MyGdImage $brush) * @method static int types() * @method bool xbm(?string $filename, ?int $foreground_color = null) * @method bool avif($file = null, int $quality = -1, int $speed = -1) * @method bool gif($file = null) * @method bool png($file = null, int $quality = -1, int $filters = -1) * @method bool webp($file = null, int $quality = -1) * @method bool jpeg($file = null, int $quality = -1) * @method bool wbmp($file = null, ?int $foreground_color = null) * @method bool gd(?string $file = null) * @method bool gd2(?string $file = null, int $chunk_size, int $mode) * @method bool bmp($file = null, bool $compressed = true) * @method bool destroy() * @method int|false colorallocate(int $red, int $green, int $blue) * @method void palettecopy(MyGdImage $src) * @method int|false colorat(int $x, int $y) * @method int colorclosest(int $red, int $green, int $blue) * @method int colorclosesthwb(int $red, int $green, int $blue) * @method bool colordeallocate(int $color) * @method int colorresolve(int $red, int $green, int $blue) * @method int colorexact(int $red, int $green, int $blue) * @method ?bool colorset(int $color, int $red, int $green, int $blue, int $alpha = 0) * @method array colorsforindex(int $color) * @method bool gammacorrect(float $input_gamma, float $output_gamma) * @method bool setpixel(int $x, int $y, int $color) * @method bool line(int $x1, int $y1, int $x2, int $y2, int $color) * @method bool dashedline(int $x1, int $y1, int $x2, int $y2, int $color) * @method bool rectangle(int $x1, int $y1, int $x2, int $y2, int $color) * @method bool filledrectangle(int $x1, int $y1, int $x2, int $y2, int $color) * @method bool arc(int $center_x, int $center_y, int $width, int $height, int $start_angle, int $end_angle, int $color) * @method bool ellipse(int $center_x, int $center_y, int $width, int $height, int $color) * @method bool filltoborder(int $x, int $y, int $border_color, int $color) * @method bool fill(int $x, int $y, int $color) * @method int colorstotal() * @method int colortransparent(?int $color = null) * @method bool interlace(?bool $enable = null) * @method bool polygon(array $points, int $num_points_or_color, ?int $color = null) * @method bool openpolygon(array $points, int $num_points_or_color, ?int $color = null) * @method bool filledpolygon(array $points, int $num_points_or_color, ?int $color = null) * @method static int fontwidth(GdFont|int $font) * @method static int fontheight(GdFont|int $font) * @method bool char(GdFont|int $font, int $x, int $y, string $char, int $color) * @method bool charup(GdFont|int $font, int $x, int $y, string $char, int $color) * @method bool string(GdFont|int $font, int $x, int $y, string $string, int $color) * @method bool stringup(GdFont|int $font, int $x, int $y, string $string, int $color) * @method bool copy(MyGdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $src_width, int $src_height) * @method bool copymerge(MyGdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $src_width, int $src_height, int $pct) * @method bool copymergegray(MyGdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $src_width, int $src_height, int $pct) * @method bool copyresized(MyGdImage $src_image, int $dst_x, int $dst_y, int $src_x, int $src_y, int $dst_width, int $dst_height, int $src_width, int $src_height) * @method int sx() * @method int sy() * @method bool setclip(int $x1, int $y1, int $x2, int $y2) * @method array getclip() * @method static array|false ftbbox(float $size, float $angle, string $font_filename, string $string, array $options = []) * @method array|false fttext(float $size, float $angle, int $x, int $y, int $color, string $font_filename, string $text, array $options = []) * @method static array|false ttfbbox(float $size, float $angle, string $font_filename, string $string, array $options = []) * @method array|false ttftext(float $size, float $angle, int $x, int $y, int $color, string $font_filename, string $text, array $options = []) * @method bool filter(int $filter, $args) * @method bool convolution(array $matrix, float $divisor, float $offset) * @method bool flip(int $mode) * @method bool antialias(bool $enable) * @method MyGdImage|false crop(array $rectangle) * @method MyGdImage|false cropauto(int $mode = IMG_CROP_DEFAULT, float $threshold = 0.5, int $color = -1) * @method MyGdImage|false scale(int $width, int $height = -1, int $mode = IMG_BILINEAR_FIXED) * @method MyGdImage|false affine(array $affine, ?array $clip = null) * @method static array|false affinematrixget(int $type, $options) * @method static array|false affinematrixconcat(array $matrix1, array $matrix2) * @method int getinterpolation() * @method bool setinterpolation(int $method = IMG_BILINEAR_FIXED) * @method array|bool resolution(?int $resolution_x = null, ?int $resolution_y = null) */ class MyGdImage { public const GD = 'gd'; public const GD2PART = 'gd2part'; public const GD2 = 'gd2'; public const GIF = 'gif'; public const JPEG = 'jpeg'; public const PNG = 'png'; public const WBMP = 'wbmp'; public const XBM = 'xbm'; public const XPM = 'xpm'; /** * Factory known types. */ private static array $factoryTypes = [ self::GD, self::GD2PART, self::GD2, self::GIF, self::JPEG, self::PNG, self::WBMP, self::XBM, self::XPM, ]; /** * Image resource. */ private ?GdImage $gdImage = null; /** * Private constructor. */ private function __construct(GdImage|false $gdImage) { $this->gdImage = $gdImage; } /** * Destructor. */ public function __destruct() { if (!is_null($this->gdImage)) { imagedestroy($this->gdImage); } } /** * Factory to create a MyGdImage instance from width and height parameters. * * @param int $width Image width * @param int $height Image height * @param bool $trueColor creates a true color image if true and a palette based image otherwise * * @return MyGdImage * * @throws MyGdImageException when image creation fails */ public static function createFromSize(int $width, int $height, bool $trueColor = true): self { $gdImage = $trueColor ? @imagecreatetruecolor($width, $height) : @imagecreate($width, $height); if (false === $gdImage) { throw new MyGdImageException('Failed to create GD resource'); } return new self($gdImage); } /** * Factory to create a MyGdImage instance from filename and filetype paramters. * * @param string $filename name of the file * @param string $filetype type of the file (must be an element of self::$_factory_types) * * @return MyGdImage * * @throws MyGdImageException when image creation fails */ public static function createFromFile(string $filename, string $filetype): self { if (!is_file($filename)) { throw new MyGdImageException("{$filename}: no such file"); } if (!in_array($filetype, self::$factoryTypes)) { throw new MyGdImageException("unknown filetype '{$filetype}'"); } $functionName = "imageCreateFrom{$filetype}"; if (($gdImage = @$functionName($filename)) === false) { throw new MyGdImageException("unable to load file '{$filename}'"); } return new self($gdImage); } /** * Factory to create a MyGdImage instance from filename and filetype parameters. * * @return MyGdImage * * @throws MyGdImageException when image creation fails */ public static function createFromString(string $data): self { if (($gdImage = imagecreatefromstring($data)) === false) { throw new MyGdImageException('unable to load data'); } return new self($gdImage); } /** * @return mixed|MyGdImage */ public function __call(string $methodName, array $methodArguments) { $gdFunction = "image{$methodName}"; if (!function_exists($gdFunction)) { throw new BadMethodCallException('Unknown method call: '.get_class($this)."::{$methodName}"); } // Prevent direct call of imageCreateFrom... if (mb_eregi('^imageCreateFrom', $gdFunction)) { throw new BadMethodCallException('Forbidden method call '.get_class($this)."::{$methodName}"); } // Special case of copy functions if (mb_eregi('^(copy|colorMatch)', $methodName)) { // First parameter of the method should be an instance of the class if (!isset($methodArguments[0]) || !$methodArguments[0] instanceof self) { $type = is_object($methodArguments[0]) ? get_class($methodArguments[0]) : gettype($methodArguments[0]); throw new InvalidArgumentException("First parameter of '".get_class($this)."::{$methodName}' should be an instance of ".get_class($this).', '.$type.' given'); } // Preparing argument for GD function call $methodArguments[0] = $methodArguments[0]->gdImage; } // Avoid function which first parameter is not an image resource if (!mb_eregi('^(imageFont|imageFtBbox|imageGrab|imageLoadFont|imagePs|imageTypes)', $gdFunction)) { // First parameter should be the image resource array_unshift($methodArguments, $this->gdImage); } // Call GD function $returnValue = @call_user_func_array($gdFunction, $methodArguments); if (null === $returnValue) { throw new BadMethodCallException('Error in '.get_class($this)."::{$methodName}"); } if (is_a($returnValue, GdImage::class)) { return new self($returnValue); } else { return $returnValue; } } /** * Retrieve information about the currently installed GD library. * * @return array returns an associative array */ public static function info(): array { return gd_info(); } /** * Get the size of an image. * * @param string $filename this parameter specifies the file you wish to retrieve information about * @param array $imageInfo This optional parameter allows you to extract some extended information from the image file * * @return array an array with up to 7 elements. Not all image types will include the channels and bits elements. Index 0 and 1 contains respectively the width and the height of the image. */ public static function getImageSize(string $filename, array &$imageInfo = []): array { return @getimagesize($filename, $imageInfo); } /** * Trap "inaccessible static methods" to invoke GD functions, if available. * If a method named 'colorAllocate' is trapped, it will try to invoke 'imageColorAllocate' function. * * @param string $methodName name of the "inaccessible static method" * @param array $methodArguments array of the arguments of the "inaccessible static method" * * @return mixed * * @throws BadMethodCallException */ public static function __callStatic(string $methodName, array $methodArguments) { $gdFunction = !function_exists($methodName) ? "image{$methodName}" : $methodName; if (!function_exists($gdFunction) || mb_eregi('^imageCreateFrom', $gdFunction)) { throw new BadMethodCallException('Call to unknown static method '.get_class()."::{$methodName}"); } $returnValue = call_user_func_array($gdFunction, $methodArguments); if (null === $returnValue) { throw new BadMethodCallException('Error in '.get_class()."::{$methodName}"); } return $returnValue; } /** * Clone. * * @throws MyGdImageException when resource creation fails */ public function __clone() { if (!imageistruecolor($this->gdImage)) { if (($resource = @imagecreate(imagesx($this->gdImage), imagesy($this->gdImage))) === false) { throw new MyGdImageException('unable to clone MyGdImage'); } imagepalettecopy($resource, $this->gdImage); } else { if (($resource = @imagecreatetruecolor(imagesx($this->gdImage), imagesy($this->gdImage))) === false) { throw new MyGdImageException('unable to clone MyGdImage'); } } imagecopy($resource, $this->gdImage, 0, 0, 0, 0, imagesx($this->gdImage), imagesy($this->gdImage)); $this->gdImage = $resource; } /** * @throws MyGdImageException when output buffer capture fails */ protected static function captureOutputBuffer(callable $callback): string { ob_start(); $callback(); $capturedOutputBuffer = ob_get_clean(); if (false === $capturedOutputBuffer) { throw new MyGdImageException('failed to capture output buffer'); } return $capturedOutputBuffer; } /** * @throws MyGdImageException when output buffer capture fails */ public function getJpeg(int $quality = -1): string { return self::captureOutputBuffer(fn () => $this->jpeg(null, $quality)); } /** * @throws MyGdImageException when output buffer capture fails */ public function getPng(int $quality = -1, int $filters = -1): string { return self::captureOutputBuffer(fn () => $this->png(null, $quality, $filters)); } /** * @throws MyGdImageException when output buffer capture fails */ public function getGif(): string { return self::captureOutputBuffer(fn () => $this->gif(null)); } }