Merge commit '38cc05c' into develop

This commit is contained in:
Shish 2019-07-31 14:58:24 +01:00
commit d57b624079
32 changed files with 1788 additions and 689 deletions

View File

@ -58,7 +58,7 @@ class BaseThemelet
$tsize = get_thumbnail_size($image->width, $image->height); $tsize = get_thumbnail_size($image->width, $image->height);
} else { } else {
//Use max thumbnail size if using thumbless filetype //Use max thumbnail size if using thumbless filetype
$tsize = get_thumbnail_size($config->get_int('thumb_width'), $config->get_int('thumb_height')); $tsize = get_thumbnail_size($config->get_int(ImageConfig::THUMB_WIDTH), $config->get_int(ImageConfig::THUMB_WIDTH));
} }
$custom_classes = ""; $custom_classes = "";

View File

@ -54,6 +54,19 @@ class Image
/** @var boolean */ /** @var boolean */
public $locked = false; public $locked = false;
/** @var boolean */
public $lossless = null;
/** @var boolean */
public $video = null;
/** @var boolean */
public $audio = null;
/** @var int */
public $length = null;
/** /**
* One will very rarely construct an image directly, more common * One will very rarely construct an image directly, more common
* would be to use Image::by_id, Image::by_hash, etc. * would be to use Image::by_id, Image::by_hash, etc.
@ -463,7 +476,7 @@ class Image
*/ */
public function get_image_link(): string public function get_image_link(): string
{ {
return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext'); return $this->get_link(ImageConfig::ILINK, '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext');
} }
/** /**
@ -480,8 +493,8 @@ class Image
public function get_thumb_link(): string public function get_thumb_link(): string
{ {
global $config; global $config;
$ext = $config->get_string("thumb_type"); $ext = $config->get_string(ImageConfig::THUMB_TYPE);
return $this->get_link('image_tlink', '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext); return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
} }
/** /**
@ -512,7 +525,7 @@ class Image
public function get_tooltip(): string public function get_tooltip(): string
{ {
global $config; global $config;
$tt = $this->parse_link_template($config->get_string('image_tip'), "no_escape"); $tt = $this->parse_link_template($config->get_string(ImageConfig::TIP), "no_escape");
// Removes the size tag if the file is an mp3 // Removes the size tag if the file is an mp3
if ($this->ext === 'mp3') { if ($this->ext === 'mp3') {

View File

@ -95,7 +95,6 @@ function get_extension_from_mime(String $file_path): String
throw new UploadException("Could not determine file mime type: ".$file_path); throw new UploadException("Could not determine file mime type: ".$file_path);
} }
/** /**
* Given a full size pair of dimensions, return a pair scaled down to fit * Given a full size pair of dimensions, return a pair scaled down to fit
* into the configured thumbnail square, with ratio intact. * into the configured thumbnail square, with ratio intact.
@ -126,23 +125,30 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
if($use_dpi_scaling) { if($use_dpi_scaling) {
$max_size = get_thumbnail_max_size_scaled(); list($max_width, $max_height) = get_thumbnail_max_size_scaled();
$max_width = $max_size[0];
$max_height = $max_size[1];
} else { } else {
$max_width = $config->get_int('thumb_width'); $max_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$max_height = $config->get_int('thumb_height'); $max_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
} }
$xscale = ($max_height / $orig_height); $output = get_scaled_by_aspect_ratio($orig_width, $orig_height, $max_width, $max_height);
$yscale = ($max_width / $orig_width);
$scale = ($xscale < $yscale) ? $xscale : $yscale;
if ($scale > 1 && $config->get_bool('thumb_upscale')) { if ($output[2] > 1 && $config->get_bool('thumb_upscale')) {
return [(int)$orig_width, (int)$orig_height]; return [(int)$orig_width, (int)$orig_height];
} else { } else {
return [(int)($orig_width*$scale), (int)($orig_height*$scale)]; return $output;
} }
}
function get_scaled_by_aspect_ratio(int $original_width, int $original_height, int $max_width, int $max_height) : array
{
$xscale = ($max_width/ $original_width);
$yscale = ($max_height/ $original_height);
$scale = ($yscale < $xscale) ? $yscale : $xscale ;
return [(int)($original_width*$scale), (int)($original_height*$scale), $scale];
} }
/** /**
@ -154,355 +160,60 @@ function get_thumbnail_max_size_scaled(): array
{ {
global $config; global $config;
$scaling = $config->get_int("thumb_scaling"); $scaling = $config->get_int(ImageConfig::THUMB_SCALING);
$max_width = $config->get_int('thumb_width') * ($scaling/100); $max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling/100);
$max_height = $config->get_int('thumb_height') * ($scaling/100); $max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling/100);
return [$max_width, $max_height]; return [$max_width, $max_height];
} }
/**
* Creates a thumbnail file using ImageMagick's convert command. function create_image_thumb(string $hash, string $type, string $engine = null) {
*
* @param $hash
* @param string $input_type Optional, allows specifying the input format. Usually not necessary.
* @return bool true is successful, false if not.
*/
function create_thumbnail_convert($hash, $input_type = ""): bool
{
global $config; global $config;
$inname = warehouse_path(Image::IMAGE_DIR, $hash); $inname = warehouse_path(Image::IMAGE_DIR, $hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash); $outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$tsize = get_thumbnail_max_size_scaled();
$q = $config->get_int("thumb_quality"); if(empty($engine)) {
$convert = $config->get_string("thumb_convert_path"); $engine = $config->get_string(ImageConfig::THUMB_ENGINE);
if ($convert==null||$convert=="") {
return false;
} }
// ffff imagemagick fails sometimes, not sure why $output_format = $config->get_string(ImageConfig::THUMB_TYPE);
//$format = "'%s' '%s[0]' -format '%%[fx:w] %%[fx:h]' info:"; if($output_format=="webp") {
//$cmd = sprintf($format, $convert, $inname); $output_format = Media::WEBP_LOSSY;
//$size = shell_exec($cmd);
//$size = explode(" ", trim($size));
list($w, $h) = get_thumbnail_max_size_scaled();
// 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 .= "\>";
} }
$bg = "black"; send_event(new MediaResizeEvent(
if ($type=="webp") { $engine,
$bg = "none"; $inname,
} $type,
if (!empty($input_type)) { $outname,
$input_type = $input_type.":"; $tsize[0],
} $tsize[1],
$format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1'; false,
$cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg, $input_type, $inname, $type, $outname); $output_format,
$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 $config->get_int(ImageConfig::THUMB_QUALITY),
exec($cmd, $output, $ret); true,
if ($ret!=0) { $config->get_bool('thumb_upscale', false)
log_warning('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret, outputting ".implode("\r\n", $output)); ));
} else {
log_debug('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret");
} }
if ($config->get_bool("thumb_optim", false)) {
exec("jpegoptim $outname", $output, $ret);
}
return true; const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
} function format_milliseconds(int $input): string
/**
* Creates a thumbnail using ffmpeg.
*
* @param $hash
* @return bool true if successful, false if not.
*/
function create_thumbnail_ffmpeg($hash): bool
{ {
global $config; $output = "";
$ffmpeg = $config->get_string("thumb_ffmpeg_path"); $remainder = floor($input / 1000);
if ($ffmpeg==null||$ffmpeg=="") {
return false;
}
$inname = warehouse_path(Image::IMAGE_DIR, $hash); foreach (TIME_UNITS AS $unit=>$conversion) {
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash); $count = $remainder % $conversion;
$remainder = floor($remainder / $conversion);
$orig_size = video_size($inname); if($count==0&&$remainder<1) {
$scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true);
$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;
}
}
/**
* Determines the dimensions of a video file using ffmpeg.
*
* @param string $filename
* @return array [width, height]
*/
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
*
* @param array $info The output of getimagesize() for the source file in question.
* @return int The number of bytes an image resize operation is estimated to use.
*/
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;
}
/**
* Performs a resize operation on an image file using GD.
*
* @param String $image_filename The source file to be resized.
* @param array $info The output of getimagesize() for the source file.
* @param int $new_width
* @param int $new_height
* @param string $output_filename
* @param string|null $output_type If set to null, the output file type will be automatically determined via the $info parameter. Otherwise an exception will be thrown.
* @param int $output_quality Defaults to 80.
* @throws ImageResizeException
* @throws InsufficientMemoryException if the estimated memory usage exceeds the memory limit.
*/
function image_resize_gd(
String $image_filename,
array $info,
int $new_width,
int $new_height,
string $output_filename,
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));
$image_resized = imagecreatetruecolor($new_width, $new_height);
try {
if ($image===false) {
throw new ImageResizeException("Could not load image: ".$image_filename);
}
if ($image_resized===false) {
throw new ImageResizeException("Could not create output image with dimensions $new_width c $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']);
if ($transparency===false) {
throw new ImageResizeException("Unable to allocate transparent color");
}
// Completely fill the background of the new image with allocated color.
if (imagefill($image_resized, 0, 0, $transparency)===false) {
throw new ImageResizeException("Unable to fill new image with transparent color");
}
// 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
//
if (imagealphablending($image_resized, false)===false) {
throw new ImageResizeException("Unable to disable image alpha blending");
}
if (imagesavealpha($image_resized, true)===false) {
throw new ImageResizeException("Unable to enable image save alpha");
}
$transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127);
if ($transparent_color===false) {
throw new ImageResizeException("Unable to allocate transparent color");
}
if (imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color)===false) {
throw new ImageResizeException("Unable to fill new image with transparent color");
}
break; break;
} }
$output = "$count".$unit." ".$output;
// Actually resize the image.
if (imagecopyresampled(
$image_resized,
$image,
0,
0,
0,
0,
$new_width,
$new_height,
$width,
$height
)===false) {
throw new ImageResizeException("Unable to copy resized image data to new image");
} }
return trim($output);
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");
}
} finally {
imagedestroy($image);
imagedestroy($image_resized);
}
}
/**
* Determines if a file is an animated gif.
*
* @param String $image_filename The path of the file to check.
* @return bool true if the file is an animated gif, false if it is not.
*/
function is_animated_gif(String $image_filename)
{
$is_anim_gif = 0;
if (($fh = @fopen($image_filename, 'rb'))) {
//check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
while (!feof($fh) && $is_anim_gif < 2) {
$chunk = fread($fh, 1024 * 100);
$is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
}
}
return ($is_anim_gif == 0);
}
function image_to_id(Image $image): int
{
return $image->id;
} }

