Added webp upload and thumbnailing support

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
This commit is contained in:
Matthew Barbour 2019-06-09 13:22:48 -05:00
parent 49cb6f7233
commit eb4292316d
23 changed files with 650 additions and 364 deletions

View File

@ -27,7 +27,7 @@
<IfModule mod_expires.c>
ExpiresActive On
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|css|js))$">
<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|webp|css|js))$">
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=2629743"
</IfModule>
@ -46,6 +46,7 @@
AddType image/jpeg jpg jpeg
AddType image/gif gif
AddType image/png png
AddType image/webp webp
#EXT: handle_ico
AddType image/x-icon ico ani cur

View File

@ -35,3 +35,10 @@ class ImageDoesNotExist extends SCoreException
class InvalidInput extends SCoreException
{
}
/*
* This is used by the image resizing code when there is not enough memory to perform a resize.
*/
class InsufficientMemoryException extends SCoreException
{
}

View File

@ -219,12 +219,20 @@ abstract class DataHandlerExtension extends Extension
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{
$result = false;
if ($this->supported_ext($event->type)) {
if (method_exists($this, 'create_thumb_force') && $event->force == true) {
$this->create_thumb_force($event->hash);
if($event->force) {
$result = $this->create_thumb($event->hash);
} else {
$this->create_thumb($event->hash);
$outname = warehouse_path("thumbs", $event->hash);
if(file_exists($outname)) {
return;
}
$result = $this->create_thumb($event->hash);
}
}
if($result) {
$event->generated = true;
}
}

View File

@ -99,6 +99,10 @@ class ThumbnailGenerationEvent extends Event
/** @var bool */
public $force;
/** @var bool */
public $generated;
/**
* Request a thumbnail be made for an image object
*/
@ -107,6 +111,7 @@ class ThumbnailGenerationEvent extends Event
$this->hash = $hash;
$this->type = $type;
$this->force = $force;
$this->generated = false;
}
}

View File

