Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** Class InputfieldImage** Inputfield for FieldtypeImage fields** ProcessWire 3.x, Copyright 2023 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' => 127,'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 100if($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.'),));$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 $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 resizeif(!$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));$this->wrapAttr('data-resize', "$this->maxWidth;$this->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'" : "";return "<li$id class='ImageOuter $this->itemClass'>$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 fileif($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');$out ="<div " ."data-maxfilesize='$this->maxFilesize' " ."data-extensions='$extensions' " ."data-fieldname='$attrs[name]' " ."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 $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 InputfieldPageEditImageSelectif(!$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(' ', ' ', $pagefile->filesizeStr) . ", {$pagefile->width}×{$pagefile->height} ";foreach($pagefile->extras() as $name => $extra) {if($extra->exists()) $fileStats .= " • $extra->filesizeStr $name ($extra->savingsPct)";}$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 fileforeach($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(' ', ' ', $pagefile->filesizeStr) . ", {$pagefile->width}×{$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>";// Focusif($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 Pagefile|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'),);$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'];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) {return $this->wire()->config->urls->admin . "page/image/variations/" ."?id={$pagefile->page->id}" ."&file=$pagefile->name" ."&modal=1" ."&varcnt=varcnt_$id";}/*** Get variations for the given Pagefile** @param Pagefile|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 Pagefile|Pageimage $pagefile* @param int $pageID* @return string**/protected function getEditUrl(Pagefile $pagefile, $pageID) {$name = $this->editFieldName ? $this->editFieldName : $this->name;return $this->wire()->config->urls->admin . "page/image/edit/" ."?id=$pageID" ."&file=$pageID,$pagefile->name" ."&rte=0" ."&field=$name";}/*** Render the description field input** @param Pagefile|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(' ', ' ', $pagefile->filesizeStr)),array($this->labels['variations'],count($this->getPagefileVariations($pagefile))));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 requestsforeach($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 Pagefile|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;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 {/** @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');}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;}}