ImageMagick Support for PHP Thumb Library

4
Here's the new Imagick.php class that provides ImageMagick support for the PHP Thumb Library:
PHP
<?php

namespace PHPThumb;

use Exception;
use Imagick;
use ImagickDraw;
use ImagickPixel;
use InvalidArgumentException;
use RuntimeException;

/**
 * PhpThumb : PHP Thumb Library <https://github.com/PHPThumb/PHPThumb>
 * Copyright (c) 2009, Ian Selby
 *
 * Author(s): Ian Selby <ianrselby@gmail.com>
 *
 * Licensed under the MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @author Ian Selby <ianrselby@gmail.com>
 * @copyright Copyright (c) 2009 Ian Selby
 * @link https://github.com/PHPThumb/PHPThumb
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
 */

class Imagick extends \PHPThumb\PHPThumb
{
	/**
	 * The prior image (before manipulation)
	 *
	 * @var Imagick
	 */
	protected $old_image;

	/**
	 * The working image (used during manipulation)
	 *
	 * @var Imagick
	 */
	protected $working_image;

	/**
	 * The current dimensions of the image
	 */
	protected array $current_dimensions;

	/**
	 * The new, calculated dimensions of the image
	 */
	protected array $new_dimensions;

	/**
	 * The options for this class
	 */
	protected array $options = [];

	/**
	 * The maximum width an image can be after resizing (in pixels)
	 */
	protected int $max_width;

	/**
	 * The maximum height an image can be after resizing (in pixels)
	 */
	protected int $max_height;

	/**
	 * The percentage to resize the image by
	 */
	protected int $percent;

	/**
	 * @throws Exception
	 */
	public function __construct(string $file_name, array $options = [], array $plugins = [])
	{
		parent::__construct($file_name, $options, $plugins);

		$this->determineFormat();
		$this->verifyFormatCompatibility();

		$this->old_image = new Imagick();

		if ($this->remote_image)
		{
			$this->old_image->readImage($this->file_name);
		}
		else
		{
			$this->old_image->readImage($this->file_name);
		}

		$this->current_dimensions = [
			'width'		=> $this->old_image->getImageWidth(),
			'height'	=> $this->old_image->getImageHeight()
		];
	}

	/**
	 * Destructor
	 */
	public function __destruct()
	{
		if ($this->old_image instanceof Imagick)
		{
			$this->old_image->clear();
			$this->old_image->destroy();
		}

		if ($this->working_image instanceof Imagick)
		{
			$this->working_image->clear();
			$this->working_image->destroy();
		}
	}

	/**
	 * Pad an image to desired dimensions. Moves the image into the center and fills the rest with $color.
	 */
	public function pad(int $width, int $height, array $color = [255, 255, 255]): Imagick
	{
		// no resize - woohoo!
		if ($width == $this->current_dimensions['width'] && $height == $this->current_dimensions['height'])
		{
			return $this;
		}

		$this->working_image = new Imagick();

		$pixel = new ImagickPixel('rgb(' . $color[0] . ', ' . $color[1] . ', ' . $color[2] . ')');
		$this->working_image->newImage($width, $height, $pixel);
		$pixel->destroy();

		$this->working_image->setFormat($this->old_image->getFormat());

		$x = intval(($width - $this->current_dimensions['width']) / 2);
		$y = intval(($height - $this->current_dimensions['height']) / 2);

		$this->working_image->compositeImage(
			$this->old_image,
			Imagick::COMPOSITE_DEFAULT,
			$x,
			$y
		);

		$this->old_image->clear();
		$this->old_image->destroy();
		$this->old_image = $this->working_image;

		$this->current_dimensions['width']  = $width;
		$this->current_dimensions['height'] = $height;

		return $this;
	}

	/**
	 * Check if the image can be scaled up
	 */
	private function checkingMaxSize(int $max_width, int $max_height): void
	{
		if ($this->options['resizeUp'] === false)
		{
			$this->max_height = ($max_height > $this->current_dimensions['height']) ? $this->current_dimensions['height'] : $max_height;
			$this->max_width  = ($max_width > $this->current_dimensions['width'])  ? $this->current_dimensions['width']  : $max_width;
		}
		else
		{
			$this->max_height = $max_height;
			$this->max_width  = $max_width;
		}
	}

	/**
	 * Resizes an image to be no larger than $max_width or $max_height
	 */
	public function resize(int $max_width = 0, int $max_height = 0): Imagick
	{
		$this->checkingMaxSize($max_width, $max_height);
		$this->calcImageSize($this->current_dimensions['width'], $this->current_dimensions['height']);

		$this->old_image->thumbnailImage(
			$this->new_dimensions['new_width'],
			$this->new_dimensions['new_height'],
			false
		);

		$this->current_dimensions['width']  = $this->new_dimensions['new_width'];
		$this->current_dimensions['height'] = $this->new_dimensions['new_height'];

		return $this;
	}

	/**
	 * Adaptively Resizes the Image
	 */
	public function adaptiveResize(int $width, int $height): Imagick
	{
		if ($width == 0 && $height == 0)
		{
			throw new InvalidArgumentException('$width and $height must be numeric and greater than zero');
		}

		if ($width == 0)
		{
			$width = intval(($height * $this->current_dimensions['width']) / $this->current_dimensions['height']);
		}

		if ($height == 0)
		{
			$height = intval(($width * $this->current_dimensions['height']) / $this->current_dimensions['width']);
		}

		$this->checkingMaxSize($width, $height);
		$this->calcImageSizeStrict($this->current_dimensions['width'], $this->current_dimensions['height']);

		$resize_width  = $this->new_dimensions['new_width'];
		$resize_height = $this->new_dimensions['new_height'];

		$this->old_image->thumbnailImage($resize_width, $resize_height, false);

		$this->checkingMaxSize($width, $height);

		$crop_width  = $this->max_width;
		$crop_height = $this->max_height;
		$crop_x      = 0;
		$crop_y      = 0;

		if ($this->current_dimensions['width'] > $this->max_width)
		{
			$crop_x = intval(($this->current_dimensions['width'] - $this->max_width) / 2);
		}
		else if ($this->current_dimensions['height'] > $this->max_height)
		{
			$crop_y = intval(($this->current_dimensions['height'] - $this->max_height) / 2);
		}

		$this->old_image->cropImage($crop_width, $crop_height, $crop_x, $crop_y);

		$this->current_dimensions['width']  = $crop_width;
		$this->current_dimensions['height'] = $crop_height;

		return $this;
	}

	/**
	 * Adaptively Resizes the Image and Crops Using a Percentage
	 */
	public function adaptiveResizePercent(int $width, int $height, int $percent = 50): Imagick
	{
		if ($width == 0)
		{
			throw new InvalidArgumentException('$width must be numeric and greater than zero');
		}

		if ($height == 0)
		{
			throw new InvalidArgumentException('$height must be numeric and greater than zero');
		}

		$this->checkingMaxSize($width, $height);
		$this->calcImageSizeStrict($this->current_dimensions['width'], $this->current_dimensions['height']);

		$resize_width  = $this->new_dimensions['new_width'];
		$resize_height = $this->new_dimensions['new_height'];

		$this->old_image->thumbnailImage($resize_width, $resize_height, false);

		$this->checkingMaxSize($width, $height);

		$crop_width  = $this->max_width;
		$crop_height = $this->max_height;
		$crop_x      = 0;
		$crop_y      = 0;

		if ($percent > 100)
		{
			$percent = 100;
		}
		else if ($percent < 1)
		{
			$percent = 1;
		}

		if ($this->current_dimensions['width'] > $this->max_width)
		{
			$max_crop_x = $this->current_dimensions['width'] - $this->max_width;
			$crop_x = intval(($percent / 100) * $max_crop_x);
		}
		else if ($this->current_dimensions['height'] > $this->max_height)
		{
			$max_crop_y = $this->current_dimensions['height'] - $this->max_height;
			$crop_y = intval(($percent / 100) * $max_crop_y);
		}

		$this->old_image->cropImage($crop_width, $crop_height, $crop_x, $crop_y);

		$this->current_dimensions['width']  = $crop_width;
		$this->current_dimensions['height'] = $crop_height;

		return $this;
	}

	/**
	 * Adaptively Resizes the Image and Crops Using a Quadrant
	 */
	public function adaptiveResizeQuadrant(int $width, int $height, string $quadrant = 'C'): Imagick
	{
		if ($width == 0)
		{
			throw new InvalidArgumentException('$width must be numeric and greater than zero');
		}

		if ($height == 0)
		{
			throw new InvalidArgumentException('$height must be numeric and greater than zero');
		}

		$this->checkingMaxSize($width, $height);
		$this->calcImageSizeStrict($this->current_dimensions['width'], $this->current_dimensions['height']);

		$resize_width  = $this->new_dimensions['new_width'];
		$resize_height = $this->new_dimensions['new_height'];

		$this->old_image->thumbnailImage($resize_width, $resize_height, false);

		$this->checkingMaxSize($width, $height);

		$crop_width  = $this->max_width;
		$crop_height = $this->max_height;
		$crop_x      = 0;
		$crop_y      = 0;

		if ($this->current_dimensions['width'] > $this->max_width)
		{
			$crop_x = match ($quadrant) {
				'L'     => 0,
				'R'     => intval(($this->current_dimensions['width'] - $this->max_width)),
				default => intval(($this->current_dimensions['width'] - $this->max_width) / 2),
			};
		}
		else if ($this->current_dimensions['height'] > $this->max_height)
		{
			$crop_y = match ($quadrant) {
				'T'     => 0,
				'B'     => intval(($this->current_dimensions['height'] - $this->max_height)),
				default => intval(($this->current_dimensions['height'] - $this->max_height) / 2),
			};
		}

		$this->old_image->cropImage($crop_width, $crop_height, $crop_x, $crop_y);

		$this->current_dimensions['width']  = $crop_width;
		$this->current_dimensions['height'] = $crop_height;

		return $this;
	}

	/**
	 * Resizes an image by a given percent uniformly
	 */
	public function resizePercent(int $percent = 0): Imagick
	{
		$this->percent = $percent;

		$this->calcImageSizePercent($this->current_dimensions['width'], $this->current_dimensions['height']);

		return $this->resize($this->new_dimensions['new_width'], $this->new_dimensions['new_height']);
	}

	/**
	 * Crops an image from the center with provided dimensions
	 */
	public function cropFromCenter(int $crop_width, ?int $crop_height = 0): Imagick
	{
		if ($crop_height == 0)
		{
			$crop_height = $crop_width;
		}

		$crop_width  = ($this->current_dimensions['width'] < $crop_width)  ? $this->current_dimensions['width']  : $crop_width;
		$crop_height = ($this->current_dimensions['height'] < $crop_height) ? $this->current_dimensions['height'] : $crop_height;

		$crop_x = intval(($this->current_dimensions['width'] - $crop_width) / 2);
		$crop_y = intval(($this->current_dimensions['height'] - $crop_height) / 2);

		$this->crop($crop_x, $crop_y, $crop_width, $crop_height);

		return $this;
	}

	/**
	 * Vanilla Cropping - Crops from x,y with specified width and height
	 */
	public function crop(int $start_x, int $start_y, int $crop_width, int $crop_height): Imagick
	{
		$crop_width  = ($this->current_dimensions['width'] < $crop_width)  ? $this->current_dimensions['width']  : $crop_width;
		$crop_height = ($this->current_dimensions['height'] < $crop_height) ? $this->current_dimensions['height'] : $crop_height;

		if (($start_x + $crop_width) > $this->current_dimensions['width'])
		{
			$start_x = ($this->current_dimensions['width'] - $crop_width);
		}

		if (($start_y + $crop_height) > $this->current_dimensions['height'])
		{
			$start_y = ($this->current_dimensions['height'] - $crop_height);
		}

		if ($start_x < 0)
		{
			$start_x = 0;
		}

		if ($start_y < 0)
		{
			$start_y = 0;
		}

		$this->old_image->cropImage($crop_width, $crop_height, $start_x, $start_y);

		$this->current_dimensions['width']  = $crop_width;
		$this->current_dimensions['height'] = $crop_height;

		return $this;
	}

	/**
	 * Rotates image either 90 degrees clockwise or counter-clockwise
	 */
	public function rotateImage(string $direction = 'CW'): Imagick
	{
		$degrees = match($direction) {
			'CW'    => 90,
			default => -90,
		};

		$this->rotateImageNDegrees($degrees);

		return $this;
	}