@ -385,12 +385,22 @@ class Image
return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
}
/**
* Get the nicely formatted version of the file name
*/
public function get_nice_image_name(): string
{
return $this->parse_link_template('$id - $tags.$ext');
}
/**
* Get the URL for the thumbnail
*/
public function get_thumb_link(): string
{
return $this->get_link('image_tlink', '_thumbs/$hash/thumb.jpg', 'thumb/$id.jpg');
global $config;
$ext = $config->get_string("thumb_type");
return $this->get_link('image_tlink', '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
}
/**

View File

@ -105,3 +105,309 @@ function get_thumbnail_size(int $orig_width, int $orig_height): array
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);
}

View File

@ -264,7 +264,8 @@ const MIME_TYPE_MAP = [
'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav',
'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php',
'mp4' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm'
'mp4' => 'video/mp4', 'ogv' => 'video/ogg', 'webm' => 'video/webm',
'webp' => 'image/webp'
];
/**

View File

@ -36,12 +36,14 @@ class BulkActionEvent extends Event
public $action;
public $items;
public $page_request;
public $running_total;
function __construct(String $action, PageRequestEvent $pageRequestEvent, array $items)
function __construct(String $action, PageRequestEvent $pageRequestEvent, array $items, int $running_total = 0)
{
$this->action = $action;
$this->page_request = $pageRequestEvent;
$this->items = $items;
$this->running_total = $running_total;
}
}
@ -78,7 +80,8 @@ class BulkActions extends Extension
switch ($event->action) {
case "Delete":
if ($user->can("delete_image")) {
$this->delete_items($event->items);
$event->running_total += $this->delete_items($event->items);
flash_message("Deleted $event->running_total items");
}
break;
case "Tag":
@ -92,7 +95,8 @@ class BulkActions extends Extension
$replace = true;
}
$this->tag_items($event->items, $tags, $replace);
$event->running_total += $this->tag_items($event->items, $tags, $replace);
flash_message("Tagged $event->running_total items");
}
break;
case "Set Source":
@ -101,7 +105,8 @@ class BulkActions extends Extension
}
if ($user->can("bulk_edit_image_source")) {
$source = $_POST['bulk_source'];
$this->set_source($event->items, $source);
$event->running_total += $this->set_source($event->items, $source);
flash_message("Set source for $event->running_total items");
}
break;
}
@ -144,10 +149,10 @@ class BulkActions extends Extension
}
reset($items); // rewind to first element in array.
$newEvent = new BulkActionEvent($action, $event, $items);
$newEvent = new BulkActionEvent($action, $event, $items, $n);
send_event($newEvent);
$n += 100;
$n = $newEvent->running_total;
}
}
}
@ -162,7 +167,7 @@ class BulkActions extends Extension
}
}
private function delete_items(array $items)
private function delete_items(array $items): int
{
$total = 0;
foreach ($items as $item) {
@ -173,11 +178,10 @@ class BulkActions extends Extension
flash_message("Error while removing $item->id: " . $e->getMessage(), "error");
}
}
flash_message("Deleted $total items");
return $total;
}
private function tag_items(array $items, string $tags, bool $replace)
private function tag_items(array $items, string $tags, bool $replace): int
{
$tags = Tag::explode($tags);
@ -211,10 +215,10 @@ class BulkActions extends Extension
}
}
flash_message("Tagged $total items");
return $total;
}
private function set_source(array $items, String $source)
private function set_source(array $items, String $source): int
{
$total = 0;
foreach ($items as $item) {
@ -226,6 +230,6 @@ class BulkActions extends Extension
}
}
flash_message("Set source for $total items");
return $total;
}
}

View File

@ -58,6 +58,7 @@ class ET extends Extension
$info['thumb_width'] = $config->get_int('thumb_width');
$info['thumb_height'] = $config->get_int('thumb_height');
$info['thumb_scaling'] = $config->get_int('thumb_scaling');
$info['thumb_type'] = $config->get_string('thumb_type');
$info['thumb_mem'] = $config->get_int("thumb_mem_limit");
$info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images");

View File

@ -37,6 +37,7 @@ Disk use: {$info['sys_disk']}
Thumbnail Generation:
Engine: {$info['thumb_engine']}
Type: {$info['thumb_type']}
Memory: {$info['thumb_mem']}
Quality: {$info['thumb_quality']}
Width: {$info['thumb_width']}

View File

@ -3,14 +3,18 @@
* Name: Handle Flash
* Author: Shish <webmaster@shishnet.org>
* Link: http://code.shishnet.org/shimmie2/
* Description: Handle Flash files. (No thumbnail is generated for flash files)
* Description: Handle Flash files.
*/
class FlashFileHandler extends DataHandlerExtension
{
protected function create_thumb(string $hash): bool
{
global $config;
if(!create_thumbnail_ffmpeg($hash)) {
copy("ext/handle_flash/thumb.jpg", warehouse_path("thumbs", $hash));
}
return true;
}

View File

@ -24,13 +24,6 @@ class IcoFileHandler extends Extension
}
}
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
{
if ($this->supported_ext($event->type)) {
$this->create_thumb($event->hash);
}
}
public function onDisplayingImage(DisplayingImageEvent $event)
{
global $page;

View File

@ -3,14 +3,14 @@
* Name: Handle Pixel
* Author: Shish <webmaster@shishnet.org>
* Link: http://code.shishnet.org/shimmie2/
* Description: Handle JPEG, PNG, GIF, etc files
* Description: Handle JPEG, PNG, GIF, WEBP, etc files
*/
class PixelFileHandler extends DataHandlerExtension
{
protected function supported_ext(string $ext): bool
{
$exts = ["jpg", "jpeg", "gif", "png"];
$exts = ["jpg", "jpeg", "gif", "png", "webp"];
$ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext;
return in_array(strtolower($ext), $exts);
}
@ -39,7 +39,7 @@ class PixelFileHandler extends DataHandlerExtension
protected function check_contents(string $tmpname): bool
{
$valid = [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG];
$valid = [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_WEBP];
if (!file_exists($tmpname)) {
return false;
}
@ -54,15 +54,6 @@ class PixelFileHandler extends DataHandlerExtension
}
protected function create_thumb(string $hash): bool
{
$outname = warehouse_path("thumbs", $hash);
if (file_exists($outname)) {
return true;
}
return $this->create_thumb_force($hash);
}
protected function create_thumb_force(string $hash): bool
{
global $config;
@ -77,7 +68,7 @@ class PixelFileHandler extends DataHandlerExtension
$ok = $this->make_thumb_gd($inname, $outname);
break;
case 'convert':
$ok = $this->make_thumb_convert($inname, $outname);
$ok = create_thumbnail_convert($hash);
break;
}
@ -98,90 +89,31 @@ class PixelFileHandler extends DataHandlerExtension
", 20);
}
// IM thumber {{{
private function make_thumb_convert(string $inname, string $outname): bool
// GD thumber {{{
private function make_thumb_gd(string $inname, string $outname): bool
{
global $config;
$q = $config->get_int("thumb_quality");
$convert = $config->get_string("thumb_convert_path");
// 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));
$size = getimagesize($inname);
$tsize = get_thumbnail_size_scaled($size[0] , $size[1]);
$w = $tsize[0];
$h = $tsize[1];
// running the call with cmd.exe requires quoting for our paths
$format = '"%s" "%s[0]" -extent %ux%u -flatten -strip -thumbnail %ux%u -quality %u jpg:"%s"';
$cmd = sprintf($format, $convert, $inname, $size[0], $size[1], $w, $h, $q, $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);
try {
$info = getimagesize($inname);
$tsize = get_thumbnail_size_scaled($info[0], $info[1]);
$image = image_resize_gd($inname, $info, $tsize[0], $tsize[1],
$outname, $config->get_string('thumb_type'),$config->get_int('thumb_quality'));
} catch(InsufficientMemoryException $e) {
$tsize = get_thumbnail_max_size_scaled();
$thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64));
$white = imagecolorallocate($thumb, 255, 255, 255);
$black = imagecolorallocate($thumb, 0, 0, 0);
imagefill($thumb, 0, 0, $white);
log_warning("handle_pixel","Insufficient memory while creating thumbnail: ".$e->getMessage());
imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black);
return true;
} catch(Exception $e) {
log_error("handle_pixel","Error while creating thumbnail: ".$e->getMessage());
return false;
}
return true;
}
// }}}
// GD thumber {{{
private function make_thumb_gd(string $inname, string $outname): bool
{
global $config;
$thumb = $this->get_thumb($inname);
$ok = imagejpeg($thumb, $outname, $config->get_int('thumb_quality'));
imagedestroy($thumb);
return $ok;
}
private function get_thumb(string $tmpname)
{
global $config;
$info = getimagesize($tmpname);
$width = $info[0];
$height = $info[1];
$memory_use = (filesize($tmpname)*2) + ($width*$height*4) + (4*1024*1024);
$memory_limit = get_memory_limit();
if ($memory_use > $memory_limit) {
$tsize = get_thumbnail_size_scaled($width, $height);
$w = $tsize[0];
$h = $tsize[1];
$thumb = imagecreatetruecolor($w, min($h, 64));
$white = imagecolorallocate($thumb, 255, 255, 255);
$black = imagecolorallocate($thumb, 0, 0, 0);
imagefill($thumb, 0, 0, $white);
imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black);
return $thumb;
} else {
$image = imagecreatefromstring(file_get_contents($tmpname));
$tsize = get_thumbnail_size_scaled($width, $height);
$thumb = imagecreatetruecolor($tsize[0], $tsize[1]);
imagecopyresampled(
$thumb,
$image,
0,
0,
0,
0,
$tsize[0],
$tsize[1],
$width,
$height
);
return $thumb;
}
}
// }}}
}

