941 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			941 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php declare(strict_types=1);
 | 
						|
 | 
						|
require_once "config.php";
 | 
						|
require_once "events.php";
 | 
						|
require_once "media_engine.php";
 | 
						|
require_once "video_codecs.php";
 | 
						|
 | 
						|
/*
 | 
						|
* This is used by the media code when there is an error
 | 
						|
*/
 | 
						|
class MediaException extends SCoreException
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
class Media extends Extension
 | 
						|
{
 | 
						|
    /** @var MediaTheme */
 | 
						|
    protected ?Themelet $theme;
 | 
						|
 | 
						|
    private const LOSSLESS_FORMATS = [
 | 
						|
        MimeType::WEBP_LOSSLESS,
 | 
						|
        MimeType::PNG,
 | 
						|
        MimeType::PSD,
 | 
						|
        MimeType::BMP,
 | 
						|
        MimeType::ICO,
 | 
						|
        MimeType::ANI,
 | 
						|
        MimeType::GIF
 | 
						|
    ];
 | 
						|
 | 
						|
    private const ALPHA_FORMATS = [
 | 
						|
        MimeType::WEBP_LOSSLESS,
 | 
						|
        MimeType::WEBP,
 | 
						|
        MimeType::PNG,
 | 
						|
    ];
 | 
						|
 | 
						|
    public const RESIZE_TYPE_FIT = "Fit";
 | 
						|
    public const RESIZE_TYPE_FIT_BLUR = "Fit Blur";
 | 
						|
    public const RESIZE_TYPE_FIT_BLUR_PORTRAIT = "Fit Blur Tall, Fill Wide";
 | 
						|
    public const RESIZE_TYPE_FILL =  "Fill";
 | 
						|
    public const RESIZE_TYPE_STRETCH =  "Stretch";
 | 
						|
    public const DEFAULT_ALPHA_CONVERSION_COLOR = "#00000000";
 | 
						|
 | 
						|
    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 = $event->panel->create_new_block("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();
 | 
						|
    }
 | 
						|
 | 
						|
    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)
 | 
						|
    {
 | 
						|
        if (!in_array(
 | 
						|
            $event->resize_type,
 | 
						|
            MediaEngine::RESIZE_TYPE_SUPPORT[$event->engine]
 | 
						|
        )) {
 | 
						|
            throw new MediaException("Resize type $event->resize_type not supported by selected media engine $event->engine");
 | 
						|
        }
 | 
						|
 | 
						|
        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_mime,
 | 
						|
                    $event->alpha_color,
 | 
						|
                    $event->resize_type,
 | 
						|
                    $event->target_quality,
 | 
						|
                    $event->allow_upscale
 | 
						|
                );
 | 
						|
 | 
						|
                break;
 | 
						|
            case MediaEngine::IMAGICK:
 | 
						|
//                if (self::imagick_available()) {
 | 
						|
