diff --git a/wire/core/ImageSizer.php b/wire/core/ImageSizer.php index 9b7e423a..f556281a 100755 --- a/wire/core/ImageSizer.php +++ b/wire/core/ImageSizer.php @@ -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); + } + } diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index c17553c6..1bd721c0 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -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 * diff --git a/wire/core/ImageSizerEngineGD.php b/wire/core/ImageSizerEngineGD.php index e67a19be..0ca4b4db 100755 --- a/wire/core/ImageSizerEngineGD.php +++ b/wire/core/ImageSizerEngineGD.php @@ -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); + } + + } diff --git a/wire/core/Pagefiles.php b/wire/core/Pagefiles.php index 91de533c..9bedcf42 100644 --- a/wire/core/Pagefiles.php +++ b/wire/core/Pagefiles.php @@ -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? * diff --git a/wire/modules/ImageSizerEngineIMagick.module b/wire/modules/ImageSizerEngineIMagick.module index 3dc389d7..904b91ea 100755 --- a/wire/modules/ImageSizerEngineIMagick.module +++ b/wire/modules/ImageSizerEngineIMagick.module @@ -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 * diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css index e4c3ed86..25a7e894 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.css @@ -533,4 +533,6 @@ text-align: center; display: block; } -/*# sourceMappingURL=InputfieldImage.css.map */ +.InputfieldFileActionNote { + display: none; + white-space: nowrap; } diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js index 9720233e..6757d664 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js @@ -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 **********************************************************************************/ diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js index 22648368..1e7f7e4e 100644 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js @@ -1 +1 @@ -function InputfieldImage(v){var k=null;var b={file:"",item:null,edit:null};var F={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var r=[];function s(){var M=window.File&&window.FileList&&window.FileReader;var L=v(".InputfieldAllowAjaxUpload").length>0;var N=v("#PageIDIndicator").length>0;return(M&&(N||L))}function y(N,L,M){L||(L=250);var O,P;return function(){var S=M||this;var R=+new Date(),Q=arguments;if(O&&R .gridImage",start:function(Q,P){var O=E(M.closest(".Inputfield"),"size");P.placeholder.append(v("
").css({display:"block",height:O+"px",width:O+"px"}));N=window.setTimeout(function(){G(M,null)},100);M.addClass("InputfieldImageSorting")},stop:function(Q,O){var P=v(this);if(N!==null){O.item.find(".InputfieldImageEdit__edit").click();clearTimeout(N)}P.children("li").each(function(S){var R=v(this).find(".InputfieldFileSort");if(R.val()!=S){R.val(S).change()}});M.removeClass("InputfieldImageSorting")},cancel:".InputfieldImageEdit"};M.sortable(L)}function p(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){var O=v(N.el).attr("data-original");if(typeof O=="undefined"||!O){O=v(N.el).attr("src")}N.src=O}};L.gallery={enabled:true};M.find("img").magnificPopup(L)}function t(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){N.src=v(N.el).attr("src")}};L.gallery={enabled:false};M.find("img").magnificPopup(L)}function C(L){return L.find(".InputfieldImageEdit--active")}function u(L){return v("#"+L.find(".InputfieldImageEdit__edit").attr("data-current"))}function D(N){var L=N.is(":checked");var M=N.parents(".gridImages").find(".gridImage__deletebox");if(L){M.prop("checked","checked").change()}else{M.removeAttr("checked").change()}}function J(M){if(typeof M=="undefined"){var L=v(".gridImages")}else{var L=M.find(".gridImages")}L.each(function(){var N=v(this),O=C(N);if(O.length){i(u(O),O)}})}function w(R){var N=[];var Q=[];var P=0,L=0;var O;if(typeof R=="undefined"){O=v(".InputfieldImage.Inputfield")}else{O=R}O.removeClass("InputfieldImageNarrow");O.each(function(){var T=v(this);var U=T.width();if(U<1){return}if(U<=500){N[P]=T;P++}});for(var S=0;S=Q){O.css("max-height","100%").css("max-width","none");O.attr("height",M).removeAttr("width")}else{if(Q>L){O.css("max-height","none").css("max-width","100%");O.attr("width",M).removeAttr("height")}else{O.css("max-height","100%").css("max-width","none");O.removeAttr("width").attr("height",M)}}}var L=O.width();if(L){N.css({width:(P?L+"px":M+"px"),height:M+"px"})}else{var R=N.attr("data-tries");if(!R){R=0}if(typeof R=="undefined"){R=0}R=parseInt(R);if(R>3){N.css({width:M+"px",height:M+"px"})}else{r.push(N);N.attr("data-tries",R+1)}}}function A(M){if(M.find(".InputfieldImageListToggle").length){return}var P=v("").append("");var R=v("").append("");var L=v("").append("");var Q="InputfieldImageListToggle--active";var O="";var N=function(W){var V=v(this);var U=V.closest(".Inputfield");var S=V.attr("href");var T;V.parent().children("."+Q).removeClass(Q);V.addClass(Q);if(S=="list"){if(!U.hasClass("InputfieldImageEditAll")){U.find(".InputfieldImageEdit--active .InputfieldImageEdit__close").click();U.addClass("InputfieldImageEditAll")}T=E(U,"listSize");l(U,T);e(U,"mode","list")}else{if(S=="left"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,true);e(U,"mode","left");J()}else{if(S=="grid"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,false);e(U,"mode","grid")}}}B(U.find(".gridImages"));V.blur();return false};P.click(N);R.click(N);L.click(N);if(M.hasClass("InputfieldImage")){M.find(".InputfieldHeader").append(P).append(R).append(L);O=E(M,"mode")}else{v(".InputfieldImage .InputfieldHeader",M).append(P).append(R).append(L)}if(O=="list"){P.click()}else{if(O=="left"){R.click()}else{}}}function z(Q){var N=Q.children(".InputfieldHeader");if(N.children(".InputfieldImageSizeSlider").length){return}var P=Q.find(".gridImages");var M=P.attr("data-gridsize");var O=M/2;var L=M*2;var R=v('');N.append(R);R.slider({min:O,max:L,value:E(Q,"size"),range:"min",slide:function(U,W){var V=W.value;var X=15;var Y=Math.floor(M/X);var S=V-O;var T=Math.floor(X+(S/Y));if(Q.hasClass("InputfieldImageEditAll")){e(Q,"size",V);l(Q,T)}else{e(Q,"listSize",T);j(Q,V)}},start:function(S,T){if(Q.find(".InputfieldImageEdit:visible").length){Q.find(".InputfieldImageEdit__close").click()}},stop:function(S,T){J(Q)}})}function e(M,P,O){var N=E(M);var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"";if(!L.length||typeof O=="undefined"){return}if(N[L][P]==O){return}N[L][P]=O;v.cookie("InputfieldImage",N);c=N}function E(M,P){if(c&&typeof P=="undefined"){return c}var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"na";var O=c?c:v.cookie("InputfieldImage");var N=null;if(!O){var O={}}if(typeof O[L]=="undefined"){O[L]={}}if(typeof O[L].size=="undefined"){O[L].size=parseInt(M.find(".gridImages").attr("data-size"))}if(typeof O[L].listSize=="undefined"){O[L].listSize=23}if(typeof O[L].mode=="undefined"){O[L].mode=M.find(".gridImages").attr("data-gridMode")}if(c==null){c=O}if(typeof P=="undefined"){N=O}else{if(P===true){N=O[L]}else{if(typeof O[L][P]!="undefined"){N=O[L][P]}}}return N}function a(P){if(P.hasClass("InputfieldStateCollapsed")){return}var Q=parseInt(P.find(".InputfieldImageMaxFiles").val());var O=P.find(".gridImages");var N=E(P,"size");var R=E(P,"mode");var M=R=="left"?true:false;if(!N){N=O.attr("data-gridsize")}N=parseInt(N);if(P.hasClass("InputfieldImageEditAll")||R=="list"){var L=E(P,"listSize");l(P,L)}else{j(P,N,M)}if(!P.hasClass("InputfieldImageInit")){P.addClass("InputfieldImageInit");if(P.hasClass("InputfieldRenderValueMode")){return p(P)}else{if(Q==1){P.addClass("InputfieldImageMax1");t(P)}else{B(O)}}A(P);z(P)}w(P)}function I(){v("body").addClass("ie-no-drop");v(".InputfieldImage.InputfieldFileMultiple").each(function(){var M=v(this),O=parseInt(M.find(".InputfieldFileMaxFiles").val()),L=M.find(".gridImages"),N=M.find(".InputfieldImageUpload");N.on("change","input[type=file]",function(){var S=v(this),Q=S.parent(".InputMask");if(S.val().length>1){Q.addClass("ui-state-disabled")}else{Q.removeClass("ui-state-disabled")}if(S.next("input.InputfieldFile").length>0){return}var P=L.children("li").length+N.find("input[type=file]").length+1;if(O>0&&P>=O){return}N.find(".InputMask").not(":last").each(function(){var T=v(this);if(T.find("input[type=file]").val()<1){T.remove()}});var R=Q.clone().removeClass("ui-state-disabled");R.children("input[type=file]").val("");R.insertAfter(Q)})})}function K(N){var M;if(N.length>0){M=N.find(".InputfieldImageUpload")}else{M=v(".InputfieldImageUpload")}M.each(function(Q){var R=v(this);var P=R.closest(".InputfieldContent");if(R.hasClass("InputfieldImageInitUpload")){return}O(P,Q);R.addClass("InputfieldImageInitUpload")});function O(Y,ak){var X=Y.parents("form");var P=Y.closest(".InputfieldRepeaterItem");var T=P.length?P.attr("data-editUrl"):X.attr("action");T+=(T.indexOf("?")>-1?"&":"?")+"InputfieldFileAjax=1";var ap=X.find("input._post_token");var W=ap.attr("name");var ab=ap.val();var aa=Y.find(".InputfieldImageErrors").first();var S=Y.find(".InputfieldImageUpload").data("fieldname");S=S.slice(0,-2);var ai=Y.closest(".Inputfield.InputfieldImage");var ao=Y.find(".InputfieldImageUpload").data("extensions").toLowerCase();var ah=Y.find(".InputfieldImageUpload").data("maxfilesize");var Z=Y.find("input[type=file]").get(0);var R=Y.find(".gridImages");var al=R.get(0);var ad=R.data("gridsize");var ae=null;var ac=parseInt(Y.find(".InputfieldImageMaxFiles").val());var an=n(ai);var aj=an.maxWidth>0||an.maxHeight>0||an.maxSize>0;am(Y);if(ac!=1){ag(R)}R.children().addClass("InputfieldFileItemExisting");ai.on("pwimageupload",function(aq,ar){af([ar.file],ar.xhr)});function V(ar,aq){if(typeof aq!=="undefined"){ar=""+aq+": "+ar}return"
  • "+ar+"
  • "}function Q(ar){var aq=new String(ar).substring(ar.lastIndexOf("/")+1);if(aq.lastIndexOf(".")!=-1){aq=aq.substring(0,aq.lastIndexOf("."))}return aq}function am(ar){if(ar.hasClass("InputfieldImageDropzoneInit")){return}var av=ar.get(0);var au=ar.closest(".Inputfield");function aq(){if(au.hasClass("pw-drag-in-file")){return}ar.addClass("ui-state-hover");au.addClass("pw-drag-in-file")}function at(){if(!au.hasClass("pw-drag-in-file")){return}ar.removeClass("ui-state-hover");au.removeClass("pw-drag-in-file")}av.addEventListener("dragleave",function(){at()},false);av.addEventListener("dragenter",function(aw){aw.preventDefault();aq()},false);av.addEventListener("dragover",function(aw){if(!ar.is("ui-state-hover")){aq()}aw.preventDefault();aw.stopPropagation();return false},false);av.addEventListener("drop",function(aw){af(aw.dataTransfer.files);at();aw.preventDefault();aw.stopPropagation();return false},false);ar.addClass("InputfieldImageDropzoneInit")}function ag(az){var aD=null;var aB=false;var ar=null;var aq=az.closest(".Inputfield");function aw(){aq.addClass("pw-drag-in-file")}function aC(){aq.removeClass("pw-drag-in-file")}function av(aF){var aJ=aF.offset();var aG=aF.width();var aE=aF.height();var aI=aJ.left+aG/2;var aH=aJ.top+aE/2;return{clientX:aI,clientY:aH}}function ay(){return az.find(".InputfieldImageEdit--active").length>0}function ax(aF){if(ay()){return}aF.preventDefault();aF.stopPropagation();aw();aB=false;if(aD==null){var aE=az.attr("data-size")+"px";var aG=v("
    ").addClass("gridImage__overflow");if(az.closest(".InputfieldImageEditAll").length){aG.css({width:"100%",height:aE})}else{aG.css({width:aE,height:aE})}aD=v("
  • ").addClass("ImageOuter gridImage gridImagePlaceholder").append(aG);az.append(aD)}var aH=av(aD);aD.simulate("mousedown",aH)}function aA(aE){if(ay()){return}aE.preventDefault();aE.stopPropagation();aw();aB=false;if(aD==null){return}var aF={clientX:aE.originalEvent.clientX,clientY:aE.originalEvent.clientY};aD.simulate("mousemove",aF)}function au(aE){if(ay()){return}aE.preventDefault();aE.stopPropagation();if(aD==null){return false}aB=true;if(ar){clearTimeout(ar)}ar=setTimeout(function(){if(!aB||aD==null){return}aD.remove();aD=null;aC()},1000)}function at(aE){if(ay()){return}aC();aB=false;var aF={clientX:aE.clientX,clientY:aE.clientY};aD.simulate("mouseup",aF);k=aD.next(".gridImage");aD.remove();aD=null}if(az.length&&!az.hasClass("gridImagesInitDropInPlace")){az.on("dragenter",ax);az.on("dragover",aA);az.on("dragleave",au);az.on("drop",at);az.addClass("gridImagesInitDropInPlace")}}function U(aP,aD,az){var aM=ProcessWire.config.InputfieldImage.labels;var ax=parseInt(aP.size/1024,10)+" kB";var aO='
    '+aM.dimensions+''+aM.na+"
    "+aM.filesize+""+ax+"
    "+aM.variations+"0
    ";var aR=v('
  • '),aJ=v(aO),ay=v('
    '),aq=v('
    '),aG=v("
    "),aI=v(""),aL=v(' '),aK=v('
    '),ar,aB,aQ,aE=URL.createObjectURL(aP),at=ai.find(".gridImages"),av=ac==1,aH=E(ai,"size"),aw=E(ai,"listSize"),au=ai.hasClass("InputfieldImageEditAll"),aA=v('');ay.append(aA);aG.find(".gridImage__inner").append(aL);aG.find(".gridImage__inner").append(aK.css("display","none"));aG.find(".gridImage__inner").append(aI);aq.append(v('

    '+aP.name+'

    '+ax+""));if(au){ay.css("width",aw+"%");aq.css("width",(100-aw)+"%")}else{ay.css({width:aH+"px",height:aH+"px"})}aR.append(aJ).append(ay).append(aG).append(aq);aA.attr({src:aE,"data-original":aE});img=new Image();img.addEventListener("load",function(){aJ.find(".dimensions").html(this.width+" × "+this.height);var aS=Math.min(this.width,this.height)/aH;aA.attr({width:this.width/aS,height:this.height/aS})},false);img.src=aE;if(typeof az!="undefined"){aB=az}else{aB=new XMLHttpRequest()}function aC(aS){if(typeof aS!="undefined"){if(!aS.lengthComputable){return}aI.attr("value",parseInt((aS.loaded/aS.total)*100))}v("body").addClass("pw-uploading");aK.css("display","block")}aB.upload.addEventListener("progress",aC,false);aB.addEventListener("load",function(){aB.getAllResponseHeaders();var aV=v.parseJSON(aB.responseText);if(typeof aV.ajaxResponse!="undefined"){aV=aV.ajaxResponse}var aT=aV.length>1;if(aV.error!==undefined){aV=[aV]}for(var aU=0;aU-1){aY=aY.substring(0,aY.indexOf("?"))}var aW=aY.substring(aY.lastIndexOf(".")+1).toLowerCase();aY=aY.substring(0,aY.lastIndexOf("."));if(aW==a2){a3.children("span").text(aY).removeAttr("contenteditable")}aX.find(".gridImage__edit").click()}b.file="";b.item=null;b.edit=null}if(ae){clearTimeout(ae)}k=null;ae=setTimeout(function(){if(ac!=1){B(at)}else{t(ai)}v("body").removeClass("pw-uploading");at.trigger("AjaxUploadDone")},500);ai.trigger("change").removeClass("InputfieldFileEmpty")},false);if(b.edit){b.edit.find(".InputfieldImageEdit__close").click()}else{if(ai.find(".InputfieldImageEdit:visible").length){ai.find(".InputfieldImageEdit__close").click()}}if(b.item){b.item.replaceWith(aR);b.item=aR}else{if(k&&k.length){k.before(aR)}else{at.append(aR)}}function aN(aS,aU){if(typeof az=="undefined"){aB.open("POST",T,true)}aB.setRequestHeader("X-FILENAME",encodeURIComponent(aS.name));aB.setRequestHeader("X-FIELDNAME",S);if(b.item){aB.setRequestHeader("X-REPLACENAME",b.file)}aB.setRequestHeader("Content-Type","application/octet-stream");aB.setRequestHeader("X-"+W,ab);aB.setRequestHeader("X-REQUESTED-WITH","XMLHttpRequest");if(typeof aU!="undefined"&&aU!=false){aB.send(aU)}else{aB.send(aS)}J();ai.trigger("change");var aT=ai.find(".InputfieldFileItem").length;if(aT==1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileMultiple").addClass("InputfieldFileSingle")}else{if(aT>1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileSingle").addClass("InputfieldFileMultiple")}}}aC();if(aj){var aF=new PWImageResizer(an);aK.addClass("pw-resizing");aF.resize(aP,function(aS){aK.removeClass("pw-resizing");aN(aP,aS)})}else{aN(aP)}}function af(aq,ay){var aw=function(aA){return parseInt(aA/1024,10)};if(typeof aq==="undefined"){al.innerHTML="No support for the File API in this web browser";return}for(var au=0,at=aq.length;auah&&ah>2000000){var ar=aw(aq[au].size),av=aw(ah);az="Filesize "+ar+" kb is too big. Maximum allowed is "+av+" kb";aa.append(V(az,aq[au].name))}else{if(typeof ay!="undefined"){U(aq[au],ax,ay)}else{U(aq[au],ax)}}}if(ac==1){break}}}Z.addEventListener("change",function(aq){af(this.files);aq.preventDefault();aq.stopPropagation();this.value=""},false)}function L(){var P=".InputfieldImageEdit__imagewrapper img";v(document).on("dragenter",P,function(){var S=v(this);if(S.closest(".InputfieldImageMax1").length){return}var T=S.attr("src");var Q=S.closest(".InputfieldImageEdit");var R=S.closest(".InputfieldImageEdit__imagewrapper");R.addClass("InputfieldImageEdit__replace");b.file=new String(T).substring(T.lastIndexOf("/")+1);b.item=v("#"+Q.attr("data-for"));b.edit=Q}).on("dragleave",P,function(){var R=v(this);if(R.closest(".InputfieldImageMax1").length){return}var Q=R.closest(".InputfieldImageEdit__imagewrapper");Q.removeClass("InputfieldImageEdit__replace");b.file="";b.item=null;b.edit=null})}L()}function n(M){var L={maxWidth:0,maxHeight:0,maxSize:0,quality:1,autoRotate:true,debug:ProcessWire.config.debug};var N=M.attr("data-resize");if(typeof N!="undefined"&&N.length){N=N.split(";");L.maxWidth=parseInt(N[0]);L.maxHeight=parseInt(N[1]);L.maxSize=parseFloat(N[2]);L.quality=parseFloat(N[3])}return L}function H(){v(".InputfieldImage.Inputfield").each(function(){a(v(this))});x();if(s()){K("")}else{I()}v(document).on("reloaded",".InputfieldImage",function(){var L=v(this);a(L);K(L)}).on("wiretabclick",function(N,M,L){M.find(".InputfieldImage").each(function(){a(v(this))})}).on("opened",".InputfieldImage",function(){a(v(this))})}H()}jQuery(document).ready(function(a){InputfieldImage(a)}); \ No newline at end of file +function InputfieldImage(v){var k=null;var b={file:"",item:null,edit:null};var F={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var r=[];function s(){var M=window.File&&window.FileList&&window.FileReader;var L=v(".InputfieldAllowAjaxUpload").length>0;var N=v("#PageIDIndicator").length>0;return(M&&(N||L))}function y(N,L,M){L||(L=250);var O,P;return function(){var S=M||this;var R=+new Date(),Q=arguments;if(O&&R .gridImage",start:function(Q,P){var O=E(M.closest(".Inputfield"),"size");P.placeholder.append(v("
    ").css({display:"block",height:O+"px",width:O+"px"}));N=window.setTimeout(function(){G(M,null)},100);M.addClass("InputfieldImageSorting")},stop:function(Q,O){var P=v(this);if(N!==null){O.item.find(".InputfieldImageEdit__edit").click();clearTimeout(N)}P.children("li").each(function(S){var R=v(this).find(".InputfieldFileSort");if(R.val()!=S){R.val(S).change()}});M.removeClass("InputfieldImageSorting")},cancel:".InputfieldImageEdit,input,textarea,button,select,option"};M.sortable(L)}function p(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){var O=v(N.el).attr("data-original");if(typeof O=="undefined"||!O){O=v(N.el).attr("src")}N.src=O}};L.gallery={enabled:true};M.find("img").magnificPopup(L)}function t(M){var L=v.extend(true,{},F);L.callbacks={elementParse:function(N){N.src=v(N.el).attr("src")}};L.gallery={enabled:false};M.find("img").magnificPopup(L)}function C(L){return L.find(".InputfieldImageEdit--active")}function u(L){return v("#"+L.find(".InputfieldImageEdit__edit").attr("data-current"))}function D(N){var L=N.is(":checked");var M=N.parents(".gridImages").find(".gridImage__deletebox");if(L){M.prop("checked","checked").change()}else{M.removeAttr("checked").change()}}function J(M){if(typeof M=="undefined"){var L=v(".gridImages")}else{var L=M.find(".gridImages")}L.each(function(){var N=v(this),O=C(N);if(O.length){i(u(O),O)}})}function w(L){var O=[];var T=[];var Q=[];var N=0,S=0,M=0;var R;if(typeof L=="undefined"){R=v(".InputfieldImage.Inputfield")}else{R=L}R.removeClass("InputfieldImageNarrow InputfieldImageMedium InputfieldImageWide");R.each(function(){var V=v(this);var W=V.width();if(W<1){return}if(W<=500){O[N]=V;N++}else{if(W<=900){T[S]=V;S++}else{Q[M]=V;M++}}});for(var P=0;P=Q){O.css("max-height","100%").css("max-width","none");O.attr("height",M).removeAttr("width")}else{if(Q>L){O.css("max-height","none").css("max-width","100%");O.attr("width",M).removeAttr("height")}else{O.css("max-height","100%").css("max-width","none");O.removeAttr("width").attr("height",M)}}}var L=O.width();if(L){N.css({width:(P?L+"px":M+"px"),height:M+"px"})}else{var R=N.attr("data-tries");if(!R){R=0}if(typeof R=="undefined"){R=0}R=parseInt(R);if(R>3){N.css({width:M+"px",height:M+"px"})}else{r.push(N);N.attr("data-tries",R+1)}}}function A(M){if(M.find(".InputfieldImageListToggle").length){return}var P=v("").append("");var R=v("").append("");var L=v("").append("");var Q="InputfieldImageListToggle--active";var O="";var N=function(W){var V=v(this);var U=V.closest(".Inputfield");var S=V.attr("href");var T;V.parent().children("."+Q).removeClass(Q);V.addClass(Q);if(S=="list"){if(!U.hasClass("InputfieldImageEditAll")){U.find(".InputfieldImageEdit--active .InputfieldImageEdit__close").click();U.addClass("InputfieldImageEditAll")}T=E(U,"listSize");l(U,T);e(U,"mode","list")}else{if(S=="left"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,true);e(U,"mode","left");J()}else{if(S=="grid"){U.removeClass("InputfieldImageEditAll");T=E(U,"size");j(U,T,false);e(U,"mode","grid")}}}B(U.find(".gridImages"));V.blur();return false};P.click(N);R.click(N);L.click(N);if(M.hasClass("InputfieldImage")){M.find(".InputfieldHeader").append(P).append(R).append(L);O=E(M,"mode")}else{v(".InputfieldImage .InputfieldHeader",M).append(P).append(R).append(L)}if(O=="list"){P.click()}else{if(O=="left"){R.click()}else{}}}function z(Q){var N=Q.children(".InputfieldHeader");if(N.children(".InputfieldImageSizeSlider").length){return}var P=Q.find(".gridImages");var M=P.attr("data-gridsize");var O=M/2;var L=M*2;var R=v('');N.append(R);R.slider({min:O,max:L,value:E(Q,"size"),range:"min",slide:function(U,W){var V=W.value;var X=15;var Y=Math.floor(M/X);var S=V-O;var T=Math.floor(X+(S/Y));if(Q.hasClass("InputfieldImageEditAll")){e(Q,"size",V);l(Q,T)}else{e(Q,"listSize",T);j(Q,V)}},start:function(S,T){if(Q.find(".InputfieldImageEdit:visible").length){Q.find(".InputfieldImageEdit__close").click()}},stop:function(S,T){J(Q)}})}function e(M,P,O){var N=E(M);var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"";if(!L.length||typeof O=="undefined"){return}if(N[L][P]==O){return}N[L][P]=O;v.cookie("InputfieldImage",N);c=N}function E(M,P){if(c&&typeof P=="undefined"){return c}var Q=M.attr("id");var L=Q?Q.replace("wrap_Inputfield_",""):"na";var O=c?c:v.cookie("InputfieldImage");var N=null;if(!O){var O={}}if(typeof O[L]=="undefined"){O[L]={}}if(typeof O[L].size=="undefined"){O[L].size=parseInt(M.find(".gridImages").attr("data-size"))}if(typeof O[L].listSize=="undefined"){O[L].listSize=23}if(typeof O[L].mode=="undefined"){O[L].mode=M.find(".gridImages").attr("data-gridMode")}if(c==null){c=O}if(typeof P=="undefined"){N=O}else{if(P===true){N=O[L]}else{if(typeof O[L][P]!="undefined"){N=O[L][P]}}}return N}function a(P){if(P.hasClass("InputfieldStateCollapsed")){return}var Q=parseInt(P.find(".InputfieldImageMaxFiles").val());var O=P.find(".gridImages");var N=E(P,"size");var R=E(P,"mode");var M=R=="left"?true:false;if(!N){N=O.attr("data-gridsize")}N=parseInt(N);if(P.hasClass("InputfieldImageEditAll")||R=="list"){var L=E(P,"listSize");l(P,L)}else{j(P,N,M)}if(!P.hasClass("InputfieldImageInit")){P.addClass("InputfieldImageInit");if(P.hasClass("InputfieldRenderValueMode")){return p(P)}else{if(Q==1){P.addClass("InputfieldImageMax1");t(P)}else{B(O)}}A(P);z(P)}w(P);P.on("change",".InputfieldFileActionSelect",function(){var S=v(this).next(".InputfieldFileActionNote");if(v(this).val().length){S.fadeIn()}else{S.hide()}})}function I(){v("body").addClass("ie-no-drop");v(".InputfieldImage.InputfieldFileMultiple").each(function(){var M=v(this),O=parseInt(M.find(".InputfieldFileMaxFiles").val()),L=M.find(".gridImages"),N=M.find(".InputfieldImageUpload");N.on("change","input[type=file]",function(){var S=v(this),Q=S.parent(".InputMask");if(S.val().length>1){Q.addClass("ui-state-disabled")}else{Q.removeClass("ui-state-disabled")}if(S.next("input.InputfieldFile").length>0){return}var P=L.children("li").length+N.find("input[type=file]").length+1;if(O>0&&P>=O){return}N.find(".InputMask").not(":last").each(function(){var T=v(this);if(T.find("input[type=file]").val()<1){T.remove()}});var R=Q.clone().removeClass("ui-state-disabled");R.children("input[type=file]").val("");R.insertAfter(Q)})})}function K(N){var M;if(N.length>0){M=N.find(".InputfieldImageUpload")}else{M=v(".InputfieldImageUpload")}M.each(function(Q){var R=v(this);var P=R.closest(".InputfieldContent");if(R.hasClass("InputfieldImageInitUpload")){return}O(P,Q);R.addClass("InputfieldImageInitUpload")});function O(Y,ak){var X=Y.parents("form");var P=Y.closest(".InputfieldRepeaterItem");var T=P.length?P.attr("data-editUrl"):X.attr("action");T+=(T.indexOf("?")>-1?"&":"?")+"InputfieldFileAjax=1";var ap=X.find("input._post_token");var W=ap.attr("name");var ab=ap.val();var aa=Y.find(".InputfieldImageErrors").first();var S=Y.find(".InputfieldImageUpload").data("fieldname");S=S.slice(0,-2);var ai=Y.closest(".Inputfield.InputfieldImage");var ao=Y.find(".InputfieldImageUpload").data("extensions").toLowerCase();var ah=Y.find(".InputfieldImageUpload").data("maxfilesize");var Z=Y.find("input[type=file]").get(0);var R=Y.find(".gridImages");var al=R.get(0);var ad=R.data("gridsize");var ae=null;var ac=parseInt(Y.find(".InputfieldImageMaxFiles").val());var an=n(ai);var aj=an.maxWidth>0||an.maxHeight>0||an.maxSize>0;am(Y);if(ac!=1){ag(R)}R.children().addClass("InputfieldFileItemExisting");ai.on("pwimageupload",function(aq,ar){af([ar.file],ar.xhr)});function V(ar,aq){if(typeof aq!=="undefined"){ar=""+aq+": "+ar}return"
  • "+ar+"
  • "}function Q(ar){var aq=new String(ar).substring(ar.lastIndexOf("/")+1);if(aq.lastIndexOf(".")!=-1){aq=aq.substring(0,aq.lastIndexOf("."))}return aq}function am(ar){if(ar.hasClass("InputfieldImageDropzoneInit")){return}var av=ar.get(0);var au=ar.closest(".Inputfield");function aq(){if(au.hasClass("pw-drag-in-file")){return}ar.addClass("ui-state-hover");au.addClass("pw-drag-in-file")}function at(){if(!au.hasClass("pw-drag-in-file")){return}ar.removeClass("ui-state-hover");au.removeClass("pw-drag-in-file")}av.addEventListener("dragleave",function(){at()},false);av.addEventListener("dragenter",function(aw){aw.preventDefault();aq()},false);av.addEventListener("dragover",function(aw){if(!ar.is("ui-state-hover")){aq()}aw.preventDefault();aw.stopPropagation();return false},false);av.addEventListener("drop",function(aw){af(aw.dataTransfer.files);at();aw.preventDefault();aw.stopPropagation();return false},false);ar.addClass("InputfieldImageDropzoneInit")}function ag(az){var aD=null;var aB=false;var ar=null;var aq=az.closest(".Inputfield");function aw(){aq.addClass("pw-drag-in-file")}function aC(){aq.removeClass("pw-drag-in-file")}function av(aF){var aJ=aF.offset();var aG=aF.width();var aE=aF.height();var aI=aJ.left+aG/2;var aH=aJ.top+aE/2;return{clientX:aI,clientY:aH}}function ay(){return az.find(".InputfieldImageEdit--active").length>0}function ax(aF){if(ay()){return}aF.preventDefault();aF.stopPropagation();aw();aB=false;if(aD==null){var aE=az.attr("data-size")+"px";var aG=v("
    ").addClass("gridImage__overflow");if(az.closest(".InputfieldImageEditAll").length){aG.css({width:"100%",height:aE})}else{aG.css({width:aE,height:aE})}aD=v("
  • ").addClass("ImageOuter gridImage gridImagePlaceholder").append(aG);az.append(aD)}var aH=av(aD);aD.simulate("mousedown",aH)}function aA(aE){if(ay()){return}aE.preventDefault();aE.stopPropagation();aw();aB=false;if(aD==null){return}var aF={clientX:aE.originalEvent.clientX,clientY:aE.originalEvent.clientY};aD.simulate("mousemove",aF)}function au(aE){if(ay()){return}aE.preventDefault();aE.stopPropagation();if(aD==null){return false}aB=true;if(ar){clearTimeout(ar)}ar=setTimeout(function(){if(!aB||aD==null){return}aD.remove();aD=null;aC()},1000)}function at(aE){if(ay()){return}aC();aB=false;var aF={clientX:aE.clientX,clientY:aE.clientY};aD.simulate("mouseup",aF);k=aD.next(".gridImage");aD.remove();aD=null}if(az.length&&!az.hasClass("gridImagesInitDropInPlace")){az.on("dragenter",ax);az.on("dragover",aA);az.on("dragleave",au);az.on("drop",at);az.addClass("gridImagesInitDropInPlace")}}function U(aP,aD,az){var aM=ProcessWire.config.InputfieldImage.labels;var ax=parseInt(aP.size/1024,10)+" kB";var aO='
    '+aM.dimensions+''+aM.na+"
    "+aM.filesize+""+ax+"
    "+aM.variations+"0
    ";var aR=v('
  • '),aJ=v(aO),ay=v('
    '),aq=v('
    '),aG=v("
    "),aI=v(""),aL=v(' '),aK=v('
    '),ar,aB,aQ,aE=URL.createObjectURL(aP),at=ai.find(".gridImages"),av=ac==1,aH=E(ai,"size"),aw=E(ai,"listSize"),au=ai.hasClass("InputfieldImageEditAll"),aA=v('');ay.append(aA);aG.find(".gridImage__inner").append(aL);aG.find(".gridImage__inner").append(aK.css("display","none"));aG.find(".gridImage__inner").append(aI);aq.append(v('

    '+aP.name+'

    '+ax+""));if(au){ay.css("width",aw+"%");aq.css("width",(100-aw)+"%")}else{ay.css({width:aH+"px",height:aH+"px"})}aR.append(aJ).append(ay).append(aG).append(aq);aA.attr({src:aE,"data-original":aE});img=new Image();img.addEventListener("load",function(){aJ.find(".dimensions").html(this.width+" × "+this.height);var aS=Math.min(this.width,this.height)/aH;aA.attr({width:this.width/aS,height:this.height/aS})},false);img.src=aE;if(typeof az!="undefined"){aB=az}else{aB=new XMLHttpRequest()}function aC(aS){if(typeof aS!="undefined"){if(!aS.lengthComputable){return}aI.attr("value",parseInt((aS.loaded/aS.total)*100))}v("body").addClass("pw-uploading");aK.css("display","block")}aB.upload.addEventListener("progress",aC,false);aB.addEventListener("load",function(){aB.getAllResponseHeaders();var aV=v.parseJSON(aB.responseText);if(typeof aV.ajaxResponse!="undefined"){aV=aV.ajaxResponse}var aT=aV.length>1;if(aV.error!==undefined){aV=[aV]}for(var aU=0;aU-1){aY=aY.substring(0,aY.indexOf("?"))}var aW=aY.substring(aY.lastIndexOf(".")+1).toLowerCase();aY=aY.substring(0,aY.lastIndexOf("."));if(aW==a2){a3.children("span").text(aY).removeAttr("contenteditable")}aX.find(".gridImage__edit").click()}b.file="";b.item=null;b.edit=null}if(ae){clearTimeout(ae)}k=null;ae=setTimeout(function(){if(ac!=1){B(at)}else{t(ai)}v("body").removeClass("pw-uploading");at.trigger("AjaxUploadDone")},500);ai.trigger("change").removeClass("InputfieldFileEmpty")},false);if(b.edit){b.edit.find(".InputfieldImageEdit__close").click()}else{if(ai.find(".InputfieldImageEdit:visible").length){ai.find(".InputfieldImageEdit__close").click()}}if(b.item){b.item.replaceWith(aR);b.item=aR}else{if(k&&k.length){k.before(aR)}else{at.append(aR)}}function aN(aS,aU){if(typeof az=="undefined"){aB.open("POST",T,true)}aB.setRequestHeader("X-FILENAME",encodeURIComponent(aS.name));aB.setRequestHeader("X-FIELDNAME",S);if(b.item){aB.setRequestHeader("X-REPLACENAME",b.file)}aB.setRequestHeader("Content-Type","application/octet-stream");aB.setRequestHeader("X-"+W,ab);aB.setRequestHeader("X-REQUESTED-WITH","XMLHttpRequest");if(typeof aU!="undefined"&&aU!=false){aB.send(aU)}else{aB.send(aS)}J();ai.trigger("change");var aT=ai.find(".InputfieldFileItem").length;if(aT==1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileMultiple").addClass("InputfieldFileSingle")}else{if(aT>1){ai.removeClass("InputfieldFileEmpty").removeClass("InputfieldFileSingle").addClass("InputfieldFileMultiple")}}}aC();if(aj){var aF=new PWImageResizer(an);aK.addClass("pw-resizing");aF.resize(aP,function(aS){aK.removeClass("pw-resizing");aN(aP,aS)})}else{aN(aP)}}function af(aq,ay){var aw=function(aA){return parseInt(aA/1024,10)};if(typeof aq==="undefined"){al.innerHTML="No support for the File API in this web browser";return}for(var au=0,at=aq.length;auah&&ah>2000000){var ar=aw(aq[au].size),av=aw(ah);az="Filesize "+ar+" kb is too big. Maximum allowed is "+av+" kb";aa.append(V(az,aq[au].name))}else{if(typeof ay!="undefined"){U(aq[au],ax,ay)}else{U(aq[au],ax)}}}if(ac==1){break}}}Z.addEventListener("change",function(aq){af(this.files);aq.preventDefault();aq.stopPropagation();this.value=""},false)}function L(){var P=".InputfieldImageEdit__imagewrapper img";v(document).on("dragenter",P,function(){var S=v(this);if(S.closest(".InputfieldImageMax1").length){return}var T=S.attr("src");var Q=S.closest(".InputfieldImageEdit");var R=S.closest(".InputfieldImageEdit__imagewrapper");R.addClass("InputfieldImageEdit__replace");b.file=new String(T).substring(T.lastIndexOf("/")+1);b.item=v("#"+Q.attr("data-for"));b.edit=Q}).on("dragleave",P,function(){var R=v(this);if(R.closest(".InputfieldImageMax1").length){return}var Q=R.closest(".InputfieldImageEdit__imagewrapper");Q.removeClass("InputfieldImageEdit__replace");b.file="";b.item=null;b.edit=null})}L()}function n(M){var L={maxWidth:0,maxHeight:0,maxSize:0,quality:1,autoRotate:true,debug:ProcessWire.config.debug};var N=M.attr("data-resize");if(typeof N!="undefined"&&N.length){N=N.split(";");L.maxWidth=parseInt(N[0]);L.maxHeight=parseInt(N[1]);L.maxSize=parseFloat(N[2]);L.quality=parseFloat(N[3])}return L}function H(){v(".InputfieldImage.Inputfield").each(function(){a(v(this))});x();if(s()){K("")}else{I()}v(document).on("reloaded",".InputfieldImage",function(){var L=v(this);a(L);K(L)}).on("wiretabclick",function(N,M,L){M.find(".InputfieldImage").each(function(){a(v(this))})}).on("opened",".InputfieldImage",function(){a(v(this))})}H()}jQuery(document).ready(function(a){InputfieldImage(a)}); \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module index 6f137387..2c2f82c0 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module @@ -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' => "{out}", // provided by InputfieldFile 'buttonClass' => "ui-button ui-corner-all ui-state-default", 'buttonText' => "{out}", + '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

    $basename.$ext

    $fileStats
    $error
    -
    $buttons
    +
    $buttons $actions
    $description
    $additional
    @@ -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 = " $labels[variations] ($variationCount)"; $buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']); $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 = + " "; + $out .= "" . $this->_('Action applied at save.') . ""; + + 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; } } diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss index 56023545..3876bdc1 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.scss @@ -845,3 +845,8 @@ $itemPadding: 0.4em; } // .gridImage } // .InputfieldImageNarrow } // .InputfieldImageEditAll + +.InputfieldFileActionNote { + display: none; + white-space: nowrap; +}