View File

@ -3,12 +3,12 @@
* Name: Handle SVG
* Author: Shish <webmaster@shishnet.org>
* Link: http://code.shishnet.org/shimmie2/
* Description: Handle static SVG files. (No thumbnail is generated for SVG files)
* Description: Handle static SVG files.
*/
use enshrined\svgSanitize\Sanitizer;
class SVGFileHandler extends Extension
class SVGFileHandler extends DataHandlerExtension
{
public function onDataUpload(DataUploadEvent $event)
{
@ -32,13 +32,12 @@ class SVGFileHandler extends Extension
}
}
public function onThumbnailGeneration(ThumbnailGenerationEvent $event)
protected function create_thumb(string $hash): bool
{
if ($this->supported_ext($event->type)) {
$hash = $event->hash;
if(!create_thumbnail_convert($hash)) {
copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash));
}
return true;
}
public function onDisplayingImage(DisplayingImageEvent $event)
@ -68,13 +67,13 @@ class SVGFileHandler extends Extension
}
}
private function supported_ext(string $ext): bool
protected function supported_ext(string $ext): bool
{
$exts = ["svg"];
return in_array(strtolower($ext), $exts);
}
private function create_image_from_data(string $filename, array $metadata): Image
protected function create_image_from_data(string $filename, array $metadata): Image
{
$image = new Image();
@ -92,7 +91,7 @@ class SVGFileHandler extends Extension
return $image;
}
private function check_contents(string $file): bool
protected function check_contents(string $file): bool
{
if (!file_exists($file)) {
return false;

View File

@ -55,62 +55,16 @@ class VideoFileHandler extends DataHandlerExtension
*/
protected function create_thumb(string $hash): bool
{
global $config;
$ok = false;
$ffmpeg = $config->get_string("thumb_ffmpeg_path");
$inname = warehouse_path("images", $hash);
$outname = warehouse_path("thumbs", $hash);
$orig_size = $this->video_size($inname);
$scaled_size = get_thumbnail_size_scaled($orig_size[0], $orig_size[1]);
$cmd = escapeshellcmd(implode(" ", [
escapeshellarg($ffmpeg),
"-y", "-i", escapeshellarg($inname),
"-vf", "scale={$scaled_size[0]}:{$scaled_size[1]}",
"-ss", "00:00:00.0",
"-f", "image2",
"-vframes", "1",
escapeshellarg($outname),
]));
exec($cmd, $output, $ret);
if ((int)$ret == (int)0) {
$ok = true;
log_error('handle_video', "Generating thumbnail with command `$cmd`, returns $ret");
} else {
log_debug('handle_video', "Generating thumbnail with command `$cmd`, returns $ret");
}
$ok = create_thumbnail_ffmpeg($hash);
return $ok;
}
protected function video_size(string $filename)
protected 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('handle_video', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]");
return $size;
return video_size($filename);
}
protected function supported_ext(string $ext): bool

View File

@ -21,6 +21,7 @@ class ImageIO extends Extension
$config->set_default_int('thumb_height', 192);
$config->set_default_int('thumb_scaling', 100);
$config->set_default_int('thumb_quality', 75);
$config->set_default_string('thumb_type', 'jpg');
$config->set_default_int('thumb_mem_limit', parse_shorthand_int('8MB'));
$config->set_default_string('thumb_convert_path', 'convert');
@ -138,8 +139,15 @@ class ImageIO extends Extension
$thumbers['Built-in GD'] = "gd";
$thumbers['ImageMagick'] = "convert";
$thumb_types = [];
$thumb_types['JPEG'] = "jpg";
$thumb_types['WEBP'] = "webp";
$sb = new SetupBlock("Thumbnailing");
$sb->add_choice_option("thumb_engine", $thumbers, "Engine: ");
$sb->add_label("<br>");
$sb->add_choice_option("thumb_type", $thumb_types, "Filetype: ");
$sb->add_label("<br>Size ");
$sb->add_int_option("thumb_width");
@ -245,7 +253,13 @@ class ImageIO extends Extension
if (!is_null($image)) {
$page->set_mode("data");
if ($type == "thumb") {
$ext = $config->get_string("thumb_type");
if (array_key_exists($ext, MIME_TYPE_MAP)) {
$page->set_type(MIME_TYPE_MAP[$ext]);
} else {
$page->set_type("image/jpeg");
}
$file = $image->get_thumb_filename();
} else {
$page->set_type($image->get_mime_type());
@ -264,6 +278,9 @@ class ImageIO extends Extension
$page->set_data("");
} else {
$page->add_http_header("Last-Modified: $gmdate_mod");
if ($type != "thumb") {
$page->add_http_header("Content-Disposition: inline; filename=".$image->get_nice_image_name());
}
$page->set_data(file_get_contents($file));
if ($config->get_int("image_expires")) {
@ -306,6 +323,7 @@ class ImageIO extends Extension
and have it stored in a 'replaced images' list that could be
inspected later by an admin?
*/
log_debug("image", "Removing image with hash ".$existing->hash);
$existing->remove_image_only(); // Actually delete the old image file from disk
@ -324,6 +342,9 @@ class ImageIO extends Extension
]
);
/* Generate new thumbnail */
send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext)));
log_info("image", "Replaced Image #{$id} with ({$image->hash})");
}
// }}} end replace

