From b1db833d51fe6a95430effd434ab2eb9bc5df761 Mon Sep 17 00:00:00 2001 From: Matthew Barbour Date: Tue, 25 Jun 2019 15:17:13 -0500 Subject: [PATCH] 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. --- core/imageboard/image.php | 13 ++ core/imageboard/misc.php | 18 +++ ext/handle_flash/main.php | 26 +++- ext/handle_ico/main.php | 37 +++-- ext/handle_mp3/main.php | 14 ++ ext/handle_pixel/main.php | 46 +++++- ext/handle_svg/main.php | 22 ++- ext/handle_video/main.php | 58 ++++++- ext/image/main.php | 63 +++++--- ext/media/main.php | 261 ++++++++++++++++++++++++++++---- ext/media/theme.php | 24 +++ ext/resize/main.php | 6 +- ext/transcode/main.php | 21 ++- ext/transcode/script.js | 11 ++ ext/transcode/theme.php | 4 +- ext/upgrade/main.php | 57 +++++++ themes/danbooru/view.theme.php | 8 +- themes/danbooru2/view.theme.php | 8 + themes/lite/view.theme.php | 5 + 19 files changed, 607 insertions(+), 95 deletions(-) create mode 100644 ext/transcode/script.js diff --git a/core/imageboard/image.php b/core/imageboard/image.php index d6b63371..90ab9e6a 100644 --- a/core/imageboard/image.php +++ b/core/imageboard/image.php @@ -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. diff --git a/core/imageboard/misc.php b/core/imageboard/misc.php index 5af65d6f..cec051d1 100644 --- a/core/imageboard/misc.php +++ b/core/imageboard/misc.php @@ -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); +} \ No newline at end of file diff --git a/ext/handle_flash/main.php b/ext/handle_flash/main.php index 3277d2b9..3c53622d 100644 --- a/ext/handle_flash/main.php +++ b/ext/handle_flash/main.php @@ -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; } diff --git a/ext/handle_ico/main.php b/ext/handle_ico/main.php index 840767c5..f67937b6 100644 --- a/ext/handle_ico/main.php +++ b/ext/handle_ico/main.php @@ -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']; diff --git a/ext/handle_mp3/main.php b/ext/handle_mp3/main.php index e0fcd5a7..81fbee49 100644 --- a/ext/handle_mp3/main.php +++ b/ext/handle_mp3/main.php @@ -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; } diff --git a/ext/handle_pixel/main.php b/ext/handle_pixel/main.php index f5c9b3f1..3b73bdfb 100644 --- a/ext/handle_pixel/main.php +++ b/ext/handle_pixel/main.php @@ -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']; diff --git a/ext/handle_svg/main.php b/ext/handle_svg/main.php index 4df1154b..79fd6613 100644 --- a/ext/handle_svg/main.php +++ b/ext/handle_svg/main.php @@ -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']; diff --git a/ext/handle_video/main.php b/ext/handle_video/main.php index 666def30..5c090faf 100644 --- a/ext/handle_video/main.php +++ b/ext/handle_video/main.php @@ -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"; diff --git a/ext/image/main.php b/ext/image/main.php index c851a90d..ff883c77 100644 --- a/ext/image/main.php +++ b/ext/image/main.php @@ -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 {$duplicate->id} " . + "already has hash {$image->hash}:

" . $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))); diff --git a/ext/media/main.php b/ext/media/main.php index 610959a9..6b87c60a 100644 --- a/ext/media/main.php +++ b/ext/media/main.php @@ -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, "
ffmpeg command: "); + $sb->add_text_option(MediaConfig::FFPROBE_PATH, "
ffprobe command: "); $sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "
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]; } diff --git a/ext/media/theme.php b/ext/media/theme.php index e213a0e9..d2c7755c 100644 --- a/ext/media/theme.php +++ b/ext/media/theme.php @@ -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 .= "
"; + $html .= ""; + $html .= "\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/"))." + + + + "; + } } diff --git a/ext/resize/main.php b/ext/resize/main.php index 166b93f1..9290f08b 100644 --- a/ext/resize/main.php +++ b/ext/resize/main.php @@ -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); diff --git a/ext/transcode/main.php b/ext/transcode/main.php index 41dfa07e..72119c82 100644 --- a/ext/transcode/main.php +++ b/ext/transcode/main.php @@ -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; diff --git a/ext/transcode/script.js b/ext/transcode/script.js new file mode 100644 index 00000000..6f78ac33 --- /dev/null +++ b/ext/transcode/script.js @@ -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?'); + } + } +} \ No newline at end of file diff --git a/ext/transcode/theme.php b/ext/transcode/theme.php index 85e948f0..bee29cc8 100644 --- a/ext/transcode/theme.php +++ b/ext/transcode/theme.php @@ -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()")." + ".$this->get_transcode_picker_html($options)."
diff --git a/ext/upgrade/main.php b/ext/upgrade/main.php index a96c2e78..68f09fe1 100644 --- a/ext/upgrade/main.php +++ b/ext/upgrade/main.php @@ -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 diff --git a/themes/danbooru/view.theme.php b/themes/danbooru/view.theme.php index 62f67c3b..f113541a 100644 --- a/themes/danbooru/view.theme.php +++ b/themes/danbooru/view.theme.php @@ -18,6 +18,7 @@ class CustomViewImageTheme extends ViewImageTheme $h_owner = html_escape($image->get_owner()->name); $h_ownerlink = "$h_owner"; $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
Posted: $h_date by $h_ownerlink
Size: {$image->width}x{$image->height}
Filesize: $h_filesize - "; +
Type: $h_type"; + + if($image->length!=null) { + $h_length = format_milliseconds($image->length); + $html .= "
Length: $h_length"; + } if (!is_null($image->source)) { $h_source = html_escape($image->source); diff --git a/themes/danbooru2/view.theme.php b/themes/danbooru2/view.theme.php index 589d054c..237f1a8c 100644 --- a/themes/danbooru2/view.theme.php +++ b/themes/danbooru2/view.theme.php @@ -17,6 +17,7 @@ class CustomViewImageTheme extends ViewImageTheme $h_owner = html_escape($image->get_owner()->name); $h_ownerlink = "$h_owner"; $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
Uploader: $h_ownerlink
Date: $h_date
Size: $h_filesize ({$image->width}x{$image->height}) +
Type: $h_type "; + if($image->length!=null) { + $h_length = format_milliseconds($image->length); + $html .= "
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://") { diff --git a/themes/lite/view.theme.php b/themes/lite/view.theme.php index 4c1beac9..a4613467 100644 --- a/themes/lite/view.theme.php +++ b/themes/lite/view.theme.php @@ -34,6 +34,11 @@ class CustomViewImageTheme extends ViewImageTheme
Filesize: $h_filesize
Type: ".$h_type." "; + if($image->length!=null) { + $h_length = format_milliseconds($image->length); + $html .= "
Length: $h_length"; + } + if (!is_null($image->source)) { $h_source = html_escape($image->source);