1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 16:26:59 +02:00

Add support for image file actions to InputfieldImage and update ImageSizer engines to support new rotate, flip and color actions

This commit is contained in:
Ryan Cramer
2018-01-05 10:54:21 -05:00
parent 9bdad6fc86
commit fb39ded94d
10 changed files with 1017 additions and 60 deletions

View File

@@ -88,20 +88,8 @@ class ImageSizer extends Wire {
*
*/
public function __construct($filename = '', $options = array()) {
if(isset($options['forceEngine'])) {
$this->forceEngineName = $options['forceEngine'];
unset($options['forceEngine']);
}
$this->filename = $filename;
$this->initialOptions = $options;
if(strlen($filename)) {
$imageInspector = new ImageInspector($filename);
$this->inspectionResult = $imageInspector->inspect($filename, true);
$this->engine = $this->newImageSizerEngine($filename, $options, $this->inspectionResult);
}
if(!empty($options)) $this->setOptions($options);
if(!empty($filename)) $this->setFilename($filename);
}
/**
@@ -158,6 +146,7 @@ class ImageSizer extends Wire {
if(empty($inspectionResult) && $filename && is_readable($filename)) {
$imageInspector = new ImageInspector($filename);
$this->wire($imageInspector);
$inspectionResult = $imageInspector->inspect($filename, true);
$this->inspectionResult = $inspectionResult;
}
@@ -226,17 +215,8 @@ class ImageSizer extends Wire {
*/
public function ___resize($targetWidth, $targetHeight = 0) {
if(empty($this->filename)) throw new WireException('No file to resize: please call setFilename($file) before resize()');
if(empty($this->engine)) {
// set the engine, and check if the engine is ready to use
$this->engine = $this->newImageSizerEngine();
if(!$this->engine) {
throw new WireException('There seems to be no support for the GD image library on your host?');
}
}
$success = $this->engine->resize($targetWidth, $targetHeight);
$engine = $this->getEngine();
$success = $engine->resize($targetWidth, $targetHeight);
if(!$success) {
// fallback to GD
@@ -297,8 +277,12 @@ class ImageSizer extends Wire {
*
*/
public function setOptions(array $options) {
if(isset($options['forceEngine'])) {
$this->setForceEngine($options['forceEngine']);
unset($options['forceEngine']);
}
$this->initialOptions = array_merge($this->initialOptions, $options);
if($this->engine) $this->engine->setOptions($options);
if($this->engine) $this->engine->setOptions($this->initialOptions);
return $this;
}
@@ -329,16 +313,48 @@ class ImageSizer extends Wire {
public function setUpscaling($value = true) { return $this->setOptions(array('upscaling', $value)); }
public function setUseUSM($value = true) { return $this->setOptions(array('useUSM', $value)); }
// getters (@todo phpdocs)
public function getWidth() { return $this->engine->image['width']; }
public function getHeight() { return $this->engine->image['height']; }
public function getFilename() { return $this->engine->filename; }
public function getExtension() { return $this->engine->extension; }
public function getImageType() { return $this->engine->imageType; }
public function isModified() { return $this->engine->modified; }
public function getOptions() { return $this->engine->getOptions(); }
public function getEngine() { return $this->engine; }
public function __get($key) { return $this->engine->__get($key); }
public function getWidth() {
$image = $this->getEngine()->get('image');
return $image['width'];
}
public function getHeight() {
$image = $this->getEngine()->get('image');
return $image['height'];
}
public function getFilename() { return $this->getEngine()->filename; }
public function getExtension() { return $this->getEngine()->extension; }
public function getImageType() { return $this->getEngine()->imageType; }
public function isModified() { return $this->getEngine()->modified; }
public function getOptions() { return $this->getEngine()->getOptions(); }
/**
* Get the current ImageSizerEngine
*
* @return ImageSizerEngine
* @throws WireException
*
*/
public function getEngine() {
if($this->engine) return $this->engine;
if(empty($this->filename)) {
throw new WireException('No file to process: please call setFilename($file) before calling other methods');
}
$imageInspector = new ImageInspector($this->filename);
$this->inspectionResult = $imageInspector->inspect($this->filename, true);
$this->engine = $this->newImageSizerEngine($this->filename, $this->initialOptions, $this->inspectionResult);
// set the engine, and check if the engine is ready to use
if(!$this->engine) {
throw new WireException('There seems to be no support for the GD image library on your host?');
}
return $this->engine;
}
public function __get($key) { return $this->getEngine()->__get($key); }
/**
* ImageInformation from Image Inspector in short form or full RawInfoData
@@ -348,7 +364,8 @@ class ImageSizer extends Wire {
*
*/
public function getImageInfo($rawData = false) {
$this->getEngine();
if($rawData) return $this->inspectionResult;
$imageType = $this->inspectionResult['info']['imageType'];
$type = '';
@@ -507,7 +524,9 @@ class ImageSizer extends Wire {
*
*/
static public function imageResetIPTC($image) {
$wire = null;
if($image instanceof Pageimage) {
$wire = $image;
$filename = $image->filename;
} else if(is_readable($image)) {
$filename = $image;
@@ -515,8 +534,72 @@ class ImageSizer extends Wire {
return null;
}
$sizer = new ImageSizerEngineGD($filename);
if($wire) $wire->wire($sizer);
$result = false !== $sizer->writeBackIPTC($filename) ? true : false;
return $result;
}
/**
* Rotate image by given degrees
*
* @param int $degrees
* @return bool
*
*/
public function rotate($degrees) {
return $this->getEngine()->rotate($degrees);
}
/**
* Flip image vertically
*
* @return bool
*
*/
public function flipVertical() {
return $this->getEngine()->flipVertical();
}
/**
* Flip image horizontally
*
* @return bool
*
*/
public function flipHorizontal() {
return $this->getEngine()->flipHorizontal();
}
/**
* Flip both vertically and horizontally
*
* @return bool
*
*/
public function flipBoth() {
return $this->getEngine()->flipBoth();
}
/**
* Convert image to greyscale (black and white)
*
* @return bool
*
*/
public function convertToGreyscale() {
return $this->getEngine()->convertToGreyscale();
}
/**
* Convert image to sepia tone
*
* @param int $sepia Sepia amount
* @return bool
*
*/
public function convertToSepia($sepia = 55) {
return $this->getEngine()->convertToSepia('', $sepia);
}
}

View File

@@ -398,6 +398,36 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
*/
abstract protected function processResize($srcFilename, $dstFilename, $fullWidth, $fullHeight, $finalWidth, $finalHeight);
/**
* Process rotate of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270
* @return bool
*
*/
protected function processRotate($srcFilename, $dstFilename, $degrees) {
if($srcFilename && $dstFilename && $degrees) {}
$this->error('rotate not implemented for ' . $this->className());
return false;
}
/**
* Process vertical or horizontal flip of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param bool $flipVertical True if flip is vertical, false if flip is horizontal
* @return bool
*
*/
protected function processFlip($srcFilename, $dstFilename, $flipVertical) {
if($srcFilename && $dstFilename && $flipVertical) {}
$this->error('flip not implemented for ' . $this->className());
return false;
}
/**
* Get array of image file extensions this ImageSizerModule can process
*
@@ -1469,6 +1499,118 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
return true;
}
/**
* Just rotate image by number of degrees
*
* @param int $degrees
* @param string $dstFilename Optional destination filename. If not present, source will be overwritten.
* @return bool True on success, false on fail
*
*/
public function rotate($degrees, $dstFilename = '') {
$degrees = (int) $degrees;
$srcFilename = $this->filename;
if(empty($dstFilename)) $dstFilename = $srcFilename;
if($degrees > 360) $degrees = 360 - $degrees;
if($degrees < -360) $degrees = $degrees - 360;
if($degrees == 0 || $degrees == 360 || $degrees == -360) {
if($dstFilename != $this->filename) wireCopy($this->filename, $dstFilename);
return true;
}
if($srcFilename == $dstFilename) {
// src and dest are the same, so use a temporary file
$n = 1;
do {
$tmpFilename = dirname($dstFilename) . "/.ise$n-" . basename($dstFilename);
} while(file_exists($tmpFilename) && $n++);
} else {
// src and dest are different files
$tmpFilename = $dstFilename;
}
$result = $this->processRotate($srcFilename, $tmpFilename, $degrees);
if($result) {
// success
if($tmpFilename != $dstFilename) {
if(is_file($dstFilename)) unlink($dstFilename);
rename($tmpFilename, $dstFilename);
}
wireChmod($dstFilename);
} else {
// fail
if(is_file($tmpFilename)) unlink($tmpFilename);
}
return $result;
}
/**
* Flip vertically
*
* @param string $dstFilename
* @return bool
*
*/
public function flipVertical($dstFilename = '') {
if(empty($dstFilename)) $dstFilename = $this->filename;
return $this->processFlip($this->filename, $dstFilename, 'vertical');
}
/**
* Flip horizontally
*
* @param string $dstFilename
* @return bool
*
*/
public function flipHorizontal($dstFilename = '') {
if(empty($dstFilename)) $dstFilename = $this->filename;
return $this->processFlip($this->filename, $dstFilename, 'horizontal');
}
/**
* Flip both vertically and horizontally
*
* @param string $dstFilename
* @return bool
*
*/
public function flipBoth($dstFilename = '') {
if(empty($dstFilename)) $dstFilename = $this->filename;
return $this->processFlip($this->filename, $dstFilename, 'both');
}
/**
* Convert image to greyscale
*
* @param string $dstFilename If different from source file
* @return bool
*
*/
public function convertToGreyscale($dstFilename = '') {
if($dstFilename) {}
return false;
}
/**
* Convert image to sepia
*
* @param string $dstFilename If different from source file
* @param float|int $sepia Sepia value
* @return bool
*
*/
public function convertToSepia($dstFilename = '', $sepia = 55) {
if($dstFilename && $sepia) {}
return false;
}
/**
* Get an integer representing the resize method to use
*

View File

@@ -204,12 +204,16 @@ class ImageSizerEngineGD extends ImageSizerEngine {
// this is the case if the original size is requested or a greater size but upscaling is set to false
// current version is already the desired result, we only may have to compress JPEGs but leave GIF and PNG as is:
/*
* the following commented block of code prevents PNG/GIF cropping from working
if($this->imageType == \IMAGETYPE_PNG || $this->imageType == \IMAGETYPE_GIF) {
$result = @copy($srcFilename, $dstFilename);
if(isset($image) && is_resource($image)) @imagedestroy($image); // clean up
if(isset($image)) $image = null;
return $result; // early return !
}
*/
// process JPEGs
if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) {
@@ -316,7 +320,6 @@ class ImageSizerEngineGD extends ImageSizerEngine {
return $result;
}
/**
* Rotate image (@horst)
*
@@ -330,7 +333,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$degree = (is_float($degree) || is_int($degree)) && $degree > -361 && $degree < 361 ? $degree : false;
if($degree === false) return $im;
if(in_array($degree, array(-360, 0, 360))) return $im;
return @imagerotate($im, $degree, imagecolorallocate($im, 0, 0, 0));
$angle = 360 - $degree; // because imagerotate() expects counterclockwise angle rather than degrees
return @imagerotate($im, $angle, imagecolorallocate($im, 0, 0, 0));
}
/**
@@ -738,11 +742,12 @@ class ImageSizerEngineGD extends ImageSizerEngine {
* @param array $sourceDimensions - array with three values: width, height, number of channels
* @param array|bool $targetDimensions - optional - mixed: bool true | false or array with three values:
* width, height, number of channels
* @param int|float Multiply needed memory by this factor
*
* @return bool|null if a calculation was possible (true|false), or null if the calculation could not be done
*
*/
static public function checkMemoryForImage($sourceDimensions, $targetDimensions = false) {
static public function checkMemoryForImage($sourceDimensions, $targetDimensions = false, $factor = 1) {
// with this static we only once need to read from php.ini and calculate phpMaxMem,
// regardless how often this function is called in a request
@@ -772,10 +777,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
}
// calculate $sourceDimensions
if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1])
|| !isset($sourceDimensions[2]) || !is_int($sourceDimensions[0])
|| !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])
) {
if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1]) || !isset($sourceDimensions[2]) ||
!is_int($sourceDimensions[0]) || !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])) {
return null;
}
@@ -788,10 +791,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
} else if(is_array($targetDimensions)) {
// we have to add ram for a targetimage
if(!isset($targetDimensions[0]) || !isset($targetDimensions[1])
|| !isset($targetDimensions[2]) || !is_int($targetDimensions[0])
|| !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])
) {
if(!isset($targetDimensions[0]) || !isset($targetDimensions[1]) || !isset($targetDimensions[2]) ||
!is_int($targetDimensions[0]) || !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])) {
return null;
}
@@ -802,7 +803,239 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$curMem = memory_get_usage(true); // memory_get_usage() is always available with PHP since 5.2.1
// check if there is enough RAM loading the image(s), plus 3 MB for GD to use for calculations/transforms
return ($phpMaxMem - $curMem >= $imgMem + (3 * 1048576)) ? true : false;
$extraMem = 3 * 1048576;
$availableMem = $phpMaxMem - $curMem;
$neededMem = ($imgMem + $extraMem) * $factor;
return $availableMem >= $neededMem;
}
/**
* Additional functionality on top of existing checkMemoryForImage function for the flip/rotate actions
*
* @param string $filename Filename to check. Default is whatever was set to this ImageSizer.
* @param bool $double Need enough for both src and dst files loaded at same time? (default=true)
* @param int|float $factor Tweak factor (multiply needed memory by this factor), i.e. 2 for rotate actions. (default=1)
* @param string $action Name of action (if something other than "action")
* @param bool $throwIfNot Throw WireException if not enough memory? (default=false)
* @return bool
* @throws WireException
*
*/
protected function hasEnoughMemory($filename = '', $double = true, $factor = 1, $action = 'action', $throwIfNot = false) {
$error = '';
if(empty($filename)) $filename = $this->filename;
if($filename) {
if($filename != $this->filename || empty($this->info['width'])) {
$this->prepare($filename); // to populate $this->info
}
} else {
$error = 'No filename to check memory for';
}
if(!$error) {
$hasEnough = self::checkMemoryForImage(array(
$this->info['width'],
$this->info['height'],
$this->info['channels']
), $double, $factor);
if($hasEnough === false) {
$error = sprintf($this->_('Not enough memory for “%1$s” on image file: %2$s'), $action, basename($filename));
}
}
if($error) {
if($throwIfNot) {
throw new WireException($error);
} else {
$this->error($error);
return false;
}
}
return true;
}
/**
* Process a rotate or flip action
*
* @param string $srcFilename
* @param string $dstFilename
* @param string $action One of 'rotate' or 'flip'
* @param int|string $value If rotate, specify int of degrees. If flip, specify one of 'vertical', 'horizontal' or 'both'.
* @return bool
* @throws WireException
*
*/
private function processAction($srcFilename, $dstFilename, $action, $value) {
$action = strtolower($action);
$ext = strtolower(pathinfo($srcFilename, PATHINFO_EXTENSION));
$useTransparency = true;
$memFactor = 1;
$img = null;
if(empty($dstFilename)) $dstFilename = $srcFilename;
if($action == 'rotate') $memFactor *= 2;
if(!$this->hasEnoughMemory($srcFilename, true, $memFactor, $action, false)) return false;
if($ext == 'jpg' || $ext == 'jpeg') {
$img = imagecreatefromjpeg($srcFilename);
$useTransparency = false;
} else if($ext == 'png') {
$img = imagecreatefrompng($srcFilename);
} else if($ext == 'gif') {
$img = imagecreatefromgif($srcFilename);
}
if(!$img) {
$this->error("imagecreatefrom$ext failed", Notice::debug);
return false;
}
if($useTransparency) {
imagealphablending($img, true);
imagesavealpha($img, true);
}
$success = true;
$method = '_processAction' . ucfirst($action);
$imgNew = $this->$method($img, $value);
if($imgNew === false) {
// action fail
$success = false;
$this->error($this->className() . ".$method(img, $value) returned fail", Notice::debug);
} else if($imgNew !== $img) {
// a new img object was created
imagedestroy($img);
$img = $imgNew;
if($useTransparency) {
imagealphablending($img, true);
imagesavealpha($img, true);
}
} else {
// existing img object was updated
$img = $imgNew;
}
if($success) {
if($ext == 'png') {
$success = imagepng($img, $dstFilename, 9);
} else if($ext == 'gif') {
$success = imagegif($img, $dstFilename);
} else {
$success = imagejpeg($img, $dstFilename, $this->quality);
}
if(!$success) $this->error("image{$ext}() failed", Notice::debug);
}
imagedestroy($img);
return $success;
}
/**
* Process flip action (internal)
*
* @param resource $img
* @param string $flipType vertical, horizontal or both
* @return bool|resource
*
*/
private function _processActionFlip(&$img, $flipType) {
if(!function_exists('imageflip')) {
$this->error("Image flip requires PHP 5.5 or newer");
return false;
}
if(!in_array($flipType, array('vertical', 'horizontal', 'both'))) {
$this->error("Image flip type must be one of: 'vertical', 'horizontal', 'both'");
return false;
}
$constantName = 'IMG_FLIP_' . strtoupper($flipType);
$flipType = constant($constantName);
if($flipType === null) {
$this->error("Unknown constant for image flip: $constantName");
return false;
}
$success = imageflip($img, $flipType);
return $success ? $img : false;
}
/**
* Process rotate action (internal)
*
* @param resource $img
* @param $degrees
* @return bool|resource
*
*/
private function _processActionRotate(&$img, $degrees) {
$degrees = (int) $degrees;
$angle = 360 - $degrees; // imagerotate is anti-clockwise
$imgNew = imagerotate($img, $angle, 0);
return $imgNew ? $imgNew : false;
}
private function _processActionGreyscale(&$img, $unused) {
if($unused) {}
imagefilter($img, IMG_FILTER_GRAYSCALE);
return $img;
}
private function _processActionSepia(&$img, $sepia = 55) {
imagefilter($img, IMG_FILTER_GRAYSCALE);
imagefilter($img, IMG_FILTER_BRIGHTNESS, -30);
imagefilter($img, IMG_FILTER_COLORIZE, 90, (int) $sepia, 30);
return $img;
}
/**
* Process rotate of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270
* @return bool
*
*/
protected function processRotate($srcFilename, $dstFilename, $degrees) {
return $this->processAction($srcFilename, $dstFilename, 'rotate', $degrees);
}
/**
* Process vertical or horizontal flip of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param string $flipType Specify vertical, horizontal, or both
* @return bool
*
*/
protected function processFlip($srcFilename, $dstFilename, $flipType) {
return $this->processAction($srcFilename, $dstFilename, 'flip', $flipType);
}
/**
* Convert image to greyscale
*
* @param string $dstFilename If different from source file
* @return bool
*
*/
public function convertToGreyscale($dstFilename = '') {
return $this->processAction($this->filename, $dstFilename, 'greyscale', null);
}
/**
* Convert image to sepia
*
* @param string $dstFilename If different from source file
* @param float|int $sepia Sepia value
* @return bool
*
*/
public function convertToSepia($dstFilename = '', $sepia = 55) {
return $this->processAction($this->filename, $dstFilename, 'sepia', $sepia);
}
}

