Improved image resizing code and added GD fallback.

This commit is contained in:
flash 2019-12-04 00:45:34 +01:00
parent e50cb31a6e
commit d4e4b17a42
4 changed files with 245 additions and 40 deletions

View file

@ -1,7 +1,7 @@
<?php
namespace Misuzu;
use Imagick;
use Misuzu\Imaging\Image;
$userAssetsMode = !empty($_GET['m']) && is_string($_GET['m']) ? (string)$_GET['m'] : '';
$misuzuBypassLockdown = $userAssetsMode === 'avatar';
@ -56,45 +56,9 @@ switch($userAssetsMode) {
try {
mkdirs($avatarStorage, true);
$avatarImage = new Imagick($avatarOriginal);
$avatarImage->setImageFormat($avatarImage->getNumberImages() > 1 ? 'gif' : 'png');
$avatarImage = $avatarImage->coalesceImages();
$avatarOriginalWidth = $avatarImage->getImageWidth();
$avatarOriginalHeight = $avatarImage->getImageHeight();
if($avatarOriginalWidth > $avatarOriginalHeight) {
$avatarWidth = $avatarOriginalWidth * $dimensions / $avatarOriginalHeight;
$avatarHeight = $dimensions;
} else {
$avatarWidth = $dimensions;
$avatarHeight = $avatarOriginalHeight * $dimensions / $avatarOriginalWidth;
}
do {
$avatarImage->resizeImage(
$avatarWidth,
$avatarHeight,
Imagick::FILTER_LANCZOS,
0.9
);
$avatarImage->cropImage(
$dimensions,
$dimensions,
($avatarWidth - $dimensions) / 2,
($avatarHeight - $dimensions) / 2
);
$avatarImage->setImagePage(
$dimensions,
$dimensions,
0,
0
);
} while($avatarImage->nextImage());
$avatarImage->deconstructImages()->writeImages($filename = $avatarCropped, true);
$avatarImage = Image::create($avatarOriginal);
$avatarImage->squareCrop($dimensions);
$avatarImage->save($filename = $avatarCropped);
} catch(Exception $ex) {}
}
}

112
src/Imaging/GdImage.php Normal file
View file

@ -0,0 +1,112 @@
<?php
namespace Misuzu\Imaging;
use InvalidArgumentException;
final class GdImage extends Image {
private $gd;
private int $type;
private const CONSTRUCTORS = [
IMAGETYPE_GIF => 'imagecreatefromgif',
IMAGETYPE_JPEG => 'imagecreatefromjpeg',
IMAGETYPE_PNG => 'imagecreatefrompng',
IMAGETYPE_BMP => 'imagecreatefrombmp',
IMAGETYPE_WBMP => 'imagecreatefromwbmp',
IMAGETYPE_WEBP => 'imagecreatefromwebp',
];
private const SAVERS = [
IMAGETYPE_GIF => 'imagegif',
IMAGETYPE_JPEG => 'imagejpeg',
IMAGETYPE_PNG => 'imagepng',
IMAGETYPE_BMP => 'imagebmp',
IMAGETYPE_WBMP => 'imagewbmp',
IMAGETYPE_WEBP => 'imagewebp',
];
public function __construct($pathOrWidth, int $height = -1) {
parent::__construct($pathOrWidth, $height);
if(is_int($pathOrWidth)) {
$this->gd = imagecreatetruecolor($pathOrWidth, $height < 1 ? $pathOrWidth : $height);
$this->type = IMAGETYPE_PNG;
} elseif(is_string($pathOrWidth)) {
$imageInfo = getimagesize($pathOrWidth);
if($imageInfo !== false) {
$this->type = $imageInfo[2];
if(isset(self::CONSTRUCTORS[$this->type]))
$this->gd = self::CONSTRUCTORS[$this->type]($pathOrWidth);
}
}
if(!isset($this->gd)) {
throw new InvalidArgumentException('Unsupported image format.');
}
}
public function __destruct() {
if(isset($this->gd))
$this->destroy();
}
public function getWidth(): int {
return imagesx($this->gd);
}
public function getHeight(): int {
return imagesy($this->gd);
}
public function hasFrames(): bool {
return false;
}
public function next(): bool {
return false;
}
public function resize(int $width, int $height): bool {
$resized = imagescale($this->gd, $width, $height, IMG_BICUBIC_FIXED);
if($resized === false)
return false;
imagedestroy($this->gd);
$this->gd = $resized;
return true;
}
public function crop(int $width, int $height, int $x, int $y): bool {
$cropped = imagecrop($this->gd, compact('width', 'height', 'x', 'y'));
if($cropped === false)
return false;
imagedestroy($this->gd);
$this->gd = $cropped;
return true;
}
public function setPage(int $width, int $height, int $x, int $y): bool {
return false;
}
public function save(string $path): bool {
if(isset(self::SAVERS[$this->type]))
return self::SAVERS[$this->type]($this->gd, $path);
return false;
}
public function destroy(): void {
if(imagedestroy($this->gd)) {
$this->gd = null;
$this->type = 0;
}
}
}

