1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 08:17:12 +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()) { public function __construct($filename = '', $options = array()) {
if(!empty($options)) $this->setOptions($options);
if(isset($options['forceEngine'])) { if(!empty($filename)) $this->setFilename($filename);
$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);
}
} }
/** /**
@@ -158,6 +146,7 @@ class ImageSizer extends Wire {
if(empty($inspectionResult) && $filename && is_readable($filename)) { if(empty($inspectionResult) && $filename && is_readable($filename)) {
$imageInspector = new ImageInspector($filename); $imageInspector = new ImageInspector($filename);
$this->wire($imageInspector);
$inspectionResult = $imageInspector->inspect($filename, true); $inspectionResult = $imageInspector->inspect($filename, true);
$this->inspectionResult = $inspectionResult; $this->inspectionResult = $inspectionResult;
} }
@@ -226,17 +215,8 @@ class ImageSizer extends Wire {
*/ */
public function ___resize($targetWidth, $targetHeight = 0) { public function ___resize($targetWidth, $targetHeight = 0) {
if(empty($this->filename)) throw new WireException('No file to resize: please call setFilename($file) before resize()'); $engine = $this->getEngine();
$success = $engine->resize($targetWidth, $targetHeight);
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);
if(!$success) { if(!$success) {
// fallback to GD // fallback to GD
@@ -297,8 +277,12 @@ class ImageSizer extends Wire {
* *
*/ */
public function setOptions(array $options) { public function setOptions(array $options) {
if(isset($options['forceEngine'])) {
$this->setForceEngine($options['forceEngine']);
unset($options['forceEngine']);
}
$this->initialOptions = array_merge($this->initialOptions, $options); $this->initialOptions = array_merge($this->initialOptions, $options);
if($this->engine) $this->engine->setOptions($options); if($this->engine) $this->engine->setOptions($this->initialOptions);
return $this; return $this;
} }
@@ -329,16 +313,48 @@ class ImageSizer extends Wire {
public function setUpscaling($value = true) { return $this->setOptions(array('upscaling', $value)); } public function setUpscaling($value = true) { return $this->setOptions(array('upscaling', $value)); }
public function setUseUSM($value = true) { return $this->setOptions(array('useUSM', $value)); } public function setUseUSM($value = true) { return $this->setOptions(array('useUSM', $value)); }
// getters (@todo phpdocs) public function getWidth() {
public function getWidth() { return $this->engine->image['width']; } $image = $this->getEngine()->get('image');
public function getHeight() { return $this->engine->image['height']; } return $image['width'];
public function getFilename() { return $this->engine->filename; } }
public function getExtension() { return $this->engine->extension; } public function getHeight() {
public function getImageType() { return $this->engine->imageType; } $image = $this->getEngine()->get('image');
public function isModified() { return $this->engine->modified; } return $image['height'];
public function getOptions() { return $this->engine->getOptions(); } }
public function getEngine() { return $this->engine; }
public function __get($key) { return $this->engine->__get($key); } 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 * ImageInformation from Image Inspector in short form or full RawInfoData
@@ -349,6 +365,7 @@ class ImageSizer extends Wire {
*/ */
public function getImageInfo($rawData = false) { public function getImageInfo($rawData = false) {
$this->getEngine();
if($rawData) return $this->inspectionResult; if($rawData) return $this->inspectionResult;
$imageType = $this->inspectionResult['info']['imageType']; $imageType = $this->inspectionResult['info']['imageType'];
$type = ''; $type = '';
@@ -507,7 +524,9 @@ class ImageSizer extends Wire {
* *
*/ */
static public function imageResetIPTC($image) { static public function imageResetIPTC($image) {
$wire = null;
if($image instanceof Pageimage) { if($image instanceof Pageimage) {
$wire = $image;
$filename = $image->filename; $filename = $image->filename;
} else if(is_readable($image)) { } else if(is_readable($image)) {
$filename = $image; $filename = $image;
@@ -515,8 +534,72 @@ class ImageSizer extends Wire {
return null; return null;
} }
$sizer = new ImageSizerEngineGD($filename); $sizer = new ImageSizerEngineGD($filename);
if($wire) $wire->wire($sizer);
$result = false !== $sizer->writeBackIPTC($filename) ? true : false; $result = false !== $sizer->writeBackIPTC($filename) ? true : false;
return $result; 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); 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 * Get array of image file extensions this ImageSizerModule can process
* *
@@ -1469,6 +1499,118 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable
return true; 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 * 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 // 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: // 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) { if($this->imageType == \IMAGETYPE_PNG || $this->imageType == \IMAGETYPE_GIF) {
$result = @copy($srcFilename, $dstFilename); $result = @copy($srcFilename, $dstFilename);
if(isset($image) && is_resource($image)) @imagedestroy($image); // clean up if(isset($image) && is_resource($image)) @imagedestroy($image); // clean up
if(isset($image)) $image = null; if(isset($image)) $image = null;
return $result; // early return ! return $result; // early return !
} }
*/
// process JPEGs // process JPEGs
if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) { if(self::checkMemoryForImage(array(imagesx($image), imagesy($image), 3)) === false) {
@@ -316,7 +320,6 @@ class ImageSizerEngineGD extends ImageSizerEngine {
return $result; return $result;
} }
/** /**
* Rotate image (@horst) * Rotate image (@horst)
* *
@@ -330,7 +333,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
$degree = (is_float($degree) || is_int($degree)) && $degree > -361 && $degree < 361 ? $degree : false; $degree = (is_float($degree) || is_int($degree)) && $degree > -361 && $degree < 361 ? $degree : false;
if($degree === false) return $im; if($degree === false) return $im;
if(in_array($degree, array(-360, 0, 360))) 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 $sourceDimensions - array with three values: width, height, number of channels
* @param array|bool $targetDimensions - optional - mixed: bool true | false or array with three values: * @param array|bool $targetDimensions - optional - mixed: bool true | false or array with three values:
* width, height, number of channels * 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 * @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, // 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 // regardless how often this function is called in a request
@@ -772,10 +777,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
} }
// calculate $sourceDimensions // calculate $sourceDimensions
if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1]) if(!isset($sourceDimensions[0]) || !isset($sourceDimensions[1]) || !isset($sourceDimensions[2]) ||
|| !isset($sourceDimensions[2]) || !is_int($sourceDimensions[0]) !is_int($sourceDimensions[0]) || !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])) {
|| !is_int($sourceDimensions[1]) || !is_int($sourceDimensions[2])
) {
return null; return null;
} }
@@ -788,10 +791,8 @@ class ImageSizerEngineGD extends ImageSizerEngine {
} else if(is_array($targetDimensions)) { } else if(is_array($targetDimensions)) {
// we have to add ram for a targetimage // we have to add ram for a targetimage
if(!isset($targetDimensions[0]) || !isset($targetDimensions[1]) if(!isset($targetDimensions[0]) || !isset($targetDimensions[1]) || !isset($targetDimensions[2]) ||
|| !isset($targetDimensions[2]) || !is_int($targetDimensions[0]) !is_int($targetDimensions[0]) || !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])) {
|| !is_int($targetDimensions[1]) || !is_int($targetDimensions[2])
) {
return null; 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 $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 // 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 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 * @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 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(); 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 * 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() { public function hookPageSave() {
if($this->page && $this->field && !$this->page->isChanged($this->field->name)) return $this; 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) { foreach($this->unlinkQueue as $item) {
$item->unlink(); $item->unlink();
} }
foreach($this->renameQueue as $item) { foreach($this->renameQueue as $item) {
$name = $item->get('_rename'); $name = $item->get('_rename');
if(!$name) continue; if(!$name) continue;
$item->rename($name); $item->rename($name);
} }
$this->unTempQueue = array();
$this->unlinkQueue = array(); $this->unlinkQueue = array();
$this->renameQueue = array();
$this->removeHooks(); $this->removeHooks();
return $this; return $this;
} }
protected function addSaveHook() { 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'); $this->hookIDs[] = $this->page->filesManager->addHookBefore('save', $this, 'hookPageSave');
} }
} }
@@ -440,6 +460,64 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return $this; 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 * Return the full disk path where files are stored
* *
@@ -757,6 +835,19 @@ class Pagefiles extends WireArray implements PageFieldValueInterface {
return count($removed); 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? * Is the given Pagefiles identical to this one?
* *

View File

@@ -11,7 +11,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
public static function getModuleInfo() { public static function getModuleInfo() {
return array( return array(
'title' => 'IMagick Image Sizer', 'title' => 'IMagick Image Sizer',
'version' => 1, 'version' => 2,
'summary' => "Upgrades image manipulations to use PHP's ImageMagick library when possible.", 'summary' => "Upgrades image manipulations to use PHP's ImageMagick library when possible.",
'author' => 'Horst Nogajski', 'author' => 'Horst Nogajski',
'autoload' => false, 'autoload' => false,
@@ -378,6 +378,138 @@ class ImageSizerEngineIMagick extends ImageSizerEngine {
return 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 * Sharpen the image
* *

View File

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

View File

@@ -125,7 +125,7 @@ function InputfieldImage($) {
}); });
$el.removeClass('InputfieldImageSorting'); $el.removeClass('InputfieldImageSorting');
}, },
cancel: ".InputfieldImageEdit" cancel: ".InputfieldImageEdit,input,textarea,button,select,option"
}; };
$el.sortable(sortableOptions); $el.sortable(sortableOptions);
@@ -232,8 +232,9 @@ function InputfieldImage($) {
function checkInputfieldWidth($inputfield) { function checkInputfieldWidth($inputfield) {
var narrowItems = []; var narrowItems = [];
var mediumItems = [];
var wideItems = []; var wideItems = [];
var ni = 0, wi = 0; var ni = 0, mi = 0, wi = 0;
var $inputfields; var $inputfields;
if(typeof $inputfield == "undefined") { if(typeof $inputfield == "undefined") {
@@ -242,7 +243,7 @@ function InputfieldImage($) {
$inputfields = $inputfield; $inputfields = $inputfield;
} }
$inputfields.removeClass('InputfieldImageNarrow'); $inputfields.removeClass('InputfieldImageNarrow InputfieldImageMedium InputfieldImageWide');
$inputfields.each(function() { $inputfields.each(function() {
var $item = $(this); var $item = $(this);
@@ -251,6 +252,12 @@ function InputfieldImage($) {
if(width <= 500) { if(width <= 500) {
narrowItems[ni] = $item; narrowItems[ni] = $item;
ni++; ni++;
} else if(width <= 900) {
mediumItems[mi] = $item;
mi++;
} else {
wideItems[wi] = $item;
wi++;
} }
}); });
@@ -258,6 +265,14 @@ function InputfieldImage($) {
var $item = narrowItems[n]; var $item = narrowItems[n];
$item.addClass('InputfieldImageNarrow'); $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); checkInputfieldWidth($inputfield);
$inputfield.on('change', '.InputfieldFileActionSelect', function() {
var $note = $(this).next('.InputfieldFileActionNote');
if($(this).val().length) {
$note.fadeIn();
} else {
$note.hide();
}
});
} }
/*** UPLOAD **********************************************************************************/ /*** 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 renderButtons(Pageimage $pagefile, $id, $n)
* @method string renderAdditionalFields(Pageimage $pagefile, $id, $n) * @method string renderAdditionalFields(Pageimage $pagefile, $id, $n)
* @method array buildTooltipData(Pageimage $pagefile) * @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( return array(
'title' => __('Images', __FILE__), // Module Title 'title' => __('Images', __FILE__), // Module Title
'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary 'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
'version' => 120, 'version' => 121,
'permanent' => true, '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 // 'error' => "<span class='ui-state-error-text'>{out}</span>", // provided by InputfieldFile
'buttonClass' => "ui-button ui-corner-all ui-state-default", 'buttonClass' => "ui-button ui-corner-all ui-state-default",
'buttonText' => "<span class='ui-button-text'>{out}</span>", 'buttonText' => "<span class='ui-button-text'>{out}</span>",
'selectClass' => '',
); );
$themeSettings = $this->wire('config')->InputfieldImage; $themeSettings = $this->wire('config')->InputfieldImage;
$themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults; $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 * Get Pagefile to pull description and tags from
* *
* @param Pagefile $pagefile * @param Pagefile $pagefile
* @return Pageimage * @return Pageimage|Pagefile
* *
*/ */
protected function getMetaPagefile(Pagefile $pagefile) { protected function getMetaPagefile(Pagefile $pagefile) {
@@ -613,6 +616,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$metaPagefile = $this->getMetaPagefile($pagefile); $metaPagefile = $this->getMetaPagefile($pagefile);
$description = $this->renderItemDescriptionField($metaPagefile, $id, $n); $description = $this->renderItemDescriptionField($metaPagefile, $id, $n);
$additional = $this->renderAdditionalFields($metaPagefile, $id, $n); $additional = $this->renderAdditionalFields($metaPagefile, $id, $n);
$actions = $this->renderFileActionSelect($metaPagefile, $id);
$error = ''; $error = '';
if($thumb['error']) { if($thumb['error']) {
$error = str_replace('{out}', $sanitizer->entities($thumb['error']), $this->themeSettings['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> <h2 class='InputfieldImageEdit__name'><span contenteditable='true'>$basename</span>.$ext</h2>
<span class='InputfieldImageEdit__info'>$fileStats</span> <span class='InputfieldImageEdit__info'>$fileStats</span>
<div class='InputfieldImageEdit__errors'>$error</div> <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__core'>$description</div>
<div class='InputfieldImageEdit__additional'>$additional</div> <div class='InputfieldImageEdit__additional'>$additional</div>
<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' /> <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 * 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 Pagefile|Pageimage $pagefile
* @param string $id * @param string $id
* @param int $n * @param int $n
@@ -753,6 +758,108 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
return $out; 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 * Render non-editable value
* *
@@ -783,6 +890,18 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
*/ */
protected function ___renderAdditionalFields($pagefile, $id, $n) { } 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 * 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; return $this->adminThumbScale > 0 && ((float) $this->adminThumbScale) != 1.0;
} }
/**
* Process input
*
* @param WireInputData $input
* @return $this
*
*/
public function ___processInput(WireInputData $input) { public function ___processInput(WireInputData $input) {
/** @var Sanitizer $sanitizer */
$sanitizer = $this->wire('sanitizer');
parent::___processInput($input); parent::___processInput($input);
if((int) $this->wire('input')->post("_refresh_thumbnails_$this->name")) { if((int) $this->wire('input')->post("_refresh_thumbnails_$this->name")) {
foreach($this->value as $img) { foreach($this->value as $img) {
$this->getAdminThumb($img, false, true); $this->getAdminThumb($img, false, true);
} }
$this->message($this->_('Recreated all legacy thumbnails') . " - $this->name"); $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 } // .gridImage
} // .InputfieldImageNarrow } // .InputfieldImageNarrow
} // .InputfieldImageEditAll } // .InputfieldImageEditAll
.InputfieldFileActionNote {
display: none;
white-space: nowrap;
}