Added additional media properties to the images table, video, audio, length, and lossless.

Added new event to handle fetching media properties like height, width, and the newly added fields, and admin controls to manually scan files for their properties.
Added a search terms content:video and content:audio to search for images that do (or do not) have those flags.
This commit is contained in:
Matthew Barbour 2019-06-25 15:17:13 -05:00 committed by matthew
parent a41e99d1af
commit b1db833d51
19 changed files with 607 additions and 95 deletions

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.

View file

@ -194,3 +194,21 @@ function create_image_thumb(string $hash, string $type, string $engine = null) {
}
const TIME_UNITS = ["s"=>60,"m"=>60,"h"=>24,"d"=>365,"y"=>PHP_INT_MAX];
function format_milliseconds(int $input): string
{
$output = "";
$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;
}
return trim($output);
}

View file

@ -8,6 +8,26 @@
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;
@ -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'];

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'];

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)) {
@ -82,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

@ -48,6 +48,60 @@ class VideoFileHandler extends DataHandlerExtension
$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.
*/
@ -65,10 +119,6 @@ class VideoFileHandler extends DataHandlerExtension
{
$image = new Image();
$size = Media::video_size($filename);
$image->width = $size[0];
$image->height = $size[1];
switch (getMimeType($filename)) {
case "video/webm":
$image->ext = "webm";

View file

@ -241,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');
@ -264,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
@ -340,33 +347,49 @@ class ImageIO extends Extension
throw new ImageReplaceException("Image to replace does not exist!");
}
$duplicate = Image::by_hash($image->hash);
if(!is_null($duplicate)) {
$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)));

View file