View File

@@ -47,6 +47,7 @@
* @property Page $page Returns the Page that contains this set of files, same as the getPage() method. #pw-group-other
* @property Field $field Returns the Field that contains this set of files, same as the getField() method. #pw-group-other
* @method Pagefiles delete() delete(Pagefile $file) Removes the file and deletes from disk when page is saved. #pw-group-manipulation
* @method Pagefile|bool clone(Pagefile $item, array $options = array()) Duplicate a file and return it. #pw-group-manipulation
*
*/
@@ -84,6 +85,14 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*/
protected $renameQueue = array();
/**
* Items to be made non-temp upon page save (like duplicated files)
*
* @var array
*
*/
protected $unTempQueue = array();
/**
* IDs of any hooks added in this instance, used by the destructor
*
@@ -342,22 +351,33 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
*
*/
public function hookPageSave() {
if($this->page && $this->field && !$this->page->isChanged($this->field->name)) return $this;
foreach($this->unTempQueue as $item) {
$item->isTemp(false);
}
foreach($this->unlinkQueue as $item) {
$item->unlink();
}
foreach($this->renameQueue as $item) {
$name = $item->get('_rename');
if(!$name) continue;
$item->rename($name);
}
$this->unTempQueue = array();
$this->unlinkQueue = array();
$this->renameQueue = array();
$this->removeHooks();
return $this;
}
protected function addSaveHook() {
if(!count($this->unlinkQueue) && !count($this->renameQueue)) {
if(!count($this->unlinkQueue) && !count($this->renameQueue) && !count($this->unTempQueue)) {
$this->hookIDs[] = $this->page->filesManager->addHookBefore('save', $this, 'hookPageSave');
}
}
@@ -440,6 +460,64 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return $this;
}
/**
* Duplicate the Pagefile and add to this Pagefiles instance
*
* After duplicating a file, you must follow up with a save of the page containing it.
* Otherwise the file is marked for deletion.
*
* @param Pagefile $item Pagefile item to duplicate
* @param array $options Options to modify default behavior:
* - `action` (string): Specify "append", "prepend", "after", "before" or blank to only return Pagefile. (default="after")
* - `pagefiles` (Pagefiles): Pagefiles instance file should be duplicated to. (default=$this)
* @return Pagefile|bool Returns new Pagefile or boolean false on fail
*
*/
public function ___clone(Pagefile $item, array $options = array()) {
$defaults = array(
'action' => 'after',
'pagefiles' => $this,
);
$options = array_merge($defaults, $options);
/** @var Pagefiles $pagefiles */
$pagefiles = $options['pagefiles'];
$itemCopy = false;
$path = $pagefiles->path();
$parts = explode('.', $item->basename(), 2);
$n = $path === $this->path() ? 1 : 0;
if($n && preg_match('/^(.+?)-(\d+)$/', $parts[0], $matches)) {
$parts[0] = $matches[1];
$n = (int) $matches[2];
}
do {
$pathname = $n ? ($path . $parts[0] . "-$n." . $parts[1]) : ($path . $item->basename);
} while(file_exists($pathname) && $n++);
if(copy($item->filename(), $pathname)) {
$this->wire('files')->chmod($pathname);
$itemCopy = clone $item;
$itemCopy->setPagefilesParent($pagefiles);
$itemCopy->setFilename($pathname);
$itemCopy->isTemp(true);
switch($options['action']) {
case 'append': $pagefiles->append($itemCopy); break;
case 'prepend': $pagefiles->prepend($itemCopy); break;
case 'before': $pagefiles->insertBefore($itemCopy, $item); break;
case 'after': $pagefiles->insertAfter($itemCopy, $item); break;
}
$pagefiles->unTempQueue($itemCopy);
}
return $itemCopy;
}
/**
* Return the full disk path where files are stored
*
@@ -757,6 +835,19 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return count($removed);
}
/**
* Add Pagefile as item to have temporary status removed when Page is saved
*
* #pw-internal
*
* @param Pagefile $pagefile
*
*/
public function unTempQueue(Pagefile $pagefile) {
$this->addSaveHook();
$this->unTempQueue[] = $pagefile;
}
/**
* Is the given Pagefiles identical to this one?
*

View File

@@ -11,7 +11,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
public static function getModuleInfo() {
return array(
'title' => 'IMagick Image Sizer',
'version' => 1,
'version' => 2,
'summary' => "Upgrades image manipulations to use PHP's ImageMagick library when possible.",
'author' => 'Horst Nogajski',
'autoload' => false,
@@ -377,7 +377,139 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
$this->modified = true;
return true;
}
/**
* Process rotate of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param int $degrees Clockwise degrees, i.e. 90, 180, 270, -90, -180, -270
* @return bool
*
*/
protected function processRotate($srcFilename, $dstFilename, $degrees) {
$success = false;
$imagick = $this->getImagick($srcFilename);
if($imagick->rotateImage(new \ImagickPixel('#00000000'), $degrees)) {
$success = $this->processSave($imagick, $dstFilename);
}
return $success;
}
/**
* Process vertical or horizontal flip of an image
*
* @param string $srcFilename
* @param string $dstFilename
* @param string $flipType Specify vertical, horizontal or both
* @return bool
*
*/
protected function processFlip($srcFilename, $dstFilename, $flipType) {
$imagick = $this->getImagick($srcFilename);
if($flipType == 'vertical') {
$success = $imagick->flipImage();
} else if($flipType == 'horizontal') {
$success = $imagick->flopImage();
} else {
$success = $imagick->flipImage() && $imagick->flopImage();
}
if($success) $success = $this->processSave($imagick, $dstFilename);
return $success;
}
/**
* Reduce dimensions of image by half (using Imagick minifyImage method)
*
* @param string $dstFilename If different from filename specified by setFilename()
* @return bool
*
*/
public function reduceByHalf($dstFilename = '') {
$imagick = $this->getImagick($this->filename);
$success = $imagick->minifyImage();
if($success) $success = $this->processSave($imagick, $dstFilename);
return $success;
}
/**
* Convert image to greyscale
*
* @param string $dstFilename
* @return bool
*
*/
public function convertToGreyscale($dstFilename = '') {
$imagick = $this->getImagick($this->filename);
$success = $imagick->transformImageColorspace(\imagick::COLORSPACE_GRAY);
if($success) $success = $this->processSave($imagick, $dstFilename);
return $success;
}
/**
* Convert image to sepia
*
* @param string $dstFilename
* @param float|int $sepia Sepia threshold
* @return bool
*
*/
public function convertToSepia($dstFilename = '', $sepia = 55) {
$sepia += 35;
$imagick = $this->getImagick($this->filename);
$success = $imagick->sepiaToneImage((float) $sepia);
if($success) $success = $this->processSave($imagick, $dstFilename);
return $success;
}
/**
* Save action image to file
*
* @param \IMagick $imagick
* @param string $dstFilename
* @return bool
*
*/
protected function processSave(\IMagick $imagick, $dstFilename) {
if(empty($dstFilename)) $dstFilename = $this->filename;
$ext = strtolower(pathinfo($dstFilename, PATHINFO_EXTENSION));
if(in_array($ext, array('jpg', 'jpeg'))) {
if($this->interlace) {
$imagick->setInterlaceScheme(\Imagick::INTERLACE_JPEG);
}
}
$imagick->setImageCompressionQuality($this->quality);
$fp = fopen($dstFilename, 'wb');
if($fp === false) return false;
$success = $imagick->writeImageFile($fp);
fclose($fp);
return $success;
}
/**
* Get instance of Imagick
*
* @param string $filename Optional filename to read
* @return \Imagick
* @throws WireException
*
*/
public function getImagick($filename = '') {
$imagick = new \Imagick();
if($filename) {
if(!$imagick->readImage($filename)) {
throw new WireException("Imagick unable to load file: " . basename($filename));
}
}
return $imagick;
}
/**
* Sharpen the image
*

View File

@@ -533,4 +533,6 @@
text-align: center;
display: block; }
/*# sourceMappingURL=InputfieldImage.css.map */
.InputfieldFileActionNote {
display: none;
white-space: nowrap; }

