c5fed66237
All tested with PHP 8.4 and PHP 8.3 too Major changes: - cube root Math (cbrt) now throws InvalidArgumentException if NAN is returned instead of returning NAN - Byte convert from string to int will throw errors if value is too large (\LengthException) - new flag for returning string type but for this bcmath must be installed (\RuntimeException if no bcmath) - Updated curl class and remove close handler as not needed and deprecated as of PHP 8.5 - Curl phpunit tests: convert string to JSON convert flow for return content check (to avoid per PHP version check) - image close handler for ImageMagick removed as not needed and deprecated as of PHP 8.5 - updated all check calls too use phive tools if possible (except phpunit) and all scripts can have dynamic php version set
547 lines
18 KiB
PHP
547 lines
18 KiB
PHP
<?php
|
|
|
|
/*
|
|
* image thumbnail, rotate, etc
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace CoreLibs\Output;
|
|
|
|
class Image
|
|
{
|
|
/**
|
|
* converts picture to a thumbnail with max x and max y size
|
|
* TOOD: needs mandatory options for ImageMagic convert, paths, etc folders
|
|
*
|
|
* @param string $pic source image file with or without path
|
|
* @param int $size_x maximum size width
|
|
* @param int $size_y maximum size height
|
|
* @param string $dummy empty, or file_type to show an icon
|
|
* instead of nothing if file is not found
|
|
* @param string $path if source start is not ROOT path,
|
|
* if empty ROOT is choosen
|
|
* @param string $cache_source cache path, if not given TMP is used
|
|
* @param bool $clear_cache if set to true, will create thumb all the tame
|
|
* @return string thumbnail name
|
|
* @throws \RuntimeException no ImageMagick convert command found
|
|
*/
|
|
public static function createThumbnail(
|
|
string $pic,
|
|
int $size_x,
|
|
int $size_y,
|
|
string $dummy = '',
|
|
string $path = '',
|
|
string $cache_source = '',
|
|
bool $clear_cache = false
|
|
): string {
|
|
// get image type flags
|
|
$image_types = [
|
|
0 => 'UNKOWN-IMAGE',
|
|
1 => 'gif',
|
|
2 => 'jpg',
|
|
3 => 'png'
|
|
];
|
|
$return_data = '';
|
|
$CONVERT = '';
|
|
// if CONVERT is not defined, abort
|
|
/** @phan-suppress-next-line PhanUndeclaredConstant */
|
|
if (defined('CONVERT') && is_executable(CONVERT)) {
|
|
/** @phan-suppress-next-line PhanUndeclaredConstant */
|
|
$CONVERT = CONVERT;
|
|
} else {
|
|
throw new \RuntimeException('CONVERT set binary is not executable or CONVERT is not defined');
|
|
}
|
|
if (!empty($cache_source)) {
|
|
$tmp_src = $cache_source;
|
|
} else {
|
|
$tmp_src = BASE . TMP;
|
|
}
|
|
// check if pic has a path, and override next sets
|
|
if (strstr($pic, '/') === false) {
|
|
if (empty($path)) {
|
|
$path = BASE;
|
|
}
|
|
$filename = $path . MEDIA . PICTURES . $pic;
|
|
} else {
|
|
$filename = $pic;
|
|
// and get the last part for pic (the filename)
|
|
$tmp = explode('/', $pic);
|
|
$pic = $tmp[(count($tmp) - 1)];
|
|
}
|
|
// does this picture exist and is it a picture
|
|
if (!file_exists($filename) || !is_file($filename)) {
|
|
if (!empty($dummy) && strstr($dummy, '/') === false) {
|
|
// check if we have the "dummy" image flag set
|
|
$filename = PICTURES . ICONS . strtoupper($dummy) . ".png";
|
|
/** @phpstan-ignore-next-line */
|
|
if (!empty($dummy) && file_exists($filename) && is_file($filename)) {
|
|
$return_data = $filename;
|
|
} else {
|
|
throw new \RuntimeException('Could not set dummy return file: ' . $dummy . ' in ' . $filename);
|
|
}
|
|
} else {
|
|
$return_data = $dummy;
|
|
}
|
|
return $return_data;
|
|
}
|
|
// resize image
|
|
[$width, $height, $type] = getimagesize($filename) ?: [0, 0, 0];
|
|
$convert_prefix = '';
|
|
$create_file = false;
|
|
$delete_filename = '';
|
|
// check if we can skip the PDF creation: if we have size, if do not have type, we assume type png
|
|
if (!$type) {
|
|
$check_thumb = $tmp_src . 'thumb_' . $pic . '_' . $size_x . 'x' . $size_y . '.' . $image_types[3];
|
|
if (!is_file($check_thumb)) {
|
|
$create_file = true;
|
|
} else {
|
|
$type = 3;
|
|
}
|
|
}
|
|
// if type is not in the list, but returns as PDF, we need to convert to JPEG before
|
|
if (!$type) {
|
|
$output = [];
|
|
$return = null;
|
|
// is this a PDF, if no, return from here with nothing
|
|
$convert_prefix = 'png:';
|
|
# TEMP convert to PNG, we then override the file name
|
|
$convert_string = $CONVERT . ' ' . $filename . ' ' . $convert_prefix . $filename . '_TEMP';
|
|
$status = exec($convert_string, $output, $return);
|
|
$filename .= '_TEMP';
|
|
// for delete, in case we need to glob
|
|
$delete_filename = $filename;
|
|
// find file, if we can't find base name, use -0 as the first one (ignore other pages in multiple ones)
|
|
if (!is_file($filename)) {
|
|
$filename .= '-0';
|
|
}
|
|
[$width, $height, $type] = getimagesize($filename) ?: [0, 0, 0];
|
|
}
|
|
// if no size given, set size to original
|
|
if (!$size_x || $size_x < 1) {
|
|
$size_x = $width;
|
|
}
|
|
if (!$size_y || $size_y < 1) {
|
|
$size_y = $height;
|
|
}
|
|
$thumb = 'thumb_' . $pic . '_' . $size_x . 'x' . $size_y . '.' . $image_types[$type];
|
|
$thumbnail = $tmp_src . $thumb;
|
|
// check if we already have this picture converted
|
|
if (!is_file($thumbnail) || $clear_cache == true) {
|
|
// convert the picture
|
|
if ($width > $size_x) {
|
|
$convert_string = $CONVERT . ' -geometry ' . $size_x . 'x ' . $filename . ' ' . $thumbnail;
|
|
$status = exec($convert_string, $output, $return);
|
|
// get the size of the converted data, if converted
|
|
if (is_file($thumbnail)) {
|
|
[$width, $height, $type] = getimagesize($thumbnail) ?: [0, 0, 0];
|
|
}
|
|
}
|
|
if ($height > $size_y) {
|
|
$convert_string = $CONVERT . ' -geometry x' . $size_y . ' ' . $filename . ' ' . $thumbnail;
|
|
$status = exec($convert_string, $output, $return);
|
|
}
|
|
}
|
|
if (!is_file($thumbnail)) {
|
|
copy($filename, $thumbnail);
|
|
}
|
|
$return_data = $thumb;
|
|
// if we have a delete filename, delete here with glob
|
|
if ($delete_filename) {
|
|
array_map('unlink', glob($delete_filename . '*') ?: []);
|
|
}
|
|
return $return_data;
|
|
}
|
|
|
|
/**
|
|
* simple thumbnail creation for jpeg, png only
|
|
* TODO: add other types like gif, etc
|
|
* - bails with false on failed create
|
|
* - if either size_x or size_y are empty (0)
|
|
* the resize is to max of one size
|
|
* if both are set, those are the max sizes (aspect ration is always ekpt)
|
|
* - if path is not given will cache folder for current path set
|
|
*
|
|
* @param string $filename source file name with full path
|
|
* @param int $thumb_width thumbnail width
|
|
* @param int $thumb_height thumbnail height
|
|
* @param string|null $cache_folder path for thumbnail cache
|
|
* @param string|null $web_folder frontend path for output
|
|
* @param bool $create_dummy if we encounter an invalid file
|
|
* create a dummy image file and return it
|
|
* @param bool $use_cache default to true, set to false to skip
|
|
* creating new image if exists
|
|
* @param bool $high_quality default to true, uses sample version,
|
|
* set to false to not use (default true)
|
|
* to use quick but less nice version
|
|
* @param int $jpeg_quality default 80, set image quality for jpeg only
|
|
* @return string thumbnail with path
|
|
* @throws \UnexpectedValueException input values for filename or cache_folder are wrong
|
|
* @throws \RuntimeException convert (gd) failed
|
|
*/
|
|
public static function createThumbnailSimple(
|
|
string $filename,
|
|
int $thumb_width = 0,
|
|
int $thumb_height = 0,
|
|
?string $cache_folder = null, // will be not null in future
|
|
?string $web_folder = null,
|
|
bool $create_dummy = true,
|
|
bool $use_cache = true,
|
|
bool $high_quality = true,
|
|
int $jpeg_quality = 80
|
|
): string {
|
|
$thumbnail = false;
|
|
$exception_message = 'Could not create thumbnail';
|
|
// $this->debug('IMAGE PREPARE', "FILE: $filename (exists "
|
|
// .(string)file_exists($filename)."), WIDTH: $thumb_width, HEIGHT: $thumb_height");
|
|
if (
|
|
$cache_folder === null ||
|
|
$web_folder === null
|
|
) {
|
|
/** @deprecated Do use cache folder and web folder parameters */
|
|
trigger_error(
|
|
'params $cache_folder and $web_folder must be set. Setting via constants is deprecated',
|
|
E_USER_DEPRECATED
|
|
);
|
|
// NOTE: we need to depracte this
|
|
$cache_folder = BASE . CONTENT_PATH . LAYOUT . CACHE . IMAGES;
|
|
$web_folder = LAYOUT . CACHE . IMAGES;
|
|
if (!is_dir($cache_folder)) {
|
|
if (false === mkdir($cache_folder)) {
|
|
$cache_folder = BASE . CONTENT_PATH . LAYOUT . CACHE;
|
|
$web_folder = LAYOUT . CACHE;
|
|
}
|
|
}
|
|
}
|
|
// check that input image exists and is either jpeg or png
|
|
// also fail if the basic CACHE folder does not exist at all
|
|
if (!file_exists($filename)) {
|
|
// return $thumbnail;
|
|
throw new \UnexpectedValueException('Missing image file: ' . $filename);
|
|
}
|
|
if (!is_dir($cache_folder)) {
|
|
// return $thumbnail;
|
|
throw new \UnexpectedValueException('Cache folder is not a directory: ' . $cache_folder);
|
|
}
|
|
if (!is_writable($cache_folder)) {
|
|
// return $thumbnail;
|
|
throw new \UnexpectedValueException('Cache folder is not writeable: ' . $cache_folder);
|
|
}
|
|
// $this->debug('IMAGE PREPARE', "FILENAME OK, THUMB WIDTH/HEIGHT OK");
|
|
[$inc_width, $inc_height, $img_type] = getimagesize($filename) ?: [0, 0, null];
|
|
$thumbnail_write_path = null;
|
|
$thumbnail_web_path = null;
|
|
// path set first
|
|
if (
|
|
$img_type == IMAGETYPE_JPEG ||
|
|
$img_type == IMAGETYPE_PNG ||
|
|
$create_dummy === true
|
|
) {
|
|
// $this->debug('IMAGE PREPARE', "IMAGE TYPE OK: ".$inc_width.'x'.$inc_height);
|
|
// set thumbnail paths
|
|
$thumbnail_write_path = $cache_folder;
|
|
$thumbnail_web_path = $web_folder;
|
|
}
|
|
// do resize or fall back on dummy run
|
|
if (
|
|
$img_type == IMAGETYPE_JPEG ||
|
|
$img_type == IMAGETYPE_PNG
|
|
) {
|
|
// if missing width or height in thumb, use the set one
|
|
if ($thumb_width == 0) {
|
|
$thumb_width = $inc_width;
|
|
}
|
|
if ($thumb_height == 0) {
|
|
$thumb_height = $inc_height;
|
|
}
|
|
// check resize parameters
|
|
if ($inc_width > $thumb_width || $inc_height > $thumb_height) {
|
|
$thumb_width_r = 1;
|
|
$thumb_height_r = 1;
|
|
// we need to keep the aspect ration on longest side
|
|
if (
|
|
($inc_height > $inc_width &&
|
|
// and the height is bigger than thumb set
|
|
$inc_height > $thumb_height) ||
|
|
// or the height is smaller or equal width
|
|
// but the width for the thumb is equal to the image height
|
|
($inc_height <= $inc_width &&
|
|
$inc_width == $thumb_width
|
|
)
|
|
) {
|
|
// $this->debug('IMAGE PREPARE', 'HEIGHT > WIDTH');
|
|
$ratio = $inc_height / $thumb_height;
|
|
$thumb_width_r = (int)ceil($inc_width / $ratio);
|
|
$thumb_height_r = $thumb_height;
|
|
} else {
|
|
// $this->debug('IMAGE PREPARE', 'WIDTH > HEIGHT');
|
|
$ratio = $inc_width / $thumb_width;
|
|
$thumb_width_r = $thumb_width;
|
|
$thumb_height_r = (int)ceil($inc_height / $ratio);
|
|
}
|
|
// $this->debug('IMAGE PREPARE', "Ratio: $ratio, Target size $thumb_width_r x $thumb_height_r");
|
|
// set output thumbnail name
|
|
$thumbnail = 'thumb-' . pathinfo($filename)['filename'] . '-'
|
|
. $thumb_width_r . 'x' . $thumb_height_r;
|
|
if (
|
|
$use_cache === false ||
|
|
!file_exists($thumbnail_write_path . $thumbnail)
|
|
) {
|
|
// image, copy source image, offset in image, source x/y, new size, source image size
|
|
if ($thumb_width_r < 1) {
|
|
$thumb_width_r = 1;
|
|
}
|
|
if ($thumb_height_r < 1) {
|
|
$thumb_height_r = 1;
|
|
}
|
|
$thumb = imagecreatetruecolor($thumb_width_r, $thumb_height_r);
|
|
if ($thumb === false) {
|
|
throw new \RuntimeException(
|
|
'imagecreatetruecolor failed: ' . $thumbnail . ', ' . $filename,
|
|
1
|
|
);
|
|
}
|
|
if ($img_type == IMAGETYPE_PNG) {
|
|
$imagecolorallocatealpha = imagecolorallocatealpha($thumb, 0, 0, 0, 127);
|
|
if ($imagecolorallocatealpha === false) {
|
|
throw new \RuntimeException(
|
|
'imagecolorallocatealpha failed: ' . $thumbnail . ', ' . $filename,
|
|
2
|
|
);
|
|
}
|
|
// preservere transaprency
|
|
imagecolortransparent(
|
|
$thumb,
|
|
$imagecolorallocatealpha
|
|
);
|
|
imagealphablending($thumb, false);
|
|
imagesavealpha($thumb, true);
|
|
}
|
|
$source = null;
|
|
switch ($img_type) {
|
|
case IMAGETYPE_JPEG:
|
|
$source = imagecreatefromjpeg($filename);
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
$source = imagecreatefrompng($filename);
|
|
break;
|
|
}
|
|
// check that we have a source image resource
|
|
if ($source !== null && $source !== false) {
|
|
// resize no shift
|
|
if ($high_quality === true) {
|
|
imagecopyresized(
|
|
$thumb,
|
|
$source,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
$thumb_width_r,
|
|
$thumb_height_r,
|
|
$inc_width,
|
|
$inc_height
|
|
);
|
|
} else {
|
|
imagecopyresampled(
|
|
$thumb,
|
|
$source,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
$thumb_width_r,
|
|
$thumb_height_r,
|
|
$inc_width,
|
|
$inc_height
|
|
);
|
|
}
|
|
// write file
|
|
switch ($img_type) {
|
|
case IMAGETYPE_JPEG:
|
|
imagejpeg($thumb, $thumbnail_write_path . $thumbnail, $jpeg_quality);
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
imagepng($thumb, $thumbnail_write_path . $thumbnail);
|
|
break;
|
|
}
|
|
} else {
|
|
throw new \RuntimeException(
|
|
'Invalid source image file. Only JPEG/PNG are allowed: ' . $filename,
|
|
3
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// we just copy over the image as is, we never upscale
|
|
$thumbnail = 'thumb-' . pathinfo($filename)['filename'] . '-' . $inc_width . 'x' . $inc_height;
|
|
if (
|
|
$use_cache === false ||
|
|
!file_exists($thumbnail_write_path . $thumbnail)
|
|
) {
|
|
copy($filename, $thumbnail_write_path . $thumbnail);
|
|
}
|
|
}
|
|
// add output path
|
|
$thumbnail = $thumbnail_web_path . $thumbnail;
|
|
} elseif ($create_dummy === true) {
|
|
// create dummy image in the thumbnail size
|
|
// if one side is missing, use the other side to create a square
|
|
if (!$thumb_width) {
|
|
$thumb_width = $thumb_height;
|
|
}
|
|
if (!$thumb_height) {
|
|
$thumb_height = $thumb_width;
|
|
}
|
|
// do we have an image already?
|
|
$thumbnail = 'thumb-' . pathinfo($filename)['filename'] . '-' . $thumb_width . 'x' . $thumb_height;
|
|
if (
|
|
$use_cache === false ||
|
|
!file_exists($thumbnail_write_path . $thumbnail)
|
|
) {
|
|
// if both are unset, set to 250
|
|
if ($thumb_height < 1) {
|
|
$thumb_height = 250;
|
|
}
|
|
if ($thumb_width < 1) {
|
|
$thumb_width = 250;
|
|
}
|
|
$thumb = imagecreatetruecolor($thumb_width, $thumb_height);
|
|
if ($thumb === false) {
|
|
throw new \RuntimeException(
|
|
'imagecreatetruecolor dummy failed: ' . $thumbnail . ', ' . $filename,
|
|
3
|
|
);
|
|
}
|
|
// add outside border px = 5% (rounded up)
|
|
// eg 50px -> 2.5px
|
|
$gray = imagecolorallocate($thumb, 200, 200, 200);
|
|
$white = imagecolorallocate($thumb, 255, 255, 255);
|
|
if ($gray === false || $white === false) {
|
|
throw new \RuntimeException(
|
|
'imagecolorallocate/imagecolorallocate dummy failed: ' . $thumbnail . ', ' . $filename,
|
|
2
|
|
);
|
|
}
|
|
// fill gray background
|
|
imagefill($thumb, 0, 0, $gray);
|
|
// now create rectangle
|
|
if (imagesx($thumb) < imagesy($thumb)) {
|
|
$width = (int)round(imagesx($thumb) / 100 * 5);
|
|
} else {
|
|
$width = (int)round(imagesy($thumb) / 100 * 5);
|
|
}
|
|
imagefilledrectangle(
|
|
$thumb,
|
|
0 + $width,
|
|
0 + $width,
|
|
imagesx($thumb) - $width,
|
|
imagesy($thumb) - $width,
|
|
$white
|
|
);
|
|
// add "No valid images source"
|
|
// OR add circle
|
|
// * find center
|
|
// * width/height is 75% of size - border
|
|
// smaller size is taken
|
|
$base_width = imagesx($thumb) > imagesy($thumb) ? imagesy($thumb) : imagesx($thumb);
|
|
// get 75% width
|
|
$cross_width = (int)round((($base_width - ($width * 2)) / 100 * 75) / 2);
|
|
$center_x = (int)round(imagesx($thumb) / 2);
|
|
$center_y = (int)round(imagesy($thumb) / 2);
|
|
imagefilledellipse($thumb, $center_x, $center_y, $cross_width, $cross_width, $gray);
|
|
// find top left and bottom left for first line
|
|
imagepng($thumb, $thumbnail_write_path . $thumbnail);
|
|
}
|
|
// add web path
|
|
$thumbnail = $thumbnail_web_path . $thumbnail;
|
|
}
|
|
// if still false -> throw exception
|
|
if ($thumbnail === false) {
|
|
throw new \RuntimeException($exception_message);
|
|
}
|
|
// else return the thumbnail name + output path web
|
|
return $thumbnail;
|
|
}
|
|
|
|
/**
|
|
* reads the rotation info of an file and rotates it to be correctly upright
|
|
* this is done because not all software honers the exit Orientation flag
|
|
* only works with jpg or png
|
|
*
|
|
* @param string $filename path + filename to rotate. This file must be writeable
|
|
* @return void
|
|
* @throws \RuntimeException if exit_read_data is not found
|
|
* @throws \UnexpectedValueException if file name not writeable or file name not found
|
|
*/
|
|
public static function correctImageOrientation(string $filename): void
|
|
{
|
|
// function exists & file is writeable, else do nothing
|
|
if (!function_exists('exif_read_data')) {
|
|
throw new \RuntimeException('Function \'exit_read_data\' does not exist');
|
|
}
|
|
if (!file_exists($filename) || !is_file($filename)) {
|
|
throw new \UnexpectedValueException('Missing image file: ' . $filename);
|
|
}
|
|
if (!is_writeable($filename)) {
|
|
throw new \UnexpectedValueException('File name is not writeable: ' . $filename);
|
|
}
|
|
[$inc_width, $inc_height, $img_type] = getimagesize($filename) ?: [0, 0, null];
|
|
// add @ to avoid "file not supported error"
|
|
$exif = @exif_read_data($filename);
|
|
$orientation = null;
|
|
$img = null;
|
|
if ($exif && isset($exif['Orientation'])) {
|
|
$orientation = $exif['Orientation'];
|
|
}
|
|
// only if we need to rotate, if 1 it is already upright
|
|
if ($orientation === null || $orientation == 1) {
|
|
return;
|
|
}
|
|
switch ($img_type) {
|
|
case IMAGETYPE_JPEG:
|
|
$img = imagecreatefromjpeg($filename);
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
$img = imagecreatefrompng($filename);
|
|
break;
|
|
}
|
|
// no image loaded (wrong type)
|
|
if ($img === null || $img === false) {
|
|
return;
|
|
}
|
|
$deg = 0;
|
|
// 1 top, 6: left, 8: right, 3: bottom
|
|
switch ($orientation) {
|
|
case 3:
|
|
$deg = 180;
|
|
break;
|
|
case 6:
|
|
$deg = -90;
|
|
break;
|
|
case 8:
|
|
$deg = 90;
|
|
break;
|
|
}
|
|
// rotate if needed
|
|
if ($deg) {
|
|
$img = imagerotate($img, $deg, 0);
|
|
}
|
|
// rotate failed
|
|
if ($img === false) {
|
|
return;
|
|
}
|
|
// then rewrite the rotated image back to the disk as $filename
|
|
switch ($img_type) {
|
|
case IMAGETYPE_JPEG:
|
|
imagejpeg($img, $filename);
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
imagepng($img, $filename);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// __END__
|