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; } }