//                } else {
 | 
						|
                self::image_resize_convert(
 | 
						|
                    $event->input_path,
 | 
						|
                    $event->input_mime,
 | 
						|
                    $event->target_width,
 | 
						|
                    $event->target_height,
 | 
						|
                    $event->output_path,
 | 
						|
                    $event->target_mime,
 | 
						|
                    $event->alpha_color,
 | 
						|
                    $event->resize_type,
 | 
						|
                    $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)
 | 
						|
    {
 | 
						|
        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("$field = :true", ["true"=>true]));
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    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");
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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 (empty($ffmpeg)) {
 | 
						|
            throw new MediaException("ffmpeg command not configured");
 | 
						|
        }
 | 
						|
 | 
						|
        $ok = false;
 | 
						|
        $inname = warehouse_path(Image::IMAGE_DIR, $hash);
 | 
						|
        $tmpname = tempnam(sys_get_temp_dir(), "shimmie_ffmpeg_thumb");
 | 
						|
        try {
 | 
						|
            $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);
 | 
						|
 | 
						|
            $args = [
 | 
						|
                escapeshellarg($ffmpeg),
 | 
						|
                "-y", "-i", escapeshellarg($inname),
 | 
						|
                "-vf", "thumbnail",
 | 
						|
                "-f", "image2",
 | 
						|
                "-vframes", "1",
 | 
						|
                "-c:v", "png",
 | 
						|
                escapeshellarg($tmpname),
 | 
						|
            ];
 | 
						|
 | 
						|
            $cmd = escapeshellcmd(implode(" ", $args));
 | 
						|
 | 
						|
            exec($cmd, $output, $ret);
 | 
						|
 | 
						|
            if ((int)$ret === (int)0) {
 | 
						|
                log_debug('media', "Generating thumbnail with command `$cmd`, returns $ret");
 | 
						|
                create_scaled_image($tmpname, $outname, $scaled_size, MimeType::PNG);
 | 
						|
                $ok = true;
 | 
						|
            } else {
 | 
						|
                log_error('media', "Generating thumbnail with command `$cmd`, returns $ret");
 | 
						|
            }
 | 
						|
        } finally {
 | 
						|
            @unlink($tmpname);
 | 
						|
        }
 | 
						|
        return $ok;
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    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 $mime): string
 | 
						|
    {
 | 
						|
        $ext = FileExtension::get_for_mime($mime);
 | 
						|
        if (empty($ext)) {
 | 
						|
            throw new SCoreException("Could not determine extension for $mime");
 | 
						|
        }
 | 
						|
        return $ext;
 | 
						|
    }
 | 
						|
 | 
						|
//    private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
 | 
						|
//    {
 | 
						|
//        switch ($format) {
 | 
						|
