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);
} else {
//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 = "";

View File

@ -54,6 +54,19 @@ class Image
/** @var boolean */
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
* would be to use Image::by_id, Image::by_hash, etc.
@ -463,7 +476,7 @@ class Image
*/
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
{
global $config;
$ext = $config->get_string("thumb_type");
return $this->get_link('image_tlink', '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
return $this->get_link(ImageConfig::TLINK, '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext);
}
/**
@ -512,7 +525,7 @@ class Image
public function get_tooltip(): string
{
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
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);
}
/**
* Given a full size pair of dimensions, return a pair scaled down to fit
* into the configured thumbnail square, with ratio intact.
@ -125,24 +124,31 @@ function get_thumbnail_size(int $orig_width, int $orig_height, bool $use_dpi_sca
}
if ($use_dpi_scaling) {
$max_size = get_thumbnail_max_size_scaled();
$max_width = $max_size[0];
$max_height = $max_size[1];
if($use_dpi_scaling) {
list($max_width, $max_height) = get_thumbnail_max_size_scaled();
} else {
$max_width = $config->get_int('thumb_width');
$max_height = $config->get_int('thumb_height');
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
}
$xscale = ($max_height / $orig_height);
$yscale = ($max_width / $orig_width);
$scale = ($xscale < $yscale) ? $xscale : $yscale;
$output = get_scaled_by_aspect_ratio($orig_width, $orig_height, $max_width, $max_height);
if ($scale > 1 && $config->get_bool('thumb_upscale')) {
if ($output[2] > 1 && $config->get_bool('thumb_upscale')) {
return [(int)$orig_width, (int)$orig_height];
} 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;
$scaling = $config->get_int("thumb_scaling");
$max_width = $config->get_int('thumb_width') * ($scaling/100);
$max_height = $config->get_int('thumb_height') * ($scaling/100);
$scaling = $config->get_int(ImageConfig::THUMB_SCALING);
$max_width = $config->get_int(ImageConfig::THUMB_WIDTH) * ($scaling/100);
$max_height = $config->get_int(ImageConfig::THUMB_HEIGHT) * ($scaling/100);
return [$max_width, $max_height];
}
/**
* Creates a thumbnail file using ImageMagick's convert command.
*
* @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
{
function create_image_thumb(string $hash, string $type, string $engine = null) {
global $config;
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$tsize = get_thumbnail_max_size_scaled();
$q = $config->get_int("thumb_quality");
$convert = $config->get_string("thumb_convert_path");
if ($convert==null||$convert=="") {
return false;
if(empty($engine)) {
$engine = $config->get_string(ImageConfig::THUMB_ENGINE);
}
// 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));
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 .= "\>";
$output_format = $config->get_string(ImageConfig::THUMB_TYPE);
if($output_format=="webp") {
$output_format = Media::WEBP_LOSSY;
}
$bg = "black";
if ($type=="webp") {
$bg = "none";
}
if (!empty($input_type)) {
$input_type = $input_type.":";
}
$format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1';
$cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg, $input_type, $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);
if ($ret!=0) {
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;
send_event(new MediaResizeEvent(
$engine,
$inname,
$type,
$outname,
$tsize[0],
$tsize[1],
false,
$output_format,
$config->get_int(ImageConfig::THUMB_QUALITY),
true,
$config->get_bool('thumb_upscale', false)
));
}
/**
* Creates a thumbnail using ffmpeg.
*
* @param $hash
* @return bool true if successful, false if not.
*/
function create_thumbnail_ffmpeg($hash): bool
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
function format_milliseconds(int $input): string
{
global $config;
$output = "";
$ffmpeg = $config->get_string("thumb_ffmpeg_path");
if ($ffmpeg==null||$ffmpeg=="") {
return false;
$remainder = floor($input / 1000);
foreach (TIME_UNITS AS $unit=>$conversion) {
$count = $remainder % $conversion;
$remainder = floor($remainder / $conversion);
if($count==0&&$remainder<1) {
break;
}
$output = "$count".$unit." ".$output;
}
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$orig_size = video_size($inname);
$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;
}
// 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");
}
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;
}
return trim($output);
}

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("VERSION", '2.7-beta'); // string shimmie version
_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("BASE_URL", null); // string force a specific base URL (default is auto-detect)
_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
$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) {
// we aren't going to fit, override

View File

@ -52,14 +52,17 @@ class ET extends Extension
$info['sys_disk'] = to_shorthand_int(disk_total_space("./") - disk_free_space("./")) . " / " .
to_shorthand_int(disk_total_space("./"));
$info['sys_server'] = isset($_SERVER["SERVER_SOFTWARE"]) ? $_SERVER["SERVER_SOFTWARE"] : 'unknown';
$info['thumb_engine'] = $config->get_string("thumb_engine");
$info['thumb_quality'] = $config->get_int('thumb_quality');
$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[MediaConfig::FFMPEG_PATH] = $config->get_string(MediaConfig::FFMPEG_PATH);
$info[MediaConfig::CONVERT_PATH] = $config->get_string(MediaConfig::CONVERT_PATH);
$info[MediaConfig::MEM_LIMIT] = $config->get_int(MediaConfig::MEM_LIMIT);
$info[ImageConfig::THUMB_ENGINE] = $config->get_string(ImageConfig::THUMB_ENGINE);
$info[ImageConfig::THUMB_QUALITY] = $config->get_int(ImageConfig::THUMB_QUALITY);
$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_comments'] = $database->get_one("SELECT COUNT(*) FROM comments");

View File

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

View File

@ -8,11 +8,31 @@
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
{
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));
}
return true;
@ -35,13 +55,7 @@ class FlashFileHandler extends DataHandlerExtension
$image->tag_array = is_array($metadata['tags']) ? $metadata['tags'] : Tag::explode($metadata['tags']);
$image->source = $metadata['source'];
$info = getimagesize($filename);
if (!$info) {
return null;
}
$image->width = $info[0];
$image->height = $info[1];
return $image;
}