View File

@ -11,6 +11,6 @@ class QRImage extends Extension
{
public function onDisplayingImage(DisplayingImageEvent $event)
{
$this->theme->links_block(make_http(make_link('image/'.$event->image->id.'.jpg')));
$this->theme->links_block(make_http(make_link('image/'.$event->image->id.'.'.$event->image->ext)));
}
}

View File

@ -166,6 +166,7 @@ class Ratings extends Extension
$rating = $_POST['bulk_rating'];
foreach ($event->items as $image) {
send_event(new RatingSetEvent($image, $rating));
$event->running_total++;
}
}
break;

View File

@ -15,12 +15,13 @@
class RegenThumb extends Extension
{
public function regenerate_thumbnail($image)
public function regenerate_thumbnail($image, $force = true): string
{
global $database;
send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true));
$event = new ThumbnailGenerationEvent($image->hash, $image->ext, $force);
send_event($event);
$database->cache->delete("thumb-block:{$image->id}");
return $event->generated;
}
public function onPageRequest(PageRequestEvent $event)
@ -68,7 +69,7 @@ class RegenThumb extends Extension
global $user;
if ($user->can("delete_image")) {
$event->add_action("Regen Thumbnails");
$event->add_action("Regen Thumbnails","",$this->theme->bulk_html());
}
}
@ -80,15 +81,126 @@ class RegenThumb extends Extension
switch($event->action) {
case "Regen Thumbnails":
if ($user->can("delete_image")) {
$total = 0;
foreach ($event->items as $image) {
$this->regenerate_thumbnail($image);
$total++;
$force = true;
if(isset($_POST["bulk_regen_thumb_missing_only"])
&&$_POST["bulk_regen_thumb_missing_only"]=="true")
{
$force=false;
}
flash_message("Regenerated thumbnails for $total items");
foreach ($event->items as $image) {
if($this->regenerate_thumbnail($image, $force)) {
$event->running_total++;
}
}
flash_message("Regenerated thumbnails for $event->running_total items");
}
break;
}
}
public function onAdminBuilding(AdminBuildingEvent $event)
{
$this->theme->display_admin_block();
}
public function onAdminAction(AdminActionEvent $event) {
global $database;
switch($event->action) {
case "regen_thumbs":
$event->redirect = true;
$force = false;
if(isset($_POST["regen_thumb_force"])&&$_POST["regen_thumb_force"]=="true") {
$force=true;
}
$limit = 1000;
if(isset($_POST["regen_thumb_limit"])&&is_numeric($_POST["regen_thumb_limit"])) {
$limit=intval($_POST["regen_thumb_limit"]);
}
$type = "";
if(isset($_POST["regen_thumb_limit"])) {
$type = $_POST["regen_thumb_type"];
}
$images = $this->get_images($type);
$i = 0;
foreach ($images as $image) {
if(!$force) {
$path = warehouse_path("thumbs", $image["hash"], false);
if(file_exists($path)) {
continue;
}
}
$event = new ThumbnailGenerationEvent($image["hash"], $image["ext"], $force);
send_event($event);
if($event->generated) {
$i++;
}
if($i>=$limit) {
break;
}
}
flash_message("Re-generated $i thumbnails");
break;
case "delete_thumbs":
$event->redirect = true;
if(isset($_POST["delete_thumb_type"])&&$_POST["delete_thumb_type"]!="") {
$images = $this->get_images($_POST["delete_thumb_type"]);
$i = 0;
foreach ($images as $image) {
$outname = warehouse_path("thumbs", $image["hash"]);
if(file_exists($outname)) {
unlink($outname);
$i++;
}
}
flash_message("Deleted $i thumbnails for ".$_POST["delete_thumb_type"]." images");
} else {
$dir = "data/thumbs/";
$this->remove_dir_recursively($dir);
flash_message("Deleted all thumbnails");
}
break;
}
}
function get_images(String $ext = null)
{
global $database;
$query = "SELECT hash, ext FROM images";
$args = [];
if($ext!=null&&$ext!="") {
$query .= " WHERE ext = :ext";
$args["ext"] = $ext;
}
return $database->get_all($query, $args);
}
function remove_dir_recursively($dir)
{
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
if (filetype($dir."/".$object) == "dir") {
$this->remove_dir_recursively($dir."/".$object);
} else {
unlink ($dir."/".$object);
}
}
}
reset($objects);
rmdir($dir);
}
}
}

