<?php namespace ProcessWire;

/**
 * Class InputfieldImage
 *
 * Inputfield for FieldtypeImage fields
 *
 * ProcessWire 3.x, Copyright 2024 by Ryan Cramer
 * https://processwire.com
 * 
 * Accessible Properties
 *
 * @property string $extensions Space separated list of allowed image extensions (default="JPG JPEG GIF PNG")
 * @property array $okExtensions Array of manually whitelisted extensions, for instance [ 'SVG' ] must be manually whitelisted if allowed. (default=[])
 * @property int|string $maxWidth Max width for uploaded images, larger will be sized down (default='')
 * @property int|string $maxHeight Max height for uploaded images, larger will be sized down (default='')
 * @property float $maxSize Maximum number of megapixels for client-side resize, i.e. 1.7 is ~1600x1000, alt. to maxWidth/maxHeight (default=0).
 * @property bool|int $maxReject Reject images that exceed max allowed size? (default=false)
 * @property int|string $minWidth Min width for uploaded images, smaller will be refused (default='')
 * @property int|string $minHeight Min height for uploaded images, smaller will be refused (default='')
 * @property bool|int $dimensionsByAspectRatio Switch min-/maxWidth and min-/maxHeight restriction for portrait images
 * @property string $itemClass Space separated CSS classes for items rendered by this Inputfield. Generally you should append rather than replace.
 * @property int|bool $useImageEditor Whether or not the modal image editor is allowed for this field (default=true)
 * @property int $adminThumbScale for backwards compatibility only
 * @property int|bool $resizeServer Resize to max width/height at server? 1=Server-only, 0=Use client-side resize when possible (default=0). 
 * @property int $clientQuality Quality setting to use for client-side resize. 60=60%, 90=90%, etc. (default=90). 
 * @property string $editFieldName Field name to use for linking to image editor (default=current Inputfield name)
 *
 * The following properties default values are pulled from $config->adminThumbOptions and can be overridden
 * by setting directly to an instance of this Inputfield:
 *
 * @property int $gridSize squared size of the admin thumbnails (default=130)
 * @property string $gridMode Default grid mode in admin, one of "grid", "left" or "list" (default="grid")
 * @property string $focusMode May be 'on', 'off', or 'zoom'
 * @property array $imageSizerOptions Options to pass along to the ImageSizer class. See /wire/config.php $imageSizerOptions for details.
 * 
 * 
 * Hookable Methods
 * 
 * @method string render()
 * @method string renderItem(Pageimage $pagefile, $id, $n)
 * @method string renderList(Pageimages $value)
 * @method string renderUpload(Pageimages $value)
 * @method string renderSingleItem(Pageimage $pagefile, $id, $n)
 * @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)
 * @method array getImageEditButtons($pagefile, $id, $n, $buttonClass) 3.0.212+
 * @method array getImageThumbnailActions($pagefile, $id, $n, $class) 3.0.212+
 *
 *
 */

class InputfieldImage extends InputfieldFile implements InputfieldItemList, InputfieldHasSortableValue {

	public static function getModuleInfo() {
		return array(
			'title' => __('Images', __FILE__), // Module Title
			'summary' => __('One or more image uploads (sortable)', __FILE__), // Module Summary
			'version' => 129,
			'permanent' => true,
		);
	}

	/**
	 * Default square grid item size
	 * 
	 */
	const defaultGridSize = 130;

	/**
	 * Force render value mode for dev/debug purposes
	 * 
	 */
	const debugRenderValue = false;

	/**
	 * Cached list of all image variations
	 *
	 * @var array
	 *
	 */
	protected $variations = array();
	
	/**
	 * Class used for modal editor windows
	 *
	 * @var string
	 *
	 */
	protected $modalClass = 'pw-modal-large';
	

	public function init() {
		parent::init();
		$config = $this->wire()->config;

		$this->set('extensions', 'JPG JPEG GIF PNG');
		$this->set('maxWidth', '');
		$this->set('maxHeight', '');
		$this->set('maxSize', 0.0);
		$this->set('maxReject', 0);
		$this->set('minWidth', '');
		$this->set('minHeight', '');
		$this->set('resizeServer', 0); // 0=allow client resize, 1=resize at server only
		$this->set('clientQuality', 90);
		$this->set('dimensionsByAspectRatio', 0);
		$this->set('itemClass', 'gridImage ui-widget');
		$this->set('editFieldName', ''); // field name to use for image editor (default=name of this inputfield)

		$options = $config->adminThumbOptions;
		if(!is_array($options)) $options = array();
		$gridSize = empty($options['gridSize']) ? self::defaultGridSize : (int) $options['gridSize'];
		if($gridSize < 100) $gridSize = self::defaultGridSize; // establish min of 100
		if($gridSize >= (self::defaultGridSize * 2)) $gridSize = self::defaultGridSize; // establish max of 259
		
		$this->set('gridSize', $gridSize); 
		$this->set('gridMode', 'grid'); // one of "grid", "left" or "list"
		$this->set('focusMode', 'on');  // One of "on", "zoom" or "off" 
		
		// adminThumbScale is no longer in use (here in case descending module using it)
		$this->set('adminThumbScale', empty($options['scale']) ? 1.0 : (float) $options['scale']);

		if(empty($options['imageSizerOptions'])) {
			// properties specified in $options rather than $options['imageSizerOptions'], so we copy them
			$options['imageSizerOptions'] = array();
			foreach($options as $key => $value) {
				if($key == 'height' || $key == 'width' || $key == 'scale' || $key == 'gridSize') continue;
				$options['imageSizerOptions'][$key] = $value;
			}
		}
		
		$this->set('imageSizerOptions', empty($options['imageSizerOptions']) ? array() : $options['imageSizerOptions']);
		$this->set('useImageEditor', 1);
		
		$this->labels = array_merge($this->labels, array(
			'crop' => $this->_('Crop'), 
			'focus' => $this->_('Focus'), 
			'variations' => $this->_('Variations'), 
			'dimensions' => $this->_('Dimensions'), 
			'filesize' => $this->_('Filesize'), 
			'edit' => $this->_('Edit'),
			'drag-drop-in' => $this->_('drag and drop in new images above'), 
			'na' => $this->_('N/A'), // for JS
			'changes' => $this->_('This images field may have unsaved changes that could be lost after this action. Please save before cropping, or double-click the button proceed anyway.'),
			'visibility' => $this->_('Visibility'), 
			'hidden' => $this->_('Hidden'), 
		));

		$themeDefaults = array(
			// 'error' => "<span class='ui-state-error-text'>{out}</span>", // provided by InputfieldFile
			'buttonClass' => "ui-button ui-corner-all ui-state-default",
			'buttonText' => "<span class='ui-button-text'>{out}</span>", 
			'selectClass' => '', 
		);
		$themeSettings = $config->InputfieldImage;
		$themeSettings = is_array($themeSettings) ? array_merge($themeDefaults, $themeSettings) : $themeDefaults;
		$this->themeSettings = array_merge($this->themeSettings, $themeSettings);
	}