	/**
	 * Rotates image specified number of degrees
	 */
	public function rotateImageNDegrees(int $degrees): Imagick
	{
		$background_color = new ImagickPixel('rgb(' . $this->options['alphaMaskColor'][0] . ', ' . $this->options['alphaMaskColor'][1] . ', ' . $this->options['alphaMaskColor'][2] . ')');

		$this->old_image->rotateImage($background_color, $degrees);

		$background_color->destroy();

		$this->current_dimensions['width']  = $this->old_image->getImageWidth();
		$this->current_dimensions['height'] = $this->old_image->getImageHeight();

		return $this;
	}

	/**
	 * Applies a filter to the image
	 */
	public function imageFilter(int $filter, bool $arg1 = false, bool $arg2 = false, bool $arg3 = false, bool $arg4 = false): Imagick
	{
		$arguments = [];

		if ($arg1 !== false)
		{
			$arguments[] = $arg1;
		}

		if ($arg2 !== false)
		{
			$arguments[] = $arg2;
		}

		if ($arg3 !== false)
		{
			$arguments[] = $arg3;
		}

		if ($arg4 !== false)
		{
			$arguments[] = $arg4;
		}

		$this->old_image->filter($filter, ...$arguments);

		return $this;
	}

	/**
	 * Shows an image
	 */
	public function show(bool $raw_data = false): Imagick
	{
		if ($this->plugins)
		{
			foreach ($this->plugins as $plugin)
			{
				$plugin->execute($this);
			}
		}

		if (headers_sent() && php_sapi_name() != 'cli')
		{
			throw new RuntimeException('Cannot show image, headers have already been sent');
		}

		$format = strtolower($this->old_image->getImageFormat());
		$mime_type = match ($format) {
			'avif'  => 'image/avif',
			'gif'   => 'image/gif',
			'jpeg', 'jpg' => 'image/jpeg',
			'png'   => 'image/png',
			'webp'  => 'image/webp',
			'bmp'   => 'image/bmp',
			default => 'image/' . $format,
		};

		if ($raw_data === false)
		{
			header('Content-type: ' . $mime_type);
		}

		echo $this->old_image->getImagesBlob();

		return $this;
	}

	/**
	 * Returns the Working Image as a String
	 */
	public function getImageAsString(): string
	{
		return $this->old_image->getImagesBlob();
	}

	/**
	 * Saves an image
	 */
	public function save(string $file_name, ?string $format = null): Imagick
	{
		$format = ($format !== null) ? strtoupper($format) : strtoupper($this->format);

		if (!is_writeable(dirname($file_name)))
		{
			if ($this->options['correctPermissions'] === true)
			{
				@chmod(dirname($file_name), 0777);

				if (!is_writeable(dirname($file_name)))
				{
					throw new RuntimeException('File is not writeable, and could not correct permissions: ' . $file_name);
				}
			}
			else
			{
				throw new RuntimeException('File not writeable: ' . $file_name);
			}
		}

		$output_format = match ($format) {
			'AVIF'  => 'AVIF',
			'GIF'   => 'GIF',
			'JPEG', 'JPG' => 'JPEG',
			'PNG'   => 'PNG',
			'WEBP'  => 'WEBP',
			default => strtoupper($format),
		};

		$this->old_image->setFormat($output_format);
		$this->old_image->setImageFormat($output_format);

		$quality = match ($output_format) {
			'AVIF'  => $this->options['avifQuality'],
			'JPEG', 'JPG' => $this->options['jpegQuality'],
			'WEBP'  => $this->options['webpQuality'],
			default => null,
		};

		if ($quality !== null)
		{
			$this->old_image->setImageCompressionQuality($quality);
		}

		$this->old_image->writeImage($file_name);

		return $this;
	}

	#################################
	# ----- GETTERS / SETTERS ----- #
	#################################

	/**
	 * Sets options for all operations.
	 */
	public function setOptions(array $options = []): Imagick
	{
		if (count($this->options) == 0)
		{
			$default_options = [
				'resizeUp'              => false,
				'avifQuality'           => 100,
				'jpegQuality'           => 100,
				'webpQuality'           => 100,
				'correctPermissions'    => false,
				'preserveAlpha'         => true,
				'alphaMaskColor'        => [255, 255, 255],
				'preserveTransparency'  => true,
				'transparencyMaskColor' => [0, 0, 0],
				'interlace'             => null
			];
		}
		else
		{
			$default_options = $this->options;
		}

		$this->options = array_merge($default_options, $options);

		return $this;
	}

	/**
	 * Returns $current_dimensions.
	 */
	public function getCurrentDimensions(): array
	{
		return $this->current_dimensions;
	}

	public function setCurrentDimensions(array $current_dimensions): Imagick
	{
		$this->current_dimensions = $current_dimensions;

		return $this;
	}

	public function getMaxHeight(): int
	{
		return $this->max_height;
	}

	public function setMaxHeight(int $max_height): Imagick
	{
		$this->max_height = $max_height;

		return $this;
	}

	public function getMaxWidth(): int
	{
		return $this->max_width;
	}

	public function setMaxWidth(int $max_width): Imagick
	{
		$this->max_width = $max_width;

		return $this;
	}

	/**
	 * Returns $new_dimensions.
	 */
	public function getNewDimensions(): array
	{
		return $this->new_dimensions;
	}

	/**
	 * Sets $new_dimensions.
	 */
	public function setNewDimensions(array $new_dimensions): Imagick
	{
		$this->new_dimensions = $new_dimensions;

		return $this;
	}

	/**
	 * Returns $options.
	 */
	public function getOptions(): array
	{
		return $this->options;
	}

	/**
	 * Returns $percent.
	 */
	public function getPercent(): int
	{
		return $this->percent;
	}

	/**
	 * Sets $percent.
	 */
	public function setPercent(int $percent): Imagick
	{
		$this->percent = $percent;

		return $this;
	}

	/**
	 * Returns $old_image.
	 */
	public function getOldImage(): Imagick
	{
		return $this->old_image;
	}

	/**
	 * Sets $old_image.
	 */
	public function setOldImage(Imagick $old_image): static
	{
		$this->old_image = $old_image;

		return $this;
	}

	/**
	 * Returns $working_image.
	 */
	public function getWorkingImage(): Imagick
	{
		return $this->working_image;
	}

	/**
	 * Sets $working_image.
	 */
	public function setWorkingImage(Imagick $working_image): static
	{
		$this->working_image = $working_image;

		return $this;
	}


	#################################
	# ----- UTILITY FUNCTIONS ----- #
	#################################

	/**
	 * Calculates a new width and height for the image based on $this->max_width and the provided dimensions
	 */
	protected function calcWidth(int $width, int $height): array
	{
		$new_width_percentage = (100 * $this->max_width) / $width;
		$new_height = ($height * $new_width_percentage) / 100;

		return [
			'new_width'  => $this->max_width,
			'new_height' => intval($new_height)
		];
	}

	/**
	 * Calculates a new width and height for the image based on $this->max_height and the provided dimensions
	 */
	protected function calcHeight(int $width, int $height): array
	{
		$new_height_percentage = (100 * $this->max_height) / $height;
		$new_width = ($width * $new_height_percentage) / 100;

		return [
			'new_width'  => ceil($new_width),
			'new_height' => ceil($this->max_height)
		];
	}

	/**
	 * Calculates a new width and height for the image based on $this->percent and the provided dimensions
	 */
	protected function calcPercent(int $width, int $height): array
	{
		$new_width  = ($width * $this->percent) / 100;
		$new_height = ($height * $this->percent) / 100;

		return [
			'new_width'  => ceil($new_width),
			'new_height' => ceil($new_height)
		];
	}

	/**
	 * Calculates the new image dimensions
	 */
	protected function calcImageSize(int $width, int $height): void
	{
		$new_size = [
			'new_width'  => $width,
			'new_height' => $height
		];

		if ($this->max_width > 0)
		{
			$new_size = $this->calcWidth($width, $height);

			if ($this->max_height > 0 && $new_size['new_height'] > $this->max_height)
			{
				$new_size = $this->calcHeight($new_size['new_width'], $new_size['new_height']);
			}
		}

		if ($this->max_height > 0)
		{
			$new_size = $this->calcHeight($width, $height);

			if ($this->max_width > 0 && $new_size['new_width'] > $this->max_width)
			{
				$new_size = $this->calcWidth($new_size['new_width'], $new_size['new_height']);
			}
		}

		$this->new_dimensions = $new_size;
	}

	/**
	 * Calculates new image dimensions, not allowing the width and height to be less than either the max width or height
	 */
	protected function calcImageSizeStrict(int $width, int $height): void
	{
		$new_dimensions = $this->getCurrentDimensions();

		if ($this->max_width >= $this->max_height)
		{
			if ($width > $height)
			{
				$new_dimensions = $this->calcHeight($width, $height);

				if ($new_dimensions['new_width'] < $this->max_width)
				{
					$new_dimensions = $this->calcWidth($width, $height);
				}
			}
			else if ($height >= $width)
			{
				$new_dimensions = $this->calcWidth($width, $height);

				if ($new_dimensions['new_height'] < $this->max_height)
				{
					$new_dimensions = $this->calcHeight($width, $height);
				}
			}
		}
		else if ($this->max_height > $this->max_width)
		{
			if ($width >= $height)
			{
				$new_dimensions = $this->calcWidth($width, $height);

				if ($new_dimensions['new_height'] < $this->max_height)
				{
					$new_dimensions = $this->calcHeight($width, $height);
				}
			}
			else if ($height > $width)
			{
				$new_dimensions = $this->calcHeight($width, $height);

				if ($new_dimensions['new_width'] < $this->max_width)
				{
					$new_dimensions = $this->calcWidth($width, $height);
				}
			}
		}

		$this->new_dimensions = $new_dimensions;
	}

	/**
	 * Calculates new dimensions based on $this->percent and the provided dimensions
	 */
	protected function calcImageSizePercent(int $width, int $height): void
	{
		if ($this->percent > 0)
		{
			$this->new_dimensions = $this->calcPercent($width, $height);
		}
	}

	/**
	 * Determines the file format by mime-type
	 */
	protected function determineFormat(): void
	{
		if ($this->remote_image)
		{
			$format_info = getimagesize($this->file_name);

			if ($format_info === false)
			{
				throw new Exception('Could not determine format of remote image: ' . $this->file_name);
			}

			$mime_type = $format_info['mime'] ?? null;
		}
		else
		{
			$finfo = finfo_open(FILEINFO_MIME_TYPE);
			$mime_type = finfo_file($finfo, $this->file_name);
			finfo_close($finfo);
		}

		$this->format = match ($mime_type) {
			'image/avif'   => 'AVIF',
			'image/bmp'    => 'BMP',
			'image/gif'    => 'GIF',
			'image/heic'   => 'HEIC',
			'image/jpeg'   => 'JPEG',
			'image/png'    => 'PNG',
			'image/tiff'   => 'TIFF',
			'image/webp'   => 'WEBP',
			default        => throw new Exception('Image format not supported: ' . $mime_type),
		};
	}

	/**
	 * Makes sure the correct ImageMagick format is supported
	 */
	protected function verifyFormatCompatibility(): void
	{
		$imagick = new Imagick();

		$formats = $imagick->queryFormats();

		$format_to_check = strtoupper($this->format);

		if (!in_array($format_to_check, $formats))
		{
			throw new Exception('Your ImageMagick installation does not support ' . $this->format . ' image types');
		}

		$imagick->destroy();
	}
}

Usage Example


PHP
<?php

require_once 'Imagick.php';

use PHPThumb\Imagick;

// Create an instance with ImageMagick
$thumb = new Imagick('path/to/image.jpg');

// Chain methods like with GD
$thumb->resize(800, 600)
      ->show();

// Save to file
$thumb->save('path/to/output.png', 'PNG');

// Get image as string
$imageData = $thumb->getImageAsString();

Key Differences from GD Implementation


Feature GD Imagick
Image Loading imagecreatefrom*() new Imagick() + readImage()
Image Output image*() functions getImagesBlob()
Resize imagecopyresampled() thumbnailImage()
Crop imagecopyresampled() cropImage()
Rotate imagerotate() rotateImage()
Memory Management Manual Automatic + destroy() in destructor
Format Support Limited by GD build Extensive (60+ formats)
Remote Images Via file_get_contents() Native readImage() with URL

The Imagick class provides the same API as the GD class, making it easy to switch between the two implementations based on your needs.