//            case FileExtension::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 $mime): bool
 | 
						|
    {
 | 
						|
        if (in_array($mime, self::LOSSLESS_FORMATS)) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        switch ($mime) {
 | 
						|
            case MimeType::WEBP:
 | 
						|
                return MimeType::is_lossless_webp($filename);
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    public static function image_resize_convert(
 | 
						|
        string $input_path,
 | 
						|
        string $input_mime,
 | 
						|
        int $new_width,
 | 
						|
        int $new_height,
 | 
						|
        string $output_filename,
 | 
						|
        string $output_mime = null,
 | 
						|
        string $alpha_color = Media::DEFAULT_ALPHA_CONVERSION_COLOR,
 | 
						|
        string $resize_type = self::RESIZE_TYPE_FIT,
 | 
						|
        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_mime)) {
 | 
						|
            $output_mime = $input_mime;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($output_mime==MimeType::WEBP && self::is_lossless($input_path, $input_mime)) {
 | 
						|
            $output_mime = MimeType::WEBP_LOSSLESS;
 | 
						|
        }
 | 
						|
 | 
						|
        $bg = "\"$alpha_color\"";
 | 
						|
        if (self::supports_alpha($output_mime)) {
 | 
						|
            $bg = "none";
 | 
						|
        }
 | 
						|
 | 
						|
        $resize_suffix = "";
 | 
						|
        if (!$allow_upscale) {
 | 
						|
            $resize_suffix .= "\>";
 | 
						|
        }
 | 
						|
        if ($resize_type==Media::RESIZE_TYPE_STRETCH) {
 | 
						|
            $resize_suffix .= "\!";
 | 
						|
        }
 | 
						|
 | 
						|
        $args = " -auto-orient ";
 | 
						|
        $resize_arg = "-resize";
 | 
						|
        if ($minimize) {
 | 
						|
            $args .= "-strip ";
 | 
						|
            $resize_arg = "-thumbnail";
 | 
						|
        }
 | 
						|
 | 
						|
        $input_ext = self::determine_ext($input_mime);
 | 
						|
 | 
						|
        $file_arg = "${input_ext}:\"${input_path}[0]\"";
 | 
						|
 | 
						|
        if ($resize_type===Media::RESIZE_TYPE_FIT_BLUR_PORTRAIT) {
 | 
						|
            if ($new_height>$new_width) {
 | 
						|
                $resize_type = Media::RESIZE_TYPE_FIT_BLUR;
 | 
						|
            } else {
 | 
						|
                $resize_type = Media::RESIZE_TYPE_FILL;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        switch ($resize_type) {
 | 
						|
            case Media::RESIZE_TYPE_FIT:
 | 
						|
            case Media::RESIZE_TYPE_STRETCH:
 | 
						|
                $args .= "${file_arg} ${resize_arg} ${new_width}x${new_height}${resize_suffix} -background ${bg} -flatten ";
 | 
						|
                break;
 | 
						|
            case Media::RESIZE_TYPE_FILL:
 | 
						|
                $args .= "${file_arg} ${resize_arg} ${new_width}x${new_height}\^ -background ${bg} -flatten -gravity center -extent ${new_width}x${new_height} ";
 | 
						|
                break;
 | 
						|
            case Media::RESIZE_TYPE_FIT_BLUR:
 | 
						|
                $blur_size = max(ceil(max($new_width, $new_height) / 25), 5);
 | 
						|
                $args .= "${file_arg} ".
 | 
						|
                    "\( -clone 0 -auto-orient -resize ${new_width}x${new_height}\^ -background ${bg} -flatten -gravity center -fill black -colorize 50% -extent ${new_width}x${new_height} -blur 0x${blur_size} \) ".
 | 
						|
                    "\( -clone 0 -auto-orient -resize ${new_width}x${new_height} \) ".
 | 
						|
                    "-delete 0 -gravity center -compose over -composite";
 | 
						|
                break;
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
        switch ($output_mime) {
 | 
						|
            case MimeType::WEBP_LOSSLESS:
 | 
						|
                $args .= ' -define webp:lossless=true';
 | 
						|
                break;
 | 
						|
            case MimeType::PNG:
 | 
						|
                $args .= ' -define png:compression-level=9';
 | 
						|
                break;
 | 
						|
        }
 | 
						|
 | 
						|
 | 
						|
        $args .= " -quality ${output_quality} ";
 | 
						|
 | 
						|
 | 
						|
        $output_ext = self::determine_ext($output_mime);
 | 
						|
 | 
						|
        $format = '"%s"  %s   %s:"%s" 2>&1';
 | 
						|
        $cmd = sprintf($format, $convert, $args, $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 $output_mime 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_mime = null,
 | 
						|
        string $alpha_color = Media::DEFAULT_ALPHA_CONVERSION_COLOR,
 | 
						|
        string $resize_type = self::RESIZE_TYPE_FIT,
 | 
						|
        int $output_quality = 80,
 | 
						|
        bool $allow_upscale = true
 | 
						|
    ) {
 | 
						|
        $width = $info[0];
 | 
						|
        $height = $info[1];
 | 
						|
 | 
						|
        if ($output_mime == null) {
 | 
						|
            /* If not specified, output to the same format as the original image */
 | 
						|
            switch ($info[2]) {
 | 
						|
                case IMAGETYPE_GIF:
 | 
						|
                    $output_mime = MimeType::GIF;
 | 
						|
                    break;
 | 
						|
                case IMAGETYPE_JPEG:
 | 
						|
                    $output_mime = MimeType::JPEG;
 | 
						|
                    break;
 | 
						|
                case IMAGETYPE_PNG:
 | 
						|
                    $output_mime = MimeType::PNG;
 | 
						|
                    break;
 | 
						|
                case IMAGETYPE_WEBP:
 | 
						|
                    $output_mime = MimeType::WEBP;
 | 
						|
                    break;
 | 
						|
                case IMAGETYPE_BMP:
 | 
						|
                    $output_mime = MimeType::BMP;
 | 
						|
                    break;
 | 
						|
                default:
 | 
						|
                    throw new MediaException("Failed to save the new image - Unsupported MIME 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 ($resize_type==Media::RESIZE_TYPE_FIT) {
 | 
						|
            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_mime) {
 | 
						|
                case MimeType::BMP:
 | 
						|
                case MimeType::JPEG:
 | 
						|
                    // In case of alpha channels
 | 
						|
                    $width = imagesx($image_resized);
 | 
						|
                    $height = imagesy($image_resized);
 | 
						|
                    $new_image = imagecreatetruecolor($width, $height);
 | 
						|
                    if ($new_image===false) {
 | 
						|
                        throw new ImageTranscodeException("Could not create image with dimensions $width x $height");
 | 
						|
                    }
 | 
						|
 | 
						|
                    $background_color = Media::hex_color_allocate($new_image, $alpha_color);
 | 
						|
                    if ($background_color===false) {
 | 
						|
                        throw new ImageTranscodeException("Could not allocate background color");
 | 
						|
                    }
 | 
						|
                    if (imagefilledrectangle($new_image, 0, 0, $width, $height, $background_color)===false) {
 | 
						|
                        throw new ImageTranscodeException("Could not fill background color");
 | 
						|
                    }
 | 
						|
                    if (imagecopy($new_image, $image_resized, 0, 0, 0, 0, $width, $height)===false) {
 | 
						|
                        throw new ImageTranscodeException("Could not copy source image to new image");
 | 
						|
                    }
 | 
						|
 | 
						|
                    imagedestroy($image_resized);
 | 
						|
                    $image_resized = $new_image;
 | 
						|
                break;
 | 
						|
 | 
						|
            }
 | 
						|
 | 
						|
            switch ($output_mime) {
 | 
						|
                case MimeType::BMP:
 | 
						|
                    $result = imagebmp($image_resized, $output_filename, true);
 | 
						|
                    break;
 | 
						|
                case MimeType::WEBP:
 | 
						|
                    $result = imagewebp($image_resized, $output_filename, $output_quality);
 | 
						|
                    break;
 | 
						|
                case MimeType::JPEG:
 | 
						|
                    $result = imagejpeg($image_resized, $output_filename, $output_quality);
 | 
						|
                    break;
 | 
						|
                case MimeType::PNG:
 | 
						|
                    $result = imagepng($image_resized, $output_filename, 9);
 | 
						|
                    break;
 | 
						|
                case MimeType::GIF:
 | 
						|
                    $result = imagegif($image_resized, $output_filename);
 | 
						|
                    break;
 | 
						|
                default:
 | 
						|
                    throw new MediaException("Failed to save the new image - Unsupported image type: $output_mime");
 | 
						|
            }
 | 
						|
            if ($result === false) {
 | 
						|
                throw new MediaException("Failed to save the new image, function returned false when saving type: $output_mime");
 | 
						|
            }
 | 
						|
        } finally {
 | 
						|
            @imagedestroy($image);
 | 
						|
            @imagedestroy($image_resized);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    public static function supports_alpha(string $mime): bool
 | 
						|
    {
 | 
						|
        return MimeType::matches_array($mime, self::ALPHA_FORMATS, true);
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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("ALTER TABLE images ADD COLUMN image BOOLEAN 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;
 | 
						|
            }
 | 
						|
 | 
						|
            $this->set_version(MediaConfig::VERSION, 2);
 | 
						|
        }
 | 
						|
 | 
						|
        if ($this->get_version(MediaConfig::VERSION) < 3) {
 | 
						|
            $database->execute("ALTER TABLE images ADD COLUMN video_codec varchar(512) NULL");
 | 
						|
            $this->set_version(MediaConfig::VERSION, 3);
 | 
						|
        }
 | 
						|
 | 
						|
        if ($this->get_version(MediaConfig::VERSION) < 4) {
 | 
						|
            $database->standardise_boolean("images", "image");
 | 
						|
            $this->set_version(MediaConfig::VERSION, 4);
 | 
						|
        }
 | 
						|
 | 
						|
        if ($this->get_version(MediaConfig::VERSION) < 5) {
 | 
						|
            $database->execute("UPDATE images SET image = :f WHERE ext IN ('swf','mp3','ani','flv','mp4','m4v','ogv','webm')", ["f"=>false]);
 | 
						|
            $database->execute("UPDATE images SET image = :t WHERE ext IN ('jpg','jpeg','ico','cur','png')", ["t"=>true]);
 | 
						|
            $this->set_version(MediaConfig::VERSION, 5);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public static function hex_color_allocate($im, $hex)
 | 
						|
    {
 | 
						|
        $hex = ltrim($hex, '#');
 | 
						|
        $a = hexdec(substr($hex, 0, 2));
 | 
						|
        $b = hexdec(substr($hex, 2, 2));
 | 
						|
        $c = hexdec(substr($hex, 4, 2));
 | 
						|
        return imagecolorallocate($im, $a, $b, $c);
 | 
						|
    }
 | 
						|
}
 |