	/**
	 * Get setting or attribute
	 * 
	 * @param string $key
	 * @return array|bool|mixed|string|null
	 * 
	 */
	public function get($key) {
		if($key == 'themeSettings') return $this->themeSettings;
		return parent::get($key);
	}

	/**
	 * Called right before Inputfield render
	 * 
	 * @param Inputfield|null $parent Parent Inputfield
	 * @param bool $renderValueMode Whether or not we are in renderValue mode
	 * @return bool
	 * 
	 */
	public function renderReady(?Inputfield $parent = null, $renderValueMode = false) {

		if(self::debugRenderValue) {
			// force render value mode for dev/debugging purposes
			$renderValueMode = true;
			$this->renderValueMode = true;
			$this->addClass('InputfieldRenderValueMode', 'wrapClass');
		}
	
		$config = $this->wire()->config;
		$modules = $this->wire()->modules;
		
		/** @var JqueryCore $jqueryCore */
		$jqueryCore = $modules->get('JqueryCore');
		$jqueryCore->use('simulate');
		$jqueryCore->use('cookie');
		$modules->loadModuleFileAssets('InputfieldFile');
		$modules->getInstall('JqueryMagnific');
		
		if(!$renderValueMode && $this->focusMode == 'zoom') {
			$this->addClass('InputfieldImageFocusZoom', 'wrapClass'); 
		}

		$settings = $config->get('InputfieldImage'); 
		if(!is_array($settings)) $settings = array();
		if(empty($settings['ready'])) {
			$settings['labels'] = $this->labels;
			$settings['ready'] = true;
			$config->js('InputfieldImage', $settings);
		}

		// client side image resize 
		if(!$this->resizeServer && ($this->maxWidth || $this->maxHeight || $this->maxSize)) {
			$moduleInfo = self::getModuleInfo();
			$thisURL = $config->urls('InputfieldImage');
			$jsExt = $config->debug ? "js" : "min.js";
			$config->scripts->add($thisURL . "piexif.$jsExt");
			$config->scripts->add($thisURL . "PWImageResizer.$jsExt?v={$config->version}-$moduleInfo[version]");
			$maxSize = str_replace(',', '.', $this->maxSize); 
			$quality = str_replace(',', '.', (float) ($this->clientQuality / 100));
			$maxWidth = $this->maxWidth ? (int) $this->maxWidth : 99999;
			$maxHeight = $this->maxHeight ? (int) $this->maxHeight : 99999; 
			$this->wrapAttr('data-resize', "$maxWidth;$maxHeight;$maxSize;$quality");
		}
		
		$value = $this->val();
		if(!$value instanceof Pageimages) $value = null;
		
		if(!$renderValueMode && $value) {
			$page = $this->getRootHasPage();
			if($page->id && $this->wire()->user->hasPermission('page-edit-images', $page)) {
				$jQueryUI = $modules->get('JqueryUI'); /** @var JqueryUI $jQueryUI */
				$jQueryUI->use('modal');
			} else {
				$this->useImageEditor = 0;
			}
		}
	
		if($value) $this->variations = $value->getAllVariations();
		
		return parent::renderReady($parent, $renderValueMode);
	}

	/**
	 * Render Inputfield
	 * 
	 * @return string
	 * 
	 */
	public function ___render() {
		if($this->isAjax) clearstatcache();
		$out = parent::___render();
		return $out;
	}
	
	/**
	 * Render list of images
	 * 
	 * @param Pageimages|array $value
	 * @return string
	 * @throws WireException
	 * 
	 */
	protected function ___renderList($value) {
		
		$out = '';
		$n = 0;
	
		$this->renderListReady($value);
		
		if(!$this->uploadOnlyMode && WireArray::iterable($value)) {
			
			foreach($value as $pagefile) {
				$id = $this->pagefileId($pagefile);
				$this->currentItem = $pagefile;

				$out .= $this->renderItemWrap($this->renderItem($pagefile, $id, $n++));
				/*
				if($this->maxFiles != 1) {
					$out .= $this->renderItemWrap($this->renderItem($pagefile, $id, $n++));
				} else {
					$out .= $this->renderSingleItem($pagefile, $id, $n++);
				}
				*/
			}

			if(!$this->renderValueMode) {
				$sanitizer = $this->wire()->sanitizer;
				$dropNew = $sanitizer->entities1($this->_('drop in new image file to replace')); 
				$focus = $sanitizer->entities1($this->_('drag circle to center of focus'));
				$out .= "
					<div class='InputfieldImageEdit'>
						<div class='InputfieldImageEdit__inner'>
							<div class='InputfieldImageEdit__arrow'></div>
							<div class='InputfieldImageEdit__close'><span class='fa fa-times'></span></div>
							<div class='InputfieldImageEdit__imagewrapper'>
								<div>
									<img class='InputfieldImageEdit__image' src='' alt=''>
									<small class='detail detail-upload'>$dropNew</small>
									<small class='detail detail-focus'>$focus</small>
								</div>
							</div>
							<div class='InputfieldImageEdit__edit'></div>
						</div>
					</div>
				";
			}
		}

		$class = 'InputfieldImageList gridImages ui-helper-clearfix';
		if($this->uploadOnlyMode) $class .= " InputfieldImageUploadOnly";
		if($this->overwrite && !$this->renderValueMode) $class .= " InputfieldFileOverwrite";
		
		$out = "<ul class='$class' data-gridSize='$this->gridSize' data-gridMode='$this->gridMode'>$out</ul>";
		$out = "<ul class='InputfieldImageErrors'></ul>$out";

		return $out;
	}

