eb4292316d
Bug fixes and consolidation of various thumbnail and resize functionality Changed resize/rotate extensions to use replace image event Added content-disposition header to image responses to provide a human-friendly filename when saving Added more bulk thumbnail regeneration tools Tweaks to bulk actions to correct totals when batching items
413 lines
No EOL
13 KiB
PHP
413 lines
No EOL
13 KiB
PHP
<?php
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
|
|
* Misc functions *
|
|
\* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
/**
|
|
* Move a file from PHP's temporary area into shimmie's image storage
|
|
* hierarchy, or throw an exception trying.
|
|
*
|
|
* @throws UploadException
|
|
*/
|
|
function move_upload_to_archive(DataUploadEvent $event): void
|
|
{
|
|
$target = warehouse_path("images", $event->hash);
|
|
if (!@copy($event->tmpname, $target)) {
|
|
$errors = error_get_last();
|
|
throw new UploadException(
|
|
"Failed to copy file from uploads ({$event->tmpname}) to archive ($target): ".
|
|
"{$errors['type']} / {$errors['message']}"
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a directory full of images
|
|
*
|
|
* #return string[]
|
|
*/
|
|
function add_dir(string $base): array
|
|
{
|
|
$results = [];
|
|
|
|
foreach (list_files($base) as $full_path) {
|
|
$short_path = str_replace($base, "", $full_path);
|
|
$filename = basename($full_path);
|
|
|
|
$tags = path_to_tags($short_path);
|
|
$result = "$short_path (".str_replace(" ", ", ", $tags).")... ";
|
|
try {
|
|
add_image($full_path, $filename, $tags);
|
|
$result .= "ok";
|
|
} catch (UploadException $ex) {
|
|
$result .= "failed: ".$ex->getMessage();
|
|
}
|
|
$results[] = $result;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
function add_image(string $tmpname, string $filename, string $tags): void
|
|
{
|
|
assert(file_exists($tmpname));
|
|
|
|
$pathinfo = pathinfo($filename);
|
|
if (!array_key_exists('extension', $pathinfo)) {
|
|
throw new UploadException("File has no extension");
|
|
}
|
|
$metadata = [];
|
|
$metadata['filename'] = $pathinfo['basename'];
|
|
$metadata['extension'] = $pathinfo['extension'];
|
|
$metadata['tags'] = Tag::explode($tags);
|
|
$metadata['source'] = null;
|
|
$event = new DataUploadEvent($tmpname, $metadata);
|
|
send_event($event);
|
|
if ($event->image_id == -1) {
|
|
throw new UploadException("File type not recognised");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a full size pair of dimensions, return a pair scaled down to fit
|
|
* into the configured thumbnail square, with ratio intact
|
|
*
|
|
* #return int[]
|
|
*/
|
|
function get_thumbnail_size(int $orig_width, int $orig_height): array
|
|
{
|
|
global $config;
|
|
|
|
if ($orig_width === 0) {
|
|
$orig_width = 192;
|
|
}
|
|
if ($orig_height === 0) {
|
|
$orig_height = 192;
|
|
}
|
|
|
|
if ($orig_width > $orig_height * 5) {
|
|
$orig_width = $orig_height * 5;
|
|
}
|
|
if ($orig_height > $orig_width * 5) {
|
|
$orig_height = $orig_width * 5;
|
|
}
|
|
|
|
$max_width = $config->get_int('thumb_width');
|
|
$max_height = $config->get_int('thumb_height');
|
|
|
|
$xscale = ($max_height / $orig_height);
|
|
$yscale = ($max_width / $orig_width);
|
|
$scale = ($xscale < $yscale) ? $xscale : $yscale;
|
|
|
|
if ($scale > 1 && $config->get_bool('thumb_upscale')) {
|
|
return [(int)$orig_width, (int)$orig_height];
|
|
} else {
|
|
return [(int)($orig_width*$scale), (int)($orig_height*$scale)];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a full size pair of dimensions, return a pair scaled down to fit
|
|
* into the configured thumbnail square, with ratio intact, using thumb_scaling
|
|
*
|
|
* #return int[]
|
|
*/
|
|
function get_thumbnail_size_scaled(int $orig_width, int $orig_height): array
|
|
{
|
|
global $config;
|
|
|
|
if ($orig_width === 0) {
|
|
$orig_width = 192;
|
|
}
|
|
if ($orig_height === 0) {
|
|
$orig_height = 192;
|
|
}
|
|
|
|
if ($orig_width > $orig_height * 5) {
|
|
$orig_width = $orig_height * 5;
|
|
}
|
|
if ($orig_height > $orig_width * 5) {
|
|
$orig_height = $orig_width * 5;
|
|
}
|
|
|
|
$max_size = get_thumbnail_max_size_scaled();
|
|
$max_width = $max_size[0];
|
|
$max_height = $max_size[1];
|
|
|
|
$xscale = ($max_height / $orig_height);
|
|
$yscale = ($max_width / $orig_width);
|
|
$scale = ($xscale < $yscale) ? $xscale : $yscale;
|
|
|
|
if ($scale > 1 && $config->get_bool('thumb_upscale')) {
|
|
return [(int)$orig_width, (int)$orig_height];
|
|
} else {
|
|
return [(int)($orig_width*$scale), (int)($orig_height*$scale)];
|
|
}
|
|
}
|
|
|
|
function get_thumbnail_max_size_scaled(): array
|
|
{
|
|
global $config;
|
|
|
|
$scaling = $config->get_int("thumb_scaling");
|
|
$max_width = $config->get_int('thumb_width') * ($scaling/100);
|
|
$max_height = $config->get_int('thumb_height') * ($scaling/100);
|
|
return [$max_width, $max_height];
|
|
}
|
|
|
|
function create_thumbnail_convert($hash): bool
|
|
{
|
|
global $config;
|
|
|
|
$inname = warehouse_path("images", $hash);
|
|
$outname = warehouse_path("thumbs", $hash);
|
|
|
|
$q = $config->get_int("thumb_quality");
|
|
$convert = $config->get_string("thumb_convert_path");
|
|
|
|
if($convert==null||$convert=="")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// ffff imagemagick fails sometimes, not sure why
|
|
//$format = "'%s' '%s[0]' -format '%%[fx:w] %%[fx:h]' info:";
|
|
//$cmd = sprintf($format, $convert, $inname);
|
|
//$size = shell_exec($cmd);
|
|
//$size = explode(" ", trim($size));
|
|
$tsize = get_thumbnail_max_size_scaled();
|
|
$w = $tsize[0];
|
|
$h = $tsize[1];
|
|
|
|
|
|
// running the call with cmd.exe requires quoting for our paths
|
|
$type = $config->get_string('thumb_type');
|
|
|
|
$options = "";
|
|
if (!$config->get_bool('thumb_upscale')) {
|
|
$options .= "\>";
|
|
}
|
|
|
|
if($type=="webp") {
|
|
$format = '"%s" -thumbnail %ux%u%s -quality %u -background none "%s[0]" %s:"%s"';
|
|
} else {
|
|
$format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u "%s[0]" %s:"%s"';
|
|
}
|
|
$cmd = sprintf($format, $convert, $w, $h, $options, $q, $inname, $type, $outname);
|
|
$cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
|
|
exec($cmd, $output, $ret);
|
|
|
|
log_debug('handle_pixel', "Generating thumbnail with command `$cmd`, returns $ret");
|
|
|
|
if ($config->get_bool("thumb_optim", false)) {
|
|
exec("jpegoptim $outname", $output, $ret);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function create_thumbnail_ffmpeg($hash): bool
|
|
{
|
|
global $config;
|
|
|
|
$ffmpeg = $config->get_string("thumb_ffmpeg_path");
|
|
if($ffmpeg==null||$ffmpeg=="") {
|
|
return false;
|
|
}
|
|
|
|
$inname = warehouse_path("images", $hash);
|
|
$outname = warehouse_path("thumbs", $hash);
|
|
|
|
$orig_size = video_size($inname);
|
|
$scaled_size = get_thumbnail_size_scaled($orig_size[0], $orig_size[1]);
|
|
|
|
$codec = "mjpeg";
|
|
$quality = $config->get_int("thumb_quality");
|
|
if($config->get_string("thumb_type")=="webp") {
|
|
$codec = "libwebp";
|
|
} else {
|
|
// mjpeg quality ranges from 2-31, with 2 being the best quality.
|
|
$quality = floor(31 - (31 * ($quality/100)));
|
|
if($quality<2) {
|
|
$quality = 2;
|
|
}
|
|
}
|
|
|
|
$args = [
|
|
escapeshellarg($ffmpeg),
|
|
"-y", "-i", escapeshellarg($inname),
|
|
"-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}",
|
|
"-f", "image2",
|
|
"-vframes", "1",
|
|
"-c:v", $codec,
|
|
"-q:v", $quality,
|
|
escapeshellarg($outname),
|
|
];
|
|
|
|
$cmd = escapeshellcmd(implode(" ", $args));
|
|
|
|
exec($cmd, $output, $ret);
|
|
|
|
if ((int)$ret == (int)0) {
|
|
log_debug('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret");
|
|
return true;
|
|
} else {
|
|
log_error('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function video_size(string $filename): array
|
|
{
|
|
global $config;
|
|
$ffmpeg = $config->get_string("thumb_ffmpeg_path");
|
|
$cmd = escapeshellcmd(implode(" ", [
|
|
escapeshellarg($ffmpeg),
|
|
"-y", "-i", escapeshellarg($filename),
|
|
"-vstats"
|
|
]));
|
|
$output = shell_exec($cmd . " 2>&1");
|
|
// error_log("Getting size with `$cmd`");
|
|
|
|
$regex_sizes = "/Video: .* ([0-9]{1,4})x([0-9]{1,4})/";
|
|
if (preg_match($regex_sizes, $output, $regs)) {
|
|
if (preg_match("/displaymatrix: rotation of (90|270).00 degrees/", $output)) {
|
|
$size = [$regs[2], $regs[1]];
|
|
} else {
|
|
$size = [$regs[1], $regs[2]];
|
|
}
|
|
} else {
|
|
$size = [1, 1];
|
|
}
|
|
log_debug('imageboard/misc', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]");
|
|
return $size;
|
|
}
|
|
|
|
/**
|
|
* Check Memory usage limits
|
|
*
|
|
* Old check: $memory_use = (filesize($image_filename)*2) + ($width*$height*4) + (4*1024*1024);
|
|
* New check: $memory_use = $width * $height * ($bits_per_channel) * channels * 2.5
|
|
*
|
|
* It didn't make sense to compute the memory usage based on the NEW size for the image. ($width*$height*4)
|
|
* We need to consider the size that we are GOING TO instead.
|
|
*
|
|
* The factor of 2.5 is simply a rough guideline.
|
|
* http://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
|
|
*/
|
|
function calc_memory_use(array $info): int
|
|
{
|
|
if (isset($info['bits']) && isset($info['channels'])) {
|
|
$memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024;
|
|
} else {
|
|
// If we don't have bits and channel info from the image then assume default values
|
|
// of 8 bits per color and 4 channels (R,G,B,A) -- ie: regular 24-bit color
|
|
$memory_use = ($info[0] * $info[1] * 1 * 4 * 2.5) / 1024;
|
|
}
|
|
return (int)$memory_use;
|
|
}
|
|
|
|
function image_resize_gd(String $image_filename, array $info, int $new_width, int $new_height,
|
|
string $output_filename=null, string $output_type=null, int $output_quality = 80)
|
|
{
|
|
$width = $info[0];
|
|
$height = $info[1];
|
|
|
|
if($output_type==null) {
|
|
/* If not specified, output to the same format as the original image */
|
|
switch ($info[2]) {
|
|
case IMAGETYPE_GIF: $output_type = "gif"; break;
|
|
case IMAGETYPE_JPEG: $output_type = "jpeg"; break;
|
|
case IMAGETYPE_PNG: $output_type = "png"; break;
|
|
case IMAGETYPE_WEBP: $output_type = "webp"; break;
|
|
case IMAGETYPE_BMP: $output_type = "bmp"; break;
|
|
default: throw new ImageResizeException("Failed to save the new image - Unsupported image type.");
|
|
}
|
|
}
|
|
|
|
$memory_use = calc_memory_use($info);
|
|
$memory_limit = get_memory_limit();
|
|
if ($memory_use > $memory_limit) {
|
|
throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
|
|
}
|
|
|
|
$image = imagecreatefromstring(file_get_contents($image_filename));
|
|
|
|
if($image==false) {
|
|
throw new ImageResizeException("Could not load image: ".$image_filename);
|
|
}
|
|
|
|
$image_resized = imagecreatetruecolor($new_width, $new_height);
|
|
|
|
// Handle transparent images
|
|
switch($info[2]) {
|
|
case IMAGETYPE_GIF:
|
|
$transparency = imagecolortransparent($image);
|
|
$palletsize = imagecolorstotal($image);
|
|
|
|
// If we have a specific transparent color
|
|
if ($transparency >= 0 && $transparency < $palletsize) {
|
|
// Get the original image's transparent color's RGB values
|
|
$transparent_color = imagecolorsforindex($image, $transparency);
|
|
|
|
// Allocate the same color in the new image resource
|
|
$transparency = imagecolorallocate($image_resized, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
|
|
|
|
// Completely fill the background of the new image with allocated color.
|
|
imagefill($image_resized, 0, 0, $transparency);
|
|
|
|
// Set the background color for new image to transparent
|
|
imagecolortransparent($image_resized, $transparency);
|
|
}
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
case IMAGETYPE_WEBP:
|
|
//
|
|
// More info here: http://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
|
|
//
|
|
imagealphablending($image_resized, false);
|
|
imagesavealpha($image_resized, true);
|
|
$transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127);
|
|
imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color);
|
|
break;
|
|
}
|
|
|
|
// Actually resize the image.
|
|
imagecopyresampled(
|
|
$image_resized,
|
|
$image,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
$new_width,
|
|
$new_height,
|
|
$width,
|
|
$height
|
|
);
|
|
|
|
switch($output_type) {
|
|
case "bmp":
|
|
$result = imagebmp($image_resized, $output_filename, true);
|
|
break;
|
|
case "webp":
|
|
$result = imagewebp($image_resized, $output_filename, $output_quality);
|
|
break;
|
|
case "jpg":
|
|
case "jpeg":
|
|
$result = imagejpeg($image_resized, $output_filename, $output_quality);
|
|
break;
|
|
case "png":
|
|
$result = imagepng($image_resized, $output_filename, 9);
|
|
break;
|
|
case "gif":
|
|
$result = imagegif($image_resized, $output_filename);
|
|
break;
|
|
default:
|
|
throw new ImageResizeException("Failed to save the new image - Unsupported image type: $output_type");
|
|
}
|
|
if($result==false) {
|
|
throw new ImageResizeException("Failed to save the new image, function returned false when saving type: $output_type");
|
|
}
|
|
imagedestroy($image_resized);
|
|
} |