441 lines
14 KiB
PHP
441 lines
14 KiB
PHP
<?php
|
|
class ImageManipulator
|
|
{
|
|
protected int $width;
|
|
protected int $height;
|
|
/** @var resource|\GdImage */
|
|
protected $image;
|
|
protected ?string $file = null;
|
|
|
|
/**
|
|
* Image manipulator constructor
|
|
*
|
|
* @param string|null $file Path to image file or image data as string
|
|
*/
|
|
public function __construct(?string $file = null)
|
|
{
|
|
if ($file !== null) {
|
|
$this->file = $file;
|
|
|
|
if (is_file($file)) {
|
|
$this->setImageFile($file);
|
|
} else {
|
|
$this->setImageString($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set image resource from file
|
|
*
|
|
* @param string $file Path to image file
|
|
* @return self
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function setImageFile(string $file): self
|
|
{
|
|
if (!(is_readable($file) && is_file($file))) {
|
|
throw new InvalidArgumentException("Image file $file is not readable");
|
|
}
|
|
|
|
if (isset($this->image) && $this->isValidImageResource($this->image)) {
|
|
imagedestroy($this->image);
|
|
}
|
|
|
|
[$width, $height, $type] = getimagesize($file);
|
|
|
|
if ($width === false || $height === false) {
|
|
throw new InvalidArgumentException("Unable to get image size for $file");
|
|
}
|
|
|
|
error_log("Loaded image size from file: width: $width, height: $height, type: $type");
|
|
|
|
switch ($type) {
|
|
case IMAGETYPE_GIF:
|
|
$this->image = imagecreatefromgif($file);
|
|
break;
|
|
case IMAGETYPE_JPEG:
|
|
$this->image = imagecreatefromjpeg($file);
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
$this->image = imagecreatefrompng($file);
|
|
break;
|
|
case IMAGETYPE_WEBP:
|
|
$this->image = imagecreatefromwebp($file);
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException("Image type $type not supported");
|
|
}
|
|
|
|
if (!$this->isValidImageResource($this->image)) {
|
|
throw new InvalidArgumentException("Failed to create image from $file");
|
|
}
|
|
|
|
$this->width = imagesx($this->image);
|
|
$this->height = imagesy($this->image);
|
|
|
|
error_log("Set image dimensions: width: {$this->width}, height: {$this->height}");
|
|
|
|
if ($this->width === 0 || $this->height === 0) {
|
|
throw new InvalidArgumentException("Image dimensions are invalid (width: $this->width, height: $this->height)");
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set image resource from string data
|
|
*
|
|
* @param string $data Image data as string
|
|
* @return self
|
|
* @throws RuntimeException
|
|
*/
|
|
public function setImageString(string $data): self
|
|
{
|
|
if (isset($this->image) && $this->isValidImageResource($this->image)) {
|
|
imagedestroy($this->image);
|
|
}
|
|
|
|
$image = imagecreatefromstring($data);
|
|
if (!$this->isValidImageResource($image)) {
|
|
throw new RuntimeException('Cannot create image from data string');
|
|
}
|
|
|
|
$this->image = $image;
|
|
$this->width = imagesx($this->image);
|
|
$this->height = imagesy($this->image);
|
|
|
|
error_log("Set image dimensions from string: width: {$this->width}, height: {$this->height}");
|
|
|
|
if ($this->width === 0 || $this->height === 0) {
|
|
throw new RuntimeException("Image dimensions are invalid (width: $this->width, height: $this->height)");
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Resamples the current image
|
|
*
|
|
* @param int $width New width
|
|
* @param int $height New height
|
|
* @param bool $constrainProportions Constrain current image proportions when resizing
|
|
* @return self
|
|
* @throws RuntimeException
|
|
*/
|
|
public function resample(int $width, int $height, bool $constrainProportions = true): self
|
|
{
|
|
if (!isset($this->image) || !$this->isValidImageResource($this->image)) {
|
|
throw new RuntimeException('No image set');
|
|
}
|
|
|
|
if ($constrainProportions) {
|
|
if ($this->height === 0) {
|
|
throw new RuntimeException('Image height is zero, cannot calculate aspect ratio');
|
|
}
|
|
|
|
$aspectRatio = $this->width / $this->height;
|
|
|
|
// Ustaw domyślną wysokość, jeśli podana jest równa zero
|
|
if ($height === 0) {
|
|
$height = (int) round($width / $aspectRatio);
|
|
}
|
|
|
|
if ($width / $height > $aspectRatio) {
|
|
$width = (int) round($height * $aspectRatio);
|
|
} else {
|
|
$height = (int) round($width / $aspectRatio);
|
|
}
|
|
|
|
if ($width <= 0 || $height <= 0) {
|
|
throw new RuntimeException('Calculated dimensions are invalid (width: ' . $width . ', height: ' . $height . ')');
|
|
}
|
|
}
|
|
|
|
// reszta kodu metody
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Enlarge canvas
|
|
*
|
|
* @param int $width Canvas width
|
|
* @param int $height Canvas height
|
|
* @param array $rgb RGB colour values [R, G, B]
|
|
* @param int|null $xpos X-Position of image in new canvas, null for centre
|
|
* @param int|null $ypos Y-Position of image in new canvas, null for centre
|
|
* @return self
|
|
* @throws RuntimeException
|
|
*/
|
|
public function enlargeCanvas(int $width, int $height, array $rgb = [], ?int $xpos = null, ?int $ypos = null): self
|
|
{
|
|
if (!isset($this->image) || !$this->isValidImageResource($this->image)) {
|
|
throw new RuntimeException('No image set');
|
|
}
|
|
|
|
$width = max($width, $this->width);
|
|
$height = max($height, $this->height);
|
|
|
|
$temp = imagecreatetruecolor($width, $height);
|
|
if (!$this->isValidImageResource($temp)) {
|
|
throw new RuntimeException('Failed to create a new image for enlarging canvas');
|
|
}
|
|
|
|
// Fill background if RGB provided
|
|
if (count($rgb) === 3) {
|
|
[$r, $g, $b] = $rgb;
|
|
$bg = imagecolorallocate($temp, $r, $g, $b);
|
|
imagefill($temp, 0, 0, $bg);
|
|
} else {
|
|
// Preserve transparency
|
|
imagealphablending($temp, false);
|
|
imagesavealpha($temp, true);
|
|
$transparent = imagecolorallocatealpha($temp, 255, 255, 255, 127);
|
|
imagefilledrectangle($temp, 0, 0, $width, $height, $transparent);
|
|
}
|
|
|
|
// Calculate positions
|
|
if ($xpos === null) {
|
|
$xpos = (int) round(($width - $this->width) / 2);
|
|
}
|
|
if ($ypos === null) {
|
|
$ypos = (int) round(($height - $this->height) / 2);
|
|
}
|
|
|
|
// Logowanie przed kopiowaniem obrazu na nowe płótno
|
|
error_log("Enlarging canvas: xpos: $xpos, ypos: $ypos");
|
|
|
|
if (!imagecopy(
|
|
$temp,
|
|
$this->image,
|
|
$xpos,
|
|
$ypos,
|
|
0,
|
|
0,
|
|
$this->width,
|
|
$this->height
|
|
)) {
|
|
throw new RuntimeException('Failed to copy image onto enlarged canvas');
|
|
}
|
|
|
|
return $this->_replace($temp);
|
|
}
|
|
|
|
/**
|
|
* Crop image
|
|
*
|
|
* @param int|array $x1 Top left x-coordinate of crop box or array of coordinates [x1, y1, x2, y2]
|
|
* @param int $y1 Top left y-coordinate of crop box
|
|
* @param int $x2 Bottom right x-coordinate of crop box
|
|
* @param int $y2 Bottom right y-coordinate of crop box
|
|
* @return self
|
|
* @throws RuntimeException
|
|
*/
|
|
public function crop($x1, int $y1 = 0, int $x2 = 0, int $y2 = 0): self
|
|
{
|
|
if (!isset($this->image) || !$this->isValidImageResource($this->image)) {
|
|
throw new RuntimeException('No image set');
|
|
}
|
|
|
|
if (is_array($x1) && count($x1) === 4) {
|
|
[$x1, $y1, $x2, $y2] = $x1;
|
|
}
|
|
|
|
$x1 = max((int)$x1, 0);
|
|
$y1 = max($y1, 0);
|
|
$x2 = min($x2, $this->width);
|
|
$y2 = min($y2, $this->height);
|
|
|
|
$cropWidth = $x2 - $x1;
|
|
$cropHeight = $y2 - $y1;
|
|
|
|
// Logowanie wymiarów do przycięcia
|
|
error_log("Cropping image: x1: $x1, y1: $y1, x2: $x2, y2: $y2, cropWidth: $cropWidth, cropHeight: $cropHeight");
|
|
|
|
if ($cropWidth <= 0 || $cropHeight <= 0) {
|
|
throw new RuntimeException('Invalid crop dimensions');
|
|
}
|
|
|
|
$temp = imagecreatetruecolor($cropWidth, $cropHeight);
|
|
if (!$this->isValidImageResource($temp)) {
|
|
throw new RuntimeException('Failed to create a new image for cropping');
|
|
}
|
|
|
|
// Preserve transparency
|
|
imagealphablending($temp, false);
|
|
imagesavealpha($temp, true);
|
|
$transparent = imagecolorallocatealpha($temp, 255, 255, 255, 127);
|
|
imagefilledrectangle($temp, 0, 0, $cropWidth, $cropHeight, $transparent);
|
|
|
|
if (!imagecopy(
|
|
$temp,
|
|
$this->image,
|
|
0,
|
|
0,
|
|
$x1,
|
|
$y1,
|
|
$cropWidth,
|
|
$cropHeight
|
|
)) {
|
|
throw new RuntimeException('Failed to crop image');
|
|
}
|
|
|
|
return $this->_replace($temp);
|
|
}
|
|
|
|
/**
|
|
* Replace current image resource with a new one
|
|
*
|
|
* @param resource|\GdImage $res New image resource
|
|
* @return self
|
|
* @throws UnexpectedValueException
|
|
*/
|
|
protected function _replace($res): self
|
|
{
|
|
if (!$this->isValidImageResource($res)) {
|
|
throw new UnexpectedValueException('Invalid image resource');
|
|
}
|
|
|
|
if (isset($this->image) && $this->isValidImageResource($this->image)) {
|
|
imagedestroy($this->image);
|
|
}
|
|
|
|
$this->image = $res;
|
|
$this->width = imagesx($res);
|
|
$this->height = imagesy($res);
|
|
|
|
error_log("Replaced image dimensions: width: {$this->width}, height: {$this->height}");
|
|
|
|
if ($this->width === 0 || $this->height === 0) {
|
|
throw new UnexpectedValueException("Replaced image has invalid dimensions (width: $this->width, height: $this->height)");
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Save current image to file
|
|
*
|
|
* @param string $fileName Path to save the image
|
|
* @param int|null $type Image type (IMAGETYPE_*) or null to auto-detect from file extension
|
|
* @return void
|
|
* @throws RuntimeException
|
|
*/
|
|
public function save(string $fileName, ?int $type = null): void
|
|
{
|
|
$dir = dirname($fileName);
|
|
if (!is_dir($dir)) {
|
|
if (!mkdir($dir, 0755, true) && !is_dir($dir)) {
|
|
throw new RuntimeException('Error creating directory ' . $dir);
|
|
}
|
|
}
|
|
|
|
// Auto-detect type from file extension if not provided
|
|
if ($type === null) {
|
|
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
|
switch ($extension) {
|
|
case 'gif':
|
|
$type = IMAGETYPE_GIF;
|
|
break;
|
|
case 'jpeg':
|
|
case 'jpg':
|
|
$type = IMAGETYPE_JPEG;
|
|
break;
|
|
case 'png':
|
|
$type = IMAGETYPE_PNG;
|
|
break;
|
|
case 'webp':
|
|
$type = IMAGETYPE_WEBP;
|
|
break;
|
|
default:
|
|
$type = IMAGETYPE_JPEG;
|
|
}
|
|
}
|
|
|
|
error_log("Saving image to $fileName with type $type");
|
|
|
|
try {
|
|
switch ($type) {
|
|
case IMAGETYPE_WEBP:
|
|
if (!imagewebp($this->image, $fileName)) {
|
|
throw new RuntimeException('Failed to save image as WEBP');
|
|
}
|
|
break;
|
|
case IMAGETYPE_GIF:
|
|
if (!imagegif($this->image, $fileName)) {
|
|
throw new RuntimeException('Failed to save image as GIF');
|
|
}
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
if (!imagepng($this->image, $fileName)) {
|
|
throw new RuntimeException('Failed to save image as PNG');
|
|
}
|
|
break;
|
|
case IMAGETYPE_JPEG:
|
|
default:
|
|
if (!imagejpeg($this->image, $fileName, 95)) {
|
|
throw new RuntimeException('Failed to save image as JPEG');
|
|
}
|
|
}
|
|
error_log("Image saved successfully to $fileName");
|
|
} catch (\Exception $ex) {
|
|
throw new RuntimeException('Error saving image file to ' . $fileName . ': ' . $ex->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the GD image resource
|
|
*
|
|
* @return resource|\GdImage
|
|
*/
|
|
public function getResource()
|
|
{
|
|
return $this->image;
|
|
}
|
|
|
|
/**
|
|
* Get current image width
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getWidth(): int
|
|
{
|
|
return $this->width;
|
|
}
|
|
|
|
/**
|
|
* Get current image height
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getHeight(): int
|
|
{
|
|
return $this->height;
|
|
}
|
|
|
|
/**
|
|
* Destructor to clean up the image resource
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
if (isset($this->image) && $this->isValidImageResource($this->image)) {
|
|
imagedestroy($this->image);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compatibility helper for PHP 7.4 (resource) and PHP 8+ (GdImage).
|
|
*
|
|
* @param mixed $image
|
|
*/
|
|
private function isValidImageResource($image): bool
|
|
{
|
|
if (is_resource($image)) {
|
|
return true;
|
|
}
|
|
|
|
return class_exists('GdImage', false) && $image instanceof \GdImage;
|
|
}
|
|
}
|