53
src/Imaging/Image.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace Misuzu\Imaging;
use InvalidArgumentException;
use UnexpectedValueException;
abstract class Image {
public function __construct($pathOrWidth, int $height = -1) {
if(!is_int($pathOrWidth) && !is_string($pathOrWidth))
throw new InvalidArgumentException('The first argument must be or type string to open an image file, or int to set a width for a new image.');
}
public static function create($pathOrWidth, int $height = -1): Image {
if(extension_loaded('imagick'))
return new ImagickImage($pathOrWidth, $height);
if(extension_loaded('gd'))
return new GdImage($pathOrWidth, $height);
throw new UnexpectedValueException('No image manipulation extensions are available.');
}
abstract public function getWidth(): int;
abstract public function getHeight(): int;
abstract public function hasFrames(): bool;
abstract public function next(): bool;
abstract public function resize(int $width, int $height): bool;
abstract public function crop(int $width, int $height, int $x, int $y): bool;
abstract public function setPage(int $width, int $height, int $x, int $y): bool;
abstract public function save(string $path): bool;
abstract public function destroy(): void;
public function squareCrop(int $dimensions): void {
$originalWidth = $this->getWidth();
$originalHeight = $this->getHeight();
if($originalWidth > $originalHeight) {
$targetWidth = $originalWidth * $dimensions / $originalHeight;
$targetHeight = $dimensions;
} else {
$targetWidth = $dimensions;
$targetHeight = $originalHeight * $dimensions / $originalWidth;
}
do {
$this->resize($targetWidth, $targetHeight);
$this->crop(
$dimensions, $dimensions,
($targetWidth - $dimensions) / 2,
($targetHeight - $dimensions) / 2
);
$this->setPage($dimensions, $dimensions, 0, 0);
} while($this->next());
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace Misuzu\Imaging;
use Imagick;
use InvalidArgumentException;
final class ImagickImage extends Image {
private ?Imagick $imagick = null;
public function __construct($pathOrWidth, int $height = -1) {
parent::__construct($pathOrWidth, $height);
if(is_int($pathOrWidth)) {
$this->imagick = new Imagick();
$this->newImage($pathOrWidth, $height < 1 ? $pathOrWidth : $height, 'none');
$this->setImageFormat('png');
} elseif(is_string($pathOrWidth)) {
$imagick = new Imagick($pathOrWidth);
$imagick->setImageFormat($imagick->getNumberImages() > 1 ? 'gif' : 'png');
$this->imagick = $imagick->coalesceImages();
}
if(!isset($this->imagick))
throw new InvalidArgumentException('Unsupported image format.');
}
public function __destruct() {
if(isset($this->imagick))
$this->destroy();
}
public function getImagick(): Imagick {
return $this->imagick;
}
public function getWidth(): int {
return $this->imagick->getImageWidth();
}
public function getHeight(): int {
return $this->imagick->getImageHeight();
}
public function hasFrames(): bool {
return $this->imagick->getNumberImages() > 1;
}
public function next(): bool {
return $this->imagick->nextImage();
}
public function resize(int $width, int $height): bool {
return $this->imagick->resizeImage(
$width, $height, Imagick::FILTER_LANCZOS, 0.9
);
}
public function crop(int $width, int $height, int $x, int $y): bool {
return $this->imagick->cropImage($width, $height, $x, $y);
}
public function setPage(int $width, int $height, int $x, int $y): bool {
return $this->imagick->setImagePage($width, $height, $x, $y);
}
public function save(string $path): bool {
return $this->imagick
->deconstructImages()
->writeImages($path, true);
}
public function destroy(): void {
if($this->imagick->destroy())
$this->imagick = null;
}
}