Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 69 additions & 87 deletions src/Database/Attach/Resizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,56 +29,56 @@
class Resizer
{
/**
* @var Resource The symfony uploaded file object.
* @var FileObj The symfony uploaded file object.
*/
protected $file;
protected FileObj $file;

/**
* @var string The extension of the uploaded file.
*/
protected $extension;
protected string $extension;

/**
* @var string The mime type of the uploaded file.
*/
protected $mime;
protected string $mime;

/**
* @var GdImage The image (on disk) that's being resized.
*/
protected $image;
protected GdImage $image;

/**
* @var GdImage The cached, original image.
*/
protected $originalImage;
protected GdImage $originalImage;

/**
* @var int Original width of the image being resized.
*/
protected $width;
protected int $width;

/**
* @var int Original height of the image being resized.
*/
protected $height;
protected int $height;

/**
* @var int|null Exif orientation of image
*/
protected $orientation;
protected ?int $orientation;

/**
* @var array Array of options used for resizing.
*/
protected $options = [];
protected array $options = [];

/**
* Instantiates the Resizer and receives the path to an image we're working with
* @param mixed $file The file array provided by Laravel's Input::file('field_name') or a path to a file
* @param FileObj|string $file The file array provided by Laravel's Input::file('field_name') or a path to a file
* @throws Exception
*/
public function __construct($file)
public function __construct(FileObj|string $file)
{
if (!extension_loaded('gd')) {
throw new Exception('The GD PHP extension is required to resize images.');
Expand Down Expand Up @@ -109,21 +109,19 @@ public function __construct($file)
/**
* Static call, Laravel style.
* Returns a new Resizer object, allowing for chainable calls
* @param mixed $file The file array provided by Laravel's Input::file('field_name') or a path to a file
* @param FileObj|string $file The file array provided by Laravel's Input::file('field_name') or a path to a file
* @return Resizer
* @throws Exception
*/
public static function open($file)
public static function open(FileObj|string $file): static
{
return new Resizer($file);
}

/**
* Manipulate an image resource in order to keep transparency for PNG and GIF files.
* @param GdImage $img
* @return void
*/
protected function retainImageTransparency($img)
protected function retainImageTransparency(GdImage $img): void
{
if ($this->mime === 'image/gif') {
$alphaIndex = imagecolortransparent($this->image);
Expand All @@ -145,7 +143,7 @@ protected function retainImageTransparency($img)
* Resets the image back to the original.
* @return self
*/
public function reset()
public function reset(): static
{
$this->image = $this->originalImage;

Expand All @@ -162,7 +160,7 @@ public function reset()
* @param array $options Set of resizing option
* @return self
*/
public function setOptions($options)
public function setOptions(array $options): static
{
$this->options = array_merge([
'mode' => 'auto',
Expand All @@ -181,7 +179,7 @@ public function setOptions($options)
* @param mixed $value Option value to set
* @return self
*/
protected function setOption($option, $value)
protected function setOption(string $option, mixed $value): static
{
$this->options[$option] = $value;

Expand All @@ -193,7 +191,7 @@ protected function setOption($option, $value)
* @param string $option Option name to get
* @return mixed Depends on the option
*/
protected function getOption($option)
protected function getOption(string $option): mixed
{
return array_get($this->options, $option);
}
Expand All @@ -203,7 +201,7 @@ protected function getOption($option)
* @param \Symfony\Component\HttpFoundation\File\File $file
* @return int|null
*/
protected function getOrientation($file)
protected function getOrientation(FileObj $file): ?int
{
$filePath = $file->getPathname();

Expand Down Expand Up @@ -233,7 +231,7 @@ protected function getOrientation($file)
* the exif orientation
* @return int
*/
protected function getWidth()
protected function getWidth(): int
{
switch ($this->orientation) {
case 6:
Expand All @@ -252,7 +250,7 @@ protected function getWidth()
* the exif orientation
* @return int
*/
protected function getHeight()
protected function getHeight(): int
{
switch ($this->orientation) {
case 6:
Expand All @@ -267,11 +265,9 @@ protected function getHeight()
}

/**
* Receives the original but rotated image
* according to exif orientation
* @return GdImage|false
* Receives the original but rotated image according to exif orientation
*/
protected function getRotatedOriginal()
protected function getRotatedOriginal(): GdImage|false
{
switch ($this->orientation) {
case 6:
Expand Down Expand Up @@ -303,16 +299,10 @@ protected function getRotatedOriginal()
* @return self
* @throws Exception
*/
public function resize($newWidth, $newHeight, $options = [])
public function resize(int $newWidth, int $newHeight, array $options = []): static
{
$this->setOptions($options);

/*
* Sanitize input
*/
$newWidth = (int) $newWidth;
$newHeight = (int) $newHeight;

if (!$newWidth && !$newHeight) {
$newWidth = $this->width;
$newHeight = $this->height;
Expand Down Expand Up @@ -381,7 +371,7 @@ public function resize($newWidth, $newHeight, $options = [])
* @param int $sharpness
* @return self
*/
public function sharpen($sharpness)
public function sharpen(int $sharpness): static
{
if ($sharpness <= 0 || $sharpness > 100) {
return $this;
Expand Down Expand Up @@ -417,8 +407,14 @@ public function sharpen($sharpness)
* @param int $srcHeight Source area height.
* @return self
*/
public function crop($cropStartX, $cropStartY, $newWidth, $newHeight, $srcWidth = null, $srcHeight = null)
{
public function crop(
int $cropStartX,
int $cropStartY,
int $newWidth,
int $newHeight,
int $srcWidth = null,
int $srcHeight = null
): static {
$image = $this->image;

if ($srcWidth === null) {
Expand All @@ -445,7 +441,7 @@ public function crop($cropStartX, $cropStartY, $newWidth, $newHeight, $srcWidth
* @param string $savePath Where to save the image
* @throws Exception Thrown for invalid extension
*/
public function save($savePath)
public function save(string $savePath): void
{
$image = $this->image;

Expand Down Expand Up @@ -521,10 +517,10 @@ public function save($savePath)
/**
* Open a file, detect its mime-type and create an image resource from it.
* @param \Symfony\Component\HttpFoundation\File\File $file File instance
* @return mixed
* @return GdImage
* @throws Exception Thrown for invalid MIME type
*/
protected function openImage($file)
protected function openImage(FileObj $file): GdImage
{
$filePath = $file->getPathname();

Expand Down Expand Up @@ -561,68 +557,57 @@ protected function openImage($file)

/**
* Return the image dimensions based on the option that was chosen.
* @param int $newWidth The width of the image
* @param int $newHeight The height of the image
* @param float $newWidth The width of the image
* @param float $newHeight The height of the image
* @return array
* @throws Exception Thrown for invalid dimension string
*/
protected function getDimensions($newWidth, $newHeight)
#[ArrayShape(['width' => 'float', 'height' => 'float'])]
protected function getDimensions(float $newWidth, float $newHeight): array
{
$mode = $this->getOption('mode');

switch ($mode) {
case 'exact':
return [$newWidth, $newHeight];

case 'portrait':
return [$this->getSizeByFixedHeight($newHeight), $newHeight];

case 'landscape':
return [$newWidth, $this->getSizeByFixedWidth($newWidth)];

case 'auto':
return $this->getSizeByAuto($newWidth, $newHeight);

case 'crop':
return $this->getOptimalCrop($newWidth, $newHeight);

case 'fit':
return $this->getSizeByFit($newWidth, $newHeight);

default:
throw new Exception('Invalid dimension type. Accepted types: exact, portrait, landscape, auto, crop, fit.');
}
return match ($mode) {
'exact' => [$newWidth, $newHeight],
'portrait' => [$this->getSizeByFixedHeight($newHeight), $newHeight],
'landscape' => [$newWidth, $this->getSizeByFixedWidth($newWidth)],
'auto' => $this->getSizeByAuto($newWidth, $newHeight),
'crop' => $this->getOptimalCrop($newWidth, $newHeight),
'fit' => $this->getSizeByFit($newWidth, $newHeight),
default => throw new Exception('Invalid dimension type. Accepted types: exact, portrait, landscape, auto, crop, fit.'),
};
}

/**
* Returns the width based on the image height
* @param int $newHeight The height of the image
* @return int
* @param float $newHeight The height of the image
* @return float
*/
protected function getSizeByFixedHeight($newHeight)
protected function getSizeByFixedHeight(float $newHeight): float
{
$ratio = $this->width / $this->height;
return $newHeight * $ratio;
}

/**
* Returns the height based on the image width
* @param int $newWidth The width of the image
* @return int
* @param float $newWidth The width of the image
* @return float
*/
protected function getSizeByFixedWidth($newWidth)
protected function getSizeByFixedWidth(float $newWidth): float
{
$ratio = $this->height / $this->width;
return $newWidth * $ratio;
}

/**
* Checks to see if an image is portrait or landscape and resizes accordingly.
* @param int $newWidth The width of the image
* @param int $newHeight The height of the image
* @param float $newWidth The width of the image
* @param float $newHeight The height of the image
* @return array
*/
protected function getSizeByAuto($newWidth, $newHeight)
#[ArrayShape(['width' => 'float', 'height' => 'float'])]
protected function getSizeByAuto(float $newWidth, float $newHeight): array
{
// Less than 1 pixel height and width? (revert to original)
if ($newWidth <= 1 && $newHeight <= 1) {
Expand Down Expand Up @@ -670,21 +655,17 @@ protected function getSizeByAuto($newWidth, $newHeight)
/**
* Attempts to find the best way to crop. Whether crop is based on the
* image being portrait or landscape.
* @param int $newWidth The width of the image
* @param int $newHeight The height of the image
* @param float $newWidth The width of the image
* @param float $newHeight The height of the image
* @return array
*/
protected function getOptimalCrop($newWidth, $newHeight)
#[ArrayShape(['width' => 'float', 'height' => 'float'])]
protected function getOptimalCrop(float $newWidth, float $newHeight): array
{
$heightRatio = $this->height / $newHeight;
$widthRatio = $this->width / $newWidth;

if ($heightRatio < $widthRatio) {
$optimalRatio = $heightRatio;
}
else {
$optimalRatio = $widthRatio;
}
$optimalRatio = min($heightRatio, $widthRatio);

$optimalHeight = round($this->height / $optimalRatio);
$optimalWidth = round($this->width / $optimalRatio);
Expand All @@ -694,11 +675,12 @@ protected function getOptimalCrop($newWidth, $newHeight)

/**
* Fit the image inside a bounding box using maximum width and height constraints.
* @param int $maxWidth The maximum width of the image
* @param int $maxHeight The maximum height of the image
* @param float $maxWidth The maximum width of the image
* @param float $maxHeight The maximum height of the image
* @return array
*/
protected function getSizeByFit($maxWidth, $maxHeight)
#[ArrayShape(['width' => 'float', 'height' => 'float'])]
protected function getSizeByFit(float $maxWidth, float $maxHeight): array
{

// Calculate the scaling ratios in order to get the target width and height
Expand Down