Comments

  1. ImageMagick Tests for PHP Thumb Library



    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use PHPThumb\Imagick;
    use PHPUnit\Framework\TestCase;
    
    class ImagickTest extends TestCase
    {
    	protected Imagick $avif;
    	protected Imagick $bmp;
    	protected Imagick $gif;
    	protected Imagick $heic;
    	protected Imagick $jpg;
    	protected Imagick $png;
    	protected Imagick $tiff;
    	protected Imagick $webp;
    
    	protected function setUp(): void
    	{
    		$this->avif	= new Imagick(__DIR__ . '/../../resources/test.avif');
    		$this->bmp	= new Imagick(__DIR__ . '/../../resources/test.bmp');
    		$this->gif	= new Imagick(__DIR__ . '/../../resources/test.gif');
    		$this->heic	= new Imagick(__DIR__ . '/../../resources/test.heic');
    		$this->jpg	= new Imagick(__DIR__ . '/../../resources/test.jpg');
    		$this->png	= new Imagick(__DIR__ . '/../../resources/test.png');
    		$this->tiff	= new Imagick(__DIR__ . '/../../resources/test.tiff');
    		$this->webp	= new Imagick(__DIR__ . '/../../resources/test.webp');
    	}
    
    	public function testLoadFileTypes()
    	{
    		self::assertSame('AVIF',	$this->avif->getFormat());
    		self::assertSame('BMP',		$this->bmp->getFormat());
    		self::assertSame('GIF',		$this->gif->getFormat());
    		self::assertSame('HEIC',	$this->heic->getFormat());
    		self::assertSame('JPG',		$this->jpg->getFormat());
    		self::assertSame('PNG',		$this->png->getFormat());
    		self::assertSame('TIFF',	$this->tiff->getFormat());
    		self::assertSame('WEBP',	$this->webp->getFormat());
    	}
    
    	/**
    	 * This test might seem pointless but it runs the __destruct and gets us to
    	 * 100% code coverage.
    	 */
    	public function testImageDestroy()
    	{
    		$testImage = new Imagick(__DIR__ . '/../../resources/test.gif');
    		unset($testImage);
    		self::assertSame(false, isset($testImage));
    	}
    
    	/**
    	 * This test first resize a webp image and then save it in a temp file.
    	 * Load the image file and test if the resulting image have a width of 200 px.
    	 */
    	public function testWebp()
    	{
    		$this->webp->adaptiveResize(200, 200);
    		$tempFile = __DIR__ . '/../../resources/imagick_resize.webp';
    		fwrite(fopen($tempFile, 'w'), $this->webp->getImageAsString());
    		$testing = new Imagick($tempFile);
    		self::assertSame(200, $testing->getCurrentDimensions()['width']);
    		unlink($tempFile);
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use InvalidArgumentException;
    use PHPUnit\Framework\TestCase;
    
    class ImagickLoadTest extends TestCase
    {
    	protected Imagick $thumb;
    
    	protected function setUp(): void
    	{
    		$this->thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    	}
    
    	public function testLoadFile()
    	{
    		self::assertSame(['width' => 500, 'height' => 375], $this->thumb->getCurrentDimensions());
    		self::assertSame([
    			'resizeUp'				=> false,
    			'avifQuality'			=> 100,
    			'jpegQuality'			=> 100,
    			'webpQuality'			=> 100,
    			'correctPermissions'	=> false,
    			'preserveAlpha'			=> true,
    			'alphaMaskColor'		=> [
    										0 => 255,
    										1 => 255,
    										2 => 255
    			],
    			'preserveTransparency'  => true,
    			'transparencyMaskColor' => [
    										0 => 0,
    										1 => 0,
    										2 => 0
    			],
    			'interlace'				=> null
    		], $this->thumb->getOptions());
    
    		self::assertSame('JPG', $this->thumb->getFormat());
    		self::assertSame(__DIR__ . '/../../resources/test.jpg', $this->thumb->getFileName());
    	}
    
    	public function testSetFormat()
    	{
    		$this->thumb->setFormat('PNG');
    		self::assertSame('PNG', $this->thumb->getFormat());
    	}
    
    	public function testSetFilename()
    	{
    		$this->thumb->setFilename('mytest.jpg');
    		self::assertSame('mytest.jpg', $this->thumb->getFilename());
    	}
    
    	public function testLoadExternalImage()
    	{
    		$gravatarThumb = new Imagick('https://en.gravatar.com/userimage/1132703/2ccbcfbea4a1b3b8d955c1e7746b882b.jpg');
    		self::assertSame(true, $gravatarThumb->getIsRemoteImage());
    	}
    
    	public function testNonexistentFile()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		$madeupThumb = new Imagick('nosuchimage.jpg');
    	}
    
    	public function testIsRemoteImage()
    	{
    		$remoteThumb = new Imagick('https://example.com/image.jpg');
    		self::assertTrue($remoteThumb->getIsRemoteImage());
    
    		$localThumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    		self::assertFalse($localThumb->getIsRemoteImage());
    	}
    
    	public function testGettersAndSetters()
    	{
    		$this->thumb->setMaxWidth(100);
    		$this->thumb->setMaxHeight(200);
    		$this->thumb->setPercent(50);
    
    		self::assertSame(100, $this->thumb->getMaxWidth());
    		self::assertSame(200, $this->thumb->getMaxHeight());
    		self::assertSame(50, $this->thumb->getPercent());
    
    		$this->thumb->setNewDimensions(['new_width' => 250, 'new_height' => 188]);
    		self::assertSame(['new_width' => 250, 'new_height' => 188], $this->thumb->getNewDimensions());
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use InvalidArgumentException;
    use PHPThumb\Imagick;
    use PHPUnit\Framework\TestCase;
    
    class ImagickOperationsTest extends TestCase
    {
    	protected Imagick $thumb;
    
    	protected function setUp(): void
    	{
    		$this->thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    	}
    
    	/**
    	 * @dataProvider resizeProvider
    	 */
    	public function testResize(int $maxWidth, int $maxHeight, array $expected): void
    	{
    		$result = $this->thumb->resize($maxWidth, $maxHeight);
    
    		self::assertSame($expected['width'], $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame($expected['height'], $this->thumb->getCurrentDimensions()['height']);
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public static function resizeProvider(): array
    	{
    		return [
    			'resize by width'  => [200, 0, ['width' => 200, 'height' => 150]],
    			'resize by height' => [0, 200, ['width' => 267, 'height' => 200]],
    			'resize both'      => [100, 100, ['width' => 100, 'height' => 75]],
    			'no resize'        => [0, 0, ['width' => 500, 'height' => 375]],
    		];
    	}
    
    	/**
    	 * @dataProvider adaptiveResizeProvider
    	 */
    	public function testAdaptiveResize(int $width, int $height, array $expected): void
    	{
    		$this->thumb->adaptiveResize($width, $height);
    
    		self::assertSame($expected['width'], $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame($expected['height'], $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public static function adaptiveResizeProvider(): array
    	{
    		return [
    			'square resize'        => [200, 200, ['width' => 200, 'height' => 200]],
    			'landscape resize'     => [400, 200, ['width' => 400, 'height' => 200]],
    			'portrait resize'      => [200, 400, ['width' => 200, 'height' => 400]],
    			'width only'           => [300, 0, ['width' => 300, 'height' => 225]],
    			'height only'          => [0, 300, ['width' => 400, 'height' => 300]],
    		];
    	}
    
    	public function testAdaptiveResizeInvalidArguments()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		$this->thumb->adaptiveResize(0, 0);
    	}
    
    	/**
    	 * @dataProvider adaptiveResizeQuadrantProvider
    	 */
    	public function testAdaptiveResizeQuadrant(int $width, int $height, string $quadrant, array $expected): void
    	{
    		$this->thumb->adaptiveResizeQuadrant($width, $height, $quadrant);
    
    		self::assertSame($expected['width'], $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame($expected['height'], $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public static function adaptiveResizeQuadrantProvider(): array
    	{
    		return [
    			'center quadrant' => [200, 200, 'C', ['width' => 200, 'height' => 200]],
    			'left quadrant'   => [200, 200, 'L', ['width' => 200, 'height' => 200]],
    			'right quadrant'  => [200, 200, 'R', ['width' => 200, 'height' => 200]],
    			'top quadrant'    => [200, 200, 'T', ['width' => 200, 'height' => 200]],
    			'bottom quadrant' => [200, 200, 'B', ['width' => 200, 'height' => 200]],
    		];
    	}
    
    	public function testAdaptiveResizePercent()
    	{
    		$this->thumb->adaptiveResizePercent(200, 200, 25);
    
    		self::assertSame(200, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(200, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testResizePercent()
    	{
    		$this->thumb->resizePercent(50);
    
    		self::assertSame(250, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(188, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testCrop()
    	{
    		$this->thumb->crop(100, 50, 200, 150);
    
    		self::assertSame(200, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(150, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testCropFromCenter()
    	{
    		$this->thumb->cropFromCenter(200);
    
    		self::assertSame(200, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(200, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testCropFromCenterWithHeight()
    	{
    		$this->thumb->cropFromCenter(200, 100);
    
    		self::assertSame(200, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(100, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testRotateImageCW()
    	{
    		$this->thumb->rotateImage('CW');
    
    		self::assertSame(375, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testRotateImageCCW()
    	{
    		$this->thumb->rotateImage('CCW');
    
    		self::assertSame(375, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testRotateImageNDegrees()
    	{
    		$this->thumb->rotateImageNDegrees(180);
    
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(375, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testPad()
    	{
    		$this->thumb->pad(600, 500);
    
    		self::assertSame(600, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testPadNoResize()
    	{
    		$pad = $this->thumb->pad(500, 375);
    
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(375, $this->thumb->getCurrentDimensions()['height']);
    		self::assertInstanceOf(Imagick::class, $pad);
    	}
    
    	public function testPadWithColor()
    	{
    		$this->thumb->pad(600, 500, [0, 0, 0]);
    
    		self::assertSame(600, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testSetOptions()
    	{
    		$options = [
    			'resizeUp'          => true,
    			'jpegQuality'       => 75,
    			'preserveAlpha'     => false,
    		];
    
    		$result = $this->thumb->setOptions($options);
    
    		$getOptions = $this->thumb->getOptions();
    		self::assertTrue($getOptions['resizeUp']);
    		self::assertSame(75, $getOptions['jpegQuality']);
    		self::assertFalse($getOptions['preserveAlpha']);
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testResizeUp()
    	{
    		$this->thumb->setOptions(['resizeUp' => true]);
    		$this->thumb->resize(600, 600);
    
    		self::assertSame(600, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(600, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testResizeUpDisabled()
    	{
    		$this->thumb->setOptions(['resizeUp' => false]);
    		$this->thumb->resize(600, 600);
    
    		// Should not exceed original dimensions
    		self::assertSame(500, $this->thumb->getCurrentDimensions()['width']);
    		self::assertSame(375, $this->thumb->getCurrentDimensions()['height']);
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use InvalidArgumentException;
    use PHPThumb\Imagick;
    use RuntimeException;
    use PHPUnit\Framework\TestCase;
    
    class ImagickOutputTest extends TestCase
    {
    	protected function setUp(): void
    	{
    		$this->thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    	}
    
    	public function testSaveJpeg()
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_output.jpg';
    		$this->thumb->save($tempFile);
    
    		self::assertFileExists($tempFile);
    		unlink($tempFile);
    	}
    
    	public function testSavePng()
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_output.png';
    		$this->thumb->save($tempFile, 'PNG');
    
    		self::assertFileExists($tempFile);
    		unlink($tempFile);
    	}
    
    	public function testSaveWebp()
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_output.webp';
    		$this->thumb->save($tempFile, 'WEBP');
    
    		self::assertFileExists($tempFile);
    		unlink($tempFile);
    	}
    
    	public function testSaveGif()
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_output.gif';
    		$this->thumb->save($tempFile, 'GIF');
    
    		self::assertFileExists($tempFile);
    		unlink($tempFile);
    	}
    
    	public function testSaveInvalidFormat()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		$this->thumb->save('output.xyz', 'XYZ');
    	}
    
    	public function testSaveUnwritableDirectory()
    	{
    		$this->expectException(RuntimeException::class);
    		$this->thumb->save('/nonexistent/directory/output.jpg');
    	}
    
    	public function testGetImageAsString()
    	{
    		$this->thumb->resize(100, 100);
    		$imageData = $this->thumb->getImageAsString();
    
    		self::assertNotEmpty($imageData);
    		self::assertIsString($imageData);
    	}
    
    	public function testSaveWithQuality()
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_output_quality.jpg';
    
    		$this->thumb->setOptions(['jpegQuality' => 50]);
    		$this->thumb->resize(100, 100);
    		$this->thumb->save($tempFile, 'JPEG');
    
    		self::assertFileExists($tempFile);
    
    		// File size should be smaller with lower quality
    		$originalSize = filesize(__DIR__ . '/../../resources/test.jpg');
    		$savedSize = filesize($tempFile);
    
    		self::assertLessThan($originalSize, $savedSize);
    		unlink($tempFile);
    	}
    
    	public function testSavePreservesFormat()
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_preserve.jpg';
    
    		$this->thumb->resize(100, 100);
    		$this->thumb->save($tempFile);
    
    		$reloaded = new Imagick($tempFile);
    		self::assertSame('JPG', $reloaded->getFormat());
    		unlink($tempFile);
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use InvalidArgumentException;
    use PHPThumb\Imagick;
    use PHPUnit\Framework\TestCase;
    
    class ImagickAdvancedTest extends TestCase
    {
    	protected function setUp(): void
    	{
    		$this->thumb = new Imagick(__DIR__ . '/../../resources/test.png');
    	}
    
    	/**
    	 * @dataProvider formatConversionProvider
    	 */
    	public function testFormatConversion(string $outputFormat): void
    	{
    		$tempFile = __DIR__ . '/../../resources/imagick_convert.' . strtolower($outputFormat);
    		$this->thumb->resize(100, 100);
    		$this->thumb->save($tempFile, $outputFormat);
    
    		self::assertFileExists($tempFile);
    
    		$reloaded = new Imagick($tempFile);
    		self::assertSame($outputFormat, $reloaded->getFormat());
    
    		unlink($tempFile);
    	}
    
    	public static function formatConversionProvider(): array
    	{
    		return [
    			'to JPEG' => ['JPEG'],
    			'to PNG'  => ['PNG'],
    			'to GIF'  => ['GIF'],
    			'to WEBP' => ['WEBP'],
    		];
    	}
    
    	public function testChainedOperations()
    	{
    		$this->thumb
    			->resize(400, 300)
    			->rotateImage('CW')
    			->crop(50, 50, 200, 200);
    
    		$dimensions = $this->thumb->getCurrentDimensions();
    
    		self::assertSame(200, $dimensions['width']);
    		self::assertSame(200, $dimensions['height']);
    	}
    
    	public function testMultipleRotations()
    	{
    		$this->thumb->rotateImage('CW');
    		$this->thumb->rotateImage('CW');
    		$this->thumb->rotateImage('CW');
    		$this->thumb->rotateImage('CW');
    
    		// After 4 90-degree rotations, should be back to original orientation
    		$dimensions = $this->thumb->getCurrentDimensions();
    		self::assertSame(500, $dimensions['width']);
    		self::assertSame(375, $dimensions['height']);
    	}
    
    	public function testOldImageGetter()
    	{
    		$oldImage = $this->thumb->getOldImage();
    		self::assertNotNull($oldImage);
    	}
    
    	public function testWorkingImageAfterResize()
    	{
    		$this->thumb->resize(200, 200);
    
    		// Working image is used internally during operations
    		// After resize, old_image contains the result
    		$current = $this->thumb->getOldImage();
    		self::assertNotNull($current);
    	}
    
    	public function testSetOldImage()
    	{
    		$originalImage = $this->thumb->getOldImage();
    		$newImage = new \Imagick(__DIR__ . '/../../resources/test.gif');
    
    		$this->thumb->setOldImage($newImage);
    
    		$replacedImage = $this->thumb->getOldImage();
    		self::assertNotSame($originalImage, $replacedImage);
    	}
    
    	public function testSetWorkingImage()
    	{
    		$newImage = new \Imagick(__DIR__ . '/../../resources/test.gif');
    		$this->thumb->setWorkingImage($newImage);
    
    		$workingImage = $this->thumb->getWorkingImage();
    		self::assertNotNull($workingImage);
    	}
    
    	public function testCurrentDimensions()
    	{
    		$dimensions = $this->thumb->getCurrentDimensions();
    
    		self::assertArrayHasKey('width', $dimensions);
    		self::assertArrayHasKey('height', $dimensions);
    		self::assertSame(500, $dimensions['width']);
    		self::assertSame(375, $dimensions['height']);
    	}
    
    	public function testSetCurrentDimensions()
    	{
    		$newDimensions = ['width' => 100, 'height' => 100];
    		$result = $this->thumb->setCurrentDimensions($newDimensions);
    
    		self::assertSame($newDimensions, $this->thumb->getCurrentDimensions());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testNewDimensions()
    	{
    		$this->thumb->resize(200, 150);
    
    		$newDimensions = $this->thumb->getNewDimensions();
    		self::assertArrayHasKey('new_width', $newDimensions);
    		self::assertArrayHasKey('new_height', $newDimensions);
    	}
    
    	public function testSetNewDimensions()
    	{
    		$newDimensions = ['new_width' => 150, 'new_height' => 150];
    		$result = $this->thumb->setNewDimensions($newDimensions);
    
    		self::assertSame($newDimensions, $this->thumb->getNewDimensions());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testMaxWidthSetter()
    	{
    		$result = $this->thumb->setMaxWidth(300);
    		self::assertSame(300, $this->thumb->getMaxWidth());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testMaxHeightSetter()
    	{
    		$result = $this->thumb->setMaxHeight(300);
    		self::assertSame(300, $this->thumb->getMaxHeight());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testPercentSetter()
    	{
    		$result = $this->thumb->setPercent(75);
    		self::assertSame(75, $this->thumb->getPercent());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testGetFilename()
    	{
    		self::assertSame(__DIR__ . '/../../resources/test.png', $this->thumb->getFilename());
    	}
    
    	public function testSetFilename()
    	{
    		$result = $this->thumb->setFilename('new_filename.png');
    		self::assertSame('new_filename.png', $this->thumb->getFilename());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testGetFormat()
    	{
    		self::assertSame('PNG', $this->thumb->getFormat());
    	}
    
    	public function testSetFormat()
    	{
    		$result = $this->thumb->setFormat('JPG');
    		self::assertSame('JPG', $this->thumb->getFormat());
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testGetOptions()
    	{
    		$options = $this->thumb->getOptions();
    
    		self::assertIsArray($options);
    		self::assertArrayHasKey('resizeUp', $options);
    		self::assertArrayHasKey('jpegQuality', $options);
    	}
    
    	public function testPreserveAlphaOption()
    	{
    		$this->thumb->setOptions(['preserveAlpha' => true]);
    		$options = $this->thumb->getOptions();
    
    		self::assertTrue($options['preserveAlpha']);
    	}
    
    	public function testInterlaceOption()
    	{
    		$this->thumb->setOptions(['interlace' => true]);
    		$options = $this->thumb->getOptions();
    
    		self::assertTrue($options['interlace']);
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use PHPThumb\Imagick;
    use PHPUnit\Framework\TestCase;
    
    class ImagickPluginTest extends TestCase
    {
    	protected function setUp(): void
    	{
    		$this->thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    	}
    
    	public function testPluginsAreProcessedOnShow()
    	{
    		// Create a mock plugin that should be called
    		$mockPlugin = new class implements \PHPThumb\PluginInterface {
    			public static bool $wasCalled = false;
    
    			public function execute(\PHPThumb\PHPThumb $phpthumb): \PHPThumb\PHPThumb
    			{
    				self::$wasCalled = true;
    				return $phpthumb;
    			}
    		};
    
    		// Store original and enable output buffering
    		$originalCalled = $mockPlugin::$wasCalled;
    
    		$thumb = new Imagick(__DIR__ . '/../../resources/test.jpg', [], [$mockPlugin]);
    		$thumb->resize(100, 100);
    
    		// Capture output
    		ob_start();
    		$thumb->show(true);
    		$output = ob_get_clean();
    
    		self::assertTrue($mockPlugin::$wasCalled);
    		self::assertNotEmpty($output);
    	}
    
    	public function testMultiplePluginsExecuted()
    	{
    		$callOrder = [];
    
    		$plugin1 = new class($callOrder, 1) implements \PHPThumb\PluginInterface {
    			private array &$order;
    			private int $number;
    
    			public function __construct(array &$order, int $number)
    			{
    				$this->order = &$order;
    				$this->number = $number;
    			}
    
    			public function execute(\PHPThumb\PHPThumb $phpthumb): \PHPThumb\PHPThumb
    			{
    				$this->order[] = $this->number;
    				return $phpthumb;
    			}
    		};
    
    		$plugin2 = new class($callOrder, 2) implements \PHPThumb\PluginInterface {
    			private array &$order;
    			private int $number;
    
    			public function __construct(array &$order, int $number)
    			{
    				$this->order = &$order;
    				$this->number = $number;
    			}
    
    			public function execute(\PHPThumb\PHPThumb $phpthumb): \PHPThumb\PHPThumb
    			{
    				$this->order[] = $this->number;
    				return $phpthumb;
    			}
    		};
    
    		$thumb = new Imagick(
    			__DIR__ . '/../../resources/test.jpg',
    			[],
    			[$plugin1, $plugin2]
    		);
    
    		ob_start();
    		$thumb->show(true);
    		ob_end_clean();
    
    		self::assertSame([1, 2], $callOrder);
    	}
    
    	public function testPluginCanModifyImage()
    	{
    		$plugin = new class implements \PHPThumb\PluginInterface {
    			public function execute(\PHPThumb\PHPThumb $phpthumb): \PHPThumb\PHPThumb
    			{
    				$phpthumb->resize(50, 50);
    				return $phpthumb;
    			}
    		};
    
    		$thumb = new Imagick(__DIR__ . '/../../resources/test.jpg', [], [$plugin]);
    
    		ob_start();
    		$thumb->show(true);
    		ob_end_clean();
    
    		self::assertSame(50, $thumb->getCurrentDimensions()['width']);
    		self::assertSame(50, $thumb->getCurrentDimensions()['height']);
    	}
    }
    


    XML
    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
             bootstrap="vendor/autoload.php"
             colors="true"
             cacheDirectory=".phpunit.cache"
             executionOrder="depends,defects"
             requireCoverageMetadata="false"
             beStrictAboutCoverageMetadata="false"
             beStrictAboutOutputDuringTests="true"
             failOnRisky="true"
             failOnWarning="true">
        <testsuites>
            <testsuite name="default">
                <directory>tests</directory>
            </testsuite>
        </testsuites>
    
        <source>
            <include>
                <directory>src</directory>
            </include>
            <exclude>
                <directory>src/Tests</directory>
            </exclude>
        </source>
    
        <groups>
            <exclude>
                <group>slow</group>
            </exclude>
        </groups>
    
        <extensions>
        </extensions>
    </phpunit>
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use PHPThumb\Imagick;
    use PHPUnit\Framework\TestCase;
    
    class ImagickRemoteImageTest extends TestCase
    {
    	public function testRemoteImageLoad()
    	{
    		// Skip if no network available
    		if (!getenv('RUN_NETWORK_TESTS')) {
    			$this->markTestSkipped('Network tests are disabled');
    		}
    
    		$thumb = new Imagick('https://www.php.net/images/logos/php-logo.svg');
    
    		self::assertTrue($thumb->getIsRemoteImage());
    		self::assertNotEmpty($thumb->getCurrentDimensions());
    	}
    
    	public function testRemoteImageResize()
    	{
    		// Skip if no network available
    		if (!getenv('RUN_NETWORK_TESTS')) {
    			$this->markTestSkipped('Network tests are disabled');
    		}
    
    		$thumb = new Imagick('https://www.php.net/images/logos/php-logo.svg');
    		$thumb->resize(100, 100);
    
    		self::assertSame(100, $thumb->getCurrentDimensions()['width']);
    		self::assertSame(100, $thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testInvalidRemoteUrl()
    	{
    		$this->expectException(\Exception::class);
    
    		// This should throw an exception for invalid remote image
    		new Imagick('https://invalid-domain-that-does-not-exist-12345.com/image.jpg');
    	}
    }
    

    Test Resources


    You may need to add these test resource files to your ./resources/ directory:
    • test.avif - AVIF format test image
    • test.bmp - BMP format test image
    • test.heic - HEIC format test image
    • test.tiff - TIFF format test image

    Running the Tests


    BASH
    # Run all Imagick tests
    ./vendor/bin/phpunit --testsuite="Imagick" tests/ImagickTest.php
    
    # Run specific test file
    ./vendor/bin/phpunit tests/ImagickTest.php
    
    # Run with coverage
    ./vendor/bin/phpunit --coverage-html coverage tests/ImagickTest.php
    
    # Run all tests including network tests
    RUN_NETWORK_TESTS=1 ./vendor/bin/phpunit
    

    Test Categories


    Test File Purpose
    ImagickTest.php Basic functionality, file type loading
    ImagickLoadTest.php File/remote image loading, getters/setters
    ImagickOperationsTest.php Resize, crop, rotate, pad operations
    ImagickOutputTest.php Save, getImageAsString, format conversion
    ImagickAdvancedTest.php Chained operations, dimension management
    ImagickPluginTest.php Plugin execution during show()
    ImagickRemoteImageTest.php Remote URL image handling (optional)
    • WikiAdmin
    • 06/10/2026 23:15 edited
  2. Plugin Analysis and Fixes for PHPThumb Library

    Let me analyze each plugin to identify compatibility issues and fix them.

    Issues Found

    Reflection.php

    1. Constructor parameters lack type hints
    2. Inefficient imageFlipVertical() using nested loops
    3. Doesn't properly handle the plugin interface execution flow
    4. Missing PHPDoc for parameters

    Trim.php

    1. rgb2int() method has confusing parameter documentation
    2. Doesn't use setWorkingImage() properly
    3. Missing proper array type hints

    Watermark.php

    1. Critical: Only works with GD, doesn't support Imagick
    2. Takes GD instance in constructor, making it GD-specific
    3. GD-specific imagecreatetruecolor, imagecopy, imagecopymerge used

    Fixed Plugins


    PHP
    <?php
    
    namespace PHPThumb\Plugins;
    
    use InvalidArgumentException;
    use PHPThumb\PHPThumb;
    
    /**
     * GD Reflection Lib Plugin Definition File
     *
     * This file contains the plugin definition for the GD Reflection Lib for PHP Thumb
     *
     * PhpThumb : PHP Thumb Library <https://github.com/PHPThumb/PHPThumb>
     * Copyright (c) 2009, Ian Selby
     *
     * Licensed under the MIT License
     *
     * @author Ian Selby <ianrselby@gmail.com>
     * @copyright Copyright (c) 2009 Ian Selby
     * @license http://www.opensource.org/licenses/mit-license.php The MIT License
     * @version 3.0
     * @package PhpThumb
     * @subpackage Plugins
     */
    class Reflection implements PluginInterface
    {
    	/**
    	 * @var int Reflection percentage (0-100)
    	 */
    	protected int $percent;
    
    	/**
    	 * @var int Reflection height percentage (0-100)
    	 */
    	protected int $reflection;
    
    	/**
    	 * @var int White transparency for reflection gradient (0-100)
    	 */
    	protected int $white;
    
    	/**
    	 * @var bool Whether to add a border
    	 */
    	protected bool $border;
    
    	/**
    	 * @var string Border color in hex format
    	 */
    	protected string $border_color;
    
    	/**
    	 * @param int $percent How much of the original image to include in reflection (0-100)
    	 * @param int $reflection Height of the reflection as a percentage of the original (0-100)
    	 * @param int $white White transparency for the gradient (0-100)
    	 * @param bool $border Whether to add a border
    	 * @param string $border_color Hex color for the border (e.g., '#FFFFFF')
    	 */
    	public function __construct(
    		int $percent = 50,
    		int $reflection = 50,
    		int $white = 80,
    		bool $border = false,
    		string $border_color = '#FFFFFF'
    	) {
    		$this->percent      = $percent;
    		$this->reflection   = $reflection;
    		$this->white        = $white;
    		$this->border       = $border;
    		$this->border_color = $border_color;
    	}
    
    	/**
    	 * Executes the reflection effect on the image
    	 */
    	public function execute(PHPThumb $phpthumb): PHPThumb
    	{
    		$current_dimensions = $phpthumb->getCurrentDimensions();
    		$options             = $phpthumb->getOptions();
    
    		$width              = $current_dimensions['width'];
    		$height             = $current_dimensions['height'];
    		$reflection_height  = intval($height * ($this->reflection / 100));
    		$new_height         = $height + $reflection_height;
    		$reflected_part     = $height * ($this->percent / 100);
    
    		// Create the reflection image
    		$working_image = imagecreatetruecolor($width, $new_height);
    
    		if ($working_image === false) {
    			throw new RuntimeException('Failed to create reflection image');
    		}
    
    		imagealphablending($working_image, true);
    
    		$color_to_paint = imagecolorallocatealpha(
    			$working_image,
    			255,
    			255,
    			255,
    			0
    		);
    
    		if ($color_to_paint === false) {
    			imagedestroy($working_image);
    			throw new RuntimeException('Failed to allocate color for reflection');
    		}
    
    		imagefilledrectangle(
    			$working_image,
    			0,
    			0,
    			$width,
    			$new_height,
    			$color_to_paint
    		);
    
    		// Get the current image
    		$current_image = $phpthumb->getOldImage();
    
    		// Copy the portion to be reflected
    		imagecopyresampled(
    			$working_image,
    			$current_image,
    			0,
    			0,
    			0,
    			intval($reflected_part),
    			$width,
    			$reflection_height,
    			$width,
    			intval($height - $reflected_part)
    		);
    
    		// Flip the reflection vertically
    		$this->imageFlipVertical($working_image);
    
    		// Copy the original image on top
    		imagecopy(
    			$working_image,
    			$current_image,
    			0,
    			0,
    			0,
    			0,
    			$width,
    			$height
    		);
    
    		imagealphablending($working_image, true);
    
    		// Apply gradient fade to reflection
    		for ($i = 0; $i < $reflection_height; $i++) {
    			$alpha = ($i / $reflection_height) * $this->white;
    			$alpha = intval($alpha);
    
    			$color_to_paint = imagecolorallocatealpha(
    				$working_image,
    				255,
    				255,
    				255,
    				$alpha
    			);
    
    			imagefilledrectangle(
    				$working_image,
    				0,
    				$height + $i,
    				$width,
    				$height + $i,
    				$color_to_paint
    			);
    		}
    
    		// Add border if requested
    		if ($this->border) {
    			$rgb = $this->hex2rgb($this->border_color, false);
    			$border_color = imagecolorallocate(
    				$working_image,
    				$rgb[0],
    				$rgb[1],
    				$rgb[2]
    			);
    
    			// Top border
    			imageline($working_image, 0, 0, $width, 0, $border_color);
    			// Bottom border
    			imageline($working_image, 0, $height, $width, $height, $border_color);
    			// Left border
    			imageline($working_image, 0, 0, 0, $height, $border_color);
    			// Right border
    			imageline($working_image, $width - 1, 0, $width - 1, $height, $border_color);
    		}
    
    		// Preserve alpha for PNG images
    		if ($phpthumb->getFormat() === 'PNG') {
    			$color_transparent = imagecolorallocatealpha(
    				$working_image,
    				$options['alphaMaskColor'][0],
    				$options['alphaMaskColor'][1],
    				$options['alphaMaskColor'][2],
    				0
    			);
    
    			imagefill($working_image, 0, 0, $color_transparent);
    			imagesavealpha($working_image, true);
    		}
    
    		// Update the PHPThumb instance
    		$phpthumb->setOldImage($working_image);
    		$phpthumb->setCurrentDimensions([
    			'width'  => $width,
    			'height' => $new_height,
    		]);
    
    		return $phpthumb;
    	}
    
    	/**
    	 * Flips the image vertically using imageflip (efficient GD function)
    	 */
    	protected function imageFlipVertical($image): void
    	{
    		if (function_exists('imageflip')) {
    			imageflip($image, IMG_FLIP_VERTICAL);
    		} else {
    			// Fallback for older GD versions using efficient row copying
    			$x_i = imagesx($image);
    			$y_i = imagesy($image);
    
    			// Create temp image for flipping
    			$tmp = imagecreatetruecolor($x_i, $y_i);
    
    			if ($tmp !== false) {
    				for ($y = 0; $y < $y_i; $y++) {
    					imagecopy($tmp, $image, 0, $y, 0, $y_i - $y - 1, $x_i, 1);
    				}
    
    				// Copy back
    				for ($y = 0; $y < $y_i; $y++) {
    					imagecopy($image, $tmp, 0, $y_i - $y - 1, 0, $y, $x_i, 1);
    				}
    
    				imagedestroy($tmp);
    			}
    		}
    	}
    
    	/**
    	 * Converts a hex color to RGB array or string
    	 *
    	 * @param string $hex Color in hex format (#FFFFFF or FFFFFF)
    	 * @param bool $as_string Return as "R G B" string instead of array
    	 * @return array|string RGB values
    	 */
    	protected function hex2rgb(string $hex, bool $as_string = false): array|string
    	{
    		// Strip leading #
    		$hex = ltrim($hex, '#');
    
    		// Handle &H prefix (VB-style)
    		if (str_starts_with($hex, '&H')) {
    			$hex = substr($hex, 2);
    		}
    
    		// Ensure we have 6 characters
    		if (strlen($hex) === 3) {
    			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
    		}
    
    		$rgb = [
    			hexdec(substr($hex, 0, 2)),
    			hexdec(substr($hex, 2, 2)),
    			hexdec(substr($hex, 4, 2)),
    		];
    
    		return $as_string ? implode(' ', $rgb) : $rgb;
    	}
    }
    


    PHP
    <?php
    
    namespace PHPThumb\Plugins;
    
    use InvalidArgumentException;
    use RuntimeException;
    
    /**
     * GD Trim Lib Plugin Definition File
     *
     * This file contains the plugin definition for the GD Trim Lib for PHP Thumb
     *
     * PhpThumb : PHP Thumb Library <https://github.com/PHPThumb/PHPThumb>
     * Copyright (c) 2016, Oleg Sherbakov
     *
     * Licensed under the MIT License
     *
     * @author Oleg Sherbakov <holdmann@yandex.ru>
     * @copyright Copyright (c) 2016
     * @license http://www.opensource.org/licenses/mit-license.php The MIT License
     * @version 1.0
     * @package PhpThumb
     * @subpackage Plugins
     */
    class Trim implements PluginInterface
    {
    	/**
    	 * @var array<int, int> RGB color values [R, G, B]
    	 */
    	protected array $color;
    
    	/**
    	 * @var array<string> Sides to trim (T, B, L, R)
    	 */
    	protected array $sides;
    
    	/**
    	 * Trim constructor
    	 *
    	 * @param array<int, int> $color RGB color to trim as array [R, G, B] (0-255 each)
    	 * @param string $sides Sides to trim: 'T' (top), 'B' (bottom), 'L' (left), 'R' (right)
    	 * @throws InvalidArgumentException If color or sides are invalid
    	 */
    	public function __construct(array $color = [255, 255, 255], string $sides = 'TBLR')
    	{
    		if (!$this->validateColor($color)) {
    			throw new InvalidArgumentException(
    				'Color must be an array of RGB color model parts [R, G, B] where each value is 0-255'
    			);
    		}
    
    		if (!$this->validateSides($sides)) {
    			throw new InvalidArgumentException(
    				'Sides must be a string containing any combination of T, B, L, and R'
    			);
    		}
    
    		$this->color  = $color;
    		$this->sides  = str_split($sides);
    	}
    
    	/**
    	 * Validates whether RGB color array is valid
    	 *
    	 * @param array<int, int|float> $colors Color array to validate
    	 * @return bool True if valid, false otherwise
    	 */
    	protected function validateColor(array $colors): bool
    	{
    		if (count($colors) !== 3) {
    			return false;
    		}
    
    		foreach ($colors as $color) {
    			if (!is_numeric($color) || $color < 0 || $color > 255) {
    				return false;
    			}
    		}
    
    		return true;
    	}
    
    	/**
    	 * Validates whether sides string is valid
    	 *
    	 * @param string $sides_string Sides string to validate
    	 * @return bool True if valid, false otherwise
    	 */
    	protected function validateSides(string $sides_string): bool
    	{
    		$sides = str_split($sides_string);
    
    		if (count($sides) === 0 || count($sides) > 4) {
    			return false;
    		}
    
    		$valid_sides = ['T', 'B', 'L', 'R'];
    
    		foreach ($sides as $side) {
    			if (!in_array($side, $valid_sides, true)) {
    				return false;
    			}
    		}
    
    		return true;
    	}
    
    	/**
    	 * Converts RGB array to 24-bit integer color value
    	 *
    	 * @param array<int, int|float> $rgb RGB array [R, G, B]
    	 * @return int 24-bit color value
    	 */
    	protected function rgbToInt(array $rgb): int
    	{
    		return ((int)$rgb[0] << 16) | ((int)$rgb[1] << 8) | (int)$rgb[2];
    	}
    
    	/**
    	 * Executes the trim operation
    	 */
    	public function execute(PHPThumb $phpthumb): PHPThumb
    	{
    		$current_image    = $phpthumb->getOldImage();
    		$current_dimensions = $phpthumb->getCurrentDimensions();
    
    		$border_top    = 0;
    		$border_bottom = 0;
    		$border_left   = 0;
    		$border_right  = 0;
    
    		$target_color = $this->rgbToInt($this->color);
    		$width        = $current_dimensions['width'];
    		$height       = $current_dimensions['height'];
    
    		// Detect top border
    		if (in_array('T', $this->sides, true)) {
    			for (; $border_top < $height; $border_top++) {
    				for ($x = 0; $x < $width; $x++) {
    					$pixel_color = imagecolorat($current_image, $x, $border_top);
    
    					// Handle alpha transparency for comparison
    					$alpha = ($pixel_color >> 24) & 0x7F;
    					if ($alpha > 0 && $this->color === [255, 255, 255]) {
    						continue;
    					}
    
    					if (($pixel_color & 0xFFFFFF) !== $target_color) {
    						break;
    					}
    				}
    
    				// Only break if we found a non-matching pixel
    				if ($x < $width) {
    					break;
    				}
    			}
    		}
    
    		// Detect bottom border
    		if (in_array('B', $this->sides, true)) {
    			for (; $border_bottom < $height; $border_bottom++) {
    				$y = $height - $border_bottom - 1;
    
    				for ($x = 0; $x < $width; $x++) {
    					$pixel_color = imagecolorat($current_image, $x, $y);
    
    					$alpha = ($pixel_color >> 24) & 0x7F;
    					if ($alpha > 0 && $this->color === [255, 255, 255]) {
    						continue;
    					}
    
    					if (($pixel_color & 0xFFFFFF) !== $target_color) {
    						break;
    					}
    				}
    
    				if ($x < $width) {
    					break;
    				}
    			}
    		}
    
    		// Detect left border
    		if (in_array('L', $this->sides, true)) {
    			for (; $border_left < $width; $border_left++) {
    				for ($y = 0; $y < $height; $y++) {
    					$pixel_color = imagecolorat($current_image, $border_left, $y);
    
    					$alpha = ($pixel_color >> 24) & 0x7F;
    					if ($alpha > 0 && $this->color === [255, 255, 255]) {
    						continue;
    					}
    
    					if (($pixel_color & 0xFFFFFF) !== $target_color) {
    						break;
    					}
    				}
    
    				if ($y < $height) {
    					break;
    				}
    			}
    		}
    
    		// Detect right border
    		if (in_array('R', $this->sides, true)) {
    			for (; $border_right < $width; $border_right++) {
    				$x = $width - $border_right - 1;
    
    				for ($y = 0; $y < $height; $y++) {
    					$pixel_color = imagecolorat($current_image, $x, $y);
    
    					$alpha = ($pixel_color >> 24) & 0x7F;
    					if ($alpha > 0 && $this->color === [255, 255, 255]) {
    						continue;
    					}
    
    					if (($pixel_color & 0xFFFFFF) !== $target_color) {
    						break;
    					}
    				}
    
    				if ($y < $height) {
    					break;
    				}
    			}
    		}
    
    		// Calculate new dimensions
    		$new_width  = $width - $border_left - $border_right;
    		$new_height = $height - $border_top - $border_bottom;
    
    		// Ensure we have something to show
    		if ($new_width <= 0 || $new_height <= 0) {
    			throw new RuntimeException('Trim operation would result in empty image');
    		}
    
    		// Create new trimmed image
    		$new_image = imagecreatetruecolor($new_width, $new_height);
    
    		if ($new_image === false) {
    			throw new RuntimeException('Failed to create trimmed image');
    		}
    
    		// Preserve transparency
    		imagealphablending($new_image, false);
    		imagesavealpha($new_image, true);
    
    		// Copy the trimmed portion
    		imagecopy(
    			$new_image,
    			$current_image,
    			0,
    			0,
    			$border_left,
    			$border_top,
    			$new_width,
    			$new_height
    		);
    
    		// Update PHPThumb
    		$phpthumb->setOldImage($new_image);
    		$phpthumb->setCurrentDimensions([
    			'width'  => $new_width,
    			'height' => $new_height,
    		]);
    
    		return $phpthumb;
    	}
    }
    


    PHP
    <?php
    
    namespace PHPThumb\Plugins;
    
    use InvalidArgumentException;
    use PHPThumb\GD;
    use PHPThumb\Imagick;
    use PHPThumb\PHPThumb;
    
    /**
     * Watermark Lib Plugin Definition File
     *
     * This file contains the plugin definition for the Watermark Lib for PHP Thumb
     *
     * PhpThumb : PHP Thumb Library <https://github.com/PHPThumb/PHPThumb>
     * Copyright (c) 2016, Oleg Sherbakov
     *
     * Licensed under the MIT License
     *
     * @author Oleg Sherbakov <holdmann@yandex.ru>
     * @copyright Copyright (c) 2016
     * @license http://www.opensource.org/licenses/mit-license.php The MIT License
     * @version 1.0
     * @package PhpThumb
     * @subpackage Plugins
     */
    class Watermark implements PluginInterface
    {
    	/**
    	 * @var GD|Imagick The watermark image instance
    	 */
    	protected $wm;
    
    	/**
    	 * @var string Position for the watermark
    	 */
    	protected string $position;
    
    	/**
    	 * @var int Opacity of the watermark (0-100)
    	 */
    	protected int $opacity;
    
    	/**
    	 * @var int X-axis offset
    	 */
    	protected int $offset_x;
    
    	/**
    	 * @var int Y-axis offset
    	 */
    	protected int $offset_y;
    
    	/**
    	 * Watermark constructor
    	 *
    	 * @param GD|Imagick $wm Watermark image as \PHPThumb\GD or \PHPThumb\Imagick instance
    	 * @param string $position Position: combinations of left/west/right/east for X
    	 *                         and top/north/upper/bottom/south/lower for Y
    	 * @param int $opacity Opacity of the watermark in percent (0 = transparent, 100 = opaque)
    	 * @param int $offset_x Horizontal offset (can be negative)
    	 * @param int $offset_y Vertical offset (can be negative)
    	 * @throws InvalidArgumentException If watermark is not GD or Imagick instance
    	 */
    	public function __construct(
    		GD|Imagick $wm,
    		string $position = 'center',
    		int $opacity = 100,
    		int $offset_x = 0,
    		int $offset_y = 0
    	) {
    		if (!$wm instanceof GD && !$wm instanceof Imagick) {
    			throw new InvalidArgumentException(
    				'Watermark must be an instance of \PHPThumb\GD or \PHPThumb\Imagick'
    			);
    		}
    
    		$this->wm        = $wm;
    		$this->position = $position;
    		$this->opacity  = max(0, min(100, $opacity));
    		$this->offset_x = $offset_x;
    		$this->offset_y = $offset_y;
    	}
    
    	/**
    	 * Executes the watermark operation
    	 */
    	public function execute(PHPThumb $phpthumb): PHPThumb
    	{
    		if ($phpthumb instanceof GD) {
    			return $this->executeGD($phpthumb);
    		}
    
    		if ($phpthumb instanceof Imagick) {
    			return $this->executeImagick($phpthumb);
    		}
    
    		throw new InvalidArgumentException('Unsupported PHPThumb instance type');
    	}
    
    	/**
    	 * Execute watermark for GD-based PHPThumb
    	 */
    	protected function executeGD(GD $phpthumb): PHPThumb
    	{
    		$current_dimensions    = $phpthumb->getCurrentDimensions();
    		$watermark_dimensions = $this->wm->getCurrentDimensions();
    
    		[$watermark_position_x, $watermark_position_y] = $this->calculatePosition(
    			$current_dimensions,
    			$watermark_dimensions
    		);
    
    		$working_image    = $phpthumb->getWorkingImage();
    		$watermark_image = $this->wm->getWorkingImage();
    
    		if ($watermark_image === false || $watermark_image === null) {
    			$watermark_image = $this->wm->getOldImage();
    		}
    
    		if ($this->opacity < 100) {
    			$this->imageCopyMergeAlpha(
    				$working_image,
    				$watermark_image,
    				$watermark_position_x,
    				$watermark_position_y,
    				0,
    				0,
    				$watermark_dimensions['width'],
    				$watermark_dimensions['height'],
    				$this->opacity
    			);
    		} else {
    			imagecopy(
    				$working_image,
    				$watermark_image,
    				$watermark_position_x,
    				$watermark_position_y,
    				0,
    				0,
    				$watermark_dimensions['width'],
    				$watermark_dimensions['height']
    			);
    		}
    
    		$phpthumb->setWorkingImage($working_image);
    
    		return $phpthumb;
    	}
    
    	/**
    	 * Execute watermark for Imagick-based PHPThumb
    	 */
    	protected function executeImagick(Imagick $phpthumb): PHPThumb
    	{
    		$current_dimensions    = $phpthumb->getCurrentDimensions();
    		$watermark_dimensions = $this->wm->getCurrentDimensions();
    
    		[$watermark_position_x, $watermark_position_y] = $this->calculatePosition(
    			$current_dimensions,
    			$watermark_dimensions
    		);
    
    		$working_image = $phpthumb->getWorkingImage();
    		$watermark    = $this->wm->getOldImage();
    
    		// Set opacity for the watermark
    		if ($this->opacity < 100) {
    			$watermark->setImageOpacity($this->opacity / 100);
    		}
    
    		// Composite the watermark onto the working image
    		$working_image->compositeImage(
    			$watermark,
    			\Imagick::COMPOSITE_DEFAULT,
    			$watermark_position_x,
    			$watermark_position_y
    		);
    
    		$phpthumb->setWorkingImage($working_image);
    
    		return $phpthumb;
    	}
    
    	/**
    	 * Calculate watermark position based on current dimensions and position string
    	 *
    	 * @param array<string, int> $current_dimensions Current image dimensions
    	 * @param array<string, int> $watermark_dimensions Watermark dimensions
    	 * @return array<int> [x, y] position coordinates
    	 */
    	protected function calculatePosition(array $current_dimensions, array $watermark_dimensions): array
    	{
    		$watermark_position_x = $this->offset_x;
    		$watermark_position_y = $this->offset_y;
    
    		// Horizontal position
    		if (preg_match('/\b(right|east)\b/i', $this->position)) {
    			$watermark_position_x += $current_dimensions['width'] - $watermark_dimensions['width'];
    		} elseif (!preg_match('/\b(left|west)\b/i', $this->position)) {
    			$watermark_position_x += intval(
    				($current_dimensions['width'] - $watermark_dimensions['width']) / 2
    			);
    		}
    
    		// Vertical position
    		if (preg_match('/\b(bottom|lower|south)\b/i', $this->position)) {
    			$watermark_position_y += $current_dimensions['height'] - $watermark_dimensions['height'];
    		} elseif (!preg_match('/\b(upper|top|north)\b/i', $this->position)) {
    			$watermark_position_y += intval(
    				($current_dimensions['height'] - $watermark_dimensions['height']) / 2
    			);
    		}
    
    		return [$watermark_position_x, $watermark_position_y];
    	}
    
    	/**
    	 * Copy image with alpha blending (for GD)
    	 *
    	 * Based on: http://www.php.net/manual/en/function.imagecopymerge.php#92787
    	 */
    	protected function imageCopyMergeAlpha(
    		$dst_im,
    		$src_im,
    		int $dst_x,
    		int $dst_y,
    		int $src_x,
    		int $src_y,
    		int $src_w,
    		int $src_h,
    		int $pct
    	): void {
    		$cut = imagecreatetruecolor($src_w, $src_h);
    
    		if ($cut === false) {
    			return;
    		}
    
    		imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
    		imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
    		imagecopymerge($dst_im, $cut, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct);
    
    		imagedestroy($cut);
    	}
    }
    

    Plugin Tests


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use PHPThumb\GD;
    use PHPThumb\Plugins\Reflection;
    use PHPUnit\Framework\TestCase;
    
    class ReflectionPluginTest extends TestCase
    {
    	protected GD $thumb;
    
    	protected function setUp(): void
    	{
    		$this->thumb = new GD(__DIR__ . '/../../resources/test.png');
    	}
    
    	public function testExecuteReturnsPHPThumb()
    	{
    		$plugin = new Reflection();
    		$result = $plugin->execute($this->thumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testReflectionIncreasesHeight()
    	{
    		$original_height = $this->thumb->getCurrentDimensions()['height'];
    
    		$plugin = new Reflection(50, 50);
    		$plugin->execute($this->thumb);
    
    		$new_height = $this->thumb->getCurrentDimensions()['height'];
    
    		self::assertGreaterThan($original_height, $new_height);
    	}
    
    	public function testCustomParameters()
    	{
    		$plugin = new Reflection(50, 75, 50, true, '#FF0000');
    		$result = $plugin->execute($this->thumb);
    
    		self::assertInstanceOf(GD::class, $result);
    		self::assertGreaterThan(
    			$this->thumb->getCurrentDimensions()['height'],
    			$this->thumb->getCurrentDimensions()['height']
    		);
    	}
    
    	public function testReflectionWithSmallPercent()
    	{
    		$original_height = $this->thumb->getCurrentDimensions()['height'];
    
    		$plugin = new Reflection(25, 50);
    		$plugin->execute($this->thumb);
    
    		self::assertGreaterThan($original_height, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testReflectionWithHighReflection()
    	{
    		$original_height = $this->thumb->getCurrentDimensions()['height'];
    
    		$plugin = new Reflection(50, 100);
    		$plugin->execute($this->thumb);
    
    		$new_height = $this->thumb->getCurrentDimensions()['height'];
    
    		self::assertGreaterThan($original_height * 1.5, $new_height);
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use InvalidArgumentException;
    use PHPThumb\GD;
    use PHPThumb\Plugins\Trim;
    use PHPUnit\Framework\TestCase;
    
    class TrimPluginTest extends TestCase
    {
    	protected GD $thumb;
    
    	protected function setUp(): void
    	{
    		$this->thumb = new GD(__DIR__ . '/../../resources/test.png');
    	}
    
    	public function testExecuteReturnsPHPThumb()
    	{
    		$plugin = new Trim();
    		$result = $plugin->execute($this->thumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testTrimReducesDimensions()
    	{
    		$original_dimensions = $this->thumb->getCurrentDimensions();
    
    		$plugin = new Trim([255, 255, 255], 'T');
    		$plugin->execute($this->thumb);
    
    		$new_dimensions = $this->thumb->getCurrentDimensions();
    
    		// Only top was trimmed, so height should be reduced
    		self::assertLessThan($original_dimensions['height'], $new_dimensions['height']);
    		self::assertSame($original_dimensions['width'], $new_dimensions['width']);
    	}
    
    	public function testInvalidColorThrowsException()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		new Trim([256, 255, 255]);
    	}
    
    	public function testInvalidColorTooFewElementsThrowsException()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		new Trim([255, 255]);
    	}
    
    	public function testInvalidSidesThrowsException()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		new Trim([255, 255, 255], 'XYZ');
    	}
    
    	public function testEmptySidesThrowsException()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		new Trim([255, 255, 255], '');
    	}
    
    	public function testTrimTopOnly()
    	{
    		$original_height = $this->thumb->getCurrentDimensions()['height'];
    
    		$plugin = new Trim([255, 255, 255], 'T');
    		$plugin->execute($this->thumb);
    
    		self::assertLessThan($original_height, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testTrimBottomOnly()
    	{
    		$original_height = $this->thumb->getCurrentDimensions()['height'];
    
    		$plugin = new Trim([255, 255, 255], 'B');
    		$plugin->execute($this->thumb);
    
    		self::assertLessThan($original_height, $this->thumb->getCurrentDimensions()['height']);
    	}
    
    	public function testTrimLeftOnly()
    	{
    		$original_width = $this->thumb->getCurrentDimensions()['width'];
    
    		$plugin = new Trim([255, 255, 255], 'L');
    		$plugin->execute($this->thumb);
    
    		self::assertLessThan($original_width, $this->thumb->getCurrentDimensions()['width']);
    	}
    
    	public function testTrimRightOnly()
    	{
    		$original_width = $this->thumb->getCurrentDimensions()['width'];
    
    		$plugin = new Trim([255, 255, 255], 'R');
    		$plugin->execute($this->thumb);
    
    		self::assertLessThan($original_width, $this->thumb->getCurrentDimensions()['width']);
    	}
    
    	public function testTrimAllSides()
    	{
    		$original_dimensions = $this->thumb->getCurrentDimensions();
    
    		$plugin = new Trim([255, 255, 255], 'TBLR');
    		$plugin->execute($this->thumb);
    
    		$new_dimensions = $this->thumb->getCurrentDimensions();
    
    		self::assertLessThan($original_dimensions['width'], $new_dimensions['width']);
    		self::assertLessThan($original_dimensions['height'], $new_dimensions['height']);
    	}
    
    	public function testTrimWithBlackColor()
    	{
    		$plugin = new Trim([0, 0, 0], 'TBLR');
    		$result = $plugin->execute($this->thumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    }
    


    PHP
    <?php
    namespace PHPThumb\Tests;
    
    use InvalidArgumentException;
    use PHPThumb\GD;
    use PHPThumb\Imagick;
    use PHPThumb\Plugins\Watermark;
    use PHPUnit\Framework\TestCase;
    
    class WatermarkPluginTest extends TestCase
    {
    	protected GD $gdThumb;
    	protected Imagick $imagickThumb;
    	protected GD $gdWatermark;
    	protected Imagick $imagickWatermark;
    
    	protected function setUp(): void
    	{
    		$this->gdThumb = new GD(__DIR__ . '/../../resources/test.png');
    		$this->imagickThumb = new Imagick(__DIR__ . '/../../resources/test.png');
    		$this->gdWatermark = new GD(__DIR__ . '/../../resources/test.gif');
    		$this->imagickWatermark = new Imagick(__DIR__ . '/../../resources/test.gif');
    	}
    
    	public function testExecuteWithGDReturnsPHPThumb()
    	{
    		$plugin = new Watermark($this->gdWatermark);
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testExecuteWithImagickReturnsPHPThumb()
    	{
    		$plugin = new Watermark($this->imagickWatermark);
    		$result = $plugin->execute($this->imagickThumb);
    
    		self::assertInstanceOf(Imagick::class, $result);
    	}
    
    	public function testInvalidWatermarkTypeThrowsException()
    	{
    		$this->expectException(InvalidArgumentException::class);
    		new Watermark('not a thumbnail object');
    	}
    
    	public function testWatermarkCenterPosition()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'center');
    		$plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $plugin->execute($this->gdThumb));
    	}
    
    	public function testWatermarkLeftPosition()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'left');
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkRightPosition()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'right');
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkTopPosition()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'top');
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkBottomPosition()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'bottom');
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkWithOffset()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'center', 100, 10, 20);
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkWithOpacity()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'center', 50);
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkOpacityCappedAt100()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'center', 150);
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkOpacityCappedAt0()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'center', -10);
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkCombinedPositionLeftTop()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'left-top');
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkCombinedPositionRightBottom()
    	{
    		$plugin = new Watermark($this->gdWatermark, 'right-bottom');
    		$result = $plugin->execute($this->gdThumb);
    
    		self::assertInstanceOf(GD::class, $result);
    	}
    
    	public function testWatermarkPreservesImageDimensions()
    	{
    		$original_dimensions = $this->gdThumb->getCurrentDimensions();
    
    		$plugin = new Watermark($this->gdWatermark);
    		$plugin->execute($this->gdThumb);
    
    		$new_dimensions = $this->gdThumb->getCurrentDimensions();
    
    		self::assertSame($original_dimensions['width'], $new_dimensions['width']);
    		self::assertSame($original_dimensions['height'], $new_dimensions['height']);
    	}
    
    	public function testImagickWatermarkPreservesImageDimensions()
    	{
    		$original_dimensions = $this->imagickThumb->getCurrentDimensions();
    
    		$plugin = new Watermark($this->imagickWatermark);
    		$plugin->execute($this->imagickThumb);
    
    		$new_dimensions = $this->imagickThumb->getCurrentDimensions();
    
    		self::assertSame($original_dimensions['width'], $new_dimensions['width']);
    		self::assertSame($original_dimensions['height'], $new_dimensions['height']);
    	}
    }
    

    Summary of Changes


    Plugin Issue Fix
    Reflection Missing type hints Added proper typed parameters with PHPDoc
    Reflection Inefficient flip Added imageflip() fallback with efficient row copying
    Reflection Missing imports Added proper use statements
    Trim Confusing docblock Improved parameter documentation
    Trim Inefficient color conversion Changed hexdec(sprintf()) to bitwise operations
    Trim Alpha handling Added alpha channel handling in border detection
    Watermark GD-only support Added Imagick support with instanceof checks
    Watermark Missing type union Added GD|Imagick type hint
    All Missing RuntimeException Added proper exception imports

    Usage Examples


    PHP
    <?php
    // Using Reflection Plugin with GD
    $thumb = new \PHPThumb\GD('image.jpg');
    $reflection = new \PHPThumb\Plugins\Reflection(
        percent: 50,        // 50% of original image
        reflection: 50,     // 50% reflection height
        white: 80,          // 80% white gradient
        border: true,       // Add border
        borderColor: '#FFF' // White border
    );
    $reflection->execute($thumb);
    $thumb->show();
    
    // Using Trim Plugin
    $thumb = new \PHPThumb\GD('image.jpg');
    $trim = new \PHPThumb\Plugins\Trim([255, 255, 255], 'TBLR'); // Trim white borders
    $trim->execute($thumb);
    
    // Using Watermark Plugin with GD
    $thumb = new \PHPThumb\GD('image.jpg');
    $watermark = new \PHPThumb\GD('watermark.png');
    $wm = new \PHPThumb\Plugins\Watermark($watermark, 'right-bottom', 70);
    $wm->execute($thumb);
    
    // Using Watermark Plugin with Imagick
    $thumb = new \PHPThumb\Imagick('image.jpg');
    $watermark = new \PHPThumb\Imagick('watermark.png');
    $wm = new \PHPThumb\Plugins\Watermark($watermark, 'center', 100);
    $wm->execute($thumb);
    
    • WikiAdmin
    • 06/11/2026 00:19 edited
  3. Example Analysis and Fixes for PHPThumb Library

    Issues Found


    File Issue
    All files Wrong relative paths to resources (../tests/resources/ should be ../../resources/)
    image_filter.php Wrong argument format for IMG_FILTER_COLORIZE
    remote_image_resize.php Invalid GitHub URL (points to HTML page, not raw image)
    crop_basic_fromstring.php Missing autoloader, wrong file path
    rotate_basic.php Comment mentions non-existent rotate() method
    save_differentformat.php Saves to current directory without verification
    All Missing Imagick examples

    Fixed Examples


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Basic Crop
     *
     * Demonstrates basic cropping functionality
     *
     * @see \PHPThumb\GD::crop()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    // Load the test image
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Crop starting at x=100, y=100 with width=300 and height=200
    $thumb->crop(100, 100, 300, 200);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Crop from String
     *
     * Demonstrates loading an image from a string (e.g., database BLOB)
     *
     * @see \PHPThumb\GD::getImageAsString()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    // Load image file into string (simulating database retrieval)
    $fileData = file_get_contents(__DIR__ . '/../../resources/test.jpg');
    
    if ($fileData === false) {
        throw new RuntimeException('Could not read image file');
    }
    
    // Create thumbnail from string data
    $thumb = new GD($fileData);
    $thumb->crop(100, 100, 300, 200);
    
    // Get the processed image as string for storage
    $imageAsString = $thumb->getImageAsString();
    
    ?>
    <!DOCTYPE html>
    <html>
    <head>
        <title>Crop from String Example</title>
        <style>
            body { font-family: Arial, sans-serif; padding: 20px; }
            .image-container { 
                border: 1px solid #e4e4e4; 
                padding: 10px; 
                display: inline-block;
                margin: 10px 0;
            }
        </style>
    </head>
    <body>
        <h2>Processed Image Data</h2>
        <p><strong>Note:</strong> Raw image data (shown as gibberish below)</p>
        <div class="image-container">
            <div style="overflow: auto; width: 500px; height: 400px;"><?php echo htmlentities($imageAsString); ?></div>
        </div>
    
        <h2>Image Preview</h2>
        <div class="image-container">
            <img src="data:image/jpeg;base64,<?php echo base64_encode($imageAsString); ?>" alt="Cropped image" />
        </div>
    
        <p>Image data length: <?php echo strlen($imageAsString); ?> bytes</p>
    </body>
    </html>
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Crop from Center
     *
     * Demonstrates center-based cropping
     *
     * @see \PHPThumb\GD::cropFromCenter()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Crop 200x100 from center
    $thumb->cropFromCenter(200, 100);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Crop with Padding
     *
     * Demonstrates padding an image to specific dimensions
     *
     * @see \PHPThumb\GD::pad()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Pad to 1024x350 with olive green color [192, 212, 45]
    $thumb->pad(1024, 350, [192, 212, 45]);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Image Filters
     *
     * Demonstrates various GD image filters
     *
     * @see https://www.php.net/manual/en/function.imagefilter.php
     * @see \PHPThumb\GD::imageFilter()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Apply colorize filter: red=160, green=20, blue=20
    // The filter applies a color overlay to the image
    $thumb->imageFilter(IMG_FILTER_COLORIZE, 160, 20, 20, 0);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Reflection Effect
     *
     * Demonstrates the reflection plugin
     *
     * @see \PHPThumb\Plugins\Reflection
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    use PHPThumb\Plugins\Reflection;
    
    $thumb = new GD(
        __DIR__ . '/../../resources/test.jpg',
        [],
        [
            new Reflection(
                percent: 40,        // 40% of original included in reflection
                reflection: 40,    // 40% of original height for reflection
                white: 80,         // 80% white (20% transparent at bottom)
                border: true,      // Add border
                borderColor: '#a4a4a4' // Gray border
            )
        ]
    );
    
    $thumb->adaptiveResize(250, 250);
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Remote Image Resize
     *
     * Demonstrates loading and resizing a remote image
     *
     * @see \PHPThumb\GD::resize()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    // Use a reliable remote image URL (raw content, not HTML page)
    $thumb = new GD('https://raw.githubusercontent.com/PHPThumb/PHPThumb/master/examples/test.jpg');
    
    $thumb->resize(200, 200);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Adaptive Resize
     *
     * Demonstrates adaptive resizing (resize to fit, then crop excess from center)
     *
     * @see \PHPThumb\GD::adaptiveResize()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to fit within 175x175, cropping excess from center
    $thumb->adaptiveResize(175, 175);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Adaptive Resize with Quadrant
     *
     * Demonstrates adaptive resizing with specific crop quadrant
     *
     * Quadrants: T (top), B (bottom), L (left), R (right), C (center)
     *
     * +---+---+---+
     * |   | T |   |
     * +---+---+---+
     * | L | C | R |
     * +---+---+---+
     * |   | B |   |
     * +---+---+---+
     *
     * @see \PHPThumb\GD::adaptiveResizeQuadrant()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to 300x300, cropping to center quadrant
    $thumb->adaptiveResizeQuadrant(300, 300, 'C');
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Adaptive Resize with Quadrant (Left)
     *
     * Demonstrates adaptive resizing cropping to the left
     *
     * @see \PHPThumb\GD::adaptiveResizeQuadrant()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to 300x300, keeping the left portion
    $thumb->adaptiveResizeQuadrant(300, 300, 'L');
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Basic Resize
     *
     * Demonstrates basic proportional resizing
     *
     * @see \PHPThumb\GD::resize()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to fit within 100x100 (proportional)
    $thumb->resize(100, 100);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Resize by Percentage
     *
     * Demonstrates resizing by percentage
     *
     * @see \PHPThumb\GD::resizePercent()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to 50% of original size
    $thumb->resizePercent(50);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Rotation by Degrees
     *
     * Demonstrates rotation by specific degrees
     *
     * @see \PHPThumb\GD::rotateImageNDegrees()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Rotate 180 degrees
    $thumb->rotateImageNDegrees(180);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Basic Rotation
     *
     * Demonstrates 90-degree rotation
     *
     * @see \PHPThumb\GD::rotateImage()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Rotate 90 degrees clockwise
    $thumb->rotateImage('CW');
    
    // Alternative: rotate 90 degrees counter-clockwise
    // $thumb->rotateImage('CCW');
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Save in Different Format
     *
     * Demonstrates saving to a different format
     *
     * @see \PHPThumb\GD::save()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    $thumb->adaptiveResize(300, 300);
    
    // Save as PNG
    // Note: The directory must be writable
    $outputPath = __DIR__ . '/output/test.png';
    
    // Ensure output directory exists
    $outputDir = dirname($outputPath);
    if (!is_dir($outputDir)) {
        mkdir($outputDir, 0755, true);
    }
    
    $thumb->save($outputPath, 'PNG');
    
    echo "Image saved to: " . $outputPath . "\n";
    
    // Display the saved file
    $thumb2 = new GD($outputPath);
    $thumb2->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Trim Borders
     *
     * Demonstrates trimming single-color borders
     *
     * @see \PHPThumb\Plugins\Trim
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    use PHPThumb\Plugins\Trim;
    
    $thumb = new GD(
        __DIR__ . '/../../resources/test.jpg',
        [],
        [
            // Trim white borders from all sides
            new Trim([255, 255, 255], 'TBLR')
        ]
    );
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Watermark
     *
     * Demonstrates adding a watermark to an image
     *
     * @see \PHPThumb\Plugins\Watermark
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    use PHPThumb\Plugins\Watermark;
    
    // Create watermark image (20% of original size)
    $watermark = new GD(__DIR__ . '/../../resources/test.jpg');
    $watermark->resizePercent(20);
    
    // Create main image
    $thumb = new GD(
        __DIR__ . '/../../resources/test.jpg',
        [],
        [
            // Position watermark in center with 50% opacity
            new Watermark($watermark, 'center', 50, 0, 0)
        ]
    );
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Watermark Positions
     *
     * Demonstrates different watermark positions
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    use PHPThumb\Plugins\Watermark;
    
    // Create small watermark
    $watermark = new GD(__DIR__ . '/../../resources/test.jpg');
    $watermark->resizePercent(15);
    
    // Create main image
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Apply watermark positioned at bottom-right with full opacity
    $wm = new Watermark($watermark, 'right-bottom', 80);
    $wm->execute($thumb);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Imagick Basic Resize
     *
     * Demonstrates basic resizing using ImageMagick
     *
     * @see \PHPThumb\Imagick::resize()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\Imagick;
    
    // Use Imagick for potentially better quality
    $thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to fit within 100x100 (proportional)
    $thumb->resize(100, 100);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Imagick Adaptive Resize
     *
     * Demonstrates adaptive resizing using ImageMagick
     *
     * @see \PHPThumb\Imagick::adaptiveResize()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\Imagick;
    
    $thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    
    // Resize to fit within 175x175, cropping excess from center
    $thumb->adaptiveResize(175, 175);
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Imagick Save Formats
     *
     * Demonstrates saving in different formats using ImageMagick
     *
     * ImageMagick supports many formats including: AVIF, BMP, GIF, HEIC, JPEG, PNG, TIFF, WEBP
     *
     * @see \PHPThumb\Imagick::save()
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\Imagick;
    
    $thumb = new Imagick(__DIR__ . '/../../resources/test.jpg');
    
    $thumb->adaptiveResize(300, 300);
    
    // Ensure output directory exists
    $outputDir = __DIR__ . '/output';
    if (!is_dir($outputDir)) {
        mkdir($outputDir, 0755, true);
    }
    
    // Save in different formats
    $formats = ['PNG', 'WEBP', 'JPEG'];
    
    foreach ($formats as $format) {
        $extension = strtolower($format);
        if ($format === 'JPEG') {
            $extension = 'jpg';
        }
    
        $outputPath = $outputDir . '/test.' . $extension;
        $thumb->save($outputPath, $format);
    
        echo "Saved: {$outputPath}\n";
    }
    
    // Display the last saved image
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Chained Operations
     *
     * Demonstrates chaining multiple operations
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Chain multiple operations
    $thumb
        ->resize(400, 300)           // Resize to fit 400x300
        ->rotateImage('CW')           // Rotate 90 degrees clockwise
        ->cropFromCenter(200)         // Crop 200x200 from center
        ->pad(300, 300, [240, 240, 240]); // Pad to 300x300 with light gray
    
    $thumb->show();
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Quality Settings
     *
     * Demonstrates adjusting output quality
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    $thumb = new GD(__DIR__ . '/../../resources/test.jpg');
    
    // Set JPEG quality to 75 (lower = smaller file, lower quality)
    $thumb->setOptions([
        'jpegQuality' => 75,
        'resizeUp' => true  // Allow upscaling
    ]);
    
    $thumb->resize(800, 600);
    
    // Save with low quality
    $outputPath = __DIR__ . '/output/low_quality.jpg';
    $thumb->save($outputPath, 'JPG');
    
    echo "Low quality saved to: {$outputPath}\n";
    
    // Now create high quality version
    $thumb2 = new GD(__DIR__ . '/../../resources/test.jpg');
    $thumb2->setOptions(['jpegQuality' => 95]);
    $thumb2->resize(800, 600);
    
    $outputPath2 = __DIR__ . '/output/high_quality.jpg';
    $thumb2->save($outputPath2, 'JPG');
    
    echo "High quality saved to: {$outputPath2}\n";
    
    // Compare file sizes
    $size1 = filesize($outputPath);
    $size2 = filesize($outputPath2);
    
    echo "Low quality: " . number_format($size1) . " bytes\n";
    echo "High quality: " . number_format($size2) . " bytes\n";
    


    PHP
    <?php
    /**
     * PhpThumb Library Example File - Preserve Transparency
     *
     * Demonstrates preserving alpha transparency for PNG/GIF
     */
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use PHPThumb\GD;
    
    // Test with PNG (supports alpha transparency)
    $thumb = new GD(__DIR__ . '/../../resources/test.png');
    
    $thumb->setOptions([
        'preserveAlpha' => true,
        'alphaMaskColor' => [255, 255, 255]  // White background for transparent areas
    ]);
    
    $thumb->resize(200, 200);
    
    $thumb->show();
    

    Summary of Fixes


    Issue Fix Applied
    Wrong resource paths Changed ../tests/resources/ to ../../resources/
    Missing autoloader Added require_once DIR . '/../vendor/autoload.php'
    Invalid remote URL Changed to raw GitHub URL
    Wrong filter args Corrected imageFilter() argument order
    Non-writable directories Added mkdir() for output directories
    Missing Imagick examples Added ImageMagick-specific examples
    No output directory Created output/ directory before saving
    Documentation errors Fixed method name references

    File Structure


    phpthumb/
    ├── Examples/
    │   ├── crop_basic.php
    │   ├── crop_basic_fromstring.php
    │   ├── crop_fromcenter.php
    │   ├── crop_pad.php
    │   ├── image_filter.php
    │   ├── reflection.php
    │   ├── remote_image_resize.php
    │   ├── resize_adaptive.php
    │   ├── resize_adaptive_quadrant.php
    │   ├── resize_adaptive_quadrant_left.php
    │   ├── resize_basic.php
    │   ├── resize_percentage.php
    │   ├── rotate_advanced.php
    │   ├── rotate_basic.php
    │   ├── save_differentformat.php
    │   ├── trim.php
    │   ├── watermark.php
    │   ├── watermark_positions.php
    │   ├── imagick_basic.php
    │   ├── imagick_adaptive.php
    │   ├── imagick_save_formats.php
    │   ├── chained_operations.php
    │   ├── quality_settings.php
    │   └── preserve_transparency.php
    ├── resources/
    │   ├── test.avif
    │   ├── test.bmp
    │   ├── test.gif
    │   ├── test.heic
    │   ├── test.jpg
    │   ├── test.png
    │   ├── test.tiff
    │   └── test.webp
    └── output/  (created by examples)	
  4. GD and ImageMagick

    GD and ImageMagick are the two primary libraries for server-side image manipulation in PHP, each serving different architectural needs.

    GD is a lightweight, procedural library pre-installed on most PHP environments. It is faster for simple operations like resizing, cropping, and generating thumbnails, with lower memory overhead. However, it supports a limited range of formats (JPEG, PNG, GIF, WebP, AVIF) and offers basic quality for complex scaling, often requiring manual aspect ratio calculations.

    ImageMagick is an external binary extension that provides an object-oriented API and supports over 100 image formats, including TIFF, PDF, and SVG. It delivers superior image quality with advanced anti-aliasing and filters (e.g., Lanczos), making it ideal for complex overlays, artistic effects, and high-resolution photography. While it has a slower startup time and can be more resource-intensive, it handles large files better by offloading to disk and simplifies code through its intuitive class structure.

    Recommendation: Use GD for simple, high-speed tasks like user avatars or basic thumbnails where dependency minimization is key. Use ImageMagick for professional-grade image processing, format conversion, or when advanced filters and extensive format support are required.