63b2601e67
Changed mime type map to deal with the reality that certain file types have multiple extensions and/or multiple mime types, as well as constants supporting all of the data. Created new functions using the updated mime type map to resolve mime types and extensions. Updated various items around the project that determine mime/extension to take advantage of the new functions.
1022 lines
36 KiB
PHP
1022 lines
36 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
require_once "config.php";
|
|
require_once "events.php";
|
|
require_once "media_engine.php";
|
|
|
|
/*
|
|
* This is used by the media code when there is an error
|
|
*/
|
|
class MediaException extends SCoreException
|
|
{
|
|
}
|
|
|
|
class Media extends Extension
|
|
{
|
|
/** @var MediaTheme */
|
|
protected $theme;
|
|
|
|
const WEBP_LOSSY = "webp-lossy";
|
|
const WEBP_LOSSLESS = "webp-lossless";
|
|
|
|
const IMAGE_MEDIA_ENGINES = [
|
|
"GD" => MediaEngine::GD,
|
|
"ImageMagick" => MediaEngine::IMAGICK,
|
|
];
|
|
|
|
const LOSSLESS_FORMATS = [
|
|
self::WEBP_LOSSLESS,
|
|
EXTENSION_PNG,
|
|
EXTENSION_PSD,
|
|
EXTENSION_BMP,
|
|
EXTENSION_ICO,
|
|
EXTENSION_CUR,
|
|
EXTENSION_ANI,
|
|
EXTENSION_GIF
|
|
|
|
];
|
|
|
|
const ALPHA_FORMATS = [
|
|
self::WEBP_LOSSLESS,
|
|
self::WEBP_LOSSY,
|
|
EXTENSION_WEBP,
|
|
EXTENSION_PNG,
|
|
];
|
|
|
|
const FORMAT_ALIASES = [
|
|
EXTENSION_TIF => EXTENSION_TIFF,
|
|
EXTENSION_JPEG => EXTENSION_JPG,
|
|
];
|
|
|
|
|
|
//RIFF####WEBPVP8?..............ANIM
|
|
private const WEBP_ANIMATION_HEADER =
|
|
[0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, null,
|
|
null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0x41, 0x4E, 0x49, 0x4D];
|
|
|
|
//RIFF####WEBPVP8L
|
|
private const WEBP_LOSSLESS_HEADER =
|
|
[0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x4C];
|
|
|
|
|
|
public static function imagick_available(): bool
|
|
{
|
|
return extension_loaded("imagick");
|
|
}
|
|
|
|
/**
|
|
* High priority just so that it can be early in the settings
|
|
*/
|
|
public function get_priority(): int
|
|
{
|
|
return 30;
|
|
}
|
|
|
|
public function onInitExt(InitExtEvent $event)
|
|
{
|
|
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, 'ffmpeg');
|
|
$config->set_default_string(MediaConfig::CONVERT_PATH, 'convert');
|
|
}
|
|
|
|
public function onPageRequest(PageRequestEvent $event)
|
|
{
|
|
global $page, $user;
|
|
|
|
if ($event->page_matches("media_rescan/") && $user->can(Permissions::RESCAN_MEDIA) && isset($_POST['image_id'])) {
|
|
$image = Image::by_id(int_escape($_POST['image_id']));
|
|
|
|
send_event(new MediaCheckPropertiesEvent($image));
|
|
$image->save_to_db();
|
|
|
|
$page->set_mode(PageMode::REDIRECT);
|
|
$page->set_redirect(make_link("post/view/$image->id"));
|
|
}
|
|
}
|
|
|
|
public function onSetupBuilding(SetupBuildingEvent $event)
|
|
{
|
|
$sb = new SetupBlock("Media Engines");
|
|
|
|
// if (self::imagick_available()) {
|
|
// try {
|
|
// $image = new Imagick(realpath('tests/favicon.png'));
|
|
// $image->clear();
|
|
// $sb->add_label("ImageMagick detected");
|
|
// } catch (ImagickException $e) {
|
|
// $sb->add_label("<b style='color:red'>ImageMagick not detected</b>");
|
|
// }
|
|
// } else {
|
|
$sb->start_table();
|
|
$sb->add_table_header("Commands");
|
|
|
|
$sb->add_text_option(MediaConfig::CONVERT_PATH, "convert", true);
|
|
// }
|
|
|
|
$sb->add_text_option(MediaConfig::FFMPEG_PATH, "ffmpeg", true);
|
|
$sb->add_text_option(MediaConfig::FFPROBE_PATH, "ffprobe", true);
|
|
|
|
$sb->add_shorthand_int_option(MediaConfig::MEM_LIMIT, "Mem limit: ", true);
|
|
$sb->end_table();
|
|
|
|
$event->panel->add_block($sb);
|
|
}
|
|
|
|
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(Permissions::DELETE_IMAGE)) {
|
|
$event->add_part($this->theme->get_buttons_html($event->image->id));
|
|
}
|
|
}
|
|
|
|
public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event)
|
|
{
|
|
global $user;
|
|
if ($user->can(Permissions::RESCAN_MEDIA)) {
|
|
$event->add_action("bulk_media_rescan", "Scan Media Properties");
|
|
}
|
|
}
|
|
|
|
public function onBulkAction(BulkActionEvent $event)
|
|
{
|
|
global $page, $user;
|
|
|
|
switch ($event->action) {
|
|
case "bulk_media_rescan":
|
|
if ($user->can(Permissions::RESCAN_MEDIA)) {
|
|
$total = 0;
|
|
$failed = 0;
|
|
foreach ($event->items as $image) {
|
|
try {
|
|
log_debug("media", "Rescanning media for {$image->hash} ({$image->id})");
|
|
send_event(new MediaCheckPropertiesEvent($image));
|
|
$image->save_to_db();
|
|
$total++;
|
|
} catch (MediaException $e) {
|
|
$failed++;
|
|
}
|
|
}
|
|
$page->flash("Scanned media properties for $total items, failed for $failed");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
public function onCommand(CommandEvent $event)
|
|
{
|
|
if ($event->cmd == "help") {
|
|
print "\tmedia-rescan <id / hash>\n";
|
|
print "\t\trefresh metadata for a given post\n\n";
|
|
}
|
|
if ($event->cmd == "media-rescan") {
|
|
$uid = $event->args[0];
|
|
$image = Image::by_id_or_hash($uid);
|
|
if ($image) {
|
|
send_event(new MediaCheckPropertiesEvent($image));
|
|
$image->save_to_db();
|
|
} else {
|
|
print("No post with ID '$uid'\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param MediaResizeEvent $event
|
|
* @throws MediaException
|
|
* @throws InsufficientMemoryException
|
|
*/
|
|
public function onMediaResize(MediaResizeEvent $event)
|
|
{
|
|
switch ($event->engine) {
|
|
case MediaEngine::GD:
|
|
$info = getimagesize($event->input_path);
|
|
if ($info === false) {
|
|
throw new MediaException("getimagesize failed for " . $event->input_path);
|
|
}
|
|
|
|
self::image_resize_gd(
|
|
$event->input_path,
|
|
$info,
|
|
$event->target_width,
|
|
$event->target_height,
|
|
$event->output_path,
|
|
$event->target_format,
|
|
$event->ignore_aspect_ratio,
|
|
$event->target_quality,
|
|
$event->allow_upscale
|
|
);
|
|
|
|
break;
|
|
case MediaEngine::IMAGICK:
|
|
// if (self::imagick_available()) {
|
|
// } else {
|
|
self::image_resize_convert(
|
|
$event->input_path,
|
|
$event->input_type,
|
|
$event->target_width,
|
|
$event->target_height,
|
|
$event->output_path,
|
|
$event->target_format,
|
|
$event->ignore_aspect_ratio,
|
|
$event->target_quality,
|
|
$event->minimize,
|
|
$event->allow_upscale
|
|
);
|
|
//}
|
|
break;
|
|
case MediaEngine::STATIC:
|
|
copy($event->input_path, $event->output_path);
|
|
break;
|
|
default:
|
|
throw new MediaException("Engine not supported for resize: " . $event->engine);
|
|
}
|
|
|
|
// TODO: Get output optimization tools working better
|
|
// if ($config->get_bool("thumb_optim", false)) {
|
|
// exec("jpegoptim $outname", $output, $ret);
|
|
// }
|
|
}
|
|
|
|
|
|
const CONTENT_SEARCH_TERM_REGEX = "/^content[=|:]((video)|(audio)|(image)|(unknown))$/i";
|
|
|
|
|
|
public function onSearchTermParse(SearchTermParseEvent $event)
|
|
{
|
|
global $database;
|
|
|
|
if (is_null($event->term)) {
|
|
return;
|
|
}
|
|
|
|
$matches = [];
|
|
if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term, $matches)) {
|
|
$field = $matches[1];
|
|
if ($field==="unknown") {
|
|
$event->add_querylet(new Querylet("video IS NULL OR audio IS NULL OR image IS NULL"));
|
|
} else {
|
|
$event->add_querylet(new Querylet($database->scoreql_to_sql("$field = SCORE_BOOL_Y")));
|
|
}
|
|
}
|
|
}
|
|
|
|
public function onHelpPageBuilding(HelpPageBuildingEvent $event)
|
|
{
|
|
if ($event->key===HelpPages::SEARCH) {
|
|
$block = new Block();
|
|
$block->header = "Media";
|
|
$block->body = $this->theme->get_help_html();
|
|
$event->add_block($block);
|
|
}
|
|
}
|
|
|
|
public function onTagTermCheck(TagTermCheckEvent $event)
|
|
{
|
|
if (preg_match(self::CONTENT_SEARCH_TERM_REGEX, $event->term)) {
|
|
$event->metatag = true;
|
|
}
|
|
}
|
|
|
|
public function onParseLinkTemplate(ParseLinkTemplateEvent $event)
|
|
{
|
|
if ($event->image->width && $event->image->height && $event->image->length) {
|
|
$s = ((int)($event->image->length / 100))/10;
|
|
$event->replace('$size', "{$event->image->width}x{$event->image->height}, ${s}s");
|
|
} elseif ($event->image->width && $event->image->height) {
|
|
$event->replace('$size', "{$event->image->width}x{$event->image->height}");
|
|
} elseif ($event->image->length) {
|
|
$s = ((int)($event->image->length / 100))/10;
|
|
$event->replace('$size', "${s}s");
|
|
}
|
|
|
|
$event->replace('$ext', $event->image->ext);
|
|
}
|
|
|
|
/**
|
|
* Check Memory usage limits
|
|
*
|
|
* Old check: $memory_use = (filesize($image_filename)*2) + ($width*$height*4) + (4*1024*1024);
|
|
* New check: $memory_use = $width * $height * ($bits_per_channel) * channels * 2.5
|
|
*
|
|
* It didn't make sense to compute the memory usage based on the NEW size for the image. ($width*$height*4)
|
|
* We need to consider the size that we are GOING TO instead.
|
|
*
|
|
* The factor of 2.5 is simply a rough guideline.
|
|
* https://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
|
|
*
|
|
* @param array $info The output of getimagesize() for the source file in question.
|
|
* @return int The number of bytes an image resize operation is estimated to use.
|
|
*/
|
|
public static function calc_memory_use(array $info): int
|
|
{
|
|
if (isset($info['bits']) && isset($info['channels'])) {
|
|
$memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024;
|
|
} else {
|
|
// If we don't have bits and channel info from the image then assume default values
|
|
// of 8 bits per color and 4 channels (R,G,B,A) -- ie: regular 24-bit color
|
|
$memory_use = ($info[0] * $info[1] * 1 * 4 * 2.5) / 1024;
|
|
}
|
|
return (int)$memory_use;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a thumbnail using ffmpeg.
|
|
*
|
|
* @param $hash
|
|
* @return bool true if successful, false if not.
|
|
* @throws MediaException
|
|
*/
|
|
public static function create_thumbnail_ffmpeg($hash): bool
|
|
{
|
|
global $config;
|
|
|
|
$ffmpeg = $config->get_string(MediaConfig::FFMPEG_PATH);
|
|
if ($ffmpeg == null || $ffmpeg == "") {
|
|
throw new MediaException("ffmpeg command configured");
|
|
}
|
|
|
|
$inname = warehouse_path(Image::IMAGE_DIR, $hash);
|
|
$outname = warehouse_path(Image::THUMBNAIL_DIR, $hash);
|
|
|
|
$orig_size = self::video_size($inname);
|
|
$scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1], true);
|
|
|
|
$codec = "mjpeg";
|
|
$quality = $config->get_int(ImageConfig::THUMB_QUALITY);
|
|
if ($config->get_string(ImageConfig::THUMB_TYPE) == EXTENSION_WEBP) {
|
|
$codec = "libwebp";
|
|
} else {
|
|
// mjpeg quality ranges from 2-31, with 2 being the best quality.
|
|
$quality = floor(31 - (31 * ($quality / 100)));
|
|
if ($quality < 2) {
|
|
$quality = 2;
|
|
}
|
|
}
|
|
|
|
$args = [
|
|
escapeshellarg($ffmpeg),
|
|
"-y", "-i", escapeshellarg($inname),
|
|
"-vf", "thumbnail,scale={$scaled_size[0]}:{$scaled_size[1]}",
|
|
"-f", "image2",
|
|
"-vframes", "1",
|
|
"-c:v", $codec,
|
|
"-q:v", $quality,
|
|
escapeshellarg($outname),
|
|
];
|
|
|
|
$cmd = escapeshellcmd(implode(" ", $args));
|
|
|
|
exec($cmd, $output, $ret);
|
|
|
|
if ((int)$ret == (int)0) {
|
|
log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
|
|
return true;
|
|
} else {
|
|
log_error('media', "Generating thumbnail with command `$cmd`, returns $ret");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
return json_decode($output, true);
|
|
} else {
|
|
log_error('media', "Getting media data `$cmd`, returns $ret");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public static function determine_ext(string $format): string
|
|
{
|
|
$format = self::normalize_format($format);
|
|
switch ($format) {
|
|
case self::WEBP_LOSSLESS:
|
|
case self::WEBP_LOSSY:
|
|
return EXTENSION_WEBP;
|
|
default:
|
|
return $format;
|
|
}
|
|
}
|
|
|
|
// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
|
|
// {
|
|
// switch ($format) {
|
|
// case EXTENSION_PNG:
|
|
// $result = $image->setOption('png:compression-level', 9);
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not set png compression option");
|
|
// }
|
|
// break;
|
|
// case Graphics::WEBP_LOSSLESS:
|
|
// $result = $image->setOption('webp:lossless', true);
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not set lossless webp option");
|
|
// }
|
|
// break;
|
|
// default:
|
|
// $result = $image->setImageCompressionQuality($output_quality);
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not set compression quality for $path to $output_quality");
|
|
// }
|
|
// break;
|
|
// }
|
|
//
|
|
// if (self::supports_alpha($format)) {
|
|
// $result = $image->setImageBackgroundColor(new \ImagickPixel('transparent'));
|
|
// } else {
|
|
// $result = $image->setImageBackgroundColor(new \ImagickPixel('black'));
|
|
// }
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not set background color");
|
|
// }
|
|
//
|
|
//
|
|
// if ($minimize) {
|
|
// $profiles = $image->getImageProfiles("icc", true);
|
|
// $result = $image->stripImage();
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not strip information from image");
|
|
// }
|
|
// if (!empty($profiles)) {
|
|
// $image->profileImage("icc", $profiles['icc']);
|
|
// }
|
|
// }
|
|
//
|
|
// $ext = self::determine_ext($format);
|
|
//
|
|
// $result = $image->writeImage($ext . ":" . $path);
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not write image to $path");
|
|
// }
|
|
// }
|
|
|
|
// public static function image_resize_imagick(
|
|
// String $input_path,
|
|
// String $input_type,
|
|
// int $new_width,
|
|
// int $new_height,
|
|
// string $output_filename,
|
|
// string $output_type = null,
|
|
// bool $ignore_aspect_ratio = false,
|
|
// int $output_quality = 80,
|
|
// bool $minimize = false,
|
|
// bool $allow_upscale = true
|
|
// ): void
|
|
// {
|
|
// global $config;
|
|
//
|
|
// if (!empty($input_type)) {
|
|
// $input_type = self::determine_ext($input_type);
|
|
// }
|
|
//
|
|
// try {
|
|
// $image = new Imagick($input_type . ":" . $input_path);
|
|
// try {
|
|
// $result = $image->flattenImages();
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not flatten image $input_path");
|
|
// }
|
|
//
|
|
// $height = $image->getImageHeight();
|
|
// $width = $image->getImageWidth();
|
|
// if (!$allow_upscale &&
|
|
// ($new_width > $width || $new_height > $height)) {
|
|
// $new_height = $height;
|
|
// $new_width = $width;
|
|
// }
|
|
//
|
|
// $result = $image->resizeImage($new_width, $new_width, Imagick::FILTER_LANCZOS, 0, !$ignore_aspect_ratio);
|
|
// if ($result !== true) {
|
|
// throw new GraphicsException("Could not perform image resize on $input_path");
|
|
// }
|
|
//
|
|
//
|
|
// if (empty($output_type)) {
|
|
// $output_type = $input_type;
|
|
// }
|
|
//
|
|
// self::image_save_imagick($image, $output_filename, $output_type, $output_quality);
|
|
//
|
|
// } finally {
|
|
// $image->destroy();
|
|
// }
|
|
// } catch (ImagickException $e) {
|
|
// throw new GraphicsException("Error while resizing with Imagick: " . $e->getMessage(), $e->getCode(), $e);
|
|
// }
|
|
// }
|
|
|
|
public static function is_lossless(string $filename, string $format)
|
|
{
|
|
if (in_array($format, self::LOSSLESS_FORMATS)) {
|
|
return true;
|
|
}
|
|
switch ($format) {
|
|
case EXTENSION_WEBP:
|
|
return self::is_lossless_webp($filename);
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static function image_resize_convert(
|
|
string $input_path,
|
|
string $input_type,
|
|
int $new_width,
|
|
int $new_height,
|
|
string $output_filename,
|
|
string $output_type = null,
|
|
bool $ignore_aspect_ratio = false,
|
|
int $output_quality = 80,
|
|
bool $minimize = false,
|
|
bool $allow_upscale = true
|
|
): void {
|
|
global $config;
|
|
|
|
$convert = $config->get_string(MediaConfig::CONVERT_PATH);
|
|
|
|
if (empty($convert)) {
|
|
throw new MediaException("convert command not configured");
|
|
}
|
|
|
|
if (empty($output_type)) {
|
|
$output_type = $input_type;
|
|
}
|
|
|
|
if ($output_type==EXTENSION_WEBP && self::is_lossless($input_path, $input_type)) {
|
|
$output_type = self::WEBP_LOSSLESS;
|
|
}
|
|
|
|
$bg = "black";
|
|
if (self::supports_alpha($output_type)) {
|
|
$bg = "none";
|
|
}
|
|
if (!empty($input_type)) {
|
|
$input_type = $input_type . ":";
|
|
}
|
|
|
|
|
|
$resize_args = "";
|
|
if (!$allow_upscale) {
|
|
$resize_args .= "\>";
|
|
}
|
|
if ($ignore_aspect_ratio) {
|
|
$resize_args .= "\!";
|
|
}
|
|
|
|
$args = "";
|
|
switch ($output_type) {
|
|
case Media::WEBP_LOSSLESS:
|
|
$args .= '-define webp:lossless=true';
|
|
break;
|
|
case EXTENSION_PNG:
|
|
$args .= '-define png:compression-level=9';
|
|
break;
|
|
}
|
|
|
|
if ($minimize) {
|
|
$args .= " -strip -thumbnail";
|
|
} else {
|
|
$args .= " -resize";
|
|
}
|
|
|
|
|
|
$output_ext = self::determine_ext($output_type);
|
|
|
|
$format = '"%s" %s %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1';
|
|
$cmd = sprintf($format, $convert, $args, $new_width, $new_height, $resize_args, $output_quality, $bg, $input_type, $input_path, $output_ext, $output_filename);
|
|
$cmd = str_replace("\"convert\"", "convert", $cmd); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
|
|
exec($cmd, $output, $ret);
|
|
if ($ret != 0) {
|
|
throw new MediaException("Resizing image with command `$cmd`, returns $ret, outputting " . implode("\r\n", $output));
|
|
} else {
|
|
log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs a resize operation on an image file using GD.
|
|
*
|
|
* @param String $image_filename The source file to be resized.
|
|
* @param array $info The output of getimagesize() for the source file.
|
|
* @param int $new_width
|
|
* @param int $new_height
|
|
* @param string $output_filename
|
|
* @param string|null $output_type If set to null, the output file type will be automatically determined via the $info parameter. Otherwise an exception will be thrown.
|
|
* @param int $output_quality Defaults to 80.
|
|
* @throws MediaException
|
|
* @throws InsufficientMemoryException if the estimated memory usage exceeds the memory limit.
|
|
*/
|
|
public static function image_resize_gd(
|
|
string $image_filename,
|
|
array $info,
|
|
int $new_width,
|
|
int $new_height,
|
|
string $output_filename,
|
|
string $output_type = null,
|
|
bool $ignore_aspect_ratio = false,
|
|
int $output_quality = 80,
|
|
bool $allow_upscale = true
|
|
) {
|
|
$width = $info[0];
|
|
$height = $info[1];
|
|
|
|
if ($output_type == null) {
|
|
/* If not specified, output to the same format as the original image */
|
|
switch ($info[2]) {
|
|
case IMAGETYPE_GIF:
|
|
$output_type = EXTENSION_GIF;
|
|
break;
|
|
case IMAGETYPE_JPEG:
|
|
$output_type = EXTENSION_JPEG;
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
$output_type = EXTENSION_PNG;
|
|
break;
|
|
case IMAGETYPE_WEBP:
|
|
$output_type = EXTENSION_WEBP;
|
|
break;
|
|
case IMAGETYPE_BMP:
|
|
$output_type = EXTENSION_BMP;
|
|
break;
|
|
default:
|
|
throw new MediaException("Failed to save the new image - Unsupported image type.");
|
|
}
|
|
}
|
|
|
|
$memory_use = self::calc_memory_use($info);
|
|
$memory_limit = get_memory_limit();
|
|
if ($memory_use > $memory_limit) {
|
|
throw new InsufficientMemoryException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)");
|
|
}
|
|
|
|
if (!$ignore_aspect_ratio) {
|
|
list($new_width, $new_height) = get_scaled_by_aspect_ratio($width, $height, $new_width, $new_height);
|
|
}
|
|
if (!$allow_upscale &&
|
|
($new_width > $width || $new_height > $height)) {
|
|
$new_height = $height;
|
|
$new_width = $width;
|
|
}
|
|
|
|
$image = imagecreatefromstring(file_get_contents($image_filename));
|
|
$image_resized = imagecreatetruecolor($new_width, $new_height);
|
|
try {
|
|
if ($image === false) {
|
|
throw new MediaException("Could not load image: " . $image_filename);
|
|
}
|
|
if ($image_resized === false) {
|
|
throw new MediaException("Could not create output image with dimensions $new_width c $new_height ");
|
|
}
|
|
|
|
// Handle transparent images
|
|
switch ($info[2]) {
|
|
case IMAGETYPE_GIF:
|
|
$transparency = imagecolortransparent($image);
|
|
$pallet_size = imagecolorstotal($image);
|
|
|
|
// If we have a specific transparent color
|
|
if ($transparency >= 0 && $transparency < $pallet_size) {
|
|
// Get the original image's transparent color's RGB values
|
|
$transparent_color = imagecolorsforindex($image, $transparency);
|
|
|
|
// Allocate the same color in the new image resource
|
|
$transparency = imagecolorallocate($image_resized, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
|
|
if ($transparency === false) {
|
|
throw new MediaException("Unable to allocate transparent color");
|
|
}
|
|
|
|
// Completely fill the background of the new image with allocated color.
|
|
if (imagefill($image_resized, 0, 0, $transparency) === false) {
|
|
throw new MediaException("Unable to fill new image with transparent color");
|
|
}
|
|
|
|
// Set the background color for new image to transparent
|
|
imagecolortransparent($image_resized, $transparency);
|
|
}
|
|
break;
|
|
case IMAGETYPE_PNG:
|
|
case IMAGETYPE_WEBP:
|
|
//
|
|
// More info here: https://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
|
|
//
|
|
if (imagealphablending($image_resized, false) === false) {
|
|
throw new MediaException("Unable to disable image alpha blending");
|
|
}
|
|
if (imagesavealpha($image_resized, true) === false) {
|
|
throw new MediaException("Unable to enable image save alpha");
|
|
}
|
|
$transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127);
|
|
if ($transparent_color === false) {
|
|
throw new MediaException("Unable to allocate transparent color");
|
|
}
|
|
if (imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color) === false) {
|
|
throw new MediaException("Unable to fill new image with transparent color");
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Actually resize the image.
|
|
if (imagecopyresampled(
|
|
$image_resized,
|
|
$image,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
$new_width,
|
|
$new_height,
|
|
$width,
|
|
$height
|
|
) === false) {
|
|
throw new MediaException("Unable to copy resized image data to new image");
|
|
}
|
|
|
|
switch ($output_type) {
|
|
case EXTENSION_BMP:
|
|
$result = imagebmp($image_resized, $output_filename, true);
|
|
break;
|
|
case EXTENSION_WEBP:
|
|
case Media::WEBP_LOSSY:
|
|
$result = imagewebp($image_resized, $output_filename, $output_quality);
|
|
break;
|
|
case EXTENSION_JPG:
|
|
case EXTENSION_JPEG:
|
|
$result = imagejpeg($image_resized, $output_filename, $output_quality);
|
|
break;
|
|
case EXTENSION_PNG:
|
|
$result = imagepng($image_resized, $output_filename, 9);
|
|
break;
|
|
case EXTENSION_GIF:
|
|
$result = imagegif($image_resized, $output_filename);
|
|
break;
|
|
default:
|
|
throw new MediaException("Failed to save the new image - Unsupported image type: $output_type");
|
|
}
|
|
if ($result === false) {
|
|
throw new MediaException("Failed to save the new image, function returned false when saving type: $output_type");
|
|
}
|
|
} finally {
|
|
@imagedestroy($image);
|
|
@imagedestroy($image_resized);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if a file is an animated gif.
|
|
*
|
|
* @param String $image_filename The path of the file to check.
|
|
* @return bool true if the file is an animated gif, false if it is not.
|
|
*/
|
|
public static function is_animated_gif(string $image_filename): bool
|
|
{
|
|
$is_anim_gif = 0;
|
|
if (($fh = @fopen($image_filename, 'rb'))) {
|
|
try {
|
|
//check if gif is animated (via https://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
|
|
while (!feof($fh) && $is_anim_gif < 2) {
|
|
$chunk = fread($fh, 1024 * 100);
|
|
$is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
|
|
}
|
|
} finally {
|
|
@fclose($fh);
|
|
}
|
|
}
|
|
return ($is_anim_gif == 0);
|
|
}
|
|
|
|
|
|
private static function compare_file_bytes(string $file_name, array $comparison): bool
|
|
{
|
|
$size = filesize($file_name);
|
|
if ($size < count($comparison)) {
|
|
// Can't match because it's too small
|
|
return false;
|
|
}
|
|
|
|
if (($fh = @fopen($file_name, 'rb'))) {
|
|
try {
|
|
$chunk = unpack("C*", fread($fh, count($comparison)));
|
|
|
|
for ($i = 0; $i < count($comparison); $i++) {
|
|
$byte = $comparison[$i];
|
|
if ($byte == null) {
|
|
continue;
|
|
} else {
|
|
$fileByte = $chunk[$i + 1];
|
|
if ($fileByte != $byte) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} finally {
|
|
@fclose($fh);
|
|
}
|
|
} else {
|
|
throw new MediaException("Unable to open file for byte check: $file_name");
|
|
}
|
|
}
|
|
|
|
public static function is_animated_webp(string $image_filename): bool
|
|
{
|
|
return self::compare_file_bytes($image_filename, self::WEBP_ANIMATION_HEADER);
|
|
}
|
|
|
|
public static function is_lossless_webp(string $image_filename): bool
|
|
{
|
|
return self::compare_file_bytes($image_filename, self::WEBP_LOSSLESS_HEADER);
|
|
}
|
|
|
|
public static function supports_alpha(string $format): bool
|
|
{
|
|
return in_array(self::normalize_format($format), self::ALPHA_FORMATS);
|
|
}
|
|
|
|
public static function is_input_supported(string $engine, string $format, ?bool $lossless = null): bool
|
|
{
|
|
$format = self::normalize_format($format, $lossless);
|
|
if (!in_array($format, MediaEngine::INPUT_SUPPORT[$engine])) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static function is_output_supported(string $engine, string $format, ?bool $lossless = false): bool
|
|
{
|
|
$format = self::normalize_format($format, $lossless);
|
|
if (!in_array($format, MediaEngine::OUTPUT_SUPPORT[$engine])) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if a format (normally a file extension) is a variant name of another format (ie, jpg and jpeg).
|
|
* If one is found, then the maine name that the Media extension will recognize is returned,
|
|
* otherwise the incoming format is returned.
|
|
*
|
|
* @param $format
|
|
* @return string|null The format name that the media extension will recognize.
|
|
*/
|
|
public static function normalize_format(string $format, ?bool $lossless = null): ?string
|
|
{
|
|
if ($format == EXTENSION_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];
|
|
}
|
|
return $format;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines the dimensions of a video file using ffmpeg.
|
|
*
|
|
* @param string $filename
|
|
* @return array [width, height]
|
|
*/
|
|
public static function video_size(string $filename): array
|
|
{
|
|
global $config;
|
|
$ffmpeg = $config->get_string(MediaConfig::FFMPEG_PATH);
|
|
$cmd = escapeshellcmd(implode(" ", [
|
|
escapeshellarg($ffmpeg),
|
|
"-y", "-i", escapeshellarg($filename),
|
|
"-vstats"
|
|
]));
|
|
$output = shell_exec($cmd . " 2>&1");
|
|
// error_log("Getting size with `$cmd`");
|
|
|
|
$regex_sizes = "/Video: .* ([0-9]{1,4})x([0-9]{1,4})/";
|
|
if (preg_match($regex_sizes, $output, $regs)) {
|
|
if (preg_match("/displaymatrix: rotation of (90|270).00 degrees/", $output)) {
|
|
$size = [(int)$regs[2], (int)$regs[1]];
|
|
} else {
|
|
$size = [(int)$regs[1], (int)$regs[2]];
|
|
}
|
|
} else {
|
|
$size = [1, 1];
|
|
}
|
|
log_debug('media', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]");
|
|
return $size;
|
|
}
|
|
|
|
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event)
|
|
{
|
|
global $config, $database;
|
|
if ($this->get_version(MediaConfig::VERSION) < 1) {
|
|
$current_value = $config->get_string("thumb_ffmpeg_path");
|
|
if (!empty($current_value)) {
|
|
$config->set_string(MediaConfig::FFMPEG_PATH, $current_value);
|
|
} elseif ($ffmpeg = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' ffmpeg')) {
|
|
//ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
|
|
if (is_executable(strtok($ffmpeg, PHP_EOL))) {
|
|
$config->set_default_string(MediaConfig::FFMPEG_PATH, 'ffmpeg');
|
|
}
|
|
}
|
|
|
|
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);
|
|
} elseif ($convert = shell_exec((PHP_OS == 'WINNT' ? 'where' : 'which') . ' convert')) {
|
|
//ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
|
|
if (is_executable(strtok($convert, PHP_EOL))) {
|
|
$config->set_default_string(MediaConfig::CONVERT_PATH, 'convert');
|
|
}
|
|
}
|
|
|
|
$current_value = $config->get_int("thumb_mem_limit");
|
|
if (!empty($current_value)) {
|
|
$config->set_int(MediaConfig::MEM_LIMIT, $current_value);
|
|
}
|
|
|
|
$this->set_version(MediaConfig::VERSION, 1);
|
|
}
|
|
|
|
if ($this->get_version(MediaConfig::VERSION) < 2) {
|
|
$database->execute($database->scoreql_to_sql(
|
|
"ALTER TABLE images ADD COLUMN image SCORE_BOOL NULL"
|
|
));
|
|
|
|
switch ($database->get_driver_name()) {
|
|
case DatabaseDriver::PGSQL:
|
|
case DatabaseDriver::SQLITE:
|
|
$database->execute('CREATE INDEX images_image_idx ON images(image) WHERE image IS NOT NULL');
|
|
break;
|
|
default:
|
|
$database->execute('CREATE INDEX images_image_idx ON images(image)');
|
|
break;
|
|
}
|
|
|
|
$database->set_timeout(300000); // These updates can take a little bit
|
|
|
|
if ($database->transaction === true) {
|
|
$database->commit(); // Each of these commands could hit a lot of data, combining them into one big transaction would not be a good idea.
|
|
}
|
|
log_info("upgrade", "Setting predictable media values for known file types");
|
|
$database->execute($database->scoreql_to_sql("UPDATE images SET image = SCORE_BOOL_N WHERE ext IN ('swf','mp3','ani','flv','mp4','m4v','ogv','webm')"));
|
|
$database->execute($database->scoreql_to_sql("UPDATE images SET image = SCORE_BOOL_Y WHERE ext IN ('jpg','jpeg','ico','cur','png')"));
|
|
|
|
$this->set_version(MediaConfig::VERSION, 2);
|
|
|
|
$database->beginTransaction();
|
|
}
|
|
}
|
|
}
|