View File

@ -37,4 +37,48 @@ class RegenThumbTheme extends Themelet
";
return $html;
}
public function bulk_html() {
return "<label><input type='checkbox' name='bulk_regen_thumb_missing_only' id='bulk_regen_thumb_missing_only' style='width:13px' value='true' />Only missing thumbs</label>";
}
public function display_admin_block()
{
global $page, $database;
$types = [];
$results = $database->get_all("SELECT ext, count(*) count FROM images group by ext");
foreach ($results as $result) {
array_push($types,"<option value='".$result["ext"]."'>".$result["ext"]." (".$result["count"].")</option>");
}
$html = "
Will only regenerate missing thumbnails, unless force is selected. Force will override the limit and will likely take a very long time to process.
<p>".make_form(make_link("admin/regen_thumbs"))."
<table class='form'>
<tr><th><label for='regen_thumb_force'>Force</label></th><td><input type='checkbox' name='regen_thumb_force' id='regen_thumb_force' value='true' /></td></tr>
<tr><th><label for='regen_thumb_limit'>Limit</label></th><td><input type='number' name='regen_thumb_limit' id='regen_thumb_limit' value='1000' /></td></tr>
<tr><th><label for='regen_thumb_type'>Type</label></th><td>
<select name='regen_thumb_type' id='regen_thumb_type' value='1000'>
<option value=''>All</option>
".implode($types)."
</select>
</td></tr>
<tr><td colspan='2'><input type='submit' value='Regenerate Thumbnails'></td></tr>
</table>
</form></p>
<p>".make_form(make_link("admin/delete_thumbs"),"POST",False, "","return confirm('Are you sure you want to delete all thumbnails?')")."
<table class='form'>
<tr><th><label for='delete_thumb_type'>Type</label></th><td>
<select name='delete_thumb_type' id='delete_thumb_type' value='1000'>
<option value=''>All</option>
".implode($types)."
</select>
</td></tr>
<tr><td colspan='2'><input type='submit' value='Delete Thumbnails'></td></tr>
</table>
</form></p>
";
$page->add_block(new Block("Regen Thumbnails", $html));
}
}