	/**
	 * Wrap rendered item
	 * 
	 * @param string $out
	 * @return string
	 * 
	 */
	protected function renderItemWrap($out) {
		$item = $this->currentItem;
		$id = $item && !$this->renderValueMode ? " id='file_$item->hash'" : "";
		$class = "ImageOuter $this->itemClass";
		if($item->hidden()) $class .= ' InputfieldFileItemHidden';
		return "<li$id class='$class'>$out</li>";
	}

	/**
	 * Render upload
	 * 
	 * @param Pagefiles|Pageimages $value
	 * @return string
	 * 
	 */
	protected function ___renderUpload($value) {

		if($this->noUpload || $this->renderValueMode) return '';

		// enables user to choose more than one file
		if($this->maxFiles != 1) $this->setAttribute('multiple', 'multiple');

		$attrs = $this->getAttributes();
		unset($attrs['value']);
		if(substr($attrs['name'], -1) != ']') $attrs['name'] .= '[]';
		
		$attrStr = $this->getAttributesString($attrs);
		$extensions = $this->getAllowedExtensions();
		$formatExtensions = $this->formatExtensions($extensions);
		$chooseLabel = $this->labels['choose-file'];
		$chooseIcon = wireIconMarkup('folder-open-o', 'fw');
		$uploadAttrStr = $this->getAttributesString($this->getUploadAttrs());

		$out = "<div $uploadAttrStr class='InputfieldImageUpload'>";
		
		$out .= "
			<div class='InputMask ui-button ui-state-default'>
				<span class='ui-button-text'>
					$chooseIcon$chooseLabel
				</span>
				<input $attrStr>
			</div>
			<span class='InputfieldImageValidExtensions detail'>$formatExtensions</span>
			<input type='hidden' class='InputfieldImageMaxFiles' value='$this->maxFiles' />
		";

		if(!$this->noAjax) {
			$dropLabel = $this->uploadOnlyMode ? $this->labels['drag-drop'] : $this->labels['drag-drop-in'];
			$dropIcon = wireIconMarkup('cloud-upload');
			$out .= "
				<span class='AjaxUploadDropHere description'>
					<span>
						$dropIcon&nbsp;$dropLabel
					</span>
				</span>";
		}
		
		if($this->get('_hasLegacyThumbs')) {
			$label = $this->_('There are older/low quality thumbnail preview images above – check this box to re-create them.');
			$out .= "
				<p class='InputfieldImageRefresh detail'>
					<label>
						<input type='checkbox' name='_refresh_thumbnails_$this->name' value='1' />
						$label
					</label>
				</p>
			";
		}

		$out .= "</div>";

		return $out;
	}

	/**
	 * Resize images to max width/height if specified in field config and image is larger than max
	 * 
	 * #pw-hooker
	 * 
	 * @param Pagefile $pagefile
	 * @throws WireException
	 *
	 */
	protected function ___fileAdded(Pagefile $pagefile) {
		
		/** @var Pageimage $pagefile */
		
		if($pagefile->ext() === 'svg') {
			parent::___fileAdded($pagefile);
			return;
		}

		$pagefile2 = null;

		if(!$pagefile->width) {
			$pagefile->unlink();
			throw new WireException($this->_('Invalid image') . ' (width=0)');
		}

		$minWidth = $this->minWidth;
		$minHeight = $this->minHeight;

		if($this->dimensionsByAspectRatio && $pagefile->width < $pagefile->height){
			$minWidth = $this->minHeight;
			$minHeight = $this->minWidth;
		}

		if(
			($minWidth && $pagefile->width < $minWidth) ||
			($minHeight && $pagefile->height < $minHeight)
		) {
			$actualDimensions = $pagefile->width . 'x' . $pagefile->height;
			$requiredDimensions = $minWidth . 'x' . $minHeight;
			throw new WireException(
				sprintf($this->_('Image of %s does not meet minimum size requirements'), $actualDimensions) . " ($requiredDimensions)"
			);
		}

		$maxWidth = $this->maxWidth;
		$maxHeight = $this->maxHeight;

		if($this->dimensionsByAspectRatio && $pagefile->width < $pagefile->height){
			$maxWidth = $this->maxHeight;
			$maxHeight = $this->maxWidth;
		}

		if(
			($maxWidth && $pagefile->width > $maxWidth) ||
			($maxHeight && $pagefile->height > $maxHeight)
		) {
			if($this->maxReject) {
				$actualDimensions = $pagefile->width . '×' . $pagefile->height;
				$requiredDimensions = $maxWidth . '×' . $maxHeight;
				throw new WireException(
					sprintf($this->_('Image of %s exceeds maximum allowed size'), $actualDimensions) . " ($requiredDimensions)"
				);
			}
			$pagefile2 = $pagefile->size($maxWidth, $maxHeight, array('cropping' => false));
			if($pagefile->filename != $pagefile2->filename) {
				$this->wire()->files->unlink($pagefile->filename);
				$this->wire()->files->rename($pagefile2->filename, $pagefile->filename);
			}
			$pagefile->getImageInfo(true); // force it to reload its dimensions
		}

		if($pagefile2) {
			$this->message($this->_("Image resized to fit maximum allowed dimensions") . " ({$maxWidth}x{$maxHeight}");
		}

		parent::___fileAdded($pagefile);
	}

