getImage($id, $type_id); $image = $this->createThumbnail($image); $this->outputImage($image); } public function returnError($message) { wpj_debug($message); throw new \UnexpectedValueException(join(': ', $message)); } public function getImage($id, $type_id, $lang = null, ?string $version = null) { global $cfg; $type = $type_id; $settings = getImageSettings($type); if ($version && isset($settings['sizes'][$version])) { $settings = array_merge($settings, $settings['sizes'][$version]); } $settings['size'] ??= reset($settings['sizes'])['size']; $photo_type = getVal('type', $settings, $type); $fileData = $this->getFileData($id, $photo_type, $lang, $version); $path = $fileData['path']; if (!empty($fileData['data'])) { $settings['data'] = $fileData['data']; } if ($id === '0' || empty($path)) { $path = '../../'.$this->getPlaceholderFile($lang); } $ext = findModule(Modules::COMPONENTS) ? 'webp' : 'jpg'; if (isset($settings['svg']) && $settings['svg'] && pathinfo($path, PATHINFO_EXTENSION) == 'svg') { $ext = 'svg'; } // Set quality if (!$settings['quality']) { $settings['quality'] = $ext == 'webp' ? 82 : 77; if ($settings['size'][0] < 100 || $settings['size'][1] < 100 || $type_id == 0) { $settings['quality'] = $ext == 'webp' ? 90 : 85; } } // Set sharpening if (is_null($settings['sharpen'])) { $settings['sharpen'] = $type_id != 0 ? 0.7 : 0; } $path_thumbnail = getImagePath($id, $type_id, $ext, $lang, $version); $path_original = $cfg['Path']['photos'].$path; return ['original' => $path_original, 'thumbnail' => $path_thumbnail, 'settings' => $settings]; } public function scaleImage($image) { // DEBUG information wpj_debug(['scaleImage', $image]); $path_original = $image['original']; $path_thumbnail = $image['thumbnail']; $settings = $image['settings']; if (pathinfo($path_thumbnail, PATHINFO_EXTENSION) == 'svg') { $thumbnail_path_info = pathinfo($path_thumbnail); if (!is_dir($thumbnail_path_info['dirname'])) { wpj_debug('Create OUTPUT directory: '.$thumbnail_path_info['dirname']); if (!$this->createDir($thumbnail_path_info['dirname'], 0777)) { wpj_debug('Couldnt create OUTPUT directory'); } } copy(realpath($path_original), realpath('.').'/'.$path_thumbnail); return $image; } $thumbnail_path_info = pathinfo($path_thumbnail); $original_path_info = pathinfo($path_original); $original_extension = strtolower($original_path_info['extension']); if (!findModule(\Modules::COMPONENTS)) { // Force PNG source images as JPEG if PNG is not allowed if ($original_extension == 'png' && !$settings['png']) { $original_extension = 'jpeg'; } } // Initialize image try { $thumbnail = new \Imagick(realpath($path_original)); $original_size = array_values($thumbnail->getImageGeometry()); if (!$thumbnail->valid() || !$original_size[0] || !$original_size[1]) { throw new \ImagickException('Not valid image'); } } catch (\ImagickException $e) { // Mark photo as invalid if there is photo ID if ($id_photo = $settings['data']['id_photo'] ?? null) { sqlQueryBuilder()->update('photos') ->set('data', 'JSON_MERGE_PATCH(COALESCE(data, "{}"), \'{"invalid":true}\')') ->where(\Query\Operator::equals(['id' => $id_photo])) ->execute(); } throw new NotFoundHttpException('Invalid image data', $e); } // Remove invalid flag if image loaded successfully if ($settings['data']['invalid'] ?? null) { sqlQueryBuilder()->update('photos') ->set('data', 'JSON_REMOVE(data, "$.invalid")') ->where(\Query\Operator::equals(['id' => $settings['data']['id_photo']])) ->execute(); } // Set background if ($settings['background']) { $hexBackground = $settings['background']; if (is_int($settings['background'])) { $hexBackground = '#'.dechex($settings['background']); } $thumbnail->setImageBackgroundColor($hexBackground); } $formatsWithTransparentBg = ['png', 'webp', 'avif', 'gif']; if (in_array($original_extension, $formatsWithTransparentBg)) { $hexBackground ??= '#FFFFFF'; // Set background color in RGBA format // Transparent but with correct background color if image is later converted to non-transparent format $thumbnail->setImageBackgroundColor(new \ImagickPixel($hexBackground.'00')); } elseif (in_array(strtolower($thumbnail->getImageFormat()), $formatsWithTransparentBg)) { $thumbnail = $thumbnail->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN); } if ($settings['upscale'] || $original_size[0] > $settings['size'][0] || $original_size[1] > $settings['size'][1]) { if ($original_extension == 'gif') { $thumbnail = $thumbnail->coalesceImages(); do { $thumbnail = $this->cropImage($settings, $thumbnail); } while ($thumbnail->nextImage()); $thumbnail = $thumbnail->deconstructImages(); } else { $thumbnail = $this->cropImage($settings, $thumbnail); } } $thumbnail_size = array_values($thumbnail->getImageGeometry()); if ($settings['removeBackground'] ?? false) { if ($thumbnail->getImageColorspace() != \Imagick::COLORSPACE_SRGB) { $thumbnail->transformImageColorspace(\Imagick::COLORSPACE_SRGB); } [$bR, $bG, $bB] = sscanf('0x'.dechex($settings['removeBackground']), '0x%02x%02x%02x'); $r = $bR / 255; $g = $bG / 255; $b = $bB / 255; $thumbnail->colorMatrixImage([ $r, 0.0, 0.0, 0.0, 0.0, 0.0, $g, 0.0, 0.0, 0.0, 0.0, 0.0, $b, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, ]); } // Sharpen small images $scaleFactor = min($original_size[0] / $settings['size'][0], $original_size[1] / $settings['size'][1]); if ($settings['sharpen'] > 0 && $scaleFactor > 2) { if ($thumbnail->getImageColorspace() != \Imagick::COLORSPACE_SRGB) { $thumbnail->transformImageColorspace(\Imagick::COLORSPACE_SRGB); } $thumbnail->sharpenImage(1, $settings['sharpen']); } // aplikovat contrast jen na opravdu male fotky - u vetsich fotek to delalo problemy if (is_null($settings['contrast'])) { $thumbnailMaxSize = max($settings['size'][0], $settings['size'][1]); $settings['contrast'] = $thumbnailMaxSize <= 300; } if ($settings['contrast']) { $thumbnail->contrastImage(1); } // Apply watermark if ($settings['watermark']) { if (is_bool($settings['watermark'])) { if (findModule(Modules::COMPONENTS)) { $settings['watermark'] = ServiceContainer::getService(SystemImageUtils::class)->getImageSrc('watermark', 'png'); if (!file_exists($settings['watermark'])) { throw new \Exception('Watermark image does not exist'); } } else { $settings['watermark'] = 'templates/images/watermark.png'; } } $watermark = new \Imagick(realpath($settings['watermark'])); // resize the watermark $watermark->scaleImage($thumbnail_size[0], $thumbnail_size[1], true); $wm_size = array_values($watermark->getImageGeometry()); // calculate the position $wm_pos = [($thumbnail_size[0] - $wm_size[0]) / 2, $thumbnail_size[1] - $wm_size[1]]; $thumbnail->compositeImage($watermark, \Imagick::COMPOSITE_OVER, $wm_pos[0], $wm_pos[1]); } // Create output folder if necesary if (!is_dir($thumbnail_path_info['dirname'])) { wpj_debug('Create OUTPUT directory: '.$thumbnail_path_info['dirname']); if (!$this->createDir($thumbnail_path_info['dirname'], 0777)) { wpj_debug('Couldnt create OUTPUT directory'); } } if (!chmod($thumbnail_path_info['dirname'], 0777)) { wpj_debug('Could not change OUTPUT directory rights'); } // Save image using proper image function if (findModule(Modules::COMPONENTS)) { switch ($original_extension) { case 'jpg': case 'jpeg': case 'gif': $thumbnail->setImageFormat('webp'); $thumbnail->setOption('webp:lossless', 'false'); $thumbnail->setOption('webp:use-sharp-yuv', '1'); $thumbnail->setImageCompressionQuality($settings['quality']); break; case 'png': $thumbnail->setImageFormat('webp'); $thumbnail->setOption('webp:lossless', 'true'); break; default: wpj_debug('ERROR!! Unsupported OUTPUT image type!'); } } else { switch ($original_extension) { case 'jpg': case 'jpeg': $thumbnail->setImageFormat('jpeg'); $thumbnail->setImageCompressionQuality($settings['quality']); break; case 'gif': $thumbnail->setImageFormat('gif'); break; case 'png': $thumbnail->setImageFormat('png'); break; default: wpj_debug('ERROR!! Unsupported OUTPUT image type!'); } } $thumbnail->stripImage(); $path_format = $path_thumbnail; $imageFormat = strtolower($thumbnail->getImageFormat()); if (!in_array($imageFormat, ['jpeg', 'svg', 'heic'])) { $path_format .= ".{$imageFormat}"; } $thumbnail->writeImages(realpath('.').'/'.$path_format, true); if ($path_format != $path_thumbnail) { rename($path_format, $path_thumbnail); } // Change attributes, rwx+ugo chmod($path_thumbnail, 0777); wpj_debug('Image "'.$path_thumbnail.'" OK'); $thumbnail->destroy(); return $image; } public function createThumbnail($image) { $fileUtil = new FileUtil(); if (!file_exists($image['original'])) { $this->returnError(['Source file does not exist', $image['original']]); } $skipCache = getVal('skip_cache'); if (!$fileUtil->isFileNewer($image['thumbnail'], $image['original'], $time_thumbnail) || $skipCache) { if ($skipCache && getVal('wpj_debug')) { $image['thumbnail'] .= '.tmp'; } $this->scaleImage($image); $time_thumbnail = time(); } $image['thumbnail'] .= "?{$time_thumbnail}"; return $image; } public function outputImage($image) { if (getVal('wpj_debug')) { exit(" "); } $src = parse_url($image['thumbnail'], PHP_URL_PATH); if (!file_exists($src)) { $this->returnError(['Scaled image does not exists. Error in scaling?', $image]); } // Pokud mám zapnutý modul CDN, můžu vrátit rovnou data, protože se to zacachuje a nebude už příště zdržovat if (findModule(\Modules::PROXY_CACHE)) { $response = new BinaryFileResponse($src); $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); $response->setPublic()->setMaxAge(3600 * 24 * 30); return $response; } // Jinak zbaběle vracím redirect, aby browser načetl thumbnail a nemusel jsem ho tahat přes PHP redirection(StringUtil::absoluteUrl($image['thumbnail'])); } /** * @param array $image Image descriptor created using getImage method * * @return array Array with "width" and "height" keys containing dimension in pixels */ public function getOutputSize($image) { $thumbnail = new \Imagick(realpath($image['thumbnail'])); return $thumbnail->getImageGeometry(); } /** * @param $thumbnail \Imagick * * @return \Imagick */ public function cropImage(&$settings, $thumbnail) { // Apply image cropping/sizing if (is_bool($settings['crop'])) { $settings['crop'] = $settings['crop'] ? 'crop' : 'fit'; } switch ($settings['crop']) { // Just stretch image to fit thumbnail size, fill remaining with background. Returns exact thumbnail size. case 'fit': // Resize $thumbnail->resizeImage($settings['size'][0], $settings['size'][1], \Imagick::FILTER_CATROM, 1, true); // Center in the middle of canvas $size = array_values($thumbnail->getImageGeometry()); $size[0] = ($settings['size'][0] - $size[0]) / 2; $size[1] = ($settings['size'][1] - $size[1]) / 2; $thumbnail->extentImage($settings['size'][0], $settings['size'][1], -$size[0], -$size[1]); break; // Scale image to fit thumbnail size and crop remaining thumbnail edges. Modifies thumbnail size. case 'scale': $thumbnail->resizeImage($settings['size'][0], $settings['size'][1], \Imagick::FILTER_CATROM, 1, true); break; // Scale image to fill whole thumbnail and crop to get exact thumbnail size case 'crop': default: $origSize = array_values($thumbnail->getImageGeometry()); $yWhenResizedToX = $origSize[1] * ($settings['size'][0] / $origSize[0]); $settings['crop'] = ($yWhenResizedToX < $settings['size'][1]) ? 'width' : 'height'; // Scale image to fill whole thumbnail width/height and crop to get exact thumbnail size // no break case 'width': case 'height': if ($settings['crop'] == 'height') { $thumbnail->resizeImage($settings['size'][0], 0, \Imagick::FILTER_CATROM, 1, false); } else { $thumbnail->resizeImage(0, $settings['size'][1], \Imagick::FILTER_CATROM, 1, false); } $centerPoint = [($settings['data']['center_point']['x'] ?? 50) / 100, ($settings['data']['center_point']['y'] ?? 50) / 100]; $size = array_values($thumbnail->getImageGeometry()); $offset = [0, 0]; $calcOffset = function ($axis) use ($settings, $centerPoint, $size) { if ($settings['size'][$axis] > $size[$axis]) { return ($settings['size'][0] - $size[0]) / 2; } $offset = ($settings['size'][$axis] - 2 * $centerPoint[$axis] * $size[$axis]) / 2; return min(max($settings['size'][$axis] - $size[$axis], $offset), 0); }; if ($settings['crop'] == 'height') { $offset[1] = $calcOffset(1); } else { $offset[0] = $calcOffset(0); } $thumbnail->extentImage($settings['size'][0], $settings['size'][1], -$offset[0], -$offset[1]); break; } return $thumbnail; } /** * @param null $lang * * @return array */ protected function getFileData($id, $photo_type, $lang = null, ?string $version = null) { $file = null; $folder = null; switch ($photo_type) { case 'return_delivery': $file = returnSQLResult('SELECT photo FROM return_delivery WHERE id=:id', ['id' => $id]); $folder = '../return_delivery/'; break; case 'section': $file = returnSQLResult('SELECT photo FROM sections WHERE id=:id', ['id' => $id]); $folder = '../section/'; break; case 'articles_authors': $file = returnSQLResult('SELECT photo FROM articles_authors WHERE id=:id', ['id' => $id]); $folder = '../articles_authors/'; break; case 'producer': $file = returnSQLResult('SELECT photo FROM producers WHERE id=:id', ['id' => $id]); $folder = '../producer/'; break; case 'delivery': $delivery = DeliveryType::getDeliveries(true)[$id] ?? false; if (!$delivery) { $this->returnError(['Unknown delivery', $id]); } $file = $delivery->getPhotoPath(); $folder = ''; break; case 'payment': $payment = DeliveryType::getPayments(true)[$id] ?? false; if (!$payment) { $this->returnError(['Unknown payment', $id]); } $file = $payment['class']->getPhotoPath($payment['photo_name'] ?? null); $folder = ''; break; default: if (is_null($lang)) { $lang = Contexts::get(LanguageContext::class)->getDefaultId(); } $SQL = sqlQueryBuilder() ->select('ph.source, ph.image_2, ph.image_tablet, ph.image_mobile, ph.data') ->from('photos', 'ph') ->andWhere(\Query\Operator::equals(['ph.id' => $id])) ->andWhere(\Query\Translation::coalesceTranslatedFields(PhotosTranslation::class, null, $lang)) ->execute(); if ($id !== '0') { if (!sqlNumRows($SQL)) { $this->returnError(['Image ID does not exists', $id]); } $photo = sqlFetchAssoc($SQL); $defaultKey = $key = 'image_2'; if ($version) { $key = 'image_'.$version; } // fallback to default photo if specific version does not exists if (empty($photo[$key])) { $key = $defaultKey; } $file = $photo[$key]; $folder = $photo['source']; $additionalData['data'] = json_decode($photo['data'] ?? '', true); $additionalData['data']['id_photo'] = $id; break; } } if (empty($file)) { return ['path' => '']; } return array_merge(['path' => $folder.$file], $additionalData ?? []); } /** * @param null $lang * * @return string */ public function getFilePath($id, $photo_type, $lang = null, ?string $version = null) { return $this->getFileData($id, $photo_type, $lang, $version)['path']; } public function createDir($dir, $rights) { wpj_debug('wpj_create_dir('.$dir.','.$rights.')'); $return = 0; if (!is_dir($dir)) { if (strchr($dir, '/')) { $return = $this->createDir(substr($dir, 0, strlen($dir) - strlen(strrchr($dir, '/'))), $rights); } $return = $return | mkdir($dir, $rights); chmod($dir, 0777); } return $return; } private function getPlaceholderFile(?string $lang = null): string { global $cfg; $defaultImagePath = ServiceContainer::getService(SystemImageUtils::class)->getSystemImagePlaceholderPath(); if (!is_array($cfg['Photo']['placeholder'] ?? null)) { return getVal('placeholder', $cfg['Photo'], $defaultImagePath); } if (!$lang) { $lang = Contexts::get(LanguageContext::class)->getActiveId(); } return $cfg['Photo']['placeholder']['lang'][$lang] ?? $cfg['Photo']['placeholder']['default'] ?? $defaultImagePath; } } if (empty($subclass)) { class Image extends ImageBase { } }