This repository has been archived on 2024-09-05. You can view files and clone it, but cannot push or open issues or pull requests.
shimmie2/ext/media/main.php
Matthew Barbour b1db833d51 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.
2019-07-08 08:07:09 -05:00

1102 lines
37 KiB
PHP

<?php
/*
* Name: Media
* Author: Matthew Barbour <matthew@darkholme.net>
* Description: Provides common functions and settings used for media operations.
* License: MIT
* Version: 1.0
*/
/*
* This is used by the media code when there is an error
*/
abstract class MediaConfig
{
const FFMPEG_PATH = "media_ffmpeg_path";
const FFPROBE_PATH = "media_ffprobe_path";
const CONVERT_PATH = "media_convert_path";
const VERSION = "ext_media_version";
const MEM_LIMIT = 'media_mem_limit';
}
abstract class MediaEngine
{
public const GD = "gd";
public const IMAGICK = "convert";
public const FFMPEG = "ffmpeg";
public const ALL = [
MediaEngine::GD,
MediaEngine::FFMPEG,
MediaEngine::IMAGICK
];
public const OUTPUT_SUPPORT = [
MediaEngine::GD => [
"gif",
"jpg",
"png",
"webp",
Media::WEBP_LOSSY,
],
MediaEngine::IMAGICK => [
"gif",
"jpg",
"png",
"webp",
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
],
MediaEngine::FFMPEG => [
"jpg",
"webp",
"png"
]
];
public const INPUT_SUPPORT = [
MediaEngine::GD => [
"bmp",
"gif",
"jpg",
"png",
"webp",
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS
],
MediaEngine::IMAGICK => [
"bmp",
"gif",
"jpg",
"png",
"psd",
"tiff",
"webp",
Media::WEBP_LOSSY,
Media::WEBP_LOSSLESS,
"ico",
],
MediaEngine::FFMPEG => [
"avi",
"mkv",
"webm",
"mp4",
"mov",
"flv"
]
];
}
class MediaException extends SCoreException
{
}
class MediaResizeEvent extends Event
{
public $engine;
public $input_path;
public $input_type;
public $output_path;
public $target_format;
public $target_width;
public $target_height;
public $target_quality;
public $minimize;
public $ignore_aspect_ratio;
public $allow_upscale;
public function __construct(String $engine, string $input_path, string $input_type, string $output_path,
int $target_width, int $target_height,
bool $ignore_aspect_ratio = false,
string $target_format = null,
int $target_quality = 80,
bool $minimize = false,
bool $allow_upscale = true)
{
assert(in_array($engine, MediaEngine::ALL));
$this->engine = $engine;
$this->input_path = $input_path;
$this->input_type = $input_type;
$this->output_path = $output_path;
$this->target_height = $target_height;
$this->target_width = $target_width;
$this->target_format = $target_format;
$this->target_quality = $target_quality;
$this->minimize = $minimize;
$this->ignore_aspect_ratio = $ignore_aspect_ratio;
$this->allow_upscale = $allow_upscale;
}
}
class MediaCheckPropertiesEvent extends Event
{
public $file_name;
public $ext;
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)
{
$this->file_name = $file_name;
$this->ext = $ext;
}
}
class Media extends Extension
{
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,
"png",
"psd",
"bmp",
"ico",
"cur",
"ani",
"gif"
];
const ALPHA_FORMATS = [
self::WEBP_LOSSLESS,
self::WEBP_LOSSY,
"webp",
"png",
];
const FORMAT_ALIASES = [
"tif" => "tiff",
"jpeg" => "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];
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');
if ($config->get_int(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);
}
$config->set_int(MediaConfig::VERSION, 1);
log_info("media", "extension installed");
}
}
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");
// 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->add_text_option(MediaConfig::CONVERT_PATH, "convert command: ");
// }
$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: ");
$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("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
* @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;
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))$/i";
public function onSearchTermParse(SearchTermParseEvent $event)
{
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
*
* Old check: $memory_use = (filesize($image_filename)*2) + ($width*$height*4) + (4*1024*1024);
* New check: $memory_use = $width * $height * ($bits_per_channel) * channels * 2.5
*
* It didn't make sense to compute the memory usage based on the NEW size for the image. ($width*$height*4)
* We need to consider the size that we are GOING TO instead.
*
* The factor of 2.5 is simply a rough guideline.
* http://stackoverflow.com/questions/527532/reasonable-php-memory-limit-for-image-resize
*
* @param array $info The output of getimagesize() for the source file in question.
* @return int The number of bytes an image resize operation is estimated to use.
*/
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) == "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);
$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);
switch ($format) {
case self::WEBP_LOSSLESS:
case self::WEBP_LOSSY:
return "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 "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 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;
}
$bg = "black";
if (self::supports_alpha($output_type)) {
$bg = "none";
}
if (!empty($input_type)) {
$input_type = $input_type . ":";
}
$args = "";
if ($minimize) {
$args = " -strip -thumbnail";
}
$resize_args = "";
if (!$allow_upscale) {
$resize_args .= "\>";
}
if ($ignore_aspect_ratio) {
$resize_args .= "\!";
}
$format = '"%s" -flatten %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_type, $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 = "gif";
break;
case IMAGETYPE_JPEG:
$output_type = "jpeg";
break;
case IMAGETYPE_PNG:
$output_type = "png";
break;
case IMAGETYPE_WEBP:
$output_type = "webp";
break;
case IMAGETYPE_BMP:
$output_type = "bmp";
break;
default:
throw new 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: http://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 "bmp":
$result = imagebmp($image_resized, $output_filename, true);
break;
case "webp":
case Media::WEBP_LOSSY:
$result = imagewebp($image_resized, $output_filename, $output_quality);
break;
case "jpg":
case "jpeg":
$result = imagejpeg($image_resized, $output_filename, $output_quality);
break;
case "png":
$result = imagepng($image_resized, $output_filename, 9);
break;
case "gif":
$result = imagegif($image_resized, $output_filename);
break;
default:
throw new 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 http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
while (!feof($fh) && $is_anim_gif < 2) {
$chunk = fread($fh, 1024 * 100);
$is_anim_gif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
}
} 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)
{
return in_array(self::normalize_format($format), self::ALPHA_FORMATS);
}
public static function is_input_supported($engine, $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($engine, $format): bool
{
$format = self::normalize_format($format);
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.
*/
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];
}
return $format;
}
/**
* Determines the dimensions of a video file using ffmpeg.
*
* @param string $filename
* @return array [width, height]
*/
static public 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 = [$regs[2], $regs[1]];
} else {
$size = [$regs[1], $regs[2]];
}
} else {
$size = [1, 1];
}
log_debug('Media', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]");
return $size;
}
}