	/**
	 * @param Pagefile $pagefile
	 * @param int $n
	 * @return string
	 * 
	 */
	protected function fileAddedGetMarkup(Pagefile $pagefile, $n) {
		/** @var Pageimage $pagefile */
		/*
		$markup = $this->maxFiles == 1
			? $this->renderSingleItem($pagefile, $this->pagefileId($pagefile), $n)
			: $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n));
		*/
		$markup = $this->renderItemWrap($this->renderItem($pagefile, $this->pagefileId($pagefile), $n));
		return $markup;
	}

	/**
	 * Get thumbnail image info
	 *
	 * @param Pageimage $img Image to get thumb for
	 * @param bool $useSizeAttributes Whether width and or height size attributes should be included in the <img> tag
	 * @param bool $remove Specify true to remove legacy thumbnail file
	 *
	 * @return array of(
	 *    'thumb' => Pageimage object,
	 *    'attr' => associative array of image attributes
	 *    'markup' => string of markup for <img>,
	 *    'amarkup' => same as above but wrapped in <a> tag
	 *    'error' => error message if applicable
	 *    'title' => potential title attribute for <a> tag with image info 
	 * );
	 *
	 */
	public function getAdminThumb(Pageimage $img, $useSizeAttributes = true, $remove = false) {

		$thumb = $img;
		$error = '';
		$attr = array();

		$_thumbHeight = $thumb->height;
		$thumbHeight = $_thumbHeight;
		$_thumbWidth = $thumb->width;
		$thumbWidth = $_thumbWidth;
		$useResize = ($img->ext == 'svg' && $thumbHeight == '100%')
			|| ($this->gridSize && $thumbHeight > $this->gridSize)
			|| ($this->gridSize && $thumbWidth > $this->gridSize);

		if($useResize) {
			
			$imageSizerOptions = $this->imageSizerOptions;
			$imageSizerOptions['upscaling'] = true;
			$imageSizerOptions['focus'] = false; // disable focus since we show focus from JS/CSS in admin thumbs
			$adminThumbOptions = $this->wire()->config->adminThumbOptions;
			$gridSize2x = $this->gridSize * 2;
			// if($adminThumbOptions['scale'] === 1.0) $gridSize2x = $this->gridSize; // force non-HiDPI
			
			// check if there is an existing thumbnail using pre-gridSize legacy settings
			$h = (int) $adminThumbOptions['height'];
			$w = (int) $adminThumbOptions['width'];
			$f = $img->pagefiles->path() . basename($img->basename(), '.' . $img->ext()) . ".{$w}x{$h}." . $img->ext();
			$exists = is_file($f);
			
			if($exists && $remove) {
				$this->wire()->files->unlink($f); 
				$exists = false;
			}
			
			if($exists) {
				// use existing legacy thumbnail (upscaled in browser to gridSize)
				$thumb = $thumb->size($w, $h, $imageSizerOptions);
				if($thumbWidth > $thumbHeight) {
					$thumbHeight = $this->gridSize;
					$thumbWidth = 0;
				} else if($thumbWidth < $thumbHeight) {
					$thumbWidth = $this->gridSize;
					$thumbHeight = 0;
				} else {
					$thumbWidth = $this->gridSize;	
					$thumbHeight = $this->gridSize;
				}
				$this->set('_hasLegacyThumbs', true);
			} else {
				// use new thumbnail size, 260px (scaled to 130px in output)
				if($thumbWidth >= $thumbHeight) {
					if($thumbHeight > $gridSize2x) {
						$thumb = $thumb->height($gridSize2x, $imageSizerOptions);
						$thumbHeight = $this->gridSize;
						$thumbWidth = 0;
					}
				} else if($thumbWidth > $gridSize2x) {
					$thumb = $thumb->width($gridSize2x, $imageSizerOptions);
					$thumbWidth = $this->gridSize;
					$thumbHeight = 0;
				}
			}

			if($thumb->error) $error = $thumb->error;
		}

		if($useSizeAttributes) {
			if($thumb->get('_requireHeight')) {
				// _requireHeight set by InputfieldPageEditImageSelect
				if(!$thumbHeight || $thumbHeight > $this->gridSize) $thumbHeight = $this->gridSize;
				$attr['height'] = $thumbHeight;
			} else if($thumbHeight && $thumbWidth) {
				$attr['width'] = $thumbWidth;
				$attr['height'] = $thumbHeight;
			} else if($thumbHeight) {
				$attr['height'] = $thumbHeight;
			} else if($thumbWidth) {
				$attr['width'] = $thumbWidth;
			}
		}

		$attr['src'] = $thumb->URL;
		$attr['alt'] = $this->wire()->sanitizer->entities1($img->description);
		$attr['data-w'] = $_thumbWidth;
		$attr['data-h'] = $_thumbHeight;
		$attr["data-original"] = $img->URL;
	
		$focus = $img->focus();
		$attr['data-focus'] = $focus['str'];
		
		$markup = "<img ";
		foreach($attr as $key => $value) $markup .= "$key=\"$value\" ";
		$markup .= " />";
		
		$title = $img->basename() . " ({$img->width}x{$img->height}) $img->filesizeStr";
		if($attr['alt']) $title .= ": $attr[alt]";
		
		$amarkup = "<a href='$img->url' title='$title'>$markup</a>";

		$a = array(
			'thumb' => $thumb,
			'attr' => $attr,
			'markup' => $markup,
			'amarkup' => $amarkup, 
			'error' => $error,
			'title' => $title, 
		);
		
		return $a;
	}