View File

@ -10,6 +10,29 @@ class IcoFileHandler extends DataHandlerExtension
const SUPPORTED_EXTENSIONS = ["ico", "ani", "cur"];
public function onMediaCheckProperties(MediaCheckPropertiesEvent $event)
{
if(in_array($event->ext, self::SUPPORTED_EXTENSIONS)) {
$event->lossless = true;
$event->video = false;
$event->audio = false;
$fp = fopen($event->file_name, "r");
try {
unpack("Snull/Stype/Scount", fread($fp, 6));
$subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
} finally {
fclose($fp);
}
$width = $subheader['width'];
$height = $subheader['height'];
$event->width = $width == 0 ? 256 : $width;
$event->height = $height == 0 ? 256 : $height;
}
}
protected function supported_ext(string $ext): bool
{
return in_array(strtolower($ext), self::SUPPORTED_EXTENSIONS);
@ -19,20 +42,6 @@ class IcoFileHandler extends DataHandlerExtension
{
$image = new Image();
$fp = fopen($filename, "r");
try {
unpack("Snull/Stype/Scount", fread($fp, 6));
$subheader = unpack("Cwidth/Cheight/Ccolours/Cnull/Splanes/Sbpp/Lsize/loffset", fread($fp, 16));
} finally {
fclose($fp);
}
$width = $subheader['width'];
$height = $subheader['height'];
$image->width = $width == 0 ? 256 : $width;
$image->height = $height == 0 ? 256 : $height;
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = $metadata['filename'];
@ -56,6 +65,12 @@ class IcoFileHandler extends DataHandlerExtension
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
{
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
{
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->source = $metadata['source'];
return $image;
}

View File

@ -10,6 +10,44 @@ class PixelFileHandler extends DataHandlerExtension
{
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
{
$ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext;
@ -20,14 +58,6 @@ class PixelFileHandler extends DataHandlerExtension
{
$image = new Image();
$info = getimagesize($filename);
if (!$info) {
return null;
}
$image->width = $info[0];
$image->height = $info[1];
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$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
{
global $config;
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
$ok = false;
switch ($config->get_string("thumb_engine")) {
default:
case 'gd':
$ok = $this->make_thumb_gd($inname, $outname);
break;
case 'convert':
$ok = create_thumbnail_convert($hash);
break;
try {
create_image_thumb($hash, $type);
return true;
} 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 $ok;
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
@ -90,38 +118,4 @@ class PixelFileHandler extends DataHandlerExtension
", 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;
$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?
$exif = @exif_read_data($image->get_image_filename(), 0, true);
if ($exif) {

View File

@ -10,6 +10,24 @@ use enshrined\svgSanitize\Sanitizer;
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)
{
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
{
if (!create_thumbnail_convert($hash)) {
try {
create_image_thumb($hash, $type, MediaEngine::IMAGICK);
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;
}
return true;
}
public function onDisplayingImage(DisplayingImageEvent $event)
@ -78,10 +100,6 @@ class SVGFileHandler extends DataHandlerExtension
{
$image = new Image();
$msp = new MiniSVGParser($filename);
$image->width = $msp->width;
$image->height = $msp->height;
$image->filesize = $metadata['size'];
$image->hash = $metadata['hash'];
$image->filename = $metadata['filename'];

View File

@ -16,20 +16,21 @@
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)
{
global $config;
if ($config->get_int("ext_handle_video_version") < 1) {
if ($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) {
//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', '');
}
// This used to set the ffmpeg path. It does not do this anymore, that is now in the base graphic extension.
$config->set_int("ext_handle_video_version", 1);
log_info("handle_video", "extension installed");
}
@ -41,37 +42,83 @@ class VideoFileHandler extends DataHandlerExtension
public function onSetupBuilding(SetupBuildingEvent $event)
{
$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_label("<br>");
$sb->add_bool_option("video_playback_loop", "Loop: ");
$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.
*/
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
{
$exts = ["flv", "mp4", "m4v", "ogv", "webm"];
return in_array(strtolower($ext), $exts);
return in_array(strtolower($ext), self::SUPPORTED_EXT);
}
protected function create_image_from_data(string $filename, array $metadata): Image
{
$image = new Image();
$size = video_size($filename);
$image->width = $size[0];
$image->height = $size[1];
switch (getMimeType($filename)) {
case "video/webm":
$image->ext = "webm";
@ -103,13 +150,7 @@ class VideoFileHandler extends DataHandlerExtension
{
return (
file_exists($tmpname) &&
in_array(getMimeType($tmpname), [
'video/webm',
'video/mp4',
'video/ogg',
'video/flv',
'video/x-flv'
])
in_array(getMimeType($tmpname), self::SUPPORTED_MIME)
);
}
}

View File

@ -13,6 +13,15 @@ class VideoFileHandlerTheme extends Themelet
$loop = $config->get_bool("video_playback_loop");
$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/>";
//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' : '');
$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]}'>
<!-- 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.
*/
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)
{
global $config;
$config->set_default_int('thumb_width', 192);
$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');
$config->set_default_int(ImageConfig::THUMB_WIDTH, 192);
$config->set_default_int(ImageConfig::THUMB_HEIGHT, 192);
$config->set_default_int(ImageConfig::THUMB_SCALING, 100);
$config->set_default_int(ImageConfig::THUMB_QUALITY, 75);
$config->set_default_string(ImageConfig::THUMB_TYPE, 'jpg');
if (function_exists("exif_read_data")) {
$config->set_default_bool('image_show_meta', false);
if (function_exists(self::EXIF_READ_FUNCTION)) {
$config->set_default_bool(ImageConfig::SHOW_META, false);
}
$config->set_default_string('image_ilink', '');
$config->set_default_string('image_tlink', '');
$config->set_default_string('image_tip', '$tags // $size // $filesize');
$config->set_default_string('upload_collision_handler', 'error');
$config->set_default_int('image_expires', (60*60*24*31)); // defaults to one month
$config->set_default_string(ImageConfig::ILINK, '');
$config->set_default_string(ImageConfig::TLINK, '');
$config->set_default_string(ImageConfig::TIP, '$tags // $size // $filesize');
$config->set_default_string(ImageConfig::UPLOAD_COLLISION_HANDLER, ImageConfig::COLLISION_ERROR);
$config->set_default_int(ImageConfig::EXPIRES, (60*60*24*31)); // defaults to one month
}
public function onPageRequest(PageRequestEvent $event)
@ -125,50 +159,36 @@ class ImageIO extends Extension
$sb = new SetupBlock("Image Options");
$sb->position = 30;
// advanced only
//$sb->add_text_option("image_ilink", "Image link: ");
//$sb->add_text_option("image_tlink", "<br>Thumbnail link: ");
$sb->add_text_option("image_tip", "Image tooltip: ");
$sb->add_choice_option("upload_collision_handler", ['Error'=>'error', 'Merge'=>'merge'], "<br>Upload collision handler: ");
if (function_exists("exif_read_data")) {
$sb->add_bool_option("image_show_meta", "<br>Show metadata: ");
//$sb->add_text_option(ImageConfig::ILINK, "Image link: ");
//$sb->add_text_option(ImageConfig::TLINK, "<br>Thumbnail link: ");
$sb->add_text_option(ImageConfig::TIP, "Image tooltip: ");
$sb->add_choice_option(ImageConfig::UPLOAD_COLLISION_HANDLER, self::COLLISION_OPTIONS, "<br>Upload collision handler: ");
if (function_exists(self::EXIF_READ_FUNCTION)) {
$sb->add_bool_option(ImageConfig::SHOW_META, "<br>Show metadata: ");
}
$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->add_choice_option("thumb_engine", $thumbers, "Engine: ");
$sb->add_choice_option(ImageConfig::THUMB_ENGINE, self::THUMBNAIL_ENGINES, "Engine: ");
$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_int_option("thumb_width");
$sb->add_int_option(ImageConfig::THUMB_WIDTH);
$sb->add_label(" x ");
$sb->add_int_option("thumb_height");
$sb->add_int_option(ImageConfig::THUMB_HEIGHT);
$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("<br>High-DPI scaling ");
$sb->add_int_option("thumb_scaling");
$sb->add_int_option(ImageConfig::THUMB_SCALING);
$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);
}
@ -193,8 +213,8 @@ class ImageIO extends Extension
*/
$existing = Image::by_hash($image->hash);
if (!is_null($existing)) {
$handler = $config->get_string("upload_collision_handler");
if ($handler == "merge" || isset($_GET['update'])) {
$handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if ($handler == ImageConfig::COLLISION_MERGE || isset($_GET['update'])) {
$merged = array_merge($image->get_tag_array(), $existing->get_tag_array());
send_event(new TagSetEvent($existing, $merged));
if (isset($_GET['rating']) && isset($_GET['update']) && ext_is_live("Ratings")) {
@ -221,12 +241,12 @@ class ImageIO extends Extension
)
VALUES (
: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,
"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');
@ -244,6 +264,13 @@ class ImageIO extends Extension
if ($image->source !== null) {
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
@ -256,7 +283,7 @@ class ImageIO extends Extension
global $page;
if (!is_null($image)) {
if ($type == "thumb") {
$ext = $config->get_string("thumb_type");
$ext = $config->get_string(ImageConfig::THUMB_TYPE);
if (array_key_exists($ext, MIME_TYPE_MAP)) {
$page->set_type(MIME_TYPE_MAP[$ext]);
} else {
@ -289,8 +316,8 @@ class ImageIO extends Extension
$page->set_file($file);
if ($config->get_int("image_expires")) {
$expires = date(DATE_RFC1123, time() + $config->get_int("image_expires"));
if ($config->get_int(ImageConfig::EXPIRES)) {
$expires = date(DATE_RFC1123, time() + $config->get_int(ImageConfig::EXPIRES));
} else {
$expires = 'Fri, 2 Sep 2101 12:42:42 GMT'; // War was beginning
}
@ -320,33 +347,50 @@ class ImageIO extends Extension
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) {
$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
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);
log_debug("image", "Removing image with hash " . $existing->hash);
$existing->remove_image_only(); // Actually delete the old image file from disk
// Update the data in the database.
$database->Execute(
"UPDATE images SET
filename = :filename, filesize = :filesize, hash = :hash,
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
]
);
try {
Media::update_image_media_properties($image->hash, $image->ext);
} catch(MediaException $e) {
log_warning("image_replace","Error while running update_image_media_properties: ".$e->getMessage());
}
/* Generate new thumbnail */
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;
// thumb
$this->preview_height = $config->get_int('thumb_height');
$this->preview_width = $config->get_int('thumb_width');
$this->preview_height = $config->get_int(ImageConfig::THUMB_HEIGHT);
$this->preview_width = $config->get_int(ImageConfig::THUMB_WIDTH);
$this->preview_url = make_http($img->get_thumb_link());
// sample (use the full image here)
@ -481,8 +481,8 @@ class OuroborosAPI extends Extension
protected function postCreate(OuroborosPost $post, string $md5 = '')
{
global $config;
$handler = $config->get_string("upload_collision_handler");
if (!empty($md5) && !($handler == 'merge')) {
$handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if (!empty($md5) && !($handler == ImageConfig::COLLISION_MERGE)) {
$img = Image::by_hash($md5);
if (!is_null($img)) {
$this->sendResponse(420, self::ERROR_POST_CREATE_DUPE);
@ -524,8 +524,8 @@ class OuroborosAPI extends Extension
if (!empty($meta['hash'])) {
$img = Image::by_hash($meta['hash']);
if (!is_null($img)) {
$handler = $config->get_string("upload_collision_handler");
if ($handler == "merge") {
$handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER);
if ($handler == ImageConfig::COLLISION_MERGE) {
$postTags = is_array($post->tags) ? $post->tags : Tag::explode($post->tags);
$merged = array_merge($postTags, $img->get_tag_array());
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 = "
<table id='reportedimage' class='zebra'>
<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
{
const SUPPORTED_EXT = ["jpg","jpeg","png","gif","webp"];
/**
* Needs to be after the data processing extensions
*/
@ -40,17 +38,18 @@ class ResizeImage extends Extension
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_bool('resize_enabled', true);
$config->set_default_bool('resize_upload', false);
$config->set_default_int('resize_default_width', 0);
$config->set_default_int('resize_default_height', 0);
$config->set_default_bool(ResizeConfig::ENABLED, true);
$config->set_default_bool(ResizeConfig::UPLOAD, false);
$config->set_default_string(ResizeConfig::ENGINE, MediaEngine::GD);
$config->set_default_int(ResizeConfig::DEFAULT_WIDTH, 0);
$config->set_default_int(ResizeConfig::DEFAULT_HEIGHT, 0);
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user, $config;
if ($user->is_admin() && $config->get_bool("resize_enabled")
&& in_array($event->image->ext, self::SUPPORTED_EXT)) {
if ($user->is_admin() && $config->get_bool(ResizeConfig::ENABLED)
&& $this->can_resize_format($event->image->ext, $event->image->lossless)) {
/* Add a link to resize the 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->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::UPLOAD, "Resize on upload: ", true);
$sb->end_table();
@ -82,15 +82,15 @@ class ResizeImage extends Extension
$image_obj = Image::by_id($event->image_id);
if ($config->get_bool("resize_upload") == true
&& in_array($event->type, self::SUPPORTED_EXT)) {
if ($config->get_bool(ResizeConfig::UPLOAD) == true
&& $this->can_resize_format($event->type, $image_obj->lossless)) {
$width = $height = 0;
if ($config->get_int("resize_default_width") !== 0) {
$height = $config->get_int("resize_default_width");
if ($config->get_int(ResizeConfig::DEFAULT_WIDTH) !== 0) {
$height = $config->get_int(ResizeConfig::DEFAULT_WIDTH);
}
if ($config->get_int("resize_default_height") !== 0) {
$height = $config->get_int("resize_default_height");
if ($config->get_int(ResizeConfig::DEFAULT_HEIGHT) !== 0) {
$height = $config->get_int(ResizeConfig::DEFAULT_HEIGHT);
}
$isanigif = 0;
if ($image_obj->ext == "gif") {
@ -169,18 +169,33 @@ 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 function resize_image(Image $image_obj, int $width, int $height)
{
global $database;
global $database, $config;
if (($height <= 0) && ($width <= 0)) {
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;
$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.");
}
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->hash = md5_file($tmp_filename);

View File

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

View File

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

View File

@ -30,7 +30,7 @@ class Rule34 extends Extension
public function onImageInfoBoxBuilding(ImageInfoBoxBuildingEvent $event)
{
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);
$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>";

View File

@ -39,6 +39,6 @@ class SetupTest extends ShimmiePHPUnitTestCase
$this->log_in_as_admin();
$this->get_page('setup/advanced');
$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 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 = [
"BMP" => "bmp",
"GIF" => "gif",
@ -88,17 +44,12 @@ class TranscodeImage extends Extension
"WEBP" => "webp",
];
const FORMAT_ALIASES = [
"tif" => "tiff",
"jpeg" => "jpg",
];
const OUTPUT_FORMATS = [
"" => "",
"JPEG (lossy)" => "jpg",
"PNG (lossless)" => "png",
"WEBP (lossy)" => "webp-lossy",
"WEBP (lossless)" => "webp-lossless",
"WEBP (lossy)" => Media::WEBP_LOSSY,
"WEBP (lossless)" => Media::WEBP_LOSSLESS,
];
/**
@ -113,13 +64,13 @@ class TranscodeImage extends Extension
public function onInitExt(InitExtEvent $event)
{
global $config;
$config->set_default_bool('transcode_enabled', true);
$config->set_default_bool('transcode_upload', false);
$config->set_default_string('transcode_engine', "gd");
$config->set_default_int('transcode_quality', 80);
$config->set_default_bool(TranscodeConfig::ENABLED, true);
$config->set_default_bool(TranscodeConfig::UPLOAD, false);
$config->set_default_string(TranscodeConfig::ENGINE, MediaEngine::GD);
$config->set_default_int(TranscodeConfig::QUALITY, 80);
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;
if ($user->is_admin() && $config->get_bool("resize_enabled")) {
$engine = $config->get_string("transcode_engine");
if ($this->can_convert_format($engine, $event->image->ext)) {
$options = $this->get_supported_output_formats($engine, $event->image->ext);
if ($user->is_admin()) {
$engine = $config->get_string(TranscodeConfig::ENGINE);
if ($this->can_convert_format($engine, $event->image->ext, $event->image->lossless)) {
$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));
}
}
@ -140,16 +91,16 @@ class TranscodeImage extends Extension
{
global $config;
$engine = $config->get_string("transcode_engine");
$engine = $config->get_string(TranscodeConfig::ENGINE);
$sb = new SetupBlock("Image Transcode");
$sb->start_table();
$sb->add_bool_option(TranscodeConfig::ENABLED, "Allow transcoding images: ", 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) {
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);
$sb->add_choice_option(TranscodeConfig::UPLOAD_PREFIX.$format, $outputs, "$display", true);
}
@ -163,23 +114,23 @@ class TranscodeImage extends Extension
{
global $config, $page;
if ($config->get_bool("transcode_upload") == true) {
if ($config->get_bool(TranscodeConfig::UPLOAD) == true) {
$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;
}
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)) {
return;
}
try {
$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);
} catch (Exception $e) {
log_error("transcode", "Error while performing upload transcode: ".$e->getMessage());
@ -227,7 +178,7 @@ class TranscodeImage extends Extension
{
global $user, $config;
$engine = $config->get_string("transcode_engine");
$engine = $config->get_string(TranscodeConfig::ENGINE);
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)));
@ -239,7 +190,7 @@ class TranscodeImage extends Extension
global $user, $database;
switch ($event->action) {
case "bulk_transcode":
case self::ACTION_BULK_TRANSCODE:
if (!isset($_POST['transcode_format'])) {
return;
}
@ -251,8 +202,9 @@ class TranscodeImage extends Extension
$database->beginTransaction();
$this->transcode_and_replace_image($image, $format);
// If a subsequent transcode fails, the database need to have everything about the previous transcodes recorded already,
// otherwise the image entries will be stuck pointing to missing image files
// If a subsequent transcode fails, the database needs to have everything about the previous
// transcodes recorded already, otherwise the image entries will be stuck pointing to
// missing image files
$database->commit();
$total++;
} 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 self::FORMAT_ALIASES[$format];
}
return $format;
return Media::is_input_supported($engine, $format, $lossless);
}
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 = [];
foreach (self::OUTPUT_FORMATS as $key=>$value) {
if ($value=="") {
$output[$key] = $value;
continue;
}
if (in_array($value, self::ENGINE_OUTPUT_SUPPORT[$engine])
&&(empty($omit_format)||$omit_format!=$this->determine_ext($value))) {
if(Media::is_output_supported($engine, $value)
&&(empty($omit_format)||$omit_format!=$value)) {
$output[$key] = $value;
}
}
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)
{
$target_format = $this->clean_format($target_format);
$original_file = warehouse_path(Image::IMAGE_DIR, $image_obj->hash);
$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->width = $image_obj->width;
$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 */
$target = warehouse_path(Image::IMAGE_DIR, $new_image->hash);
@ -346,7 +282,7 @@ class TranscodeImage extends Extension
{
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);
}
@ -357,7 +293,7 @@ class TranscodeImage extends Extension
if (!$this->can_convert_format($engine, $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");
}
@ -381,7 +317,8 @@ class TranscodeImage extends Extension
try {
$result = false;
switch ($target_format) {
case "webp-lossy":
case "webp":
case Media::WEBP_LOSSY:
$result = imagewebp($image, $tmp_name, $q);
break;
case "png":
@ -426,20 +363,20 @@ class TranscodeImage extends Extension
global $config;
$q = $config->get_int("transcode_quality");
$convert = $config->get_string("thumb_convert_path");
$convert = $config->get_string(MediaConfig::CONVERT_PATH);
if ($convert==null||$convert=="") {
throw new ImageTranscodeException("ImageMagick path not configured");
}
$ext = $this->determine_ext($target_format);
$ext = Media::determine_ext($target_format);
$args = " -flatten ";
$bg = "none";
switch ($target_format) {
case "webp-lossless":
case Media::WEBP_LOSSLESS:
$args .= '-define webp:lossless=true';
break;
case "webp-lossy":
case Media::WEBP_LOSSY:
$args .= '';
break;
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;
$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' id='image_lossless' name='image_lossless' value='{$image->lossless}'>
".$this->get_transcode_picker_html($options)."
<br><input id='transcodebutton' type='submit' value='Transcode'>
</form>

View File

@ -13,6 +13,7 @@ class Upgrade extends Extension
{
global $config, $database;
if ($config->get_bool("in_upgrade")) {
return;
}
@ -163,9 +164,68 @@ class Upgrade extends Extension
}
// SQLite doesn't support altering existing columns? This seems like a problem?
log_info("upgrade", "Database at version 16");
$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

View File

@ -18,6 +18,7 @@ class CustomViewImageTheme extends ViewImageTheme
$h_owner = html_escape($image->get_owner()->name);
$h_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>";
$h_ip = html_escape($image->owner_ip);
$h_type = html_escape($image->get_mime_type());
$h_date = autodate($image->posted);
$h_filesize = to_shorthand_int($image->filesize);
@ -31,7 +32,12 @@ class CustomViewImageTheme extends ViewImageTheme
<br>Posted: $h_date by $h_ownerlink
<br>Size: {$image->width}x{$image->height}
<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)) {
$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_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>";
$h_ip = html_escape($image->owner_ip);
$h_type = html_escape($image->get_mime_type());
$h_date = autodate($image->posted);
$h_filesize = to_shorthand_int($image->filesize);
@ -30,8 +31,15 @@ class CustomViewImageTheme extends ViewImageTheme
<br>Uploader: $h_ownerlink
<br>Date: $h_date
<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)) {
$h_source = html_escape($image->source);
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_ownerlink = "<a href='".make_link("user/$h_owner")."'>$h_owner</a>";
$h_ip = html_escape($image->owner_ip);
$h_type = html_escape($image->get_mime_type());
$h_date = autodate($image->posted);
$h_filesize = to_shorthand_int($image->filesize);
@ -31,7 +32,13 @@ class CustomViewImageTheme extends ViewImageTheme
<br>Posted: $h_date by $h_ownerlink
<br>Size: {$image->width}x{$image->height}
<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)) {
$h_source = html_escape($image->source);