uawdijnntqw1x1x1
IP : 216.73.216.8
Hostname : toronto-dev2
Kernel : Linux toronto-dev2 4.15.0-213-generic #224-Ubuntu SMP Mon Jun 19 13:30:12 UTC 2023 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
srv
/
users
/
craft4
/
apps
/
craft4-newsite-space
/
vendor
/
sabberworm
/
..
/
craftcms
/
cms
/
src
/
image
/
Raster.php
/
/
<?php /** * @link https://craftcms.com/ * @copyright Copyright (c) Pixel & Tonic, Inc. * @license https://craftcms.github.io/license/ */ namespace craft\image; use Craft; use craft\base\Image; use craft\errors\ImageException; use craft\helpers\App; use craft\helpers\FileHelper; use craft\helpers\Image as ImageHelper; use Imagick; use Imagine\Exception\NotSupportedException; use Imagine\Exception\RuntimeException; use Imagine\Gd\Imagine as GdImagine; use Imagine\Image\AbstractFont as Font; use Imagine\Image\AbstractImage; use Imagine\Image\AbstractImagine; use Imagine\Image\Box; use Imagine\Image\BoxInterface; use Imagine\Image\ImageInterface; use Imagine\Image\Metadata\ExifMetadataReader; use Imagine\Image\Palette\Color\ColorInterface; use Imagine\Image\Palette\RGB; use Imagine\Image\Point; use Imagine\Imagick\Imagine as ImagickImagine; use Throwable; use yii\base\ErrorException; /** * Raster class is used for raster image manipulations. * * @author Pixel & Tonic, Inc. <support@pixelandtonic.com> * @since 3.0.0 */ class Raster extends Image { /** * @var string|null */ private ?string $_imageSourcePath = null; /** * @var string|null */ private ?string $_extension = null; /** * @var bool */ private bool $_isAnimated = false; /** * @var int */ private int $_quality = 0; /** * @var ImageInterface|null */ private ?ImageInterface $_image = null; /** * @var AbstractImagine|null */ private ?AbstractImagine $_instance = null; /** * @var RGB|null */ private ?RGB $_palette = null; /** * @var Font|null */ private ?Font $_font = null; /** * @var ColorInterface|null */ private ?ColorInterface $_fill = null; /** * @inheritdoc */ public function __construct($config = []) { $generalConfig = Craft::$app->getConfig()->getGeneral(); $extension = strtolower($generalConfig->imageDriver); // If it's explicitly set, take their word for it. if ($extension === 'gd') { $this->_instance = new GdImagine(); } else { if ($extension === 'imagick') { $this->_instance = new ImagickImagine(); } else { // Let's try to auto-detect. if (Craft::$app->getImages()->getIsGd()) { $this->_instance = new GdImagine(); } else { $this->_instance = new ImagickImagine(); } } } $this->_quality = $generalConfig->defaultImageQuality; parent::__construct($config); } /** * Return the Imagine Image instance * * @return AbstractImage|null */ public function getImagineImage(): ?AbstractImage { return $this->_image; } /** * @inheritdoc */ public function getWidth(): int { return $this->_image->getSize()->getWidth(); } /** * @inheritdoc */ public function getHeight(): int { return $this->_image->getSize()->getHeight(); } /** * @inheritdoc */ public function getExtension(): string { return $this->_extension; } /** * @inheritdoc */ public function loadImage(string $path): self { $imageService = Craft::$app->getImages(); if (!is_file($path)) { Craft::error('Tried to load an image at ' . $path . ', but the file does not exist.', __METHOD__); throw new ImageException(Craft::t('app', 'No file exists at the given path.')); } if (!$imageService->checkMemoryForImage($path)) { throw new ImageException(Craft::t('app', 'Not enough memory available to perform this image operation.')); } // Make sure the image says it's an image $mimeType = FileHelper::getMimeType($path, null, false); if ($mimeType !== null && !str_starts_with($mimeType, 'image/') && !str_starts_with($mimeType, 'application/pdf')) { throw new ImageException(Craft::t('app', 'The file “{name}” does not appear to be an image.', [ 'name' => basename($path), ])); } try { $this->_image = $this->_instance->open($path); } catch (Throwable $e) { // Imagick can throw all sorts of errors via the open() method // we should log them to better know what's going on Craft::warning($e->getMessage(), $e->getFile()); if (($instanceException = $e->getPrevious()) !== null) { Craft::warning($instanceException->getMessage(), $instanceException->getFile() . ':' . $instanceException->getLine()); } throw new ImageException(Craft::t('app', 'The file “{name}” does not appear to be an image.', [ 'name' => basename($path), ]), 0, $e); } // For Imagick, convert CMYK to RGB, save and re-open. if ( !Craft::$app->getImages()->getIsGd() && !Craft::$app->getConfig()->getGeneral()->preserveCmykColorspace && method_exists($this->_image->getImagick(), 'getImageColorspace') && $this->_image->getImagick()->getImageColorspace() === Imagick::COLORSPACE_CMYK && method_exists($this->_image->getImagick(), 'transformImageColorspace') ) { $this->_image->getImagick()->transformImageColorspace(Imagick::COLORSPACE_SRGB); $this->_image->save(); /** @noinspection PhpIncompatibleReturnTypeInspection */ return Craft::$app->getImages()->loadImage($path); } $this->_imageSourcePath = $path; $this->_extension = pathinfo($path, PATHINFO_EXTENSION); if (in_array($this->_extension, ['gif', 'webp'])) { if (!$imageService->getIsGd() && $this->_image->layers()) { $this->_isAnimated = true; } } return $this; } /** * @inheritdoc */ public function crop(int $x1, int $x2, int $y1, int $y2): self { $width = $x2 - $x1; $height = $y2 - $y1; if ($this->_isAnimated) { // Create a new image instance to avoid object references messing up our dimensions. $newSize = new Box($width, $height); $startingPoint = new Point($x1, $y1); $gif = $this->_instance->create($newSize); $gif->layers()->remove(0); $this->_image->layers()->coalesce(); foreach ($this->_image->layers() as $layer) { $croppedLayer = $layer->crop($startingPoint, $newSize); $gif->layers()->add($croppedLayer); // Since it might take a while, send a heartbeat back $this->heartbeat(); } $this->_image = $gif; } else { $this->_image->crop(new Point($x1, $y1), new Box($width, $height)); } return $this; } /** * @inheritdoc */ public function scaleToFit(?int $targetWidth, ?int $targetHeight, bool $scaleIfSmaller = null): self { $this->normalizeDimensions($targetWidth, $targetHeight); $scaleIfSmaller = $scaleIfSmaller ?? Craft::$app->getConfig()->getGeneral()->upscaleImages; if ($scaleIfSmaller || $this->getWidth() > $targetWidth || $this->getHeight() > $targetHeight) { $factor = max($this->getWidth() / $targetWidth, $this->getHeight() / $targetHeight); $this->resize(round($this->getWidth() / $factor), round($this->getHeight() / $factor)); } return $this; } /** * Scales an image to the target size and fills empty pixels with color. * * @param int|null $targetWidth * @param int|null $targetHeight * @param string|null $fill * @param string|array $position * @param bool|null $upscale * @return Raster * @since 4.4.0 */ public function scaleToFitAndFill(?int $targetWidth, ?int $targetHeight, string $fill = null, string|array $position = 'center-center', bool $upscale = null): static { $upscale = $upscale ?? Craft::$app->getConfig()->getGeneral()->upscaleImages; $this->normalizeDimensions($targetWidth, $targetHeight); $this->scaleToFit($targetWidth, $targetHeight, $upscale); $this->setFill($fill); $box = new Box($targetWidth, $targetHeight); $canvas = $this->_instance->create($box, $this->_fill); [$verticalPosition, $horizontalPosition] = explode('-', $position); $y = match ($verticalPosition) { 'top' => 0, 'bottom' => ($box->getHeight() - $this->getHeight()), default => ($box->getHeight() - $this->getHeight()) / 2, }; $x = match ($horizontalPosition) { 'left' => 0, 'right' => ($box->getWidth() - $this->getWidth()), default => ($box->getWidth() - $this->getWidth()) / 2, }; $point = new Point($x, $y); if ($this->_isAnimated) { $canvas->layers()->remove(0); $this->_image->layers()->coalesce(); foreach ($this->_image->layers() as $layer) { $newLayer = $this->_instance->create($box, $this->_fill); $newLayer->paste($layer, $point); $canvas->layers()->add($newLayer); // Hopefully this doesn't take _too_ long, but it might $this->heartbeat(); } } else { $canvas->paste($this->_image, $point); } $this->_image = $canvas; return $this; } /** * @inheritdoc */ public function scaleAndCrop(?int $targetWidth, ?int $targetHeight, bool $scaleIfSmaller = true, array|string $cropPosition = 'center-center'): self { $this->normalizeDimensions($targetWidth, $targetHeight); // If upscaling is fine OR we have to downscale. if ($scaleIfSmaller || ($this->getWidth() > $targetWidth && $this->getHeight() > $targetHeight)) { // Scale first. $factor = min($this->getWidth() / $targetWidth, $this->getHeight() / $targetHeight); $newHeight = round($this->getHeight() / $factor); $newWidth = round($this->getWidth() / $factor); $this->resize($newWidth, $newHeight); // If we need to upscale AND that's ok } elseif (($targetWidth > $this->getWidth() || $targetHeight > $this->getHeight()) && !$scaleIfSmaller) { // Figure the crop size reductions $factor = max($targetWidth / $this->getWidth(), $targetHeight / $this->getHeight()); $newHeight = $this->getHeight(); $newWidth = $this->getWidth(); $targetHeight = round($targetHeight / $factor); $targetWidth = round($targetWidth / $factor); } else { $newHeight = $this->getHeight(); $newWidth = $this->getWidth(); } if (is_array($cropPosition)) { $centerX = $newWidth * $cropPosition['x']; $centerY = $newHeight * $cropPosition['y']; $x1 = $centerX - $targetWidth / 2; $y1 = $centerY - $targetHeight / 2; $x2 = $x1 + $targetWidth; $y2 = $y1 + $targetHeight; // Now see if we have to bump this around to make it fit the image. if ($x1 < 0) { $x2 -= $x1; $x1 = 0; } if ($y1 < 0) { $y2 -= $y1; $y1 = 0; } if ($x2 > $newWidth) { $x1 -= ($x2 - $newWidth); $x2 = $newWidth; } if ($y2 > $newHeight) { $y1 -= ($y2 - $newHeight); $y2 = $newHeight; } } else { [$verticalPosition, $horizontalPosition] = explode('-', $cropPosition); // Now crop. if ($newWidth - $targetWidth > 0) { switch ($horizontalPosition) { case 'left': $x1 = 0; $x2 = $x1 + $targetWidth; break; case 'right': $x2 = $newWidth; $x1 = $newWidth - $targetWidth; break; default: $x1 = round(($newWidth - $targetWidth) / 2); $x2 = $x1 + $targetWidth; break; } $y1 = 0; $y2 = $y1 + $targetHeight; } elseif ($newHeight - $targetHeight > 0) { switch ($verticalPosition) { case 'top': $y1 = 0; $y2 = $y1 + $targetHeight; break; case 'bottom': $y2 = $newHeight; $y1 = $newHeight - $targetHeight; break; default: $y1 = round(($newHeight - $targetHeight) / 2); $y2 = $y1 + $targetHeight; break; } $x1 = 0; $x2 = $x1 + $targetWidth; } else { $x1 = round(($newWidth - $targetWidth) / 2); $x2 = $x1 + $targetWidth; $y1 = round(($newHeight - $targetHeight) / 2); $y2 = $y1 + $targetHeight; } } $this->crop($x1, $x2, $y1, $y2); return $this; } /** * @inheritdoc */ public function resize(?int $targetWidth, ?int $targetHeight): self { $this->normalizeDimensions($targetWidth, $targetHeight); if ($this->_isAnimated) { // Create a new image instance to avoid object references messing up our dimensions. $newSize = new Box($targetWidth, $targetHeight); $gif = $this->_instance->create($newSize); $gif->layers()->remove(0); $this->_image->layers()->coalesce(); foreach ($this->_image->layers() as $layer) { $resizedLayer = $layer->resize($newSize, $this->_getResizeFilter()); $gif->layers()->add($resizedLayer); // Since it might take a while, send a heartbeat back $this->heartbeat(); } $this->_image = $gif; } else { if (Craft::$app->getImages()->getIsImagick() && Craft::$app->getConfig()->getGeneral()->optimizeImageFilesize) { $keepImageProfiles = Craft::$app->getConfig()->getGeneral()->preserveImageColorProfiles; $this->_image->smartResize(new Box($targetWidth, $targetHeight), $keepImageProfiles, true, $this->_quality); } else { $this->_image->resize(new Box($targetWidth, $targetHeight), $this->_getResizeFilter()); } if (Craft::$app->getImages()->getIsImagick()) { $this->_image->getImagick()->setImagePage(0, 0, 0, 0); } } return $this; } /** * Rotates the image by the given degrees. * * @param float $degrees * @return self Self reference */ public function rotate(float $degrees): self { $this->_image->rotate($degrees); if (Craft::$app->getImages()->getIsImagick()) { $this->_image->getImagick()->setImagePage($this->getWidth(), $this->getHeight(), 0, 0); } return $this; } /** * Flips the image horizontally. * * @return self Self reference */ public function flipHorizontally(): self { $this->_image->flipHorizontally(); return $this; } /** * Flips the image vertically. * * @return self Self reference */ public function flipVertically(): self { $this->_image->flipVertically(); return $this; } /** * Sets the image quality. * * @param int $quality * @return self Self reference */ public function setQuality(int $quality): self { $this->_quality = $quality; return $this; } /** * Sets the interlace setting. * * @param string $interlace * @return self Self reference */ public function setInterlace(string $interlace): self { $this->_image->interlace($interlace); return $this; } /** * Sets the fill color based on the image's palette. * * @param string $fill Hex color of the fill. * @return $this Self reference * @since 4.4.0 */ public function setFill(string $fill = null): self { $fill = $fill ?? 'transparent'; if ($fill === 'transparent') { $this->_fill = $this->_image->palette()->color('#ffffff', 0); } else { $this->_fill = $this->_image->palette()->color($fill); } return $this; } /** * @inheritdoc */ public function saveAs(string $targetPath, bool $autoQuality = false): bool { $extension = mb_strtolower(pathinfo($targetPath, PATHINFO_EXTENSION)); $options = $this->_getSaveOptions(null, $extension); $targetPath = pathinfo($targetPath, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . pathinfo($targetPath, PATHINFO_FILENAME) . '.' . pathinfo($targetPath, PATHINFO_EXTENSION); try { if ($autoQuality && in_array($extension, ['jpeg', 'jpg', 'png'], true)) { clearstatcache(); App::maxPowerCaptain(); $originalSize = filesize($this->_imageSourcePath); $tempFile = $this->_autoGuessImageQuality($targetPath, $originalSize, $extension, 0, 200); try { rename($tempFile, $targetPath); } catch (ErrorException $e) { Craft::warning("Unable to rename \"$tempFile\" to \"$targetPath\": " . $e->getMessage(), __METHOD__); } } else { if (Craft::$app->getImages()->getIsImagick()) { ImageHelper::cleanExifDataFromImagickImage($this->_image->getImagick()); } $this->_image->save($targetPath, $options); } } catch (RuntimeException $e) { throw new ImageException(Craft::t('app', 'Failed to save the image.'), $e->getCode(), $e); } return true; } /** * Loads an image from an SVG string. * * @param string $svgContent * @return self Self reference * @throws ImageException if the SVG string cannot be loaded. */ public function loadFromSVG(string $svgContent): self { try { $this->_image = $this->_instance->load($svgContent); } catch (RuntimeException) { try { // Invalid SVG. Maybe it's missing its DTD? $svgContent = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . $svgContent; $this->_image = $this->_instance->load($svgContent); } catch (RuntimeException $e) { throw new ImageException(Craft::t('app', 'Failed to load the SVG string.'), $e->getCode(), $e); } } // PNG should be the best fit for SVGs. $this->_extension = 'png'; return $this; } /** * @inheritdoc */ public function getIsTransparent(): bool { if (Craft::$app->getImages()->getIsImagick()) { // https://github.com/php-imagine/Imagine/issues/842#issuecomment-1402748019 $alphaRange = $this->_image->getImagick()->getImageChannelRange(Imagick::CHANNEL_ALPHA); return ( isset($alphaRange['minima'], $alphaRange['maxima']) && $alphaRange['minima'] < $alphaRange['maxima'] ); } return false; } /** * Returns EXIF metadata for a file by its path. * * @param string $filePath * @return array */ public function getExifMetadata(string $filePath): array { try { $exifReader = new ExifMetadataReader(); $this->_instance->setMetadataReader($exifReader); $exif = $this->_instance->open($filePath)->metadata(); return $exif->toArray(); } catch (NotSupportedException $exception) { Craft::error($exception->getMessage(), __METHOD__); return []; } } /** * Sets properties for text drawing on the image. * * @param string $fontFile path to the font file on server * @param int $size font size to use * @param string $color font color to use in hex format */ public function setFontProperties(string $fontFile, int $size, string $color): void { if (!isset($this->_palette)) { $this->_palette = new RGB(); } $this->_font = $this->_instance->font($fontFile, $size, $this->_palette->color($color)); } /** * Returns the bounding text box for a text string and an angle * * @param string $text * @param int $angle * @return BoxInterface * @throws ImageException if attempting to create text box with no font properties */ public function getTextBox(string $text, int $angle = 0): BoxInterface { if (!isset($this->_font)) { throw new ImageException(Craft::t('app', 'No font properties have been set. Call Raster::setFontProperties() first.')); } return $this->_font->box($text, $angle); } /** * Writes text on an image. * * @param string $text * @param int $x * @param int $y * @param int $angle * @throws ImageException If attempting to create text box with no font properties et. */ public function writeText(string $text, int $x, int $y, int $angle = 0): void { if (!isset($this->_font)) { throw new ImageException(Craft::t('app', 'No font properties have been set. Call ImageHelper::setFontProperties() first.')); } $point = new Point($x, $y); $this->_image->draw()->text($text, $this->_font, $point, $angle); } /** * Disable animation if this is an animated image. * * @return self Self-reference */ public function disableAnimation(): self { $this->_isAnimated = false; if ($this->_image->layers()->count() > 1) { // Fetching the first layer returns the built-in Imagick object // So cycle that through the loading phase to get one that sports the // `smartResize` functionality. $this->_image = $this->_instance->load((string)$this->_image->layers()->get(0)); } return $this; } /** * @param string $tempFileName * @param int $originalSize * @param string $extension * @param int $minQuality * @param int $maxQuality * @param int $step * @return string the resulting file path */ private function _autoGuessImageQuality(string $tempFileName, int $originalSize, string $extension, int $minQuality, int $maxQuality, int $step = 0): string { if ($step === 0) { $tempFileName = pathinfo($tempFileName, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . FileHelper::uniqueName(sprintf('%s.%s', pathinfo($tempFileName, PATHINFO_FILENAME), $extension)); } // Find our target quality by splitting the min and max qualities $midQuality = (int)ceil($minQuality + (($maxQuality - $minQuality) / 2)); // Set the min and max acceptable ranges. .10 means anything between 90% and 110% of the original file size is acceptable. $acceptableRange = .10; clearstatcache(); // Generate a new temp image and get it's file size. $this->_image->save($tempFileName, $this->_getSaveOptions($midQuality, $extension)); $newFileSize = filesize($tempFileName); // If we're on step 10 OR we're within our acceptable range threshold OR midQuality = maxQuality (1 == 1), // let's use the current image. if ($step == 10 || abs(1 - $originalSize / $newFileSize) < $acceptableRange || $midQuality == $maxQuality) { clearstatcache(); // Generate one last time. if (Craft::$app->getImages()->getIsImagick()) { ImageHelper::cleanExifDataFromImagickImage($this->_image->getImagick()); } $this->_image->save($tempFileName, $this->_getSaveOptions($midQuality)); return $tempFileName; } $step++; if ($newFileSize > $originalSize) { return $this->_autoGuessImageQuality($tempFileName, $originalSize, $extension, $minQuality, $midQuality, $step); } // Too much. return $this->_autoGuessImageQuality($tempFileName, $originalSize, $extension, $midQuality, $maxQuality, $step); } /** * @return string */ private function _getResizeFilter(): string { return (Craft::$app->getImages()->getIsGd() ? ImageInterface::FILTER_UNDEFINED : ImageInterface::FILTER_LANCZOS); } /** * Returns save options. * * @param int|null $quality * @param string|null $extension * @return array */ private function _getSaveOptions(?int $quality, ?string $extension = null): array { // Because it's possible for someone to set the quality to 0. $quality = $quality ?: $this->_quality; $extension = (!$extension ? mb_strtolower($this->getExtension()) : $extension); switch ($extension) { case 'jpeg': case 'jpg': return ['jpeg_quality' => $quality, 'flatten' => true]; case 'gif': return ['animated' => $this->_isAnimated]; case 'webp': return ['animated' => $this->_isAnimated, 'webp_quality' => $quality]; case 'png': // Valid PNG quality settings are 0-9, so normalize and flip, because we're talking about compression // levels, not quality, like jpg and gif. $normalizedQuality = round(($quality * 9) / 100); $normalizedQuality = 9 - $normalizedQuality; if ($normalizedQuality < 0) { $normalizedQuality = 0; } if ($normalizedQuality > 9) { $normalizedQuality = 9; } $options = [ 'png_compression_level' => $normalizedQuality, 'flatten' => false, ]; if ($this->_imageSourcePath) { $pngInfo = ImageHelper::pngImageInfo($this->_imageSourcePath); // Even though a 2 channel PNG is valid (Grayscale with alpha channel), Imagick doesn't recognize it as // a valid format: http://www.imagemagick.org/script/formats.php // So 2 channel PNGs get converted to 4 channel. if (is_array($pngInfo) && isset($pngInfo['channels']) && $pngInfo['channels'] !== 2) { $format = 'png' . (8 * $pngInfo['channels']); } else { $format = 'png32'; } } else { $format = 'png32'; } $options['png_format'] = $format; return $options; default: return [ 'quality' => $quality, ]; } } }
/srv/users/craft4/apps/craft4-newsite-space/vendor/sabberworm/../craftcms/cms/src/image/Raster.php