View File

@ -40,7 +40,7 @@ _d("SEARCH_ACCEL", false); // boolean use search accelerator
_d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse _d("WH_SPLITS", 1); // int how many levels of subfolders to put in the warehouse
_d("VERSION", '2.7-beta'); // string shimmie version _d("VERSION", '2.7-beta'); // string shimmie version
_d("TIMEZONE", null); // string timezone _d("TIMEZONE", null); // string timezone
_d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor"); // extensions to always enable _d("CORE_EXTS", "bbcode,user,mail,upload,image,view,handle_pixel,ext_manager,setup,upgrade,handle_404,handle_static,comment,tag_list,index,tag_edit,alias_editor,media"); // extensions to always enable
_d("EXTRA_EXTS", ""); // string optional extra extensions _d("EXTRA_EXTS", ""); // string optional extra extensions
_d("BASE_URL", null); // string force a specific base URL (default is auto-detect) _d("BASE_URL", null); // string force a specific base URL (default is auto-detect)
_d("MIN_PHP_VERSION", '7.1');// string minimum supported PHP version _d("MIN_PHP_VERSION", '7.1');// string minimum supported PHP version

View File

@ -79,7 +79,7 @@ function get_memory_limit(): int
// thumbnail generation requires lots of memory // thumbnail generation requires lots of memory
$default_limit = 8*1024*1024; // 8 MB of memory is PHP's default. $default_limit = 8*1024*1024; // 8 MB of memory is PHP's default.
$shimmie_limit = parse_shorthand_int($config->get_int("thumb_mem_limit")); $shimmie_limit = parse_shorthand_int($config->get_int(MediaConfig::MEM_LIMIT));
if ($shimmie_limit < 3*1024*1024) { if ($shimmie_limit < 3*1024*1024) {
// we aren't going to fit, override // we aren't going to fit, override

View File

@ -53,13 +53,16 @@ class ET extends Extension
to_shorthand_int(disk_total_space("./")); to_shorthand_int(disk_total_space("./"));
$info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown'; $info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown';
$info['thumb_engine'] = $config->get_string("thumb_engine"); $info[MediaConfig::FFMPEG_PATH] = $config->get_string(MediaConfig::FFMPEG_PATH);
$info['thumb_quality'] = $config->get_int('thumb_quality'); $info[MediaConfig::CONVERT_PATH] = $config->get_string(MediaConfig::CONVERT_PATH);
$info['thumb_width'] = $config->get_int('thumb_width'); $info[MediaConfig::MEM_LIMIT] = $config->get_int(MediaConfig::MEM_LIMIT);
$info['thumb_height'] = $config->get_int('thumb_height');
$info['thumb_scaling'] = $config->get_int('thumb_scaling'); $info[ImageConfig::THUMB_ENGINE] = $config->get_string(ImageConfig::THUMB_ENGINE);
$info['thumb_type'] = $config->get_string('thumb_type'); $info[ImageConfig::THUMB_QUALITY] = $config->get_int(ImageConfig::THUMB_QUALITY);
$info['thumb_mem'] = $config->get_int("thumb_mem_limit"); $info[ImageConfig::THUMB_WIDTH] = $config->get_int(ImageConfig::THUMB_WIDTH);
$info[ImageConfig::THUMB_HEIGHT] = $config->get_int(ImageConfig::THUMB_HEIGHT);
$info[ImageConfig::THUMB_SCALING] = $config->get_int(ImageConfig::THUMB_SCALING);
$info[ImageConfig::THUMB_TYPE] = $config->get_string(ImageConfig::THUMB_TYPE);
$info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images"); $info['stat_images'] = $database->get_one("SELECT COUNT(*) FROM images");
$info['stat_comments'] = $database->get_one("SELECT COUNT(*) FROM comments"); $info['stat_comments'] = $database->get_one("SELECT COUNT(*) FROM comments");

View File

@ -35,14 +35,16 @@ Database: {$info['sys_db']}
Server: {$info['sys_server']} Server: {$info['sys_server']}
Disk use: {$info['sys_disk']} Disk use: {$info['sys_disk']}
Media System:
Memory Limit: {$info[MediaConfig::MEM_LIMIT]}
Thumbnail Generation: Thumbnail Generation:
Engine: {$info['thumb_engine']} Engine: {$info[ImageConfig::THUMB_ENGINE]}
Type: {$info['thumb_type']} Type: {$info[ImageConfig::THUMB_TYPE]}
Memory: {$info['thumb_mem']} Quality: {$info[ImageConfig::THUMB_QUALITY]}
Quality: {$info['thumb_quality']} Width: {$info[ImageConfig::THUMB_WIDTH]}
Width: {$info['thumb_width']} Height: {$info[ImageConfig::THUMB_HEIGHT]}
Height: {$info['thumb_height']} Scaling: {$info[ImageConfig::THUMB_SCALING]}
Scaling: {$info['thumb_scaling']}
Shimmie stats: Shimmie stats:
Images: {$info['stat_images']} Images: {$info['stat_images']}

View File

@ -8,11 +8,31 @@
class FlashFileHandler extends DataHandlerExtension class FlashFileHandler extends DataHandlerExtension
{ {
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
switch ($event->ext) {
case "swf":
$event->lossless = true;
$event->video = true;
$info = getimagesize($event->file_name);
if (!$info) {
return null;
}
$event->width = $info[0];
$event->height = $info[1];
break;
}
}
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
{ {
global $config; global $config;
if (!create_thumbnail_ffmpeg($hash)) { if (!Media::create_thumbnail_ffmpeg($hash)) {
copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); copy("ext/handle_flash/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
} }
return true; return true;
@ -35,13 +55,7 @@ class FlashFileHandler extends DataHandlerExtension
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source']; $image->source = $metadata['source'];
$info = getimagesize($filename);
if (!$info) {
return null;
}
$image->width = $info[0];
$image->height = $info[1];
return $image; return $image;
} }

View File

@ -10,17 +10,14 @@ class IcoFileHandler extends DataHandlerExtension
const SUPPORTED_EXTENSIONS = ["ico", "ani", "cur"]; const SUPPORTED_EXTENSIONS = ["ico", "ani", "cur"];
protected function supported_ext(string $ext): bool public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{ {
return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS); if(in_array($event->ext, self::SUPPORTED_EXTENSIONS)) {
} $event->lossless = true;
$event->video = false;
$event->audio = false;
protected function create_image_from_data(string $filename, array $metadata) $fp = fopen($event->file_name, "r");
{
$image = new Image();
$fp = fopen($filename, "r");
try { try {
unpack("Snull/Stype/Scount", fread($fp, 6)); unpack("Snull/Stype/Scount", fread($fp, 6));
$subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16)); $subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
@ -30,8 +27,20 @@ class IcoFileHandler extends DataHandlerExtension
$width = $subheader['width']; $width = $subheader['width'];
$height = $subheader['height']; $height = $subheader['height'];
$image->width = $width == 0 ? 256 : $width; $event->width = $width == 0 ? 256 : $width;
$image->height = $height == 0 ? 256 : $height; $event->height = $height == 0 ? 256 : $height;
}
}
protected function supported_ext(string $ext): bool
{
return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS);
}
protected function create_image_from_data(string $filename, array $metadata)
{
$image = new Image();
$image->filesize = $metadata['size']; $image->filesize = $metadata['size'];
$image->hash = $metadata['hash']; $image->hash = $metadata['hash'];
@ -56,6 +65,12 @@ class IcoFileHandler extends DataHandlerExtension
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
{ {
return create_thumbnail_convert($hash, $type); try {
create_image_thumb($hash, $type, MediaEngine::IMAGICK);
return true;
} catch (MediaException $e) {
log_warning("handle_ico", "Could not generate thumbnail. " . $e->getMessage());
return false;
}
} }
} }

View File

@ -7,6 +7,19 @@
class MP3FileHandler extends DataHandlerExtension class MP3FileHandler extends DataHandlerExtension
{ {
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
switch ($event->ext) {
case "mp3":
$event->audio = true;
$event->video = false;
$event->lossless = false;
break;
}
// TODO: Buff out audio format support, length scanning
}
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
{ {
copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); copy("ext/handle_mp3/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
@ -37,6 +50,7 @@ class MP3FileHandler extends DataHandlerExtension
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']); $image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source']; $image->source = $metadata['source'];
return $image; return $image;
} }

View File

@ -10,6 +10,44 @@ class PixelFileHandler extends DataHandlerExtension
{ {
const SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "gif", "png", "webp"]; const SUPPORTED_EXTENSIONS = ["jpg", "jpeg", "gif", "png", "webp"];
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if(in_array($event->ext, Media::LOSSLESS_FORMATS)) {
$event->lossless = true;
} elseif($event->ext=="webp") {
$event->lossless = Media::is_lossless_webp($event->file_name);
}
if(in_array($event->ext,self::SUPPORTED_EXTENSIONS)) {
if($event->lossless==null) {
$event->lossless = false;
}
$event->audio = false;
switch ($event->ext) {
case "gif":
$event->video = Media::is_animated_gif($event->file_name);
break;
case "webp":
$event->video = Media::is_animated_webp($event->file_name);
break;
default:
$event->video = false;
break;
}
$info = getimagesize($event->file_name);
if (!$info) {
return null;
}
$event->width = $info[0];
$event->height = $info[1];
}
}
protected function supported_ext(string $ext): bool protected function supported_ext(string $ext): bool
{ {
$ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext; $ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext;
@ -20,14 +58,6 @@ class PixelFileHandler extends DataHandlerExtension
{ {
$image = new Image(); $image = new Image();
$info = getimagesize($filename);
if (!$info) {
return null;
}
$image->width = $info[0];
$image->height = $info[1];
$image->filesize = $metadata['size']; $image->filesize = $metadata['size'];
$image->hash = $metadata['hash']; $image->hash = $metadata['hash'];
$image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename']; $image->filename = (($pos = strpos($metadata['filename'], '?')) !== false) ? substr($metadata['filename'], 0, $pos) : $metadata['filename'];
@ -56,24 +86,22 @@ class PixelFileHandler extends DataHandlerExtension
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
{ {
global $config; try {
create_image_thumb($hash, $type);
$inname = warehouse_path(Image::IMAGE_DIR, $hash); return true;
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash); } catch (InsufficientMemoryException $e) {
$tsize = get_thumbnail_max_size_scaled();
$ok = false; $thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64));
$white = imagecolorallocate($thumb, 255, 255, 255);
switch ($config->get_string("thumb_engine")) { $black = imagecolorallocate($thumb, 0, 0, 0);
default: imagefill($thumb, 0, 0, $white);
case 'gd': log_warning("handle_pixel", "Insufficient memory while creating thumbnail: ".$e->getMessage());
$ok = $this->make_thumb_gd($inname, $outname); imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black);
break; return true;
case 'convert': } catch (Exception $e) {
$ok = create_thumbnail_convert($hash); log_error("handle_pixel", "Error while creating thumbnail: ".$e->getMessage());
break; return false;
} }
return $ok;
} }
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
@ -90,38 +118,4 @@ class PixelFileHandler extends DataHandlerExtension
", 20); ", 20);
} }
// GD thumber {{{
private function make_thumb_gd(string $inname, string $outname): bool
{
global $config;
try {
$info = getimagesize($inname);
$tsize = get_thumbnail_size($info[0], $info[1], true);
$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;
}
// }}}
} }

