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
{
}
}