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 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.'), )); $themeDefaults = array( // 'error' => "{out}", // provided by InputfieldFile 'buttonClass' => "ui-button ui-corner-all ui-state-default", 'buttonText' => "{out}", '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 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)); $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 .= "
$dropNew $focus
"; } } $class = 'InputfieldImageList gridImages ui-helper-clearfix'; if($this->uploadOnlyMode) $class .= " InputfieldImageUploadOnly"; if($this->overwrite && !$this->renderValueMode) $class .= " InputfieldFileOverwrite"; $out = ""; $out = "$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 "$out"; } /** * 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'); $out = "
"; $out .= "
$chooseIcon$chooseLabel
$formatExtensions "; if(!$this->noAjax) { $dropLabel = $this->uploadOnlyMode ? $this->labels['drag-drop'] : $this->labels['drag-drop-in']; $dropIcon = wireIconMarkup('cloud-upload'); $out .= " $dropIcon $dropLabel "; } if($this->get('_hasLegacyThumbs')) { $label = $this->_('There are older/low quality thumbnail preview images above – check this box to re-create them.'); $out .= "

"; } $out .= "
"; 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 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 , * 'amarkup' => same as above but wrapped in tag * 'error' => error message if applicable * 'title' => potential title attribute for 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 = " $value) $markup .= "$key=\"$value\" "; $markup .= " />"; $title = $img->basename() . " ({$img->width}x{$img->height}) $img->filesizeStr"; if($attr['alt']) $title .= ": $attr[alt]"; $amarkup = "$markup"; $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) . "
$thumb[markup]
"; if(!$this->isEditableInRendering($pagefile)) return $out; if($this->uploadOnlyMode) { $out .= "
"; } 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 .= "
$thumbnailActions $labels[edit]
"; $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 .= "

$basename.$ext

$fileStats
$error
$buttons $actions
$description
$additional
"; } 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'] = ""; * $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 = "
$buttons
$descriptionField
$additional
"; } else { $editableOut = ''; //$editableOut = "

" . $this->_("Not editable.") . "

"; } $trashOut = ''; if($editable && !$this->renderValueMode) $trashOut = "
"; $out = "
$trashOut
$description

$pagefile->name

$fileStats $editableOut
"; 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'] = ""; * $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 "; // 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}', " $labels[focus]", $this->themeSettings['buttonText']); $buttons['focus'] = ""; } // Variations $icon = wireIconMarkup('files-o'); $buttonText = "$icon $labels[variations] ($variationCount)"; $buttonText = str_replace('{out}', $buttonText, $this->themeSettings['buttonText']); $buttons['variations'] = ""; 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 = " "; $label = $sanitizer->entities1($this->_('Action applied at save.')); $out .= "$label"; 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 .= "$row[0]$row[1]"; } $tooltip = "
$rows
"; 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 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 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; } }