View File

@ -7,7 +7,7 @@ class PixelFileHandlerTheme extends Themelet
global $config; global $config;
$u_ilink = $image->get_image_link(); $u_ilink = $image->get_image_link();
if ($config->get_bool("image_show_meta") && function_exists("exif_read_data")) { if ($config->get_bool(ImageConfig::SHOW_META) && function_exists(ImageIO::EXIF_READ_FUNCTION)) {
# FIXME: only read from jpegs? # FIXME: only read from jpegs?
$exif = @exif_read_data($image->get_image_filename(), 0, true); $exif = @exif_read_data($image->get_image_filename(), 0, true);
if ($exif) { if ($exif) {

View File

@ -10,6 +10,24 @@ use enshrined\svgSanitize\Sanitizer;
class SVGFileHandler extends DataHandlerExtension class SVGFileHandler extends DataHandlerExtension
{ {
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
switch ($event->ext) {
case "svg":
$event->lossless = true;
$event->video = false;
$event->audio = false;
$msp = new MiniSVGParser($event->file_name);
$event->width = $msp->width;
$event->height = $msp->height;
break;
}
}
public function onDataUpload(DataUploadEvent $event) public function onDataUpload(DataUploadEvent $event)
{ {
if ($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) { if ($this->supported_ext($event->type) && $this->check_contents($event->tmpname)) {
@ -35,10 +53,14 @@ class SVGFileHandler extends DataHandlerExtension
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
{ {
if (!create_thumbnail_convert($hash)) { try {
copy("ext/handle_svg/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash)); create_image_thumb($hash, $type, MediaEngine::IMAGICK);
}
return true; return true;
} catch (MediaException $e) {
log_warning("handle_svg", "Could not generate thumbnail. " . $e->getMessage());
copy("ext/handle_svg/thumb.jpg", warehouse_path(Image::THUMBNAIL_DIR, $hash));
return false;
}
} }
public function onDisplayingImage(DisplayingImageEvent $event) public function onDisplayingImage(DisplayingImageEvent $event)
@ -78,10 +100,6 @@ class SVGFileHandler extends DataHandlerExtension
{ {
$image = new Image(); $image = new Image();
$msp = new MiniSVGParser($filename);
$image->width = $msp->width;
$image->height = $msp->height;
$image->filesize = $metadata['size']; $image->filesize = $metadata['size'];
$image->hash = $metadata['hash']; $image->hash = $metadata['hash'];
$image->filename = $metadata['filename']; $image->filename = $metadata['filename'];

View File

@ -16,20 +16,21 @@
class VideoFileHandler extends DataHandlerExtension class VideoFileHandler extends DataHandlerExtension
{ {
const SUPPORTED_MIME = [
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
];
const SUPPORTED_EXT = ["flv", "mp4", "m4v", "ogv", "webm"];
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
global $config; global $config;
if ($config->get_int("ext_handle_video_version") < 1) { if ($config->get_int("ext_handle_video_version") < 1) {
if ($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) { // This used to set the ffmpeg path. It does not do this anymore, that is now in the base graphic extension.
//ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
if (is_executable(strtok($ffmpeg, PHP_EOL))) {
$config->set_default_string('thumb_ffmpeg_path', 'ffmpeg');
}
} else {
$config->set_default_string('thumb_ffmpeg_path', '');
}
$config->set_int("ext_handle_video_version", 1); $config->set_int("ext_handle_video_version", 1);
log_info("handle_video", "extension installed"); log_info("handle_video", "extension installed");
} }
@ -41,37 +42,83 @@ class VideoFileHandler extends DataHandlerExtension
public function onSetupBuilding(SetupBuildingEvent $event) public function onSetupBuilding(SetupBuildingEvent $event)
{ {
$sb = new SetupBlock("Video Options"); $sb = new SetupBlock("Video Options");
$sb->add_label("<br>Path to ffmpeg: ");
$sb->add_text_option("thumb_ffmpeg_path");
$sb->add_label("<br>");
$sb->add_bool_option("video_playback_autoplay", "Autoplay: "); $sb->add_bool_option("video_playback_autoplay", "Autoplay: ");
$sb->add_label("<br>"); $sb->add_label("<br>");
$sb->add_bool_option("video_playback_loop", "Loop: "); $sb->add_bool_option("video_playback_loop", "Loop: ");
$event->panel->add_block($sb); $event->panel->add_block($sb);
} }
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if(in_array($event->ext, self::SUPPORTED_EXT)) {
$event->video = true;
try {
$data = Media::get_ffprobe_data($event->file_name);
if(is_array($data)) {
if(array_key_exists("streams", $data)) {
$video = false;
$audio = true;
$streams = $data["streams"];
if (is_array($streams)) {
foreach ($streams as $stream) {
if(is_array($stream)) {
if (array_key_exists("codec_type", $stream)) {
$type = $stream["codec_type"];
switch ($type) {
case "audio":
$audio = true;
break;
case "video":
$video = true;
break;
}
}
if (array_key_exists("width", $stream) && !empty($stream["width"])
&& is_numeric($stream["width"]) && intval($stream["width"]) > ($event->width) ?? 0) {
$event->width = intval($stream["width"]);
}
if (array_key_exists("height", $stream) && !empty($stream["height"])
&& is_numeric($stream["height"]) && intval($stream["height"]) > ($event->height) ?? 0) {
$event->height = intval($stream["height"]);
}
}
}
$event->video = $video;
$event->audio = $audio;
}
}
if(array_key_exists("format", $data)&& is_array($data["format"])) {
$format = $data["format"];
if(array_key_exists("duration", $format) && is_numeric($format["duration"])) {
$event->length = floor(floatval($format["duration"]) * 1000);
}
}
}
} catch(MediaException $e) {
}
}
}
/** /**
* Generate the Thumbnail image for particular file. * Generate the Thumbnail image for particular file.
*/ */
protected function create_thumb(string $hash, string $type): bool protected function create_thumb(string $hash, string $type): bool
{ {
return create_thumbnail_ffmpeg($hash); return Media::create_thumbnail_ffmpeg($hash);
} }
protected function supported_ext(string $ext): bool protected function supported_ext(string $ext): bool
{ {
$exts = ["flv", "mp4", "m4v", "ogv", "webm"]; return in_array(strtolower($ext), self::SUPPORTED_EXT);
return in_array(strtolower($ext), $exts);
} }
protected function create_image_from_data(string $filename, array $metadata): Image protected function create_image_from_data(string $filename, array $metadata): Image
{ {
$image = new Image(); $image = new Image();
$size = video_size($filename);
$image->width = $size[0];
$image->height = $size[1];
switch (getMimeType($filename)) { switch (getMimeType($filename)) {
case "video/webm": case "video/webm":
$image->ext = "webm"; $image->ext = "webm";
@ -103,13 +150,7 @@ class VideoFileHandler extends DataHandlerExtension
{ {
return ( return (
file_exists($tmpname) && file_exists($tmpname) &&
in_array(getMimeType($tmpname), [ in_array(getMimeType($tmpname), self::SUPPORTED_MIME)
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
])
); );
} }
} }

View File

@ -13,6 +13,15 @@ class VideoFileHandlerTheme extends Themelet
$loop = $config->get_bool("video_playback_loop"); $loop = $config->get_bool("video_playback_loop");
$player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf'); $player = make_link('vendor/bower-asset/mediaelement/build/flashmediaelement.swf');
$width="auto";
if($image->width>1) {
$width = $image->width."px";
}
$height="auto";
if($image->height>1) {
$height = $image->height."px";
}
$html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>"; $html = "Video not playing? <a href='$ilink'>Click here</a> to download the file.<br/>";
//Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats //Browser media format support: https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
@ -48,7 +57,8 @@ class VideoFileHandlerTheme extends Themelet
$loop = ($loop ? ' loop' : ''); $loop = ($loop ? ' loop' : '');
$html .= " $html .= "
<video controls class='shm-main-image' id='main_image' alt='main image' poster='$thumb_url' {$autoplay} {$loop} style='max-width: 100%'> <video controls class='shm-main-image' id='main_image' alt='main image' poster='$thumb_url' {$autoplay} {$loop}
style='height: $height; width: $width; max-width: 100%'>
<source src='{$ilink}' type='{$supportedExts[$ext]}'> <source src='{$ilink}' type='{$supportedExts[$ext]}'>
<!-- If browser doesn't support filetype, fallback to flash --> <!-- If browser doesn't support filetype, fallback to flash -->

View File

@ -9,30 +9,64 @@
*/ */
abstract class ImageConfig {
const THUMB_ENGINE = 'thumb_engine';
const THUMB_WIDTH = 'thumb_width';
const THUMB_HEIGHT = 'thumb_height';
const THUMB_SCALING = 'thumb_scaling';
const THUMB_QUALITY = 'thumb_quality';
const THUMB_TYPE = 'thumb_type';
const SHOW_META = 'image_show_meta';
const ILINK = 'image_ilink';
const TLINK = 'image_tlink';
const TIP = 'image_tip';
const EXPIRES = 'image_expires';
const UPLOAD_COLLISION_HANDLER = 'upload_collision_handler';
const COLLISION_MERGE = 'merge';
const COLLISION_ERROR = 'error';
}
/** /**
* A class to handle adding / getting / removing image files from the disk. * A class to handle adding / getting / removing image files from the disk.
*/ */
class ImageIO extends Extension class ImageIO extends Extension
{ {
const COLLISION_OPTIONS = ['Error'=>ImageConfig::COLLISION_ERROR, 'Merge'=>ImageConfig::COLLISION_MERGE];
const EXIF_READ_FUNCTION = "exif_read_data";
const THUMBNAIL_ENGINES = [
'Built-in GD' => MediaEngine::GD,
'ImageMagick' => MediaEngine::IMAGICK
];
const THUMBNAIL_TYPES = [
'JPEG' => "jpg",
'WEBP (Not IE/Safari compatible)' => "webp"
];
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
global $config; global $config;
$config->set_default_int('thumb_width', 192); $config->set_default_int(ImageConfig::THUMB_WIDTH, 192);
$config->set_default_int('thumb_height', 192); $config->set_default_int(ImageConfig::THUMB_HEIGHT, 192);
$config->set_default_int('thumb_scaling', 100); $config->set_default_int(ImageConfig::THUMB_SCALING, 100);
$config->set_default_int('thumb_quality', 75); $config->set_default_int(ImageConfig::THUMB_QUALITY, 75);
$config->set_default_string('thumb_type', 'jpg'); $config->set_default_string(ImageConfig::THUMB_TYPE, 'jpg');
$config->set_default_int('thumb_mem_limit', parse_shorthand_int('8MB'));
$config->set_default_string('thumb_convert_path', 'convert');
if (function_exists("exif_read_data")) { if (function_exists(self::EXIF_READ_FUNCTION)) {
$config->set_default_bool('image_show_meta', false); $config->set_default_bool(ImageConfig::SHOW_META, false);
} }
$config->set_default_string('image_ilink', ''); $config->set_default_string(ImageConfig::ILINK, '');
$config->set_default_string('image_tlink', ''); $config->set_default_string(ImageConfig::TLINK, '');
$config->set_default_string('image_tip', '$tags // $size // $filesize'); $config->set_default_string(ImageConfig::TIP, '$tags // $size // $filesize');
$config->set_default_string('upload_collision_handler', 'error'); $config->set_default_string(ImageConfig::UPLOAD_COLLISION_HANDLER, ImageConfig::COLLISION_ERROR);
$config->set_default_int('image_expires', (60*60*24*31)); // defaults to one month $config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month
} }
public function onPageRequest(PageRequestEvent $event) public function onPageRequest(PageRequestEvent $event)
@ -125,50 +159,36 @@ class ImageIO extends Extension
$sb = new SetupBlock("Image Options"); $sb = new SetupBlock("Image Options");
$sb->position = 30; $sb->position = 30;
// advanced only // advanced only
//$sb->add_text_option("image_ilink", "Image link: "); //$sb->add_text_option(ImageConfig::ILINK, "Image link: ");
//$sb->add_text_option("image_tlink", "<br>Thumbnail link: "); //$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: ");
$sb->add_text_option("image_tip", "Image tooltip: "); $sb->add_text_option(ImageConfig::TIP, "Image tooltip: ");
$sb->add_choice_option("upload_collision_handler", ['Error'=>'error', 'Merge'=>'merge'], "<br>Upload collision handler: "); $sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "<br>Upload collision handler: ");
if (function_exists("exif_read_data")) { if (function_exists(self::EXIF_READ_FUNCTION)) {
$sb->add_bool_option("image_show_meta", "<br>Show metadata: "); $sb->add_bool_option(ImageConfig::SHOW_META, "<br>Show metadata: ");
} }
$event->panel->add_block($sb); $event->panel->add_block($sb);
$thumbers = [];
$thumbers['Built-in GD'] = "gd";
$thumbers['ImageMagick'] = "convert";
$thumb_types = [];
$thumb_types['JPEG'] = "jpg";
$thumb_types['WEBP (Not IE/Safari compatible)'] = "webp";
$sb = new SetupBlock("Thumbnailing"); $sb = new SetupBlock("Thumbnailing");
$sb->add_choice_option("thumb_engine", $thumbers, "Engine: "); $sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine: ");
$sb->add_label("<br>"); $sb->add_label("<br>");
$sb->add_choice_option("thumb_type", $thumb_types, "Filetype: "); $sb->add_choice_option(ImageConfig::THUMB_TYPE, self::THUMBNAIL_TYPES, "Filetype: ");
$sb->add_label("<br>Size "); $sb->add_label("<br>Size ");
$sb->add_int_option("thumb_width"); $sb->add_int_option(ImageConfig::THUMB_WIDTH);
$sb->add_label(" x "); $sb->add_label(" x ");
$sb->add_int_option("thumb_height"); $sb->add_int_option(ImageConfig::THUMB_HEIGHT);
$sb->add_label(" px at "); $sb->add_label(" px at ");
$sb->add_int_option("thumb_quality"); $sb->add_int_option(ImageConfig::THUMB_QUALITY);
$sb->add_label(" % quality "); $sb->add_label(" % quality ");
$sb->add_label("<br>High-DPI scaling "); $sb->add_label("<br>High-DPI scaling ");
$sb->add_int_option("thumb_scaling"); $sb->add_int_option(ImageConfig::THUMB_SCALING);
$sb->add_label("%"); $sb->add_label("%");
if ($config->get_string("thumb_engine") == "convert") {
$sb->add_label("<br>ImageMagick Binary: ");
$sb->add_text_option("thumb_convert_path");
}
if ($config->get_string("thumb_engine") == "gd") {
$sb->add_shorthand_int_option("thumb_mem_limit", "<br>Max memory use: ");
}
$event->panel->add_block($sb); $event->panel->add_block($sb);
} }
@ -193,8 +213,8 @@ class ImageIO extends Extension
*/ */
$existing = Image::by_hash($image->hash); $existing = Image::by_hash($image->hash);
if (!is_null($existing)) { if (!is_null($existing)) {
$handler = $config->get_string("upload_collision_handler"); $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if ($handler == "merge" || isset($_GET['update'])) { if ($handler == ImageConfig::COLLISION_MERGE || isset($_GET['update'])) {
$merged = array_merge($image->get_tag_array(), $existing->get_tag_array()); $merged = array_merge($image->get_tag_array(), $existing->get_tag_array());
send_event(new TagSetEvent($existing, $merged)); send_event(new TagSetEvent($existing, $merged));
if (isset($_GET['rating']) && isset($_GET['update']) && ext_is_live("Ratings")) { if (isset($_GET['rating']) && isset($_GET['update']) && ext_is_live("Ratings")) {
@ -221,11 +241,11 @@ class ImageIO extends Extension
) )
VALUES ( VALUES (
:owner_id, :owner_ip, :filename, :filesize, :owner_id, :owner_ip, :filename, :filesize,
:hash, :ext, :width, :height, now(), :source :hash, :ext, 0, 0, now(), :source
)", )",
[ [
"owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'], "filename" => substr($image->filename, 0, 255), "filesize" => $image->filesize, "owner_id" => $user->id, "owner_ip" => $_SERVER['REMOTE_ADDR'], "filename" => substr($image->filename, 0, 255), "filesize" => $image->filesize,
"hash"=>$image->hash, "ext"=>strtolower($image->ext), "width"=>$image->width, "height"=>$image->height, "source"=>$image->source "hash" => $image->hash, "ext" => strtolower($image->ext), "source" => $image->source
] ]
); );
$image->id = $database->get_last_insert_id('images_id_seq'); $image->id = $database->get_last_insert_id('images_id_seq');
@ -244,6 +264,13 @@ class ImageIO extends Extension
if ($image->source !== null) { if ($image->source !== null) {
log_info("core-image", "Source for Image #{$image->id} set to: {$image->source}"); log_info("core-image", "Source for Image #{$image->id} set to: {$image->source}");
} }
try {
Media::update_image_media_properties($image->hash, strtolower($image->ext));
} catch(MediaException $e) {
log_warning("add_image","Error while running update_image_media_properties: ".$e->getMessage());
}
} }
// }}} end add // }}} end add
@ -256,7 +283,7 @@ class ImageIO extends Extension
global $page; global $page;
if (!is_null($image)) { if (!is_null($image)) {
if ($type == "thumb") { if ($type == "thumb") {
$ext = $config->get_string("thumb_type"); $ext = $config->get_string(ImageConfig::THUMB_TYPE);
if (array_key_exists($ext, MIME_TYPE_MAP)) { if (array_key_exists($ext, MIME_TYPE_MAP)) {
$page->set_type(MIME_TYPE_MAP[$ext]); $page->set_type(MIME_TYPE_MAP[$ext]);
} else { } else {
@ -289,8 +316,8 @@ class ImageIO extends Extension
$page->set_file($file); $page->set_file($file);
if ($config->get_int("image_expires")) { if ($config->get_int(ImageConfig::EXPIRES)) {
$expires = date(DATE_RFC1123, time() + $config->get_int("image_expires")); $expires = date(DATE_RFC1123, time() + $config->get_int(ImageConfig::EXPIRES));
} else { } else {
$expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning $expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning
} }
@ -320,10 +347,36 @@ class ImageIO extends Extension
throw new ImageReplaceException("Image to replace does not exist!"); throw new ImageReplaceException("Image to replace does not exist!");
} }
$duplicate = Image::by_hash($image->hash);
if(!is_null($duplicate) && $duplicate->id!=$id) {
$error = "Image <a href='" . make_link("post/view/{$duplicate->id}") . "'>{$duplicate->id}</a> " .
"already has hash {$image->hash}:<p>" . $this->theme->build_thumb_html($duplicate);
throw new ImageReplaceException($error);
}
if (strlen(trim($image->source)) == 0) { if (strlen(trim($image->source)) == 0) {
$image->source = $existing->get_source(); $image->source = $existing->get_source();
} }
// Update the data in the database.
$database->Execute(
"UPDATE images SET
filename = :filename, filesize = :filesize, hash = :hash,
ext = :ext, width = 0, height = 0, source = :source
WHERE
id = :id
",
[
"filename" => substr($image->filename, 0, 255),
"filesize" => $image->filesize,
"hash" => $image->hash,
"ext" => strtolower($image->ext),
"source" => $image->source,
"id" => $id,
]
);
/* /*
This step could be optional, ie: perhaps move the image somewhere This step could be optional, ie: perhaps move the image somewhere
and have it stored in a 'replaced images' list that could be and have it stored in a 'replaced images' list that could be
@ -333,20 +386,11 @@ class ImageIO extends Extension
log_debug("image", "Removing image with hash " . $existing->hash); log_debug("image", "Removing image with hash " . $existing->hash);
$existing->remove_image_only(); // Actually delete the old image file from disk $existing->remove_image_only(); // Actually delete the old image file from disk
// Update the data in the database. try {
$database->Execute( Media::update_image_media_properties($image->hash, $image->ext);
"UPDATE images SET } catch(MediaException $e) {
filename = :filename, filesize = :filesize, hash = :hash, log_warning("image_replace","Error while running update_image_media_properties: ".$e->getMessage());
ext = :ext, width = :width, height = :height, source = :source }
WHERE
id = :id
",
[
"filename" => substr($image->filename, 0, 255), "filesize"=>$image->filesize, "hash"=>$image->hash,
"ext"=>strtolower($image->ext), "width"=>$image->width, "height"=>$image->height, "source"=>$image->source,
"id"=>$id
]
);
/* Generate new thumbnail */ /* Generate new thumbnail */
send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext))); send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext)));

1135
ext/media/main.php Normal file

File diff suppressed because it is too large Load Diff

31
ext/media/theme.php Normal file
View File

@ -0,0 +1,31 @@
<?php
class MediaTheme extends Themelet
{
public function display_form(array $types)
{
global $page, $database;
$html = "Use this to force scanning for media properties.";
$html .= make_form(make_link("admin/media_rescan"));
$html .= "<table class='form'>";
$html .= "<tr><th>Image Type</th><td><select name='media_rescan_type'><option value=''>All</option>";
foreach ($types as $type) {
$html .= "<option value='".$type["ext"]."'>".$type["ext"]." (".$type["count"].")</option>";
}
$html .= "</select></td></tr>";
$html .= "<tr><td colspan='2'><input type='submit' value='Scan Media Information'></td></tr>";
$html .= "</table></form>\n";
$page->add_block(new Block("Media Tools", $html));
}
public function get_buttons_html(int $image_id): string
{
return "
".make_form(make_link("media_rescan/"))."
<input type='hidden' name='image_id' value='$image_id'>
<input type='submit' value='Scan Media Properties'>
</form>
";
}
}

View File

@ -231,8 +231,8 @@ class _SafeOuroborosImage
$this->has_notes = false; $this->has_notes = false;
// thumb // thumb
$this->preview_height = $config->get_int('thumb_height'); $this->preview_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
$this->preview_width = $config->get_int('thumb_width'); $this->preview_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$this->preview_url = make_http($img->get_thumb_link()); $this->preview_url = make_http($img->get_thumb_link());
// sample (use the full image here) // sample (use the full image here)
@ -481,8 +481,8 @@ class OuroborosAPI extends Extension
protected function postCreate(OuroborosPost $post, string $md5 = '') protected function postCreate(OuroborosPost $post, string $md5 = '')
{ {
global $config; global $config;
$handler = $config->get_string("upload_collision_handler"); $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if (!empty($md5) && !($handler == 'merge')) { if (!empty($md5) && !($handler == ImageConfig::COLLISION_MERGE)) {
$img = Image::by_hash($md5); $img = Image::by_hash($md5);
if (!is_null($img)) { if (!is_null($img)) {
$this->sendResponse(420, self::ERROR_POST_CREATE_DUPE); $this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
@ -524,8 +524,8 @@ class OuroborosAPI extends Extension
if (!empty($meta['hash'])) { if (!empty($meta['hash'])) {
$img = Image::by_hash($meta['hash']); $img = Image::by_hash($meta['hash']);
if (!is_null($img)) { if (!is_null($img)) {
$handler = $config->get_string("upload_collision_handler"); $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if ($handler == "merge") { if ($handler == ImageConfig::COLLISION_MERGE) {
$postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags); $postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
$merged = array_merge($postTags, $img->get_tag_array()); $merged = array_merge($postTags, $img->get_tag_array());
send_event(new TagSetEvent($img, $merged)); send_event(new TagSetEvent($img, $merged));

View File

@ -46,7 +46,7 @@ class ReportImageTheme extends Themelet
"; ";
} }
$thumb_width = $config->get_int("thumb_width"); $thumb_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$html = " $html = "
<table id='reportedimage' class='zebra'> <table id='reportedimage' class='zebra'>
<thead><td width='$thumb_width'>Image</td><td>Reason</td><td width='128'>Action</td></thead> <thead><td width='$thumb_width'>Image</td><td>Reason</td><td width='128'>Action</td></thead>

View File

@ -26,8 +26,6 @@ abstract class ResizeConfig
*/ */
class ResizeImage extends Extension class ResizeImage extends Extension
{ {
const SUPPORTED_EXT = ["jpg","jpeg","png","gif","webp"];
/** /**
* Needs to be after the data processing extensions * Needs to be after the data processing extensions
*/ */
@ -40,17 +38,18 @@ class ResizeImage extends Extension
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
global $config; global $config;
$config->set_default_bool('resize_enabled', true); $config->set_default_bool(ResizeConfig::ENABLED, true);
$config->set_default_bool('resize_upload', false); $config->set_default_bool(ResizeConfig::UPLOAD, false);
$config->set_default_int('resize_default_width', 0); $config->set_default_string(ResizeConfig::ENGINE, MediaEngine::GD);
$config->set_default_int('resize_default_height', 0); $config->set_default_int(ResizeConfig::DEFAULT_WIDTH, 0);
$config->set_default_int(ResizeConfig::DEFAULT_HEIGHT, 0);
} }
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{ {
global $user, $config; global $user, $config;
if ($user->is_admin() && $config->get_bool("resize_enabled") if ($user->is_admin() && $config->get_bool(ResizeConfig::ENABLED)
&& in_array($event->image->ext, self::SUPPORTED_EXT)) { && $this->can_resize_format($event->image->ext, $event->image->lossless)) {
/* Add a link to resize the image */ /* Add a link to resize the image */
$event->add_part($this->theme->get_resize_html($event->image)); $event->add_part($this->theme->get_resize_html($event->image));
} }
@ -60,6 +59,7 @@ class ResizeImage extends Extension
{ {
$sb = new SetupBlock("Image Resize"); $sb = new SetupBlock("Image Resize");
$sb->start_table(); $sb->start_table();
$sb->add_choice_option(ResizeConfig::ENGINE, Media::IMAGE_MEDIA_ENGINES, "Engine: ", true);
$sb->add_bool_option(ResizeConfig::ENABLED, "Allow resizing images: ", true); $sb->add_bool_option(ResizeConfig::ENABLED, "Allow resizing images: ", true);
$sb->add_bool_option(ResizeConfig::UPLOAD, "Resize on upload: ", true); $sb->add_bool_option(ResizeConfig::UPLOAD, "Resize on upload: ", true);
$sb->end_table(); $sb->end_table();
@ -82,15 +82,15 @@ class ResizeImage extends Extension
$image_obj = Image::by_id($event->image_id); $image_obj = Image::by_id($event->image_id);
if ($config->get_bool("resize_upload") == true if ($config->get_bool(ResizeConfig::UPLOAD) == true
&& in_array($event->type, self::SUPPORTED_EXT)) { && $this->can_resize_format($event->type, $image_obj->lossless)) {
$width = $height = 0; $width = $height = 0;
if ($config->get_int("resize_default_width") !== 0) { if ($config->get_int(ResizeConfig::DEFAULT_WIDTH) !== 0) {
$height = $config->get_int("resize_default_width"); $height = $config->get_int(ResizeConfig::DEFAULT_WIDTH);
} }
if ($config->get_int("resize_default_height") !== 0) { if ($config->get_int(ResizeConfig::DEFAULT_HEIGHT) !== 0) {
$height = $config->get_int("resize_default_height"); $height = $config->get_int(ResizeConfig::DEFAULT_HEIGHT);
} }
$isanigif = 0; $isanigif = 0;
if ($image_obj->ext == "gif") { if ($image_obj->ext == "gif") {
@ -170,17 +170,32 @@ class ResizeImage extends Extension
} }
} }
private function can_resize_format($format, ?bool $lossless = null): bool
{
global $config;
$engine = $config->get_string(ResizeConfig::ENGINE);
return Media::is_input_supported($engine, $format, $lossless)
&& Media::is_output_supported($engine, $format, $lossless);
}
// Private functions // Private functions
/* ----------------------------- */ /* ----------------------------- */
private function resize_image(Image $image_obj, int $width, int $height) private function resize_image(Image $image_obj, int $width, int $height)
{ {
global $database; global $database, $config;
if (($height <= 0) && ($width <= 0)) { if (($height <= 0) && ($width <= 0)) {
throw new ImageResizeException("Invalid options for height and width. ($width x $height)"); throw new ImageResizeException("Invalid options for height and width. ($width x $height)");
} }
$engine = $config->get_string(ResizeConfig::ENGINE);
if(!$this->can_resize_format($image_obj->ext, $image_obj->lossless)) {
throw new ImageResizeException("Engine $engine cannot resize selected image");
}
$hash = $image_obj->hash; $hash = $image_obj->hash;
$image_filename = warehouse_path(Image::IMAGE_DIR, $hash); $image_filename = warehouse_path(Image::IMAGE_DIR, $hash);
@ -197,7 +212,15 @@ class ResizeImage extends Extension
throw new ImageResizeException("Unable to save temporary image file."); throw new ImageResizeException("Unable to save temporary image file.");
} }
image_resize_gd($image_filename, $info, $new_width, $new_height, $tmp_filename); send_event(new MediaResizeEvent(
$engine,
$image_filename,
$image_obj->ext,
$tmp_filename,
$new_width,
$new_height,
true
));
$new_image = new Image(); $new_image = new Image();
$new_image->hash = md5_file($tmp_filename); $new_image->hash = md5_file($tmp_filename);

View File

@ -9,8 +9,8 @@ class ResizeImageTheme extends Themelet
{ {
global $config; global $config;
$default_width = $config->get_int('resize_default_width'); $default_width = $config->get_int(ResizeConfig::DEFAULT_WIDTH);
$default_height = $config->get_int('resize_default_height'); $default_height = $config->get_int(ResizeConfig::DEFAULT_HEIGHT);
if (!$default_width) { if (!$default_width) {
$default_width = $image->width; $default_width = $image->width;

View File

@ -130,7 +130,7 @@ class RotateImage extends Extension
$info = getimagesize($image_filename); $info = getimagesize($image_filename);
$memory_use =calc_memory_use($info); $memory_use = Media::calc_memory_use($info);
$memory_limit = get_memory_limit(); $memory_limit = get_memory_limit();
if ($memory_use > $memory_limit) { if ($memory_use > $memory_limit) {

View File

@ -30,7 +30,7 @@ class Rule34 extends Extension
public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event) public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event)
{ {
global $config; global $config;
$image_link = $config->get_string('image_ilink'); $image_link = $config->get_string(ImageConfig::ILINK);
$url0 = $event->image->parse_link_template($image_link, "url_escape", 0); $url0 = $event->image->parse_link_template($image_link, "url_escape", 0);
$url1 = $event->image->parse_link_template($image_link, "url_escape", 1); $url1 = $event->image->parse_link_template($image_link, "url_escape", 1);
$html = "<tr><th>Links</th><td><a href='$url0'>Image Only</a> (<a href='$url1'>Backup Server</a>)</td></tr>"; $html = "<tr><th>Links</th><td><a href='$url0'>Image Only</a> (<a href='$url1'>Backup Server</a>)</td></tr>";

View File

@ -39,6 +39,6 @@ class SetupTest extends ShimmiePHPUnitTestCase
$this->log_in_as_admin(); $this->log_in_as_admin();
$this->get_page('setup/advanced'); $this->get_page('setup/advanced');
$this->assert_title("Shimmie Setup"); $this->assert_title("Shimmie Setup");
$this->assert_text("thumb_quality"); $this->assert_text(ImageConfig::THUMB_QUALITY);
} }
} }

View File

@ -33,50 +33,6 @@ class TranscodeImage extends Extension
{ {
const ACTION_BULK_TRANSCODE = "bulk_transcode"; const ACTION_BULK_TRANSCODE = "bulk_transcode";
const CONVERSION_ENGINES = [
"GD" => "gd",
"ImageMagick" => "convert",
];
const ENGINE_INPUT_SUPPORT = [
"gd" => [
"bmp",
"gif",
"jpg",
"png",
"webp",
],
"convert" => [
"bmp",
"gif",
"jpg",
"png",
"psd",
"tiff",
"webp",
"ico",
]
];
const ENGINE_OUTPUT_SUPPORT = [
"gd" => [
"jpg",
"png",
"webp-lossy",
],
"convert" => [
"jpg",
"png",
"webp-lossy",
"webp-lossless",
]
];
const LOSSLESS_FORMATS = [
"webp-lossless",
"png",
];
const INPUT_FORMATS = [ const INPUT_FORMATS = [
"BMP" => "bmp", "BMP" => "bmp",
"GIF" => "gif", "GIF" => "gif",
@ -88,17 +44,12 @@ class TranscodeImage extends Extension
"WEBP" => "webp", "WEBP" => "webp",
]; ];
const FORMAT_ALIASES = [
"tif" => "tiff",
"jpeg" => "jpg",
];
const OUTPUT_FORMATS = [ const OUTPUT_FORMATS = [
"" => "", "" => "",
"JPEG (lossy)" => "jpg", "JPEG (lossy)" => "jpg",
"PNG (lossless)" => "png", "PNG (lossless)" => "png",
"WEBP (lossy)" => "webp-lossy", "WEBP (lossy)" => Media::WEBP_LOSSY,
"WEBP (lossless)" => "webp-lossless", "WEBP (lossless)" => Media::WEBP_LOSSLESS,
]; ];
/** /**
@ -113,13 +64,13 @@ class TranscodeImage extends Extension
public function onInitExt(InitExtEvent $event) public function onInitExt(InitExtEvent $event)
{ {
global $config; global $config;
$config->set_default_bool('transcode_enabled', true); $config->set_default_bool(TranscodeConfig::ENABLED, true);
$config->set_default_bool('transcode_upload', false); $config->set_default_bool(TranscodeConfig::UPLOAD, false);
$config->set_default_string('transcode_engine', "gd"); $config->set_default_string(TranscodeConfig::ENGINE, MediaEngine::GD);
$config->set_default_int('transcode_quality', 80); $config->set_default_int(TranscodeConfig::QUALITY, 80);
foreach (array_values(self::INPUT_FORMATS) as $format) { foreach (array_values(self::INPUT_FORMATS) as $format) {
$config->set_default_string('transcode_upload_'.$format, ""); $config->set_default_string(TranscodeConfig::UPLOAD_PREFIX.$format, "");
} }
} }
@ -127,10 +78,10 @@ class TranscodeImage extends Extension
{ {
global $user, $config; global $user, $config;
if ($user->is_admin() && $config->get_bool("resize_enabled")) { if ($user->is_admin()) {
$engine = $config->get_string("transcode_engine"); $engine = $config->get_string(TranscodeConfig::ENGINE);
if ($this->can_convert_format($engine, $event->image->ext)) { if ($this->can_convert_format($engine, $event->image->ext, $event->image->lossless)) {
$options = $this->get_supported_output_formats($engine, $event->image->ext); $options = $this->get_supported_output_formats($engine, $event->image->ext, $event->image->lossless??false);
$event->add_part($this->theme->get_transcode_html($event->image, $options)); $event->add_part($this->theme->get_transcode_html($event->image, $options));
} }
} }
@ -140,16 +91,16 @@ class TranscodeImage extends Extension
{ {
global $config; global $config;
$engine = $config->get_string("transcode_engine"); $engine = $config->get_string(TranscodeConfig::ENGINE);
$sb = new SetupBlock("Image Transcode"); $sb = new SetupBlock("Image Transcode");
$sb->start_table(); $sb->start_table();
$sb->add_bool_option(TranscodeConfig::ENABLED, "Allow transcoding images: ", true); $sb->add_bool_option(TranscodeConfig::ENABLED, "Allow transcoding images: ", true);
$sb->add_bool_option(TranscodeConfig::UPLOAD, "Transcode on upload: ", true); $sb->add_bool_option(TranscodeConfig::UPLOAD, "Transcode on upload: ", true);
$sb->add_choice_option(TranscodeConfig::ENGINE, self::CONVERSION_ENGINES, "Engine", true); $sb->add_choice_option(TranscodeConfig::ENGINE, Media::IMAGE_MEDIA_ENGINES, "Engine", true);
foreach (self::INPUT_FORMATS as $display=>$format) { foreach (self::INPUT_FORMATS as $display=>$format) {
if (in_array($format, self::ENGINE_INPUT_SUPPORT[$engine])) { if (in_array($format, MediaEngine::INPUT_SUPPORT[$engine])) {
$outputs = $this->get_supported_output_formats($engine, $format); $outputs = $this->get_supported_output_formats($engine, $format);
$sb->add_choice_option(TranscodeConfig::UPLOAD_PREFIX.$format, $outputs, "$display", true); $sb->add_choice_option(TranscodeConfig::UPLOAD_PREFIX.$format, $outputs, "$display", true);
} }
@ -163,23 +114,23 @@ class TranscodeImage extends Extension
{ {
global $config, $page; global $config, $page;
if ($config->get_bool("transcode_upload") == true) { if ($config->get_bool(TranscodeConfig::UPLOAD) == true) {
$ext = strtolower($event->type); $ext = strtolower($event->type);
$ext = $this->clean_format($ext); $ext = Media::normalize_format($ext);
if ($event->type=="gif"&&is_animated_gif($event->tmpname)) { if ($event->type=="gif"&&Media::is_animated_gif($event->tmpname)) {
return; return;
} }
if (in_array($ext, array_values(self::INPUT_FORMATS))) { if (in_array($ext, array_values(self::INPUT_FORMATS))) {
$target_format = $config->get_string("transcode_upload_".$ext); $target_format = $config->get_string(TranscodeConfig::UPLOAD_PREFIX.$ext);
if (empty($target_format)) { if (empty($target_format)) {
return; return;
} }
try { try {
$new_image = $this->transcode_image($event->tmpname, $ext, $target_format); $new_image = $this->transcode_image($event->tmpname, $ext, $target_format);
$event->set_type($this->determine_ext($target_format)); $event->set_type(Media::determine_ext($target_format));
$event->set_tmpname($new_image); $event->set_tmpname($new_image);
} catch (Exception $e) { } catch (Exception $e) {
log_error("transcode", "Error while performing upload transcode: ".$e->getMessage()); log_error("transcode", "Error while performing upload transcode: ".$e->getMessage());
@ -227,7 +178,7 @@ class TranscodeImage extends Extension
{ {
global $user, $config; global $user, $config;
$engine = $config->get_string("transcode_engine"); $engine = $config->get_string(TranscodeConfig::ENGINE);
if ($user->is_admin()) { if ($user->is_admin()) {
$event->add_action(self::ACTION_BULK_TRANSCODE, "Transcode", null,"", $this->theme->get_transcode_picker_html($this->get_supported_output_formats($engine))); $event->add_action(self::ACTION_BULK_TRANSCODE, "Transcode", null,"", $this->theme->get_transcode_picker_html($this->get_supported_output_formats($engine)));
@ -239,7 +190,7 @@ class TranscodeImage extends Extension
global $user, $database; global $user, $database;
switch ($event->action) { switch ($event->action) {
case "bulk_transcode": case self::ACTION_BULK_TRANSCODE:
if (!isset($_POST['transcode_format'])) { if (!isset($_POST['transcode_format'])) {
return; return;
} }
@ -251,8 +202,9 @@ class TranscodeImage extends Extension
$database->beginTransaction(); $database->beginTransaction();
$this->transcode_and_replace_image($image, $format); $this->transcode_and_replace_image($image, $format);
// If a subsequent transcode fails, the database need to have everything about the previous transcodes recorded already, // If a subsequent transcode fails, the database needs to have everything about the previous
// otherwise the image entries will be stuck pointing to missing image files // transcodes recorded already, otherwise the image entries will be stuck pointing to
// missing image files
$database->commit(); $database->commit();
$total++; $total++;
} catch (Exception $e) { } catch (Exception $e) {
@ -269,54 +221,38 @@ class TranscodeImage extends Extension
} }
} }
private function clean_format($format): ?string
private function can_convert_format($engine, $format, ?bool $lossless = null): bool
{ {
if (array_key_exists($format, self::FORMAT_ALIASES)) { return Media::is_input_supported($engine, $format, $lossless);
return self::FORMAT_ALIASES[$format];
}
return $format;
} }
private function can_convert_format($engine, $format): bool
{
$format = $this->clean_format($format);
if (!in_array($format, self::ENGINE_INPUT_SUPPORT[$engine])) {
return false;
}
return true;
}
private function get_supported_output_formats($engine, ?String $omit_format = null): array private function get_supported_output_formats($engine, ?String $omit_format = null, ?bool $lossless = null): array
{ {
$omit_format = $this->clean_format($omit_format); if($omit_format!=null) {
$omit_format = Media::normalize_format($omit_format, $lossless);
}
$output = []; $output = [];
foreach (self::OUTPUT_FORMATS as $key=>$value) { foreach (self::OUTPUT_FORMATS as $key=>$value) {
if ($value=="") { if ($value=="") {
$output[$key] = $value; $output[$key] = $value;
continue; continue;
} }
if (in_array($value, self::ENGINE_OUTPUT_SUPPORT[$engine]) if(Media::is_output_supported($engine, $value)
&&(empty($omit_format)||$omit_format!=$this->determine_ext($value))) { &&(empty($omit_format)||$omit_format!=$value)) {
$output[$key] = $value; $output[$key] = $value;
} }
} }
return $output; return $output;
} }
private function determine_ext(String $format): String
{
switch ($format) {
case "webp-lossless":
case "webp-lossy":
return "webp";
default:
return $format;
}
}
private function transcode_and_replace_image(Image $image_obj, String $target_format) private function transcode_and_replace_image(Image $image_obj, String $target_format)
{ {
$target_format = $this->clean_format($target_format);
$original_file = warehouse_path(Image::IMAGE_DIR, $image_obj->hash); $original_file = warehouse_path(Image::IMAGE_DIR, $image_obj->hash);
$tmp_filename = $this->transcode_image($original_file, $image_obj->ext, $target_format); $tmp_filename = $this->transcode_image($original_file, $image_obj->ext, $target_format);
@ -327,7 +263,7 @@ class TranscodeImage extends Extension
$new_image->filename = $image_obj->filename; $new_image->filename = $image_obj->filename;
$new_image->width = $image_obj->width; $new_image->width = $image_obj->width;
$new_image->height = $image_obj->height; $new_image->height = $image_obj->height;
$new_image->ext = $this->determine_ext($target_format); $new_image->ext = Media::determine_ext($target_format);
/* Move the new image into the main storage location */ /* Move the new image into the main storage location */
$target = warehouse_path(Image::IMAGE_DIR, $new_image->hash); $target = warehouse_path(Image::IMAGE_DIR, $new_image->hash);
@ -346,7 +282,7 @@ class TranscodeImage extends Extension
{ {
global $config; global $config;
if ($source_format==$this->determine_ext($target_format)) { if ($source_format==$target_format) {
throw new ImageTranscodeException("Source and target formats are the same: ".$source_format); throw new ImageTranscodeException("Source and target formats are the same: ".$source_format);
} }
@ -357,7 +293,7 @@ class TranscodeImage extends Extension
if (!$this->can_convert_format($engine, $source_format)) { if (!$this->can_convert_format($engine, $source_format)) {
throw new ImageTranscodeException("Engine $engine does not support input format $source_format"); throw new ImageTranscodeException("Engine $engine does not support input format $source_format");
} }
if (!in_array($target_format, self::ENGINE_OUTPUT_SUPPORT[$engine])) { if (!in_array($target_format, MediaEngine::OUTPUT_SUPPORT[$engine])) {
throw new ImageTranscodeException("Engine $engine does not support output format $target_format"); throw new ImageTranscodeException("Engine $engine does not support output format $target_format");
} }
@ -381,7 +317,8 @@ class TranscodeImage extends Extension
try { try {
$result = false; $result = false;
switch ($target_format) { switch ($target_format) {
case "webp-lossy": case "webp":
case Media::WEBP_LOSSY:
$result = imagewebp($image, $tmp_name, $q); $result = imagewebp($image, $tmp_name, $q);
break; break;
case "png": case "png":
@ -426,20 +363,20 @@ class TranscodeImage extends Extension
global $config; global $config;
$q = $config->get_int("transcode_quality"); $q = $config->get_int("transcode_quality");
$convert = $config->get_string("thumb_convert_path"); $convert = $config->get_string(MediaConfig::CONVERT_PATH);
if ($convert==null||$convert=="") { if ($convert==null||$convert=="") {
throw new ImageTranscodeException("ImageMagick path not configured"); throw new ImageTranscodeException("ImageMagick path not configured");
} }
$ext = $this->determine_ext($target_format); $ext = Media::determine_ext($target_format);
$args = " -flatten "; $args = " -flatten ";
$bg = "none"; $bg = "none";
switch ($target_format) { switch ($target_format) {
case "webp-lossless": case Media::WEBP_LOSSLESS:
$args .= '-define webp:lossless=true'; $args .= '-define webp:lossless=true';
break; break;
case "webp-lossy": case Media::WEBP_LOSSY:
$args .= ''; $args .= '';
break; break;
case "png": case "png":

11
ext/transcode/script.js Normal file
View File

@ -0,0 +1,11 @@
function transcodeSubmit(e) {
var format = document.getElementById('transcode_format').value;
if(format!="webp-lossless" && format != "png") {
var lossless = document.getElementById('image_lossless');
if(lossless!=null && lossless.value=='1') {
return confirm('You are about to transcode from a lossless format to a lossy format. Lossless formats compress with no quality loss, but converting to a lossy format always results in quality loss, and it will lose more quality every time it is done again on the same image. Are you sure you want to perform this transcode?');
} else {
return confirm('Converting to a lossy format always results in quality loss, and it will lose more quality every time it is done again on the same image. Are you sure you want to perform this transcode?');
}
}
}

View File

@ -10,8 +10,10 @@ class TranscodeImageTheme extends Themelet
global $config; global $config;
$html = " $html = "
".make_form(make_link("transcode/{$image->id}"), 'POST')." ".make_form(make_link("transcode/{$image->id}"), 'POST', false, "",
"return transcodeSubmit()")."
<input type='hidden' name='image_id' value='{$image->id}'> <input type='hidden' name='image_id' value='{$image->id}'>
<input type='hidden' id='image_lossless' name='image_lossless' value='{$image->lossless}'>
".$this->get_transcode_picker_html($options)." ".$this->get_transcode_picker_html($options)."
<br><input id='transcodebutton' type='submit' value='Transcode'> <br><input id='transcodebutton' type='submit' value='Transcode'>
</form> </form>

View File

@ -13,6 +13,7 @@ class Upgrade extends Extension
{ {
global $config, $database; global $config, $database;
if ($config->get_bool("in_upgrade")) { if ($config->get_bool("in_upgrade")) {
return; return;
} }
@ -163,9 +164,68 @@ class Upgrade extends Extension
} }
// SQLite doesn't support altering existing columns? This seems like a problem? // SQLite doesn't support altering existing columns? This seems like a problem?
log_info("upgrade", "Database at version 16"); log_info("upgrade", "Database at version 16");
$config->set_bool("in_upgrade", false); $config->set_bool("in_upgrade", false);
} }
if ($config->get_int("db_version") < 17) {
$config->set_bool("in_upgrade", true);
$config->set_int("db_version", 17);
log_info("upgrade", "Adding media information columns to images table");
$database->execute($database->scoreql_to_sql(
"ALTER TABLE images ADD COLUMN lossless SCORE_BOOL NULL"
));
$database->execute($database->scoreql_to_sql(
"ALTER TABLE images ADD COLUMN video SCORE_BOOL NULL"
));
$database->execute($database->scoreql_to_sql(
"ALTER TABLE images ADD COLUMN audio SCORE_BOOL NULL"
));
$database->execute("ALTER TABLE images ADD COLUMN length INTEGER NULL ");
log_info("upgrade", "Setting indexes for media columns");
switch($database->get_driver_name()) {
case DatabaseDriver::PGSQL:
case DatabaseDriver::SQLITE:
$database->execute('CREATE INDEX images_video_idx ON images(video) WHERE video IS NOT NULL');
$database->execute('CREATE INDEX images_audio_idx ON images(audio) WHERE audio IS NOT NULL');
$database->execute('CREATE INDEX images_length_idx ON images(length) WHERE length IS NOT NULL');
break;
default:
$database->execute('CREATE INDEX images_video_idx ON images(video)');
$database->execute('CREATE INDEX images_audio_idx ON images(audio)');
$database->execute('CREATE INDEX images_length_idx ON images(length)');
break;
}
if ($database->get_driver_name()==DatabaseDriver::PGSQL) { // These updates can take a little bit
$database->execute("SET statement_timeout TO 300000;");
}
log_info("upgrade", "Setting index for ext column");
$database->execute('CREATE INDEX images_ext_idx ON images(ext)');
$database->commit(); // Each of these commands could hit a lot of data, combining them into one big transaction would not be a good idea.
log_info("upgrade", "Setting predictable media values for known file types");
$database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_Y, video = SCORE_BOOL_Y WHERE ext IN ('swf')"));
$database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_N, video = SCORE_BOOL_N, audio = SCORE_BOOL_Y WHERE ext IN ('mp3')"));
$database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_N, video = SCORE_BOOL_N, audio = SCORE_BOOL_N WHERE ext IN ('jpg','jpeg')"));
$database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_Y, video = SCORE_BOOL_N, audio = SCORE_BOOL_N WHERE ext IN ('ico','ani','cur','png','svg')"));
$database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_Y, audio = SCORE_BOOL_N WHERE ext IN ('gif')"));
$database->execute($database->scoreql_to_sql("UPDATE images SET audio = SCORE_BOOL_N WHERE ext IN ('webp')"));
$database->execute($database->scoreql_to_sql("UPDATE images SET lossless = SCORE_BOOL_N, video = SCORE_BOOL_Y WHERE ext IN ('flv','mp4','m4v','ogv','webm')"));
log_info("upgrade", "Database at version 17");
$config->set_bool("in_upgrade", false);
}
} }
public function get_priority(): int public function get_priority(): int

View File

@ -18,6 +18,7 @@ class CustomViewImageTheme extends ViewImageTheme
$h_owner = html_escape($image->get_owner()->name); $h_owner = html_escape($image->get_owner()->name);
$h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>"; $h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>";
$h_ip = html_escape($image->owner_ip); $h_ip = html_escape($image->owner_ip);
$h_type = html_escape($image->get_mime_type());
$h_date = autodate($image->posted); $h_date = autodate($image->posted);
$h_filesize = to_shorthand_int($image->filesize); $h_filesize = to_shorthand_int($image->filesize);
@ -31,7 +32,12 @@ class CustomViewImageTheme extends ViewImageTheme
<br>Posted: $h_date by $h_ownerlink <br>Posted: $h_date by $h_ownerlink
<br>Size: {$image->width}x{$image->height} <br>Size: {$image->width}x{$image->height}
<br>Filesize: $h_filesize <br>Filesize: $h_filesize
"; <br>Type: $h_type";
if($image->length!=null) {
$h_length = format_milliseconds($image->length);
$html .= "<br/>Length: $h_length";
}
if (!is_null($image->source)) { if (!is_null($image->source)) {
$h_source = html_escape($image->source); $h_source = html_escape($image->source);

View File

@ -17,6 +17,7 @@ class CustomViewImageTheme extends ViewImageTheme
$h_owner = html_escape($image->get_owner()->name); $h_owner = html_escape($image->get_owner()->name);
$h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>"; $h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>";
$h_ip = html_escape($image->owner_ip); $h_ip = html_escape($image->owner_ip);
$h_type = html_escape($image->get_mime_type());
$h_date = autodate($image->posted); $h_date = autodate($image->posted);
$h_filesize = to_shorthand_int($image->filesize); $h_filesize = to_shorthand_int($image->filesize);
@ -30,8 +31,15 @@ class CustomViewImageTheme extends ViewImageTheme
<br>Uploader: $h_ownerlink <br>Uploader: $h_ownerlink
<br>Date: $h_date <br>Date: $h_date
<br>Size: $h_filesize ({$image->width}x{$image->height}) <br>Size: $h_filesize ({$image->width}x{$image->height})
<br>Type: $h_type
"; ";
if($image->length!=null) {
$h_length = format_milliseconds($image->length);
$html .= "<br/>Length: $h_length";
}
if (!is_null($image->source)) { if (!is_null($image->source)) {
$h_source = html_escape($image->source); $h_source = html_escape($image->source);
if (substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") { if (substr($image->source, 0, 7) != "http://" && substr($image->source, 0, 8) != "https://") {

View File

@ -18,6 +18,7 @@ class CustomViewImageTheme extends ViewImageTheme
$h_owner = html_escape($image->get_owner()->name); $h_owner = html_escape($image->get_owner()->name);
$h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>"; $h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>";
$h_ip = html_escape($image->owner_ip); $h_ip = html_escape($image->owner_ip);
$h_type = html_escape($image->get_mime_type());
$h_date = autodate($image->posted); $h_date = autodate($image->posted);
$h_filesize = to_shorthand_int($image->filesize); $h_filesize = to_shorthand_int($image->filesize);
@ -31,7 +32,13 @@ class CustomViewImageTheme extends ViewImageTheme
<br>Posted: $h_date by $h_ownerlink <br>Posted: $h_date by $h_ownerlink
<br>Size: {$image->width}x{$image->height} <br>Size: {$image->width}x{$image->height}
<br>Filesize: $h_filesize <br>Filesize: $h_filesize
<br>Type: ".$h_type."
"; ";
if($image->length!=null) {
$h_length = format_milliseconds($image->length);
$html .= "<br/>Length: $h_length";
}
if (!is_null($image->source)) { if (!is_null($image->source)) {
$h_source = html_escape($image->source); $h_source = html_escape($image->source);