View File

@@ -125,7 +125,7 @@ function InputfieldImage($) {
});
$el.removeClass('InputfieldImageSorting');
},
cancel: ".InputfieldImageEdit"
cancel: ".InputfieldImageEdit,input,textarea,button,select,option"
};
$el.sortable(sortableOptions);
@@ -232,8 +232,9 @@ function InputfieldImage($) {
function checkInputfieldWidth($inputfield) {
var narrowItems = [];
var mediumItems = [];
var wideItems = [];
var ni = 0, wi = 0;
var ni = 0, mi = 0, wi = 0;
var $inputfields;
if(typeof $inputfield == "undefined") {
@@ -242,7 +243,7 @@ function InputfieldImage($) {
$inputfields = $inputfield;
}
$inputfields.removeClass('InputfieldImageNarrow');
$inputfields.removeClass('InputfieldImageNarrow InputfieldImageMedium InputfieldImageWide');
$inputfields.each(function() {
var $item = $(this);
@@ -251,6 +252,12 @@ function InputfieldImage($) {
if(width <= 500) {
narrowItems[ni] = $item;
ni++;
} else if(width <= 900) {
mediumItems[mi] = $item;
mi++;
} else {
wideItems[wi] = $item;
wi++;
}
});
@@ -258,6 +265,14 @@ function InputfieldImage($) {
var $item = narrowItems[n];
$item.addClass('InputfieldImageNarrow');
}
for(var n = 0; n < mi; n++) {
var $item = mediumItems[n];
$item.addClass('InputfieldImageMedium');
}
for(var n = 0; n < wi; n++) {
var $item = wideItems[n];
$item.addClass('InputfieldImageWide');
}
}
/**
@@ -1015,6 +1030,15 @@ function InputfieldImage($) {
}
checkInputfieldWidth($inputfield);
$inputfield.on('change', '.InputfieldFileActionSelect', function() {
var $note = $(this).next('.InputfieldFileActionNote');
if($(this).val().length) {
$note.fadeIn();
} else {
$note.hide();
}
});
}
/*** UPLOAD **********************************************************************************/

File diff suppressed because one or more lines are too long

View File

@@ -40,6 +40,8 @@
* @method string renderButtons(Pageimage $pagefile, $id, $n)
* @method string renderAdditionalFields(Pageimage $pagefile, $id, $n)
* @method array buildTooltipData(Pageimage $pagefile)
* @method array getFileActions(Pagefile $pagefile)
* @method bool|null processUnknownFileAction(Pageimage $pagefile, $action, $label)
*
*
*/
@@ -50,7 +52,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
return array(
'title' => __('Images', __FILE__), // Module Title
'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
'version' => 120,
'version' => 121,
'permanent' => true,
);
}
@@ -136,6 +138,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
// 'error' => "<span class='ui-state-error-text'>{out}</span>", // provided by InputfieldFile
'buttonClass' => "ui-button ui-corner-all ui-state-default",
'buttonText' => "<span class='ui-button-text'>{out}</span>",
'selectClass' => '',
);
$themeSettings = $this->wire('config')->InputfieldImage;
$themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults;
@@ -563,7 +566,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
* Get Pagefile to pull description and tags from
*
* @param Pagefile $pagefile
* @return Pageimage
* @return Pageimage|Pagefile
*
*/
protected function getMetaPagefile(Pagefile $pagefile) {
@@ -613,6 +616,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$metaPagefile = $this->getMetaPagefile($pagefile);
$description = $this->renderItemDescriptionField($metaPagefile, $id, $n);
$additional = $this->renderAdditionalFields($metaPagefile, $id, $n);
$actions = $this->renderFileActionSelect($metaPagefile, $id);
$error = '';
if($thumb['error']) {
$error = str_replace('{out}', $sanitizer->entities($thumb['error']), $this->themeSettings['error']);
@@ -641,7 +645,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
<h2 class='InputfieldImageEdit__name'><span contenteditable='true'>$basename</span>.$ext</h2>
<span class='InputfieldImageEdit__info'>$fileStats</span>
<div class='InputfieldImageEdit__errors'>$error</div>
<div class='InputfieldImageEdit__buttons'><small>$buttons</small></div>
<div class='InputfieldImageEdit__buttons'><small>$buttons</small> $actions</div>
<div class='InputfieldImageEdit__core'>$description</div>
<div class='InputfieldImageEdit__additional'>$additional</div>
<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />
@@ -657,6 +661,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
/**
* Render a Pageimage item
*
* @deprecated No longer used by core. Left for a little while longer in case any extending module uses it.
* @param Pagefile|Pageimage $pagefile
* @param string $id
* @param int $n
@@ -749,10 +754,112 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$buttonText = "<span class='fa fa-files-o'></span> $labels[variations] <span class='ui-priority-secondary'>($variationCount)</span>";
$buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']);
$out .= "<button type='button' data-href='$variationUrl' class='$buttonClass' data-buttons='button'>$buttonText</button>";
return $out;
}
/**
* Render an image action select for given Pageimage
*
* @param Pagefile $pagefile
* @param string $id
* @return string
*
*/
protected function renderFileActionSelect(Pagefile $pagefile, $id) {
static $hooked = null;
if($hooked === null) $hooked =
$this->wire('hooks')->isHooked('InputfieldImage::getFileActions()') ||
$this->wire('hooks')->isHooked('InputfieldFile::getFileActions()');
$actions = $hooked ? $this->getFileActions($pagefile) : $this->___getFileActions($pagefile);
if(empty($actions)) return '';
$selectClass = trim($this->themeSettings['selectClass'] . ' InputfieldFileActionSelect');
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
$out =
"<select class='$selectClass' name='act_$id'>" .
"<option value=''>" . $this->_('Actions') . "</option>";
foreach($actions as $name => $label) {
$out .= "<option value='$name'>" . $sanitizer->entities($label) . "</option>";
}
$out .= "</select> ";
$out .= "<span class='InputfieldFileActionNote detail'>" . $this->_('Action applied at save.') . "</span>";
return $out;
}
/**
* Get array of actions available for given Pagefile
*
* @param Pagefile $pagefile
* @return array Associative array of ('action_name' => 'Action Label')
*
*/
public function ___getFileActions(Pagefile $pagefile) {
static $labels = null;
static $hasIMagick = null;
if($hasIMagick === null) {
$hasIMagick = $this->wire('modules')->isInstalled('ImageSizerEngineIMagick');
}
if($labels === null) $labels = array(
'flip' => $this->_('Flip'),
'rotate' => $this->_('Rotate'),
'dup' => $this->_('Duplicate'),
'rmv' => $this->_('Remove variations'),
'rbv' => $this->_('Rebuild variations'),
'vertical' => $this->_('vert'),
'horizontal' => $this->_('horiz'),
'both' => $this->_('both'),
'cop' => $this->_('Copy'),
'pas' => $this->_('Paste'),
'x50' => $this->_('Reduce 50%'),
'bw' => $this->_('B&W'), // Black and White
'sep' => $this->_('Sepia'),
);
$actions = array(
'dup' => $labels['dup'],
);
if($this->maxFiles && count($pagefile->pagefiles) >= $this->maxFiles) {
unset($actions['dup']);
}
if($pagefile->ext() != 'svg') {
// $actions['rmv'] = $labels['rmv'];
// $actions['rbv'] = $labels['rbv'];
$actions['fv'] = "$labels[flip] $labels[vertical]";
$actions['fh'] = "$labels[flip] $labels[horizontal]";
$actions['fb'] = "$labels[flip] $labels[both]";
foreach(array(90, 180, 270, -90, -180, -270) as $degrees) {
$actions["r$degrees"] = "$labels[rotate] {$degrees}°";
}
if($hasIMagick) {
$actions['x50'] = $labels['x50'];
}
$actions['bw'] = $labels['bw'];
$actions['sep'] = $labels['sep'];
}
return $actions;
}
/**
* Render non-editable value
*
@@ -783,6 +890,18 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
*/
protected function ___renderAdditionalFields($pagefile, $id, $n) { }
/*
protected function ___renderClipboard() {
$clipboard = $this->wire('session')->getFor('Pagefiles', 'clipboard');
if(!is_array($clipboard)) return '';
foreach($clipboard as $key) {
list($type, $pageID, $fieldName, $file) = explode(':', $key);
$page = $this->wire('pages')->get((int) $pageID);
$field = $this->wire('fields')->get($fieldName);
}
}
*/
/**
* Template method: allow items to be collapsed? Override default from InputfieldFile
*
@@ -1104,14 +1223,140 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
return $this->adminThumbScale > 0 && ((float) $this->adminThumbScale) != 1.0;
}
/**
* Process input
*
* @param WireInputData $input
* @return $this
*
*/
public function ___processInput(WireInputData $input) {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
parent::___processInput($input);
if((int) $this->wire('input')->post("_refresh_thumbnails_$this->name")) {
foreach($this->value as $img) {
$this->getAdminThumb($img, false, true);
}
$this->message($this->_('Recreated all legacy thumbnails') . " - $this->name");
}
if(!$this->isAjax) {
// process actions, but only on non-ajax save requests
foreach($this->value as $k => $pagefile) {
$id = $this->pagefileId($pagefile);
$action = $sanitizer->alphanumeric($input->{"act_$id"});
if(empty($action)) continue;
$actions = $this->getFileActions($pagefile);
if(!isset($actions[$action])) continue; // action not available for this file
$success = $this->processFileAction($pagefile, $action, $actions[$action]);
if($success === null) {
// action was not handled
}
}
}
return $this;
}
/**
* Process an action on a Pagefile/Pageimage
*
* @param Pageimage $pagefile Image file to process
* @param string $action Action to execute
* @param string $label Label that was provided to describe action
* @return bool|null Returns true on success, false on fail, or null if action was not handled or recognized
*
*/
protected function processFileAction(Pageimage $pagefile, $action, $label) {
$success = null;
$showSuccess = true;
if($action == 'dup') {
// duplicate image file
$_pagefile = $pagefile->pagefiles->clone($pagefile);
$success = $_pagefile ? true : false;
if($success) {
$this->wire('session')->message(
sprintf($this->_('Duplicated file %1$s => %2$s'), $pagefile->basename(), $_pagefile->basename())
);
$showSuccess = false;
}
} else if($action == 'cop') {
// copy to another page and/or field
/*
$key = 'cop:' . $pagefile->page->id . ':' . $pagefile->field->name . ':' . $pagefile->basename();
$clipboard = $this->wire('session')->getFor('Pagefiles', 'clipboard');
if(!is_array($clipboard)) $clipboard = array();
if(!in_array($key, $clipboard)) $clipboard[] = $key;
$this->wire('session')->setFor('Pagefiles', 'clipboard', $clipboard);
*/
} else if($action == 'rbv') {
// rebuild variations
} else if($action == 'rmv') {
// remove variations
} else {
/** @var ImageSizer $sizer Image sizer actions */
$sizer = $this->wire(new ImageSizer($pagefile->filename()));
$rebuildVariations = true;
if($action == 'fv') {
$success = $sizer->flipVertical();
} else if($action == 'fh') {
$success = $sizer->flipHorizontal();
} else if($action == 'fb') {
$success = $sizer->flipBoth();
} else if($action == 'bw') {
$success = $sizer->convertToGreyscale();
} else if($action == 'sep') {
$success = $sizer->convertToSepia();
} else if($action == 'x50') {
/** @var ImageSizerEngineIMagick $engine */
$engine = $sizer->getEngine();
if(method_exists($engine, 'reduceByHalf')) {
$success = $engine->reduceByHalf($pagefile->filename());
$rebuildVariations = false;
}
} else if(strpos($action, 'r') === 0 && preg_match('/^r(-?\d+)$/', $action, $matches)) {
$deg = (int) $matches[1];
$success = $sizer->rotate($deg);
}
if($success && $rebuildVariations) $pagefile->rebuildVariations();
}
if($success === null) {
// for hooks
$success = $this->processUnknownFileAction($pagefile, $action, $label);
}
if($success && $showSuccess) {
$this->message(sprintf($this->_('Executed action %1$s” on file %2$s'), $label, $pagefile->basename));
} else if($success === false) {
$this->error(sprintf($this->_('Failed action %1$s” on file %2$s'), $label, $pagefile->basename));
} else if($success === null) {
$this->error(sprintf($this->_('No handler found for action %1$s” on file %2$s'), $label, $pagefile->basename));
}
return $success;
}
/**
* Called when an action was received that InputfieldImage does not recognize (for hooking purposes)
*
* @param Pageimage $pagefile Image file to process
* @param string $action Action to execute
* @param string $label Label that was provided to describe action
* @return bool|null Returns true on success, false on fail, or null if action was not handled or recognized
*
*/
protected function ___processUnknownFileAction(Pageimage $pagefile, $action, $label) {
if($pagefile && $action && $label) {}
return null;
}
}

View File

@@ -845,3 +845,8 @@ $itemPadding: 0.4em;
} // .gridImage
} // .InputfieldImageNarrow
} // .InputfieldImageEditAll
.InputfieldFileActionNote {
display: none;
white-space: nowrap;
}