View File

@ -69,7 +69,7 @@ class ResizeImage extends Extension
$image_obj = Image::by_id($event->image_id);
if ($config->get_bool("resize_upload") == true && ($image_obj->ext == "jpg" || $image_obj->ext == "png" || $image_obj->ext == "gif")) {
if ($config->get_bool("resize_upload") == true && ($image_obj->ext == "jpg" || $image_obj->ext == "png" || $image_obj->ext == "gif" || $image_obj->ext == "webp")) {
$width = $height = 0;
if ($config->get_int("resize_default_width") !== 0) {
@ -159,11 +159,6 @@ class ResizeImage extends Extension
// Private functions
/* ----------------------------- */
/**
* This function could be made much smaller by using the ImageReplaceEvent
* ie: Pretend that we are replacing the image with a resized copy.
*/
private function resize_image(Image $image_obj, int $width, int $height)
{
global $database;
@ -174,134 +169,42 @@ class ResizeImage extends Extension
$hash = $image_obj->hash;
$image_filename = warehouse_path("images", $hash);
$info = getimagesize($image_filename);
/* Get the image file type */
$pathinfo = pathinfo($image_obj->filename);
$filetype = strtolower($pathinfo['extension']);
$info = getimagesize($image_filename);
if (($image_obj->width != $info[0]) || ($image_obj->height != $info[1])) {
throw new ImageResizeException("The current image size does not match what is set in the database! - Aborting Resize.");
}
$memory_use = $this->calc_memory_use($info);
$memory_limit = get_memory_limit();
if ($memory_use > $memory_limit) {
throw new ImageResizeException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
}
list($new_height, $new_width) = $this->calc_new_size($image_obj, $width, $height);
/* Attempt to load the image */
switch ($info[2]) {
case IMAGETYPE_GIF: $image = imagecreatefromgif($image_filename); break;
case IMAGETYPE_JPEG: $image = imagecreatefromjpeg($image_filename); break;
case IMAGETYPE_PNG: $image = imagecreatefrompng($image_filename); break;
default:
throw new ImageResizeException("Unsupported image type (Only GIF, JPEG, and PNG are supported).");
}
// Handle transparent images
$image_resized = imagecreatetruecolor($new_width, $new_height);
if ($info[2] == IMAGETYPE_GIF) {
$transparency = imagecolortransparent($image);
// If we have a specific transparent color
if ($transparency >= 0) {
// 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);
}
} elseif ($info[2] == IMAGETYPE_PNG) {
//
// 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);
}
// Actually resize the image.
imagecopyresampled($image_resized, $image, 0, 0, 0, 0, $new_width, $new_height, $image_obj->width, $image_obj->height);
/* Temp storage while we resize */
$tmp_filename = tempnam("/tmp", 'shimmie_resize');
if (empty($tmp_filename)) {
throw new ImageResizeException("Unable to save temporary image file.");
}
/* Output to the same format as the original image */
switch ($info[2]) {
case IMAGETYPE_GIF: imagegif($image_resized, $tmp_filename); break;
case IMAGETYPE_JPEG: imagejpeg($image_resized, $tmp_filename); break;
case IMAGETYPE_PNG: imagepng($image_resized, $tmp_filename); break;
default:
throw new ImageResizeException("Failed to save the new image - Unsupported image type.");
}
image_resize_gd($image_filename, $info, $new_width, $new_height, $tmp_filename);
$new_image = new Image();
$new_image->hash = md5_file($tmp_filename);
$new_image->filesize = filesize($tmp_filename);
$new_image->filename = 'resized-'.$image_obj->filename;
$new_image->width = $new_width;
$new_image->height = $new_height;
$new_image->ext = $image_obj->ext;
/* Move the new image into the main storage location */
$new_hash = md5_file($tmp_filename);
$new_size = filesize($tmp_filename);
$target = warehouse_path("images", $new_hash);
$target = warehouse_path("images", $new_image->hash);
if (!@copy($tmp_filename, $target)) {
throw new ImageResizeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)");
}
$new_filename = 'resized-'.$image_obj->filename;
/* Remove temporary file */
@unlink($tmp_filename);
/* Delete original image and thumbnail */
log_debug("image", "Removing image with hash ".$hash);
$image_obj->remove_image_only();
send_event(new ImageReplaceEvent($image_obj->id, $new_image));
/* Generate new thumbnail */
send_event(new ThumbnailGenerationEvent($new_hash, $filetype));
/* Update the database */
$database->Execute("
UPDATE images SET filename = :filename, filesize = :filesize, hash = :hash, width = :width, height = :height
WHERE id = :id
", [
"filename"=>$new_filename, "filesize"=>$new_size, "hash"=>$new_hash,
"width"=>$new_width, "height"=>$new_height, "id"=>$image_obj->id
]);
log_info("resize", "Resized Image #{$image_obj->id} - New hash: {$new_hash}");
}
/**
* 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
*/
private 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;
log_info("resize", "Resized Image #{$image_obj->id} - New hash: {$new_image->hash}");
}
/**

View File

@ -106,11 +106,6 @@ class RotateImage extends Extension
// Private functions
/* ----------------------------- */
/**
* This function could be made much smaller by using the ImageReplaceEvent
* ie: Pretend that we are replacing the image with a rotated copy.
*/
private function rotate_image(int $image_id, int $deg)
{
global $database;
@ -129,24 +124,10 @@ class RotateImage extends Extension
if (file_exists($image_filename)==false) {
throw new ImageRotateException("$image_filename does not exist.");
}
$info = getimagesize($image_filename);
/* Get the image file type */
$pathinfo = pathinfo($image_obj->filename);
$filetype = strtolower($pathinfo['extension']);
/*
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
*/
$memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024;
$memory_use =calc_memory_use ($info);
$memory_limit = get_memory_limit();
if ($memory_use > $memory_limit) {
@ -155,12 +136,10 @@ class RotateImage extends Extension
/* Attempt to load the image */
switch ($info[2]) {
case IMAGETYPE_GIF: $image = imagecreatefromgif($image_filename); break;
case IMAGETYPE_JPEG: $image = imagecreatefromjpeg($image_filename); break;
case IMAGETYPE_PNG: $image = imagecreatefrompng($image_filename); break;
default:
throw new ImageRotateException("Unsupported image type or ");
$image = imagecreatefromstring(file_get_contents($image_filename));
if($image==false) {
throw new ImageRotateException("Could not load image: ".$image_filename);
}
/* Rotate and resample the image */
@ -185,7 +164,15 @@ class RotateImage extends Extension
}
*/
$image_rotated = imagerotate($image, $deg, 0);
$background_color = 0;
switch($info[2]){
case IMAGETYPE_PNG:
case IMAGETYPE_WEBP:
$background_color = imagecolorallocatealpha($image, 0, 0, 0, 127);
break;
}
$image_rotated = imagerotate($image, $deg, $background_color);
/* Temp storage while we rotate */
$tmp_filename = tempnam(ini_get('upload_tmp_dir'), 'shimmie_rotate');
@ -195,48 +182,40 @@ class RotateImage extends Extension
/* Output to the same format as the original image */
switch ($info[2]) {
case IMAGETYPE_GIF: imagegif($image_rotated, $tmp_filename); break;
case IMAGETYPE_JPEG: imagejpeg($image_rotated, $tmp_filename); break;
case IMAGETYPE_PNG: imagepng($image_rotated, $tmp_filename); break;
case IMAGETYPE_GIF: $result = imagegif($image_rotated, $tmp_filename); break;
case IMAGETYPE_JPEG: $result = imagejpeg($image_rotated, $tmp_filename); break;
case IMAGETYPE_PNG: $result = imagepng($image_rotated, $tmp_filename,9); break;
case IMAGETYPE_WEBP: $result = imagewebp($image_rotated, $tmp_filename); break;
case IMAGETYPE_BMP: $result = imagebmp($image_rotated, $tmp_filename,true); break;
default:
throw new ImageRotateException("Unsupported image type.");
}
if($result==false) {
throw new ImageRotateException("Could not save image: ".$tmp_filename);
}
list($new_width, $new_height) = getimagesize($tmp_filename);
$new_image = new Image();
$new_image->hash = md5_file($tmp_filename);
$new_image->filesize = filesize($tmp_filename);
$new_image->filename = 'rotated-'.$image_obj->filename;
$new_image->width = $new_width;
$new_image->height = $new_height;
$new_image->ext = $image_obj->ext;
/* Move the new image into the main storage location */
$new_hash = md5_file($tmp_filename);
$new_size = filesize($tmp_filename);
$target = warehouse_path("images", $new_hash);
$target = warehouse_path("images", $new_image->hash);
if (!@copy($tmp_filename, $target)) {
throw new ImageRotateException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)");
}
$new_filename = 'rotated-'.$image_obj->filename;
list($new_width, $new_height) = getimagesize($target);
/* Remove temporary file */
@unlink($tmp_filename);
/* Delete original image and thumbnail */
log_debug("image", "Removing image with hash ".$hash);
$image_obj->remove_image_only();
send_event(new ImageReplaceEvent($image_id, $new_image));
/* Generate new thumbnail */
send_event(new ThumbnailGenerationEvent($new_hash, $filetype));
/* Update the database */
$database->Execute(
"UPDATE images SET
filename = :filename, filesize = :filesize, hash = :hash, width = :width, height = :height
WHERE
id = :id
",
[
"filename"=>$new_filename, "filesize"=>$new_size, "hash"=>$new_hash,
"width"=>$new_width, "height"=>$new_height, "id"=>$image_id
]
);
log_info("rotate", "Rotated Image #{$image_id} - New hash: {$new_hash}");
log_info("rotate", "Rotated Image #{$image_id} - New hash: {$new_image->hash}");
}
}

View File

@ -219,7 +219,7 @@ class UploadTheme extends Themelet
$html .= ' (Drag &amp; drop onto your bookmarks toolbar, then click when looking at an image)';
// Bookmarklet checks if shimmie supports ext. If not, won't upload to site/shows alert saying not supported.
$supported_ext = "jpg jpeg gif png";
$supported_ext = "jpg jpeg gif png webp";
if (class_exists("FlashFileHandler")) {
$supported_ext .= " swf";
}