@ -49,7 +49,9 @@ abstract class MediaEngine
Media::WEBP_LOSSLESS,
],
MediaEngine::FFMPEG => [
"jpg",
"webp",
"png"
]
];
public const INPUT_SUPPORT = [
@ -59,6 +61,8 @@ abstract class MediaEngine
"jpg",
"png",
"webp",
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS
],
MediaEngine::IMAGICK => [
"bmp",
@ -68,6 +72,8 @@ abstract class MediaEngine
"psd",
"tiff",
"webp",
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
"ico",
],
MediaEngine::FFMPEG => [
@ -122,11 +128,16 @@ class MediaResizeEvent extends Event
}
}
class MediaCheckLosslessEvent extends Event
class MediaCheckPropertiesEvent extends Event
{
public $file_name;
public $ext;
public $result = false;
public $lossless = null;
public $audio = null;
public $video = null;
public $length = null;
public $height = null;
public $width = null;
public function __construct(string $file_name, string $ext)
{
@ -136,6 +147,7 @@ class MediaCheckLosslessEvent extends Event
}
class Media extends Extension
{
const WEBP_LOSSY = "webp-lossy";
@ -149,11 +161,19 @@ class Media extends Extension
const LOSSLESS_FORMATS = [
self::WEBP_LOSSLESS,
"png",
"psd",
"bmp",
"ico",
"cur",
"ani",
"gif"
];
const ALPHA_FORMATS = [
self::WEBP_LOSSLESS,
self::WEBP_LOSSY,
"webp",
"png",
];
@ -191,7 +211,7 @@ class Media extends Extension
global $config;
$config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe');
$config->set_default_int(MediaConfig::MEM_LIMIT, parse_shorthand_int('8MB'));
$config->set_default_string(MediaConfig::FFMPEG_PATH, '');
$config->set_default_string(MediaConfig::FFMPEG_PATH, 'ffmpeg');
$config->set_default_string(MediaConfig::CONVERT_PATH, 'convert');
@ -206,6 +226,13 @@ class Media extends Extension
}
}
if ($ffprobe = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffprobe')) {
//ffprobe exists in PATH, check if it's executable, and if so, default to it instead of static
if (is_executable(strtok($ffprobe, PHP_EOL))) {
$config->set_default_string(MediaConfig::FFPROBE_PATH, 'ffprobe');
}
}
$current_value = $config->get_string("thumb_convert_path");
if (!empty($current_value)) {
$config->set_string(MediaConfig::CONVERT_PATH, $current_value);
@ -226,6 +253,20 @@ class Media extends Extension
}
}
public function onPageRequest(PageRequestEvent $event)
{
global $database, $page, $user;
if ($event->page_matches("media_rescan/") && $user->is_admin() && isset($_POST['image_id'])) {
$image = Image::by_id(int_escape($_POST['image_id']));
$this->update_image_media_properties($image->hash, $image->ext);
$page->set_mode(PageMode::REDIRECT);
$page->set_redirect(make_link("post/view/$image->id"));
}
}
public function onSetupBuilding(SetupBuildingEvent $event)
{
$sb = new SetupBlock("Media Engines");
@ -243,6 +284,7 @@ class Media extends Extension
// }
$sb->add_text_option(MediaConfig::FFMPEG_PATH, "<br/>ffmpeg command: ");
$sb->add_text_option(MediaConfig::FFPROBE_PATH, "<br/>ffprobe command: ");
$sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "<br />Max memory use: ");
@ -250,6 +292,66 @@ class Media extends Extension
}
public function onAdminBuilding(AdminBuildingEvent $event)
{
global $database;
$types = $database->get_all("SELECT ext, count(*) count FROM images group by ext");
$this->theme->display_form($types);
}
public function onAdminAction(AdminActionEvent $event)
{
$action = $event->action;
if (method_exists($this, $action)) {
$event->redirect = $this->$action();
}
}
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event)
{
global $user;
if ($user->can("delete_image")) {
$event->add_part($this->theme->get_buttons_html($event->image->id));
}
}
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
{
global $user;
if ($user->is_admin()) {
$event->add_action("bulk_media_rescan", "Scan Media Properties");
}
}
public function onBulkAction(BulkActionEvent $event)
{
global $user;
switch ($event->action) {
case "bulk_media_rescan":
if ($user->is_admin()) {
$total = 0;
foreach ($event->items as $id) {
$image = Image::by_id($id);
if ($image == null) {
continue;
}
try {
$this->update_image_media_properties($image->hash, $image->ext);
$total++;
} catch (MediaException $e) {
}
}
flash_message("Scanned media properties for $total items");
}
break;
}
}
/**
* @param MediaResizeEvent $event
* @throws MediaException
@ -302,22 +404,88 @@ class Media extends Extension
// }
}
public function onMediaCheckLossless(MediaCheckLosslessEvent $event)
const CONTENT_SEARCH_TERM_REGEX = "/^(-)?content[=|:]((video)|(audio))$/i";
public function onSearchTermParse(SearchTermParseEvent $event)
{
switch ($event->ext) {
case "png":
case "psd":
case "bmp":
case "gif":
case "ico":
$event->result = true;
break;
case "webp":
$event->result = Media::is_lossless_webp($event->file_name);
break;
global $database;
$matches = [];
if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term, $matches)) {
$positive = $matches[1];
$field = $matches[2];
$event->add_querylet(new Querylet("$field = " . $database->scoreql_to_sql($positive != "-" ? "SCORE_BOOL_Y" : "SCORE_BOOL_N")));
}
}
public function onTagTermParse(TagTermParseEvent $event)
{
$matches = [];
if (preg_match(self::SEARCH_TERM_REGEX, strtolower($event->term), $matches) && $event->parse) {
// Nothing to save, just helping filter out reserved tags
}
if (!empty($matches)) {
$event->metatag = true;
}
}
private function media_rescan(): bool
{
$ext = "";
if (array_key_exists("media_rescan_type", $_POST)) {
$ext = $_POST["media_rescan_type"];
}
$results = $this->get_images($ext);
foreach ($results as $result) {
$this->update_image_media_properties($result["hash"], $result["ext"]);
}
return true;
}
public static function update_image_media_properties(string $hash, string $ext)
{
global $database;
$path = warehouse_path(Image::IMAGE_DIR, $hash);
$mcpe = new MediaCheckPropertiesEvent($path, $ext);
send_event($mcpe);
$database->execute(
"UPDATE images SET
lossless = :lossless, video = :video, audio = :audio,
height = :height, width = :width,
length = :length WHERE hash = :hash",
[
"hash" => $hash,
"width" => $mcpe->width ?? 0,
"height" => $mcpe->height ?? 0,
"lossless" => $database->scoresql_value_prepare($mcpe->lossless),
"video" => $database->scoresql_value_prepare($mcpe->video),
"audio" => $database->scoresql_value_prepare($mcpe->audio),
"length" => $mcpe->length
]);
}
public function get_images(String $ext = null)
{
global $database;
$query = "SELECT id, hash, ext FROM images ";
$args = [];
if (!empty($ext)) {
$query .= " WHERE ext = :ext";
$args["ext"] = $ext;
}
return $database->get_all($query, $args);
}
/**
* Check Memory usage limits
*
@ -404,6 +572,41 @@ class Media extends Extension
}
}
public static function get_ffprobe_data($filename): array
{
global $config;
$ffprobe = $config->get_string(MediaConfig::FFPROBE_PATH);
if ($ffprobe == null || $ffprobe == "") {
throw new MediaException("ffprobe command configured");
}
$args = [
escapeshellarg($ffprobe),
"-print_format", "json",
"-v", "quiet",
"-show_format",
"-show_streams",
escapeshellarg($filename),
];
$cmd = escapeshellcmd(implode(" ", $args));
exec($cmd, $output, $ret);
if ((int)$ret == (int)0) {
log_debug('Media', "Getting media data `$cmd`, returns $ret");
$output = implode($output);
$data = json_decode($output, true);
return $data;
} else {
log_error('Media', "Getting media data `$cmd`, returns $ret");
return [];
}
}
public static function determine_ext(String $format): String
{
$format = self::normalize_format($format);
@ -773,11 +976,9 @@ class Media extends Extension
}
private static function compare_file_bytes(String $file_name, array $comparison): bool
{
$size= filesize($file_name);
$size = filesize($file_name);
if ($size < count($comparison)) {
// Can't match because it's too small
return false;
@ -785,15 +986,15 @@ class Media extends Extension
if (($fh = @fopen($file_name, 'rb'))) {
try {
$chunk = unpack("C*",fread($fh, count($comparison)));
$chunk = unpack("C*", fread($fh, count($comparison)));
for ($i = 0; $i < count($comparison); $i++) {
$byte = $comparison[$i];
if($byte==null) {
if ($byte == null) {
continue;
} else {
$fileByte = $chunk[$i+1];
if($fileByte!=$byte) {
$fileByte = $chunk[$i + 1];
if ($fileByte != $byte) {
return false;
}
}
@ -823,9 +1024,9 @@ class Media extends Extension
return in_array(self::normalize_format($format), self::ALPHA_FORMATS);
}
public static function is_input_supported($engine, $format): bool
public static function is_input_supported($engine, $format, ?bool $lossless = null): bool
{
$format = self::normalize_format($format);
$format = self::normalize_format($format, $lossless);
if (!in_array($format, MediaEngine::INPUT_SUPPORT[$engine])) {
return false;
}
@ -849,8 +1050,16 @@ class Media extends Extension
* @param $format
* @return string|null The format name that the media extension will recognize.
*/
static public function normalize_format($format): ?string
static public function normalize_format(string $format, ?bool $lossless = null): ?string
{
if ($format == "webp") {
if ($lossless === true) {
$format = Media::WEBP_LOSSLESS;
} else {
$format = Media::WEBP_LOSSY;
}
}
if (array_key_exists($format, Media::FORMAT_ALIASES)) {
return self::FORMAT_ALIASES[$format];
}

View file

@ -2,5 +2,29 @@
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 .= "<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><br/>";
$html .= "<input type='submit' value='Scan Media Information'>";
$html .= "</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

@ -49,7 +49,7 @@ class ResizeImage extends Extension
{
global $user, $config;
if ($user->is_admin() && $config->get_bool(ResizeConfig::ENABLED)
&& $this->can_resize_format($event->image->ext)) {
&& $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));
}
@ -83,7 +83,7 @@ class ResizeImage extends Extension
$image_obj = Image::by_id($event->image_id);
if ($config->get_bool(ResizeConfig::UPLOAD) == true
&& $this->can_resize_format($event->type)) {
&& $this->can_resize_format($event->type, $image_obj->lossless)) {
$width = $height = 0;
if ($config->get_int(ResizeConfig::DEFAULT_WIDTH) !== 0) {
@ -170,7 +170,7 @@ class ResizeImage extends Extension
}
}
private function can_resize_format($format): bool
private function can_resize_format($format, ?bool $lossless): bool
{
global $config;
$engine = $config->get_string(ResizeConfig::ENGINE);

View file

@ -80,8 +80,8 @@ class TranscodeImage extends Extension
if ($user->is_admin()) {
$engine = $config->get_string(TranscodeConfig::ENGINE);
if ($this->can_convert_format($engine, $event->image->ext)) {
$options = $this->get_supported_output_formats($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, $event->image->lossless??false);
$event->add_part($this->theme->get_transcode_html($event->image, $options));
}
}
@ -222,23 +222,27 @@ class TranscodeImage extends Extension
}
private function can_convert_format($engine, $format): bool
private function can_convert_format($engine, $format, ?bool $lossless = null): bool
{
return Media::is_input_supported($engine, $format);
return Media::is_input_supported($engine, $format, $lossless);
}
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 = Media::normalize_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(Media::is_output_supported($engine, $value)
&&(empty($omit_format)||$omit_format!=Media::determine_ext($value))) {
&&(empty($omit_format)||$omit_format!=$value)) {
$output[$key] = $value;
}
}
@ -278,7 +282,7 @@ class TranscodeImage extends Extension
{
global $config;
if ($source_format==Media::determine_ext($target_format)) {
if ($source_format==$target_format) {
throw new ImageTranscodeException("Source and target formats are the same: ".$source_format);
}
@ -313,6 +317,7 @@ class TranscodeImage extends Extension
try {
$result = false;
switch ($target_format) {
case "webp":
case Media::WEBP_LOSSY:
$result = imagewebp($image, $tmp_name, $q);
break;

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

@ -166,6 +166,63 @@ class Upgrade extends Extension
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("UPDATE images SET lossless = true, video = true WHERE ext IN ('swf')");
$database->execute("UPDATE images SET lossless = false, video = false, audio = true WHERE ext IN ('mp3')");
$database->execute("UPDATE images SET lossless = false, video = false, audio = false WHERE ext IN ('jpg','jpeg')");
$database->execute("UPDATE images SET lossless = true, video = false, audio = false WHERE ext IN ('ico','ani','cur','png','svg')");
$database->execute("UPDATE images SET lossless = true, audio = false WHERE ext IN ('gif')");
$database->execute("UPDATE images SET audio = false WHERE ext IN ('webp')");
$database->execute("UPDATE images SET lossless = false, video = true 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

@ -34,6 +34,11 @@ class CustomViewImageTheme extends ViewImageTheme
<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);