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:
parent
a41e99d1af
commit
b1db833d51
19 changed files with 607 additions and 95 deletions
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
11
ext/transcode/script.js
Normal 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?');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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://") {
|
||||
|
|
|
@ -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);
|
||||
|
|
Reference in a new issue