mirror of
https://github.com/getformwork/formwork.git
synced 2025-02-24 17:52:41 +01:00
Merge pull request #8 from giuscris/feature/image-processing
Add image processing feature
This commit is contained in:
commit
d74b01a796
@ -69,6 +69,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
|
||||
|
@ -69,6 +69,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
|
||||
|
@ -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
|
||||
|
@ -4,9 +4,11 @@ namespace Formwork\Admin\Controllers;
|
||||
|
||||
use Formwork\Admin\Admin;
|
||||
use Formwork\Admin\Exceptions\LocalizedException;
|
||||
use Formwork\Admin\Image;
|
||||
use Formwork\Admin\Security\Password;
|
||||
use Formwork\Admin\Uploader;
|
||||
use Formwork\Admin\Users\User;
|
||||
use Formwork\Core\Formwork;
|
||||
use Formwork\Data\DataGetter;
|
||||
use Formwork\Parsers\YAML;
|
||||
use Formwork\Router\RouteParams;
|
||||
@ -114,12 +116,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');
|
||||
|
400
admin/src/Image.php
Normal file
400
admin/src/Image.php
Normal file
@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
namespace Formwork\Admin;
|
||||
|
||||
use Formwork\Core\Formwork;
|
||||
use Formwork\Utils\FileSystem;
|
||||
use Formwork\Utils\MimeType;
|
||||
use BadMethodCallException;
|
||||
use RuntimeException;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class Image
|
||||
{
|
||||
const ORIENTATION_LANDSCAPE = 'landscape';
|
||||
|
||||
const ORIENTATION_PORTRAIT = 'portrait';
|
||||
|
||||
const RESIZE_FIT_COVER = 'cover';
|
||||
|
||||
const RESIZE_FIT_CONTAIN = 'contain';
|
||||
|
||||
protected $filename;
|
||||
|
||||
protected $info;
|
||||
|
||||
protected $width;
|
||||
|
||||
protected $height;
|
||||
|
||||
protected $image;
|
||||
|
||||
protected $sourceImage;
|
||||
|
||||
protected $JPEGQuality = 85;
|
||||
|
||||
protected $PNGCompression = 9;
|
||||
|
||||
public function __construct($filename)
|
||||
{
|
||||
if (!extension_loaded('gd')) {
|
||||
throw new RuntimeException('GD extension not loaded');
|
||||
}
|
||||
|
||||
if (!FileSystem::isReadable($filename)) {
|
||||
throw new RuntimeException('Image ' . $filename . ' must be readable to be processed');
|
||||
}
|
||||
|
||||
$this->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();
|
||||
}
|
||||
}
|
@ -86,10 +86,13 @@ class Formwork
|
||||
'cache.enabled' => false,
|
||||
'cache.path' => ROOT_PATH . 'cache' . DS,
|
||||
'cache.time' => 604800,
|
||||
'images.jpeg_quality' => 85,
|
||||
'images.png_compression' => 6,
|
||||
'backup.path' => ROOT_PATH . 'backup' . DS,
|
||||
'admin.enabled' => true,
|
||||
'admin.lang' => 'en',
|
||||
'admin.logout_redirect' => 'login'
|
||||
'admin.logout_redirect' => 'login',
|
||||
'admin.avatar_size' => 512
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user