From dda9f6e473c3ccb904e8a041d14a747797e3d56c Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione Date: Wed, 19 Sep 2018 13:52:34 +0200 Subject: [PATCH 1/3] Add `Image` class --- admin/src/Image.php | 400 +++++++++++++++++++++++++++++++++++++ formwork/Core/Formwork.php | 2 + 2 files changed, 402 insertions(+) create mode 100644 admin/src/Image.php diff --git a/admin/src/Image.php b/admin/src/Image.php new file mode 100644 index 00000000..1b277ac5 --- /dev/null +++ b/admin/src/Image.php @@ -0,0 +1,400 @@ +JPEGQuality = Formwork::instance()->option('images.jpeg_quality'); + + if ($this->JPEGQuality < 0 || $this->JPEGQuality > 100) { + throw new UnexpectedValueException('JPEG quality must be in the range 0-100, ' . $this->JPEGQuality . ' given'); + } + + $this->PNGCompression = Formwork::instance()->option('images.png_compression'); + + if ($this->PNGCompression < 0 || $this->PNGCompression > 9) { + throw new UnexpectedValueException('PNG compression level must be in range 0-9, ' . $this->PNGCompression . ' given'); + } + + $this->filename = $filename; + $this->info = getimagesize($filename); + + if (!$this->info) { + throw new RuntimeException('Cannot load image ' . $filename); + } + + switch ($this->info['mime']) { + case 'image/jpeg': + $this->image = imagecreatefromjpeg($filename); + break; + case 'image/png': + $this->image = imagecreatefrompng($filename); + break; + case 'image/gif': + $this->image = imagecreatefromgif($filename); + imagepalettetotruecolor($this->image); + break; + default: + throw new RuntimeException('Unsupported image MIME type'); + break; + } + + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + $this->sourceImage = $this->image; + } + + public function width() + { + return $this->width; + } + + public function height() + { + return $this->height; + } + + public function orientation() + { + if ($this->width >= $this->height) { + return self::ORIENTATION_LANDSCAPE; + } + return self::ORIENTATION_PORTRAIT; + } + + public function rotate($angle) + { + $backgroundColor = imagecolorallocatealpha($this->image, 0, 0, 0, 127); + $this->image = imagerotate($this->image, $angle, $backgroundColor); + return $this; + } + + public function flipHorizontal() + { + imageflip($this->image, IMG_FLIP_HORIZONTAL); + return $this; + } + + public function flipVertical() + { + imageflip($this->image, IMG_FLIP_VERTICAL); + return $this; + } + + public function flipBoth() + { + imageflip($this->image, IMG_FLIP_BOTH); + return $this; + } + + public function resize($destinationWidth, $destinationHeight) + { + $sourceWidth = $this->width; + $sourceHeight = $this->height; + + $sourceRatio = $sourceWidth / $sourceHeight; + + if (!$destinationWidth && !$destinationHeight) { + throw new BadMethodCallException(__METHOD__ . ' must be called with at least one of $destinationWidth or $destinationHeight arguments'); + } + + if (!$destinationWidth) { + $destinationWidth = $destinationHeight * $sourceRatio; + } elseif (!$destinationHeight) { + $destinationHeight = $destinationWidth / $sourceRatio; + } + + $destinationImage = imagecreatetruecolor($destinationWidth, $destinationHeight); + + if ($this->info['mime'] === 'image/png' || $this->info['mime'] === 'image/gif') { + $this->enableTransparency($destinationImage); + } + + imagecopyresampled( + $destinationImage, + $this->image, + 0, + 0, + 0, + 0, + $destinationWidth, + $destinationHeight, + $sourceWidth, + $sourceHeight + ); + + $this->image = $destinationImage; + return $this; + } + + public function scale($factor) + { + return $this->resize($factor * $this->width, $factor * $this->height); + } + + public function resizeToFit($destinationWidth, $destinationHeight, $mode = self::RESIZE_FIT_COVER) + { + $sourceWidth = $this->width; + $sourceHeight = $this->height; + + if (!$destinationWidth || !$destinationHeight) { + throw new BadMethodCallException(__METHOD__ . ' must be called with both $destinationWidth and $destinationHeight arguments'); + } + + $cropAreaWidth = $sourceWidth; + $cropAreaHeight = $sourceHeight; + + $cropOriginX = 0; + $cropOriginY = 0; + + $sourceRatio = $sourceWidth / $sourceHeight; + $destinationRatio = $destinationWidth / $destinationHeight; + + if (($mode === self::RESIZE_FIT_COVER && $sourceRatio > $destinationRatio) + || ($mode === self::RESIZE_FIT_CONTAIN && $sourceRatio < $destinationRatio)) { + $cropAreaWidth = $sourceHeight * $destinationRatio; + $cropOriginX = ($sourceWidth - $cropAreaWidth) / 2; + } else { + $cropAreaHeight = $sourceWidth / $destinationRatio; + $cropOriginY = ($sourceHeight - $cropAreaHeight) / 2; + } + + $destinationImage = imagecreatetruecolor($destinationWidth, $destinationHeight); + + if ($this->info['mime'] === 'image/png' || $this->info['mime'] === 'image/gif') { + $this->enableTransparency($destinationImage); + } + + imagecopyresampled( + $destinationImage, + $this->image, + 0, + 0, + $cropOriginX, + $cropOriginY, + $destinationWidth, + $destinationHeight, + $cropAreaWidth, + $cropAreaHeight + ); + + $this->image = $destinationImage; + + return $this; + } + + public function square($size, $mode = self::RESIZE_FIT_COVER) + { + return $this->resizeToFit($size, $size, $mode); + } + + public function crop($originX, $originY, $width, $height) + { + if (!$width || !$height) { + throw new BadMethodCallException(__METHOD__ . ' must be called with both $width and $height arguments'); + } + + $destinationImage = imagecreatetruecolor($width, $height); + + if ($this->info['mime'] === 'image/png' || $this->info['mime'] === 'image/gif') { + $this->enableTransparency($destinationImage); + } + + imagecopy( + $destinationImage, + $this->image, + 0, + 0, + $originX, + $originY, + $width, + $height + ); + + $this->image = $destinationImage; + + return $this; + } + + public function desaturate() + { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + return $this; + } + + public function invert() + { + imagefilter($this->image, IMG_FILTER_NEGATE); + return $this; + } + + public function brightness($amount) + { + if ($amount < -255 || $amount > 255) { + throw new UexpectedValueException('$amount value must be in range -255-+255, ' . $amount . ' given'); + } + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $amount); + return $this; + } + + public function contrast($amount) + { + if ($amount < -100 || $amount > 100) { + throw new UexpectedValueException('$amount value must be in range -100-+100, ' . $amount . ' given'); + } + // For GD -100 = max contrast, 100 = min contrast; we change $amount sign for a more predictable behavior + imagefilter($this->image, IMG_FILTER_CONTRAST, -$amount); + return $this; + } + + public function colorize($red, $green, $blue, $alpha = 0) + { + if ($red < 0 || $red > 255) { + throw new UexpectedValueException('$red value must be in range 0-255, ' . $red . ' given'); + } + if ($green < 0 || $green > 255) { + throw new UexpectedValueException('$green value must be in range 0-255, ' . $green . ' given'); + } + if ($blue < 0 || $blue > 255) { + throw new UexpectedValueException('$blue value must be in range 0-255, ' . $blue . ' given'); + } + if ($alpha < 0 || $alpha > 127) { + throw new UexpectedValueException('$alpha value must be in range 0-127, ' . $alpha . ' given'); + } + imagefilter($this->image, IMG_FILTER_COLORIZE, $red, $green, $blue, $alpha); + return $this; + } + + public function sepia() + { + return $this->desaturate()->colorize(76, 48, 0); + } + + public function edgedetect() + { + imagefilter($this->image, IMG_FILTER_EDGEDETECT); + return $this; + } + + public function emboss() + { + imagefilter($this->image, IMG_FILTER_EMBOSS); + return $this; + } + + public function blur($amount) + { + if ($amount < 0 || $amount > 100) { + throw new UexpectedValueException('$amount value must be in range 0-100, ' . $amount . ' given'); + } + for ($i = 0; $i < $amount; $i++) { + imagefilter($this->image, IMG_FILTER_GAUSSIAN_BLUR); + } + return $this; + } + + public function sharpen() + { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + return $this; + } + + public function smoothen($amount) + { + imagefilter($this->image, IMG_FILTER_SMOOTH, $amount); + return $this; + } + + public function pixelate($amount) + { + imagefilter($this->image, IMG_FILTER_PIXELATE, $amount); + return $this; + } + + public function save($filename = null, $destroy = true) + { + if (is_null($filename)) { + $filename = $this->filename; + } + + $extension = strtolower(FileSystem::extension($filename)); + $mimeType = MimeType::fromExtension($extension); + + switch ($mimeType) { + case 'image/jpeg': + return imagejpeg($this->image, $filename, $this->JPEGQuality); + case 'image/png': + return imagepng($this->image, $filename, $this->PNGCompression); + case 'image/gif': + return imagegif($this->image, $filename); + default: + throw new RuntimeException('Unknown image MIME type for .' . $filename . ' extension'); + break; + } + + if ($destroy) { + $this->destroy(); + } + } + + public function destroy() + { + imagedestroy($this->image); + imagedestroy($this->sourceImage); + } + + protected function enableTransparency($image) + { + $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127); + imagealphablending($image, true); + imagesavealpha($image, true); + imagecolortransparent($image, $transparent); + imagefill($image, 0, 0, $transparent); + } + + public function __destruct() + { + $this->destroy(); + } +} diff --git a/formwork/Core/Formwork.php b/formwork/Core/Formwork.php index 16f86e74..ebf1447d 100755 --- a/formwork/Core/Formwork.php +++ b/formwork/Core/Formwork.php @@ -86,6 +86,8 @@ class Formwork 'cache.enabled' => false, 'cache.path' => ROOT_PATH . 'cache' . DS, 'cache.time' => 604800, + 'images.jpeg_quality' => 85, + 'images.png_compression' => 6, 'admin.enabled' => true, 'admin.lang' => 'en', 'admin.logout_redirect' => 'login' From 0da8c06117878602f943030c0e64caf6e87352b4 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione Date: Wed, 19 Sep 2018 13:53:53 +0200 Subject: [PATCH 2/3] Add JPEG Quality and PNG Compression Level options --- admin/languages/en.yml | 3 +++ admin/languages/it.yml | 3 +++ admin/schemes/system.yml | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/admin/languages/en.yml b/admin/languages/en.yml index b3cf378c..339ad907 100755 --- a/admin/languages/en.yml +++ b/admin/languages/en.yml @@ -64,6 +64,9 @@ options.system.date-and-time.hour-format: Hour Format options.system.date-and-time.timezone: Timezone options.system.files: Files options.system.files.allowed-extensions: Allowed Extensions +options.system.images: Images +options.system.images.jpeg-quality: JPEG Quality +options.system.images.png-compression-level: PNG Compression Level options.updated: Options updated options.updates: Updates pages.attributes: Attributes diff --git a/admin/languages/it.yml b/admin/languages/it.yml index 16f08eda..b37784d8 100755 --- a/admin/languages/it.yml +++ b/admin/languages/it.yml @@ -64,6 +64,9 @@ options.system.date-and-time.hour-format: Formato ora options.system.date-and-time.timezone: Fuso orario options.system.files: File options.system.files.allowed-extensions: Estensioni consentite +options.system.images: Immagini +options.system.images.jpeg-quality: Qualità JPEG +options.system.images.png-compression-level: Livello di compressione PNG options.updated: Opzioni aggiornate options.updates: Aggiornamenti pages.attributes: Attributi diff --git a/admin/schemes/system.yml b/admin/schemes/system.yml index 59fa06a2..ab680e1e 100755 --- a/admin/schemes/system.yml +++ b/admin/schemes/system.yml @@ -169,3 +169,42 @@ rows4: options: login: '{{options.system.admin-panel.logout-redirects-to.login}}' home: '{{options.system.admin-panel.logout-redirects-to.home}}' + +section5: + type: header + label: '{{options.system.images}}' + +rows5: + type: rows + fields: + row1: + type: row + fields: + column1: + type: column + width: 1-3 + label: '{{options.system.images.jpeg-quality}}' + column2: + type: column + width: 2-3 + fields: + images.jpeg_quality: + type: range + min: 0 + max: 100 + step: 5 + row2: + type: row + fields: + column1: + type: column + width: 1-3 + label: '{{options.system.images.png-compression-level}}' + column2: + type: column + width: 2-3 + fields: + images.png_compression: + type: range + min: 0 + max: 9 From d7a3c31c825b1268b979374b880ad6c87b8f700d Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione Date: Wed, 19 Sep 2018 15:13:06 +0200 Subject: [PATCH 3/3] Resize avatars to square using `Image` class --- admin/src/Controllers/Users.php | 8 +++++++- formwork/Core/Formwork.php | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/admin/src/Controllers/Users.php b/admin/src/Controllers/Users.php index 5a8c589e..a1b670b1 100755 --- a/admin/src/Controllers/Users.php +++ b/admin/src/Controllers/Users.php @@ -3,9 +3,11 @@ namespace Formwork\Admin\Controllers; use Formwork\Admin\Admin; +use Formwork\Admin\Image; use Formwork\Admin\Security\Password; use Formwork\Admin\Uploader; use Formwork\Admin\User; +use Formwork\Core\Formwork; use Formwork\Data\DataGetter; use Formwork\Parsers\YAML; use Formwork\Router\RouteParams; @@ -113,12 +115,16 @@ class Users extends AbstractController } if (HTTPRequest::hasFiles()) { + $avatarsPath = ADMIN_PATH . 'avatars' . DS; $uploader = new Uploader( - ADMIN_PATH . 'avatars' . DS, + $avatarsPath, array('allowedMimeTypes' => array('image/gif', 'image/jpeg', 'image/png')) ); try { if ($uploader->upload(str_shuffle(uniqid()))) { + $avatarSize = Formwork::instance()->option('admin.avatar_size'); + $image = new Image($avatarsPath . $uploader->uploadedFiles()[0]); + $image->square($avatarSize)->save(); $this->deleteAvatar($user); $data['avatar'] = $uploader->uploadedFiles()[0]; $this->notify($this->label('user.avatar.uploaded'), 'success'); diff --git a/formwork/Core/Formwork.php b/formwork/Core/Formwork.php index ebf1447d..c66676d8 100755 --- a/formwork/Core/Formwork.php +++ b/formwork/Core/Formwork.php @@ -90,7 +90,8 @@ class Formwork 'images.png_compression' => 6, 'admin.enabled' => true, 'admin.lang' => 'en', - 'admin.logout_redirect' => 'login' + 'admin.logout_redirect' => 'login', + 'admin.avatar_size' => 512 ); }