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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
*
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -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?
|
||||
*
|
||||
|
@@ -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
|
||||
*
|
||||
|
@@ -533,4 +533,6 @@
|
||||
text-align: center;
|
||||
display: block; }
|
||||
|
||||
/*# sourceMappingURL=InputfieldImage.css.map */
|
||||
.InputfieldFileActionNote {
|
||||
display: none;
|
||||
white-space: nowrap; }
|
||||
|
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -845,3 +845,8 @@ $itemPadding: 0.4em;
|
||||
} // .gridImage
|
||||
} // .InputfieldImageNarrow
|
||||
} // .InputfieldImageEditAll
|
||||
|
||||
.InputfieldFileActionNote {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
Reference in New Issue
Block a user