	/**
	 * Get Pagefile to pull description and tags from 
	 * 
	 * @param Pagefile $pagefile
	 * @return Pageimage|Pagefile
	 * 
	 */
	protected function getMetaPagefile(Pagefile $pagefile) {
		if(!$this->isAjax || !isset($_SERVER['HTTP_X_REPLACENAME'])) return $pagefile;
		$metaFilename = $_SERVER['HTTP_X_REPLACENAME'];
		if(strpos($metaFilename, '?')) list($metaFilename,) = explode('?', $metaFilename); 
		$metaFilename = $this->wire()->sanitizer->name($metaFilename);
		$metaPagefile = $this->val()->get($metaFilename);
		if(!$metaPagefile instanceof Pagefile) $metaPagefile = $pagefile;
		return $metaPagefile;
	}

	/**
	 * Render a Pageimage item
	 *
	 * @param Pagefile|Pageimage $pagefile
	 * @param string $id
	 * @param int $n
	 *
	 * @return string
	 *
	 */
	protected function ___renderItem($pagefile, $id, $n) {

		$sanitizer = $this->wire()->sanitizer;
		$thumb = $this->getAdminThumb($pagefile, false);
		$fileStats = str_replace(' ', '&nbsp;', $pagefile->filesizeStr) . ", {$pagefile->width}&times;{$pagefile->height} ";
		
		foreach($pagefile->extras() as $name => $extra) {
			if($extra->exists()) $fileStats .= " &bull; $extra->filesizeStr $name ($extra->savingsPct)";
		}
		
		if($pagefile->hidden()) {
			$fileStats .= '&nbsp; ' . wireIconMarkup('eye-slash') . ' ' .
				$sanitizer->entities1($this->labels['hidden']);
		}

		$class = 'gridImage__overflow';
		if($pagefile->ext() === 'svg') {
			$class .= ' ' . ($pagefile->ratio < 1 ? 'portrait' : 'landscape');
		}
		$out = $this->getTooltip($pagefile) . "
			<div class='$class'>
				$thumb[markup]
			</div>
		";
		
		if(!$this->isEditableInRendering($pagefile)) return $out;

		if($this->uploadOnlyMode) {
			$out .= "
				<div class='ImageData'>
					<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />
				</div>
			";
		} else {
			$buttons = $pagefile->ext() == 'svg' ? '' : $this->renderButtons($pagefile, $id, $n);
			$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']);
			} 
			$labels = $this->labels;
			$thumbnailActions = array();
			if($this->hasHook('getImageThumbnailActions()')) {
				$thumbnailActions = $this->getImageThumbnailActions($pagefile, $id, $n, 'gridImage__btn');
			}
			$thumbnailActions = implode('', $thumbnailActions);
			$out .= "
				<div class='gridImage__hover'>
					<div class='gridImage__inner'>
						<label for='' class='gridImage__btn gridImage__trash'>
							<input class='gridImage__deletebox' type='checkbox' name='delete_$id' value='1' title='$labels[delete]' />
							<span class='fa fa-trash-o'></span>
						</label>$thumbnailActions
						<a class='gridImage__edit'>
							<span>$labels[edit]</span>
						</a>
					</div>
				</div>
			";

			$ext = $pagefile->ext();
			$basename = $pagefile->basename(false);
			$focus = $pagefile->focus();

			if($pagefile !== $metaPagefile) {
				// copy custom fields data from previous file
				foreach($metaPagefile->filedata() as $key => $value) {
					$pagefile->filedata($key, $value);
				}
			}

			$inputfields = $this->getItemInputfields($pagefile);
			if($inputfields) $additional .= $inputfields->render();

			$tooltip = $sanitizer->entities1($this->_('Click to rename'));
			$uploadName = $pagefile->uploadName();
			
			if($uploadName != "$basename.$ext") {
				$uploadName = $sanitizer->entities($this->_('Originally:') . ' ' . $uploadName);
			} else {
				$uploadName = '';
			}
				
			$out .= "
				<div class='ImageData'>
					<h2 class='InputfieldImageEdit__name pw-tooltip' title='$tooltip'><span title=\"$uploadName\" contenteditable='true'>$basename</span>.$ext</h2>
					<span class='InputfieldImageEdit__info'>$fileStats</span>
					<div class='InputfieldImageEdit__errors'>$error</div>
					<div class='InputfieldImageEdit__buttons'><small>$buttons</small> $actions</div>
					<div class='InputfieldImageEdit__core'>$description</div>
					<div class='InputfieldImageEdit__additional'>$additional</div>
					<input class='InputfieldFileSort' type='text' name='sort_$id' value='$n' />
					<input class='InputfieldFileReplace' type='hidden' name='replace_$id' />
					<input class='InputfieldFileRename' type='hidden' name='rename_$id' />
					<input class='InputfieldImageFocus' type='hidden' name='focus_$id' value='$focus[str]' />
				</div>
			";
		}

