Files
shopPRO/autoload/Shared/Image/ImageManipulator.php
Jacek Pyziak 431add234c ver. 0.283: Legacy class cleanup — S, Html, Email, Image, Log, Mobile_Detect → Shared namespace
- Migrate class.S → Shared\Helpers\Helpers (140+ files), remove 12 unused methods
- Migrate class.Html → Shared\Html\Html
- Migrate class.Email → Shared\Email\Email
- Migrate class.Image → Shared\Image\ImageManipulator
- Delete class.Log (unused), class.Mobile_Detect (outdated UA detection)
- Remove grid library loading from admin (index.php, ajax.php)
- Replace gridEdit usage in 10 admin templates with grid-edit-replacement.php
- Fix grid-edit-replacement.php AJAX to send values as JSON (grid.js compat)
- Remove mobile layout conditionals (m_html/m_css/m_js) from Site + LayoutsRepository
- Remove \Log::save_log() calls from OrderAdminService, ShopOrder, Order

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:06:06 +01:00

443 lines
14 KiB
PHP

<?php
namespace Shared\Image;
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;
}
}