id = $post_id; parent::__construct($path, $post_id, null); // Set AFTER PARENT, because it's overwritten. $this->imageType = self::IMAGE_TYPE_MAIN; $this->image_meta = new ImageMeta(); $this->setName($this->mainImageKey); // by definition this is the case, used for isSizeExcluded // WP 5.3 and higher. Check for original file. if (function_exists('wp_get_original_image_path')) { $this->setOriginalFile(); } if ($this->id > 0) { $this->loadMeta(); } if (false === $this->isExtensionExcluded()) { $this->checkUnlistedForNotice(); } } public function getOptimizeUrls() { $data = $this->getOptimizeData(); return array_values($data['urls']); } // Cancel any exclusions set by user. This is meant to be able to manually optimize an image that has been excluded otherwise. Just to keep things simple. Run this before getting any URLs or OptimizeData. public function cancelUserExclusions() { parent::cancelUserExclusions(); $thumbs = $this->getThumbObjects(); foreach($thumbs as $thumbnailObj) { $thumbnailObj->cancelUserExclusions(); } // reset optimizedata if any $this->optimizeData = null; } // Path will only return the filepath. For reasons, see getOptimizeFileType public function getOptimizeData() { if (! is_null($this->optimizeData)) { return $this->optimizeData; } $parameters = array( 'urls' => array(), 'params' => array(), // doubles are items that will have same result, but is diffent file. duplicates are same files, same result - only meta update. 'returnParams' => array('sizes' => array(), 'doubles' => array(), 'duplicates' => array() ), ); $settings = \wpSPIO()->settings(); $url = $this->getURL(); if ($this->hasOriginal()) { $main_url = $this->getOriginalFile()->getUrl(); } else { $main_url = $url; } if (! $url) // If the whole image URL can't be found { return $parameters; } $isSmartCrop = ($settings->useSmartcrop == true && $this->getExtension() !== 'pdf') ? true : false; $paramListArgs = array(); // args for the params, yes. if (isset($this->forceSettings['smartcrop']) && $this->getExtension() !== 'pdf') { $isSmartCrop = ($this->forceSettings['smartcrop'] == ImageModel::ACTION_SMARTCROP) ? true : false; } $paramListArgs['smartcrop'] = $isSmartCrop; $paramListArgs['url'] = $url; $paramListArgs['main_url'] = $main_url; $doubles = array(); // check via hash if same command / result is there. if ($this->isProcessable(true) || ($this->isProcessableAnyFileType() && $this->isOptimized()) ) { $paramList = $this->createParamList($paramListArgs); $parameters['urls'][$this->mainImageKey] = $paramList['url']; $parameters['paths'][$this->mainImageKey] = $this->getFullPath(); $parameters['params'][$this->mainImageKey] = $paramList; $parameters['returnParams']['sizes'][$this->mainImageKey] = $this->getFileName(); // If top URL is used, include filesize information if ($paramList['url'] !== $paramListArgs['url'] ) { $parameters['returnParams']['fileSizes'][$this->mainImageKey] = $this->getFileSize(); } $hash = md5( serialize($paramList) . $url); $doubles[$hash] = $this->mainImageKey; } $thumbObjs = $this->getThumbObjects(); $unProcessable = array(); foreach($thumbObjs as $name => $thumbObj) { if ($thumbObj->isThumbnailProcessable() || ($thumbObj->isProcessableAnyFileType() && $thumbObj->isOptimized()) ) { $url = $thumbObj->getURL(); $paramListArgs['url'] = $url; $paramListArgs['main_url'] = $main_url; $paramList = $thumbObj->createParamList($paramListArgs); $url = $paramList['url']; // createParamList also decides on URL. $hash = md5( serialize($paramList) . $url); // Hash the paramlist + url to find identical results. // This thing fly to sep function? Retutrn liast duplicat/double name => name if (isset($doubles[$hash])) { $doubleName = $doubles[$hash]; if ($doubleName === $this->mainImageKey) { $compareObj = $this; } else { $compareObj = $thumbObjs[$doubleName]; } if ($thumbObj->getFileName() == $compareObj->getFileName()) { $parameters['returnParams']['duplicates'][$name] = $doubleName; } else { // Check if in a duplicate item is in doubles, so we don't double-double it. $aDuplicate = false; foreach($parameters['returnParams']['doubles'] as $doubleNameInDoubles => $unneeded) { if ($doubleNameInDoubles !== $this->mainImageKey && $doubleNameInDoubles !== $this->originalImageKey && $thumbObjs[$doubleNameInDoubles]->getFileName() == $thumbObj->getFileName()) { $aDuplicate = true; $parameters['returnParams']['duplicates'][$name] = $doubleNameInDoubles; } } if (false === $aDuplicate) { $parameters['returnParams']['doubles'][$name] = $doubleName; } } } else { $parameters['urls'][$name] = $url; $parameters['paths'][$name] = $thumbObj->getFullPath(); $parameters['params'][$name] = $paramList; $parameters['returnParams']['sizes'][$name] = $thumbObj->getFileName(); if ($paramList['url'] !== $paramListArgs['url']) { $parameters['returnParams']['fileSizes'][$name] = $thumbObj->getFileSize(); } $doubles[$hash] = $name; } } else { // Save rejected thumbs, because they might be a duplicate of something that goes on the processing. $unProcessable[] = $thumbObj; } } // If one or more thumbnails were not processable, still check them against the process list in case identical sizes are being processed and it should be marked as a duplicate. if (isset($parameters['paths']) && count($unProcessable) > 0) { $pathVal = array_values($parameters['paths']); $pathLookup = array_flip($parameters['paths']); // lookup fullpath -> size. foreach($unProcessable as $thumbObj) { if (in_array($thumbObj->getFullPath(), $pathVal) === true) { $parameters['returnParams']['duplicates'][$thumbObj->get('name')] = $pathLookup[$thumbObj->getFullPath()]; } } } $this->optimizeData = $parameters; return $parameters; } public function flushOptimizeData() { $this->optimizeData = null; } // Overwrite a settin for optimization public function doSetting($setting, $value) { $this->forceSettings[$setting] = $value; $this->flushOptimizeData(); } // Try to get the URL via WordPress // This is now officially a heavy function. Take times, other plugins (like s3) might really delay it public function getURL() { $url = $this->fs()->checkURL(wp_get_attachment_url($this->id)); if (true === $this->getMeta()->convertMeta()->hasPlaceHolder()) { $extension = pathinfo($url, PATHINFO_EXTENSION); $url = str_replace($extension, $this->getMeta()->convertMeta()->getFileFormat(), $url); } return $url; } /** Return all urls of item. This should -not- be used for optimization. Use getOptimizeUrls(). In use for cache */ public function getAllUrls() { $urls = array(); $urls[] = $this->getUrl(); $thumbs = $this->getThumbObjects(); foreach($thumbs as $thumbObj) { $urls[] = $thumbObj->getUrl(); } return $urls; } public function getWPMetaData() { if (is_null($this->wp_metadata)) $this->wp_metadata = wp_get_attachment_metadata($this->id); return $this->wp_metadata; } /** Check if image is scaled by WordPress * * @return boolean */ public function isScaled() { return $this->is_scaled; } /** Loads an array of Thumbnailmodels based on sizes available in WordPress metadata ** @return Array consisting ofMediaLibraryThumbnailModel **/ protected function loadThumbnailsFromWP() { $wpmeta = $this->getWPMetaData(); $wpImageSizes = UtilHelper::getWordPressImageSizes(); $width = null; $height = null; if (! isset($wpmeta['width'])) { if ($this->getExtension == 'pdf') { $width = $wpmeta['full']['width']; } } else $width = $wpmeta['width']; if (! isset($wpmeta['height'])) { if ($this->getExtension == 'pdf') { $height = $wpmeta['full']['height']; } } else $height = $wpmeta['height']; if (isset($wpmeta['filesize'])) { $this->filesize = $wpmeta['filesize']; } // if meta is (mostly) empty and no sizes ( no thumbnails ) and no width, this might not be image, nor processable. if (is_null($width) && ! isset($wpmeta['sizes']) && true === $this->isExtensionExcluded()) { return array(); } if (is_null($width) || is_null($height) && ! $this->is_virtual()) { $width = (is_null($width)) ? $this->get('width') : $width; $height = (is_null($height)) ? $this->get('height') : $height; } $this->width = $width; $this->height = $height; $thumbnails = array(); if (isset($wpmeta['sizes'])) { foreach($wpmeta['sizes'] as $name => $data) { if (isset($data['file'])) { $width = (isset($data['width'])) ? $data['width'] : null; $height = (isset($data['height'])) ? $data['height'] : null; $thumbObj = $this->getThumbnailModel($this->getFileDir() . $data['file'], $name); $meta = new ImageThumbnailMeta(); $meta->originalWidth = $width; // get from WP $meta->originalHeight = $height; $thumbObj->setName($name); // name is size mostly $thumbObj->setMetaObj($meta); $thumbObj->width = $width; $thumbObj->height = $height; if (isset($wpImageSizes[$name])) { $thumbObj->setSizeDefinition($wpImageSizes[$name]); } if (isset($data['filesize'])) $thumbObj->filesize = $data['filesize']; $thumbnails[$name] = $thumbObj; } } } return $thumbnails; } protected function getRetinas() { // Don't load retina's if option is off. if (! \wpSPIO()->settings()->optimizeRetina) return; if (! is_null($this->retinas)) { return $this->retinas; } if (! isset($this->retinas[$this->mainImageKey])) { $main = $this->getRetina(); if ($main) { $main->setName($this->mainImageKey); $this->retinas[$this->mainImageKey] = $main; // to prevent any custom image sizes to get overwritten. } } if ($this->isScaled() && ! isset($this->retinas[$this->originalImageKey])) { $retscaled = $this->original_file->getRetina(); if ($retscaled) { $retscaled->setName($this->originalImageKey); $this->retinas[$this->originalImageKey] = $retscaled; //see main } } foreach ($this->thumbnails as $thumbname => $thumbObj) { if (! isset($this->retinas[$thumbname])) { $retinaObj = $thumbObj->getRetina(); if ($retinaObj) $this->retinas[$retinaObj->get('name')] = $retinaObj; } } return $this->retinas; } protected function getWebps() { $webps = array(); $main = $this->getWebp(); if ($main) $webps[$this->mainImageKey] = $main; // on purpose not a string, but number to prevent any custom image sizes to get overwritten. foreach($this->thumbnails as $thumbname => $thumbObj) { $webp = $thumbObj->getWebp(); if ($webp) $webps[$thumbname] = $webp; } if (! is_null($this->retinas)) { foreach ($this->retinas as $retinaName => $retinaObj) { $webp = $retinaObj->getWebp(); if ($webp) $webps['retina-' . $retinaName] = $webp; // adding a prefix to make sure it will not overwrite thumbnames, they share the same name. } } if ($this->isScaled()) { $webp = $this->original_file->getWebp(); if ($webp) $webps[$this->originalImageKey] = $webp; //see main } return $webps; } protected function getAvifs() { $avifs = array(); $main = $this->getAvif(); if ($main) $avifs[$this->mainImageKey] = $main; // on purpose not a string, but number to prevent any custom image sizes to get overwritten. foreach($this->thumbnails as $thumbname => $thumbObj) { $avif = $thumbObj->getAvif(); if ($avif) $avifs[$thumbname] = $avif; } if (! is_null($this->retinas)) { foreach ($this->retinas as $retinaName => $retinaObj) { $avif = $retinaObj->getAvif(); if ($avif) $avifs['retina-' . $retinaName] = $avif; // adding a prefix to make sure it will not overwrite thumbnames, they share the same name. } } if ($this->isScaled()) { $avif = $this->original_file->getAvif(); if ($avif) $avifs[$this->originalImageKey] = $avif; //see main } return $avifs; } // @todo Needs unit test. public function count($type, $args = array()) { $defaults = array( 'thumbs_only' => false, ); $args = wp_parse_args($args, $defaults); $count = 0; switch($type) { case 'thumbnails' : $count = count($this->get('thumbnails')); break; case 'webps': $count = count(array_unique($this->getWebps())); break; case 'avifs': $count = count(array_unique($this->getAvifs())); break; case 'retinas': $count = count(array_unique($this->getRetinas())); break; case 'optimized': if (false === $args['thumbs_only'] && $this->isOptimized() ) { $count++; } foreach($this->get('thumbnails') as $name => $object) { if ($object->isOptimized()) { $count++; } } break; case 'user_excluded': if (false === $args['thumbs_only'] && $this->isUserExcluded() ) { $count++; } foreach($this->get('thumbnails') as $name => $object) { if ($object->isUserExcluded()) { $count++; } } break; } return $count; } public function handleOptimized($optimizeData, $args = array()) { $return = true; $wpmeta = wp_get_attachment_metadata($this->get('id')); $WPMLduplicates = $this->getWPMLDuplicates(); $fs = \wpSPIO()->filesystem(); if (isset($optimizeData['files']) && isset($optimizeData['data'])) { $files = $optimizeData['files']; $data = $optimizeData['data']; } else { Log::addError('Something went wrong with handleOptimized', $optimizeData); } $optimized = array(); // Main file has a index. $mainFile = (isset($files) && isset($files[$this->mainImageKey])) ? $files[$this->mainImageKey] : false; // If converted and not using regular backup as leading. $isConverted = (true === $this->getMeta()->convertMeta()->isConverted() && true === $this->getMeta()->convertMeta()->omitBackup()); $args['isConverted'] = $isConverted; if (! $this->isOptimized() && isset($mainFile['image']) ) // main file might not be contained in results { $result = parent::handleOptimized($mainFile, $args); if (! $result) { return false; } if ($this->getMeta('resize') == true) { $wpmeta['width'] = $this->get('width'); $wpmeta['height'] = $this->get('height'); } $filesize = $this->getFileSize(); if ($this->is_virtual() && $filesize == -1 && $this->getMeta('compressedSize') > 0) { $filesize = $this->getMeta('compressedSize'); } $wpmeta['filesize'] = $filesize; } $this->handleOptimizedFileType($mainFile); $compressionType = $this->getMeta('compressionType'); // CompressionType not set on subimages etc. // If thumbnails should not be optimized, they should not be in result Array. // #### THUMBNAILS #### $thumbObjs = $this->getThumbObjects(); // Add doubles to the processing list. Doubles are sizes with the same result, but should be copied to it's respective thumbnail file + backup. if (isset($data['doubles'])) { foreach($data['doubles'] as $doubleName => $doubleRef) { $files[$doubleName] = $files[$doubleRef]; if (isset($thumbObjs[$doubleName])) { $doubleObj = $thumbObjs[$doubleName]; $data['sizes'][$doubleName] = $doubleObj->getFileName(); } else { Log::addError('Double thumb not set in result: ' . $doubleName, $doubleRef); } } } foreach($data['sizes'] as $sizeName => $fileName) { // Check if thumbnail is in the tempfiles return set. This might not always be the case if (! isset($files[$sizeName]) ) { continue; } if ($sizeName === $this->mainImageKey) { continue; } $resultData = $files[$sizeName]; $thumbnail = (isset($thumbObjs[$sizeName])) ? $thumbObjs[$sizeName] : false; if (! is_object($thumbnail)) { Log::addError('Thumbnail with size name: ' . $sizeName . ' is not registered in this image. This should not happen, skipping.', $thumbObjs); Log::addError('OptimizeData', $optimizeData); continue; } $thumbnail->handleOptimizedFileType($resultData); // check for webps /etc if ($thumbnail->isOptimized()) { continue; } // Catch serious issues witht thumbnails, ignore the user ones. if they come back, try to process. if (!$thumbnail->isProcessable() && false === $thumbnail->isUserExcluded() ) { Log::addWarn('Optimized thumbnail signalled as not processable :' . $sizeName); continue; // when excluded. } $result = false; $thumbnail->setMeta('compressionType', $compressionType); $result = $thumbnail->handleOptimized($resultData, $args); // Always update the WP meta - except for unlisted files. if ($thumbnail->get('imageType') == self::IMAGE_TYPE_THUMB && $thumbnail->getMeta('file') === null) { $size = $thumbnail->get('size'); if ($thumbnail->getMeta('resize') == true) { $wpmeta['sizes'][$size]['width'] = $thumbnail->get('width'); $wpmeta['sizes'][$size]['height'] = $thumbnail->get('height'); } $filesize = $thumbnail->getFileSize(); if ($thumbnail->is_virtual() && $filesize == -1 && $thumbnail->getMeta('compressedSize') > 0) { $filesize = $thumbnail->getMeta('compressedSize'); } $wpmeta['sizes'][$size]['filesize'] = $filesize; } if ($thumbnail->get('prevent_next_try') !== false) // in case of fatal issues. { $this->preventNextTry($thumbnail->get('prevent_next_try')); $return = false; //failed } } // Add duplicates. Duplicates are metadata sizes that have a same file ( identical ) defined pointing. if (isset($data['duplicates'])) { foreach($data['duplicates'] as $duplicateName => $duplicateRef) { if ($duplicateName === $this->mainImageKey) { $thumbnail = $this; } elseif ($duplicateName === $this->originalImageKey) { $thumbnail = $this->getOriginalFile(); } else { $thumbnail = isset($thumbObjs[$duplicateName]) ? $thumbObjs[$duplicateName] : null; } if ($duplicateRef === $this->mainImageKey) { $duplicateObj = $this; } elseif ($duplicateRef === $this->originalImageKey) { $duplicateObj = $this->getOriginalFile(); } else { $duplicateObj = $thumbObjs[$duplicateRef]; } if (is_object($thumbnail) && is_object($duplicateObj)) { $databaseID = $thumbnail->getMeta('databaseID'); $thumbnail->setMetaObj($duplicateObj->getMetaObj()); $thumbnail->setMeta('databaseID', $databaseID); // keep dbase id the same, otherwise it won't write this thumb to DB due to same ID. } else { Log::AddError('Duplicate Thumbnail not available: ' . $duplicateName . ' or ref ' . $duplicateRef); } } } // Remove Temp Files $this->flushOptimizeData(); $this->saveMeta(); update_post_meta($this->get('id'), '_wp_attachment_metadata', $wpmeta); if (is_array($WPMLduplicates) && count($WPMLduplicates) > 0) { // Run the WPML duplicates foreach($WPMLduplicates as $duplicate_id) { // Get this Object cacheless, because it can create records when loading. $duplicateObj = $fs->getImage($duplicate_id, 'media', false); // Save the exact same data under another post. Don't duplicate it, when already there. if ($duplicateObj->getParent() === false) { $this->createDuplicateRecord($duplicate_id); } $duplicate_meta = wp_get_attachment_metadata($duplicate_id); // If duplicate metadata doesn't not exist in error state, array_merge could fail. Just don't update without data as well. if (is_array($duplicate_meta)) { $duplicate_meta = array_merge($duplicate_meta, $wpmeta); update_post_meta($duplicate_id, '_wp_attachment_metadata', $duplicate_meta); } } } return $return; } public function getImprovements() { $improvements = array(); $count = 0; $totalsize = 0; $totalperc = 0; if ($this->isOptimized()) { $perc = $this->getImprovement(); $size = $this->getImprovement(true); if (! is_null($size)) $totalsize += $size; if (! is_null($perc)) $totalperc += $perc; $improvements['main'] = array($perc, $size); $count++; } foreach($this->thumbnails as $thumbObj) { if (! $thumbObj->isOptimized()) continue; if (! isset($improvements['thumbnails'])) { $improvements['thumbnails'] = array(); } $perc = $thumbObj->getImprovement(); $size = $thumbObj->getImprovement(true); if (! is_null($size)) { $totalsize += $size; } if (! is_null($perc)) { $totalperc += $perc; } $improvements['thumbnails'][$thumbObj->name] = array($perc, $size); $count++; } if ($count == 0) return false; // no improvements; $improvements['totalpercentage'] = round($totalperc / $count); $improvements['totalsize'] = $totalsize; return $improvements; } /** Function to go from path -> thumbnail mode. This should be used for unlisted etc, but nothing that already is loaded in thumbnails. * @param String Full Path to the Thumbnail File * @return Object ThumbnailModel * */ private function getThumbnailModel($path, $size) { $thumbObj = new MediaLibraryThumbnailModel($path, $this->id, $size); return $thumbObj; } protected function loadMeta() { $metadata = $this->getDBMeta(); $settings = \wpSPIO()->settings(); $this->image_meta = new ImageMeta(); $fs = \wpSPIO()->fileSystem(); if (! $metadata) { // Thumbnails is a an array of ThumbnailModels $this->thumbnails = $this->loadThumbnailsFromWP(); $result = $this->checkLegacy(); if ($result) { $this->saveMeta(); } } elseif (is_object($metadata) ) { $this->image_meta->fromClass($metadata->image_meta); // Loads thumbnails from the WordPress installation to ensure fresh list, discover later added, etc. $thumbnails = $this->loadThumbnailsFromWP(); foreach($thumbnails as $name => $thumbObj) { if (isset($metadata->thumbnails[$name])) // Check WP thumbs against our metadata. { $thumbMeta = new ImageThumbnailMeta(); $thumbMeta->fromClass($metadata->thumbnails[$name]); // Load Thumbnail data from our saved Meta in model $thumbnails[$name]->setMetaObj($thumbMeta); $thumbnails[$name]->verifyImage(); unset($metadata->thumbnails[$name]); } } // Load Unlisted Thumbnails. if (property_exists($metadata,'thumbnails') && count($metadata->thumbnails) > 0) // unlisted in WordPress metadata sizes. Might be special unlisted one, one that was removed etc. { foreach($metadata->thumbnails as $name => $thumbMeta) //