		return $out;
	}

	/**
	 * Get the image thumbnail icon actions/links/buttons
	 * 
	 * These are icon-only actions/links displayed next to the trash icon when hovering over an image preview. 
	 * They are also displayed as icons on the far right side of a image when in full list mode. 
	 * 
	 * Example:
	 * ~~~~~
	 * $wire->addHookAfter('InputfieldImage::getImageThumbnailActions', function(HookEvent $event) {
	 *   $image = $event->arguments(0); // Pageimage
	 *   $class = $event->arguments(3); // class to use on all returned actions
	 *   $a = $event->return; // array
	 *   $a['download'] = "<a class='$class' href='$pagefile->url' download><span class='fa fa-download'></span></a>";
	 *   $event->return = $a;
	 * }); 
	 * ~~~~~
	 * 
	 * #pw-hooker
	 * 
	 * @param Pageimage $pagefile
	 * @param string $id Image id string
	 * @param int $n Image index number
	 * @param string $class Class that should appear on all returned actions/links/buttons
	 * @return array 
	 * @since 3.0.212
	 * 
	 */
	protected function ___getImageThumbnailActions($pagefile, $id, $n, $class) {
		return array();
	}

	/**
	 * 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
	 * @return string
	 *
	 */
	protected function ___renderSingleItem($pagefile, $id, $n) {
		
		$editable = $this->isEditableInRendering($pagefile);
		$fileStats = str_replace(' ', '&nbsp;', $pagefile->filesizeStr) . ", {$pagefile->width}&times;{$pagefile->height} ";
		$description = $this->wire()->sanitizer->entities($pagefile->description);
		$deleteLabel = $this->labels['delete'];
		
		if($editable) {
			$buttons = $this->renderButtons($pagefile, $id, $n);
			$metaPagefile = $this->getMetaPagefile($pagefile);
			$descriptionField = $this->renderItemDescriptionField($metaPagefile, $id, $n);
			$additional = $this->renderAdditionalFields($metaPagefile, $id, $n);
			$editableOut = "
				<div class='InputfieldImageEdit__buttons'>$buttons</div>
				<div class='InputfieldImageEdit__core'>$descriptionField</div>
				<div class='InputfieldImageEdit__additional'>$additional</div>
				<input class='InputfieldFileSort' type='hidden' name='sort_$id' value='$n' />
				<input class='InputfieldFileReplace' type='hidden' name='replace_$id' />
				<input class='InputfieldFileRename' type='hidden' name='rename_$id' />
			";
		} else {
			$editableOut = '';
			//$editableOut = "<p>" . $this->_("Not editable.") . "</p>";
		}
	
		$trashOut = '';
		if($editable && !$this->renderValueMode) $trashOut = "
					<div class='InputfieldImageEdit__trash-single'>
						<label for='' class='gridImage__trash gridImage__trash--single'>
							<input class='gridImage__deletebox' type='checkbox' name='delete_$id' value='1' title='$deleteLabel' />
							<span class='fa fa-trash-o'></span>
						</label>
					</div>
		";
		
		$out = "
			<div class='ImageOuter InputfieldImageEdit InputfieldImageEditSingle' id='file_$pagefile->hash'>
				<div class='InputfieldImageEdit__inner'>
					$trashOut
					<div class='InputfieldImageEdit__imagewrapper'>
						<div>
							<img class='InputfieldImageEdit__image' src='$pagefile->URL' alt='$description'>
						</div>
					</div>
					<div class='InputfieldImageEdit__edit'>
						<h2 class='InputfieldImageEdit__name'>$pagefile->name</h2>
						<span class='InputfieldImageEdit__info'>$fileStats</span>
						$editableOut
					</div>
				</div>
			</div>
		";

		return $out;
	}

	/**
	 * Render buttons for image edit mode 
	 * 
	 * #pw-hooker
	 * 
	 * @param Pagefile|Pageimage $pagefile
	 * @param string $id
	 * @param int $n
	 * @return string
	 * 
	 */
	protected function ___renderButtons($pagefile, $id, $n) {
		if(!$this->useImageEditor) return '';
		$buttonClass = $this->themeSettings['buttonClass'];
		$buttons = $this->getImageEditButtons($pagefile, $id, $n, $buttonClass);
		return implode('', $buttons);
	}

	/**
	 * Get array of buttons for image edit mode
	 * 
	 * Hook after this to add or remove image edit buttons/actions. 
	 * 
	 * ~~~~~
	 * // Example of adding a download button
	 * $wire->addHookAfter('InputfieldImage::getImageEditButtons', function(HookEvent $event) {
	 *   $image = $event->arguments(0); // Pageimage
	 *   $class = $event->arguments(3);
	 *   $buttons = $event->return; // array
	 *   $icon = wireIconMarkup('download');
	 *   $buttons['download'] = "<button class='$class'><a download href='$image->url'>$icon Download</a></button>";
	 *   $event->return = $buttons;
	 * });
	 * ~~~~~
	 *
	 * #pw-hooker
	 *
	 * @param Pagefile|Pageimage $pagefile Image that buttons are for
	 * @param string $id Image/file id
	 * @param int $n Index of image/file (i.e 0=first)
	 * @param string $buttonClass Class attribute additions that should appear on all returned <button> elements
	 * @return array Array of <button> elements indexed by button name
	 * @since 3.0.212
	 *
	 */
	protected function ___getImageEditButtons($pagefile, $id, $n, $buttonClass) {
		
		$buttons = array();
		$pageID = $pagefile->pagefiles->page->id;
		$variationCount = $pagefile->variations()->count();
		$editUrl = $this->getEditUrl($pagefile, $pageID);
		$variationUrl = $this->getVariationUrl($pagefile, $id);
		$modalButtonClass = trim("$buttonClass $this->modalClass pw-modal");
		$modalAttrs = "data-buttons='#non_rte_dialog_buttons button' data-autoclose='1' data-close='#non_rte_cancel'";
		$labels = $this->labels;

		// Crop
		$icon = wireIconMarkup('crop');
		$buttonText = str_replace('{out}', "$icon $labels[crop]", $this->themeSettings['buttonText']);
		$buttons['crop'] = "<button type='button' data-href='$editUrl' class='InputfieldImageButtonCrop $modalButtonClass' $modalAttrs>$buttonText</button>";

		// Focus
		if($this->focusMode && $this->focusMode != 'off') {
			$iconA = $pagefile->hasFocus ? 'fa-check-circle-o' : 'fa-circle-o';
			$iconB = $pagefile->hasFocus ? 'fa-check-circle' : 'fa-dot-circle-o';
			$buttonText = str_replace('{out}', "<i class='fa $iconA' data-toggle='$iconA $iconB'></i> $labels[focus]", $this->themeSettings['buttonText']);
			$buttons['focus'] = "<button type='button' class='InputfieldImageButtonFocus $buttonClass'>$buttonText</button>";
		}

		// Variations
		$icon = wireIconMarkup('files-o');
		$buttonText = "$icon $labels[variations] <span class='ui-priority-secondary'>($variationCount)</span>";
		$buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']);
		$buttons['variations'] = "<button type='button' data-href='$variationUrl' class='$modalButtonClass' data-buttons='button'>$buttonText</button>";

		return $buttons;
	}

	/**
	 * Render an image action select for given Pageimage
	 * 
	 * @param Pagefile $pagefile
	 * @param string $id
	 * @return string
	 * 
	 */
	protected function renderFileActionSelect(Pagefile $pagefile, $id) {

		if(!$this->useImageEditor) return '';
	
		static $hooked = null; 
		
		$hooks = $this->wire()->hooks;
		
		if($hooked === null) $hooked = 
			$hooks->isHooked('InputfieldImage::getFileActions()') ||
			$hooks->isHooked('InputfieldFile::getFileActions()');
		
		$actions = $hooked ? $this->getFileActions($pagefile) : $this->___getFileActions($pagefile);
		
		if(empty($actions)) return '';
		
		$selectClass = trim($this->themeSettings['selectClass'] . ' InputfieldFileActionSelect');
		$sanitizer = $this->wire()->sanitizer;
	
		$label = $sanitizer->entities1($this->_('Actions'));
		$out =
			"<select class='$selectClass' name='act_$id'>" .
			"<option value=''>$label</option>";
		
		foreach($actions as $name => $label) {
			$label = $sanitizer->entities1($label);
			$out .= "<option value='$name'>$label</option>";
		}

		$out .= "</select> ";
		$label = $sanitizer->entities1($this->_('Action applied at save.'));
		$out .= "<span class='InputfieldFileActionNote detail'>$label</span>";
		
		return $out;
	}

	/**
	 * Get array of actions (displayed in select dropdown) available for given Pagefile
	 * 
	 * #pw-hooker
	 * 
	 * ~~~~~
	 * // Example of adding an “Get EXIF data” action
	 * $wire->addHookAfter('InputfieldImage::getFileActions', function(HookEvent $event) {
	 *   $image = $event->arguments(0); // Pageimage
	 *   if($image->ext == 'jpg' || $image->ext == 'jpeg') {
	 *     $actions = $event->return; // array
	 *     $actions['exif'] = 'Get EXIF data';
	 *     $event->return = $actions;
	 *   }
	 * });
	 * 
	 * // Example of handling an “Get EXIF data” action
	 * $wire->addHookAfter('InputfieldImage::processUnknownFileAction', function(HookEvent $event) {
	 *   $image = $event->arguments(0);
	 *   $action = $event->arguments(1);
	 *   if($action === 'exif') {
	 *     $exif = exif_read_data($image->filename);
	 *     $event->warning([ "EXIF data for $image->name" => $exif ], 'icon-photo nogroup');                       
	 *     $event->return = true;
	 *   }
	 * });
	 * ~~~~~
	 * 
	 * @param Pageimage $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'),
			'rmf' => $this->_('Remove focus'), 
			'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'),
			'hide' => $this->_('Hide'), 
			'unhide' => $this->_('Unhide'),
		);
		
		$actions = array();
		
		if($this->maxFiles != 1) {
			if($pagefile->hidden()) {
				$actions['unhide'] = $labels['unhide'];
			} else {
				$actions['hide'] = $labels['hide'];
			}
		}
		
		if($this->maxFiles && count($pagefile->pagefiles) >= $this->maxFiles) {
			// no dup action
		} else {
			$actions['dup'] = $labels['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'];
			
			if($pagefile->hasFocus) {
				$actions['rmf'] = $labels['rmf'];
			}
		}
		
		return $actions;
	}

	/**
	 * Render any additional fields (for hooks)
	 * 
	 * #pw-hooker
	 * 
	 * @param Pageimage|Pagefile $pagefile
	 * @param string $id
	 * @param int $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
	 *
	 * @return bool
	 *
	 */
	protected function allowCollapsedItems() {
		return false;
	}

	/**
	 * Configure field
	 *
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigInputfields() {
		$inputfields = parent::___getConfigInputfields();
		require_once(__DIR__ . '/config.php');
		$configuration = new InputfieldImageConfiguration();
		$this->wire($configuration);
		$configuration->getConfigInputfields($this, $inputfields);
		return $inputfields;
	}

	/**
	 * Is the given image editable during rendering?
	 * 
	 * @param Pagefile|Pageimage $pagefile
	 * @return bool
	 * 
	 */
	protected function isEditableInRendering($pagefile) {
		if($this->renderValueMode) {
			$editable = false;
		} else if($pagefile->ext == 'svg') {
			$editable = true;
		} else {
			$editable = true;
		}
		return $editable;
	}

	/**
	 * Get URL for viewing image variations
	 * 
	 * @param Pageimage $pagefile
	 * @param string $id
	 * @return string
	 * 
	 */
	protected function getVariationUrl($pagefile, $id) {
		$page = $pagefile->page;
		$pageId = $page->id;
		$version = (int) ((string) $page->get('_version'));
		return $this->wire()->config->urls->admin . "page/image/variations/" . 
			"?id=$pageId" . 
			"&file=$pagefile->name" . 
			"&modal=1" . 
			"&varcnt=varcnt_$id" . 
			"&version=$version";
	}

	/**
	 * Get variations for the given Pagefile
	 * 
	 * @param Pageimage $pagefile
	 * @return array
	 * 
	 */
	protected function getPagefileVariations(Pagefile $pagefile) {
		return isset($this->variations[$pagefile->name]) ? $this->variations[$pagefile->name] : array();
	}

	/**
	 * Get the image editor URL
	 * 
	 * @param Pageimage $pagefile
	 * @param int $pageID
	 * @return string
	 * 
	 */
	protected function getEditUrl(Pagefile $pagefile, $pageID) {
		$name = $this->editFieldName ? $this->editFieldName : $this->name;
		$version = (int) ((string) $pagefile->page->get('_version'));
		return $this->wire()->config->urls->admin . "page/image/edit/" . 
			"?id=$pageID" . 
			"&file=$pageID,$pagefile->name" . 
			"&rte=0" . 
			"&field=$name" . 
			"&version=$version";
	}

	/**
	 * Render the description field input
	 * 
	 * @param Pageimage $pagefile
	 * @param string $id
	 * @param int $n
	 * @return string
	 * 
	 */
	protected function renderItemDescriptionField(Pagefile $pagefile, $id, $n) {
		return parent::renderItemDescriptionField($pagefile, $id, $n); // TODO: Change the autogenerated stub
	}
	
	/**
	 * Get the hover tooltip that appears above thumbnails
	 *
	 * @param Pageimage $pagefile
	 * @return string
	 *
	 */
	protected function getTooltip($pagefile) {

		$data = $this->buildTooltipData($pagefile);
		$rows = "";

		foreach($data as $row) {
			$rows .= "<tr><th>$row[0]</th><td>$row[1]</td></tr>";
		}

		$tooltip = "<div class='gridImage__tooltip'><table>$rows</table></div>";

		return $tooltip;
	}

	/**
	 * Get the root "hasPage" being edited
	 * 
	 * @return NullPage|Page
	 * @since 3.0.168
	 * 
	 */
	protected function getRootHasPage() {
		$page = $this->hasPage;
		if(!$page || !$page->id) {
			$process = $this->wire()->process;
			$page = $process instanceof WirePageEditor ? $process->getPage() : new NullPage();
		}
		if(wireClassExists('RepeaterPage')) { /** @var RepeaterPage $page */
			while(wireInstanceOf($page, 'RepeaterPage')) $page = $page->getForPage();
		}
		return $page;
	}

	/**
	 * Build data for the tooltip that appears above the thumbnails
	 * 
	 * #pw-hooker
	 * 
	 * @param Pagefile|Pageimage $pagefile
	 * @return array
	 * 
	 */
	protected function ___buildTooltipData($pagefile) {
		
		$data = array(
			array(
				$this->labels['dimensions'],
				"{$pagefile->width}x{$pagefile->height}"
			),
			array(
				$this->labels['filesize'],
				str_replace(' ', '&nbsp;', $pagefile->filesizeStr)
			),
			array(
				$this->labels['variations'],
				count($this->getPagefileVariations($pagefile))
			)
		);

		if($pagefile->hidden()) {
			$data[] = array(
				$this->labels['visibility'], 
				wireIconMarkup('eye-slash') . ' ' . $this->labels['hidden']
			);
		}
		
		if(strlen($pagefile->description)) {
			$data[] = array(
				$this->labels['description'],
				wireIconMarkup('check')
			);
		}
		
		if($this->useTags && strlen($pagefile->tags)) {
			$data[] = array(
				$this->labels['tags'], 
				wireIconMarkup('check')
			);
		}

		return $data;
	}

	/**
	 * Return whether or not admin thumbs should be scaled
	 * 
	 * @return bool
	 * @deprecated
	 * 
	 */
	protected function getAdminThumbScale() {
		return $this->adminThumbScale > 0 && ((float) $this->adminThumbScale) != 1.0;
	}

	/**
	 * Process input
	 * 
	 * @param WireInputData $input
	 * @return $this
	 * 
	 */
	public function ___processInput(WireInputData $input) {
	
		$sanitizer = $this->wire()->sanitizer;
		$page = $this->getRootHasPage();
	
		if($page && $page->id) { 
			if(!$this->wire()->user->hasPermission('page-edit-images', $page)) $this->useImageEditor = 0;
		}
		
		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 && !$this->wire()->config->ajax) {
			// process actions, but only on non-ajax save requests
			foreach($this->value as $pagefile) {
				$id = $this->pagefileId($pagefile);
				$action = $input->{"act_$id"};
				if(empty($action)) continue;
				$action = $sanitizer->pageName($action);
				$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 input for a given Pageimage
	 * 
	 * @param WireInputData $input
	 * @param Pageimage $pagefile
	 * @param int $n
	 * @return bool
	 * 
	 */
	protected function ___processInputFile(WireInputData $input, Pagefile $pagefile, $n) {

		$changed = false;
		
		$id = $this->name . '_' . $pagefile->hash;
		$key = "focus_$id";
		$val = $input->$key;
		
		if($val !== null) {
			if(!strlen($val)) $val = '50 50 0';
			$focus = $pagefile->focus();
			if($focus['str'] !== $val) {
				$pagefile->focus($val);
				$changed = true;
				$focus = $pagefile->focus();
				$rebuild = $pagefile->rebuildVariations();
				// @todo rebuild variations only for images that specify both width and height
				$this->message(
					"Updated focus for $pagefile to: top=$focus[top]%, left=$focus[left]%, zoom=$focus[zoom] " . 
					"and rebuilt " . count($rebuild['rebuilt']) . " variations", 
					Notice::debug
				);
			}
		}
	
		if(parent::___processInputFile($input, $pagefile, $n)) $changed = true;
		
		return $changed;
	}

	/**
	 * 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) {

		if(!$this->useImageEditor) return null;
		
		$success = null;
		$showSuccess = true; 
		$rebuildVariations = false;
		$imageInfo = $pagefile->getImageInfo();
		
		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 if($action == 'rmf') {
			// remove focus
			$pagefile->focus(false);
			$success = true;
		} else if($action == 'hide') {
			$pagefile->hidden(true);
			$success = true;
		} else if($action == 'unhide') {
			$pagefile->hidden(false);
			$success = true;
		} 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) {
			$pagefile->trackChange("action-$action");
			$this->trackChange('value');
			$info = $pagefile->getImageInfo(true);
			if($info != $imageInfo && $pagefile->page && $pagefile->field) {
				$pagefile->page->trackChange($pagefile->field->name);
			}
		}
		
		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 a select dropdown action was received that InputfieldImage does not recognize (for hooking purposes)
	 * 
	 * This is what should be hooked to provide the processing for a custom action added from a hook.
	 * See the ___getFileActions() method documentation for full example including both hooks.
	 * 
	 * #pw-hooker
	 *
	 * @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) {
		return null;
	}

}
