Merge from sanmadjack:develop
This commit is contained in:
		
						commit
						d1102cd635
					
				| @ -27,7 +27,7 @@ | |||||||
| 
 | 
 | ||||||
| <IfModule mod_expires.c> | <IfModule mod_expires.c> | ||||||
| 	ExpiresActive On | 	ExpiresActive On | ||||||
| 	<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|css|js))$"> | 	<FilesMatch "([0-9a-f]{32}|\.(gif|jpe?g|png|webp|css|js))$"> | ||||||
| 		<IfModule mod_headers.c> | 		<IfModule mod_headers.c> | ||||||
| 			Header set Cache-Control "public, max-age=2629743" | 			Header set Cache-Control "public, max-age=2629743" | ||||||
| 		</IfModule> | 		</IfModule> | ||||||
| @ -46,6 +46,7 @@ | |||||||
| AddType image/jpeg  jpg jpeg | AddType image/jpeg  jpg jpeg | ||||||
| AddType image/gif   gif | AddType image/gif   gif | ||||||
| AddType image/png   png | AddType image/png   png | ||||||
|  | AddType image/webp  webp | ||||||
| 
 | 
 | ||||||
| #EXT: handle_ico | #EXT: handle_ico | ||||||
| AddType image/x-icon   ico ani cur | AddType image/x-icon   ico ani cur | ||||||
|  | |||||||
| @ -35,3 +35,22 @@ class ImageDoesNotExist extends SCoreException | |||||||
| class InvalidInput extends SCoreException | class InvalidInput extends SCoreException | ||||||
| { | { | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  |  * This is used by the image resizing code when there is not enough memory to perform a resize. | ||||||
|  |  */ | ||||||
|  | class InsufficientMemoryException extends SCoreException | ||||||
|  | { | ||||||
|  | } | ||||||
|  | /* | ||||||
|  |  * This is used by the image resizing code when there is an error while resizing | ||||||
|  |  */ | ||||||
|  | class ImageResizeException extends SCoreException | ||||||
|  | { | ||||||
|  |     public $error; | ||||||
|  | 
 | ||||||
|  |     public function __construct(string $error) | ||||||
|  |     { | ||||||
|  |         $this->error = $error; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -219,13 +219,21 @@ abstract class DataHandlerExtension extends Extension | |||||||
| 
 | 
 | ||||||
|     public function onThumbnailGeneration(ThumbnailGenerationEvent $event) |     public function onThumbnailGeneration(ThumbnailGenerationEvent $event) | ||||||
|     { |     { | ||||||
|  |         $result = false; | ||||||
|         if ($this->supported_ext($event->type)) { |         if ($this->supported_ext($event->type)) { | ||||||
|             if (method_exists($this, 'create_thumb_force') && $event->force == true) { |             if($event->force) { | ||||||
|                 $this->create_thumb_force($event->hash); |                 $result = $this->create_thumb($event->hash); | ||||||
|             } else { |             } else { | ||||||
|                 $this->create_thumb($event->hash); |                 $outname = warehouse_path("thumbs", $event->hash); | ||||||
|  |                 if(file_exists($outname)) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 $result = $this->create_thumb($event->hash); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         if($result) { | ||||||
|  |             $event->generated = true; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onDisplayingImage(DisplayingImageEvent $event) |     public function onDisplayingImage(DisplayingImageEvent $event) | ||||||
|  | |||||||
| @ -99,6 +99,10 @@ class ThumbnailGenerationEvent extends Event | |||||||
|     /** @var bool */ |     /** @var bool */ | ||||||
|     public $force; |     public $force; | ||||||
| 
 | 
 | ||||||
|  |     /** @var bool */ | ||||||
|  |     public $generated; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Request a thumbnail be made for an image object |      * Request a thumbnail be made for an image object | ||||||
|      */ |      */ | ||||||
| @ -107,6 +111,7 @@ class ThumbnailGenerationEvent extends Event | |||||||
|         $this->hash = $hash; |         $this->hash = $hash; | ||||||
|         $this->type = $type; |         $this->type = $type; | ||||||
|         $this->force = $force; |         $this->force = $force; | ||||||
|  |         $this->generated = false; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -145,6 +145,51 @@ class Image | |||||||
|         return $images; |         return $images; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Search for an array of image IDs | ||||||
|  |      * | ||||||
|  |      * #param string[] $tags
 | ||||||
|  |      * #return int[]
 | ||||||
|  |      */ | ||||||
|  |     public static function find_image_ids(int $start, int $limit, array $tags=[]): array | ||||||
|  |     { | ||||||
|  |         global $database, $user, $config; | ||||||
|  | 
 | ||||||
|  |         $images = []; | ||||||
|  | 
 | ||||||
|  |         if ($start < 0) { | ||||||
|  |             $start = 0; | ||||||
|  |         } | ||||||
|  |         if ($limit < 1) { | ||||||
|  |             $limit = 1; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (SPEED_HAX) { | ||||||
|  |             if (!$user->can("big_search") and count($tags) > 3) { | ||||||
|  |                 throw new SCoreException("Anonymous users may only search for up to 3 tags at a time"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $result = null; | ||||||
|  |         if (SEARCH_ACCEL) { | ||||||
|  |             $result = Image::get_accelerated_result($tags, $start, $limit); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!$result) { | ||||||
|  |             $querylet = Image::build_search_querylet($tags); | ||||||
|  |             $querylet->append(new Querylet(" ORDER BY ".(Image::$order_sql ?: "images.".$config->get_string("index_order")))); | ||||||
|  |             $querylet->append(new Querylet(" LIMIT :limit OFFSET :offset", ["limit"=>$limit, "offset"=>$start])); | ||||||
|  |             #var_dump($querylet->sql); var_dump($querylet->variables);
 | ||||||
|  |             $result = $database->execute($querylet->sql, $querylet->variables); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         while ($row = $result->fetch()) { | ||||||
|  |             $images[] = $row["id"]; | ||||||
|  |         } | ||||||
|  |         Image::$order_sql = null; | ||||||
|  |         return $images; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /* |     /* | ||||||
|      * Accelerator stuff |      * Accelerator stuff | ||||||
|      */ |      */ | ||||||
| @ -385,12 +430,22 @@ class Image | |||||||
|         return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext'); |         return $this->get_link('image_ilink', '_images/$hash/$id%20-%20$tags.$ext', 'image/$id.$ext'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the nicely formatted version of the file name | ||||||
|  |      */ | ||||||
|  |     public function get_nice_image_name(): string | ||||||
|  |     { | ||||||
|  |         return $this->parse_link_template('$id - $tags.$ext'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Get the URL for the thumbnail |      * Get the URL for the thumbnail | ||||||
|      */ |      */ | ||||||
|     public function get_thumb_link(): string |     public function get_thumb_link(): string | ||||||
|     { |     { | ||||||
|         return $this->get_link('image_tlink', '_thumbs/$hash/thumb.jpg', 'thumb/$id.jpg'); |         global $config; | ||||||
|  |         $ext = $config->get_string("thumb_type"); | ||||||
|  |         return $this->get_link('image_tlink', '_thumbs/$hash/thumb.'.$ext, 'thumb/$id.'.$ext); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -53,21 +53,34 @@ function add_image(string $tmpname, string $filename, string $tags): void | |||||||
|     assert(file_exists($tmpname)); |     assert(file_exists($tmpname)); | ||||||
| 
 | 
 | ||||||
|     $pathinfo = pathinfo($filename); |     $pathinfo = pathinfo($filename); | ||||||
|     if (!array_key_exists('extension', $pathinfo)) { |  | ||||||
|         throw new UploadException("File has no extension"); |  | ||||||
|     } |  | ||||||
|     $metadata = []; |     $metadata = []; | ||||||
|     $metadata['filename'] = $pathinfo['basename']; |     $metadata['filename'] = $pathinfo['basename']; | ||||||
|     $metadata['extension'] = $pathinfo['extension']; |     if (array_key_exists('extension', $pathinfo)) { | ||||||
|  |         $metadata['extension'] = $pathinfo['extension']; | ||||||
|  |     } | ||||||
|  |      | ||||||
|     $metadata['tags'] = Tag::explode($tags); |     $metadata['tags'] = Tag::explode($tags); | ||||||
|     $metadata['source'] = null; |     $metadata['source'] = null; | ||||||
|     $event = new DataUploadEvent($tmpname, $metadata); |     $event = new DataUploadEvent($tmpname, $metadata); | ||||||
|     send_event($event); |     send_event($event); | ||||||
|     if ($event->image_id == -1) { |  | ||||||
|         throw new UploadException("File type not recognised"); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | function get_extension_from_mime(String $file_path): ?String  | ||||||
|  | { | ||||||
|  |     global $config; | ||||||
|  |     $mime = mime_content_type($file_path); | ||||||
|  |     if(!empty($mime)) { | ||||||
|  |         $ext = get_extension($mime); | ||||||
|  |         if(!empty($ext)) { | ||||||
|  |             return $ext; | ||||||
|  |         }  | ||||||
|  |         throw new UploadException("Could not determine extension for mimetype ".$mime); | ||||||
|  |     } | ||||||
|  |     throw new UploadException("Could not determine file mime type: ".$file_path); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Given a full size pair of dimensions, return a pair scaled down to fit |  * Given a full size pair of dimensions, return a pair scaled down to fit | ||||||
|  * into the configured thumbnail square, with ratio intact |  * into the configured thumbnail square, with ratio intact | ||||||
| @ -105,3 +118,344 @@ function get_thumbnail_size(int $orig_width, int $orig_height): array | |||||||
|         return [(int)($orig_width*$scale), (int)($orig_height*$scale)]; |         return [(int)($orig_width*$scale), (int)($orig_height*$scale)]; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Given a full size pair of dimensions, return a pair scaled down to fit | ||||||
|  |  * into the configured thumbnail square, with ratio intact, using thumb_scaling | ||||||
|  |  * | ||||||
|  |  * #return int[]
 | ||||||
|  |  */ | ||||||
|  | function get_thumbnail_size_scaled(int $orig_width, int $orig_height): array | ||||||
|  | { | ||||||
|  |     global $config; | ||||||
|  | 
 | ||||||
|  |     if ($orig_width === 0) { | ||||||
|  |         $orig_width = 192; | ||||||
|  |     } | ||||||
|  |     if ($orig_height === 0) { | ||||||
|  |         $orig_height = 192; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ($orig_width > $orig_height * 5) { | ||||||
|  |         $orig_width = $orig_height * 5; | ||||||
|  |     } | ||||||
|  |     if ($orig_height > $orig_width * 5) { | ||||||
|  |         $orig_height = $orig_width * 5; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $max_size = get_thumbnail_max_size_scaled(); | ||||||
|  |     $max_width  = $max_size[0]; | ||||||
|  |     $max_height = $max_size[1]; | ||||||
|  | 
 | ||||||
|  |     $xscale = ($max_height / $orig_height); | ||||||
|  |     $yscale = ($max_width / $orig_width); | ||||||
|  |     $scale = ($xscale < $yscale) ? $xscale : $yscale; | ||||||
|  | 
 | ||||||
|  |     if ($scale > 1 && $config->get_bool('thumb_upscale')) { | ||||||
|  |         return [(int)$orig_width, (int)$orig_height]; | ||||||
|  |     } else { | ||||||
|  |         return [(int)($orig_width*$scale), (int)($orig_height*$scale)]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function get_thumbnail_max_size_scaled(): array | ||||||
|  | { | ||||||
|  |     global $config; | ||||||
|  | 
 | ||||||
|  |     $scaling = $config->get_int("thumb_scaling"); | ||||||
|  |     $max_width  = $config->get_int('thumb_width') * ($scaling/100); | ||||||
|  |     $max_height = $config->get_int('thumb_height') * ($scaling/100); | ||||||
|  |     return [$max_width, $max_height]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function create_thumbnail_convert($hash): bool  | ||||||
|  | { | ||||||
|  |     global $config; | ||||||
|  | 
 | ||||||
|  |     $inname  = warehouse_path("images", $hash); | ||||||
|  |     $outname = warehouse_path("thumbs", $hash); | ||||||
|  | 
 | ||||||
|  |     $q = $config->get_int("thumb_quality"); | ||||||
|  |     $convert = $config->get_string("thumb_convert_path"); | ||||||
|  | 
 | ||||||
|  |     if($convert==null||$convert=="")  | ||||||
|  |     { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     //  ffff imagemagick fails sometimes, not sure why
 | ||||||
|  |     //$format = "'%s' '%s[0]' -format '%%[fx:w] %%[fx:h]' info:";
 | ||||||
|  |     //$cmd = sprintf($format, $convert, $inname);
 | ||||||
|  |     //$size = shell_exec($cmd);
 | ||||||
|  |     //$size = explode(" ", trim($size));
 | ||||||
|  |     $tsize = get_thumbnail_max_size_scaled(); | ||||||
|  |     $w = $tsize[0]; | ||||||
|  |     $h = $tsize[1]; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     // running the call with cmd.exe requires quoting for our paths
 | ||||||
|  |     $type = $config->get_string('thumb_type'); | ||||||
|  | 
 | ||||||
|  |     $options = ""; | ||||||
|  |     if (!$config->get_bool('thumb_upscale')) { | ||||||
|  |         $options .= "\>"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $bg = "black"; | ||||||
|  |     if($type=="webp") { | ||||||
|  |         $bg = "none"; | ||||||
|  |     } | ||||||
|  |     $format = '"%s" -flatten -strip -thumbnail %ux%u%s -quality %u -background %s "%s[0]"  %s:"%s"'; | ||||||
|  | 
 | ||||||
|  |     $cmd = sprintf($format, $convert, $w, $h, $options, $q, $bg, $inname, $type, $outname); | ||||||
|  |     $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); | ||||||
|  | 
 | ||||||
|  |     log_debug('handle_pixel', "Generating thumbnail with command `$cmd`, returns $ret"); | ||||||
|  | 
 | ||||||
|  |     if ($config->get_bool("thumb_optim", false)) { | ||||||
|  |         exec("jpegoptim $outname", $output, $ret); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function create_thumbnail_ffmpeg($hash): bool | ||||||
|  | { | ||||||
|  |     global $config; | ||||||
|  | 
 | ||||||
|  |     $ffmpeg = $config->get_string("thumb_ffmpeg_path"); | ||||||
|  |     if($ffmpeg==null||$ffmpeg=="") { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $inname  = warehouse_path("images", $hash); | ||||||
|  |     $outname = warehouse_path("thumbs", $hash); | ||||||
|  | 
 | ||||||
|  |     $orig_size = video_size($inname); | ||||||
|  |     $scaled_size = get_thumbnail_size_scaled($orig_size[0], $orig_size[1]); | ||||||
|  |      | ||||||
|  |     $codec = "mjpeg"; | ||||||
|  |     $quality = $config->get_int("thumb_quality"); | ||||||
|  |     if($config->get_string("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('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret"); | ||||||
|  |         return true; | ||||||
|  |     } else { | ||||||
|  |         log_error('imageboard/misc', "Generating thumbnail with command `$cmd`, returns $ret"); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function video_size(string $filename): array | ||||||
|  | { | ||||||
|  |     global $config; | ||||||
|  |     $ffmpeg = $config->get_string("thumb_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('imageboard/misc', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]"); | ||||||
|  |     return $size; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 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 | ||||||
|  |  */ | ||||||
|  | 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; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function image_resize_gd(String $image_filename, array $info, int $new_width, int $new_height,  | ||||||
|  |         string $output_filename=null, string $output_type=null, int $output_quality = 80)  | ||||||
|  | {         | ||||||
|  |     $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 ImageResizeException("Failed to save the new image - Unsupported image type."); | ||||||
|  |         } | ||||||
|  |     }  | ||||||
|  | 
 | ||||||
|  |     $memory_use = 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)"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $image = imagecreatefromstring(file_get_contents($image_filename)); | ||||||
|  |     $image_resized = imagecreatetruecolor($new_width, $new_height); | ||||||
|  |     try { | ||||||
|  |         if($image===false) { | ||||||
|  |             throw new ImageResizeException("Could not load image: ".$image_filename); | ||||||
|  |         } | ||||||
|  |         if($image_resized===false) { | ||||||
|  |             throw new ImageResizeException("Could not create output image with dimensions $new_width c $new_height "); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Handle transparent images
 | ||||||
|  |         switch($info[2]) { | ||||||
|  |             case IMAGETYPE_GIF: | ||||||
|  |                 $transparency = imagecolortransparent($image); | ||||||
|  |                 $palletsize = imagecolorstotal($image); | ||||||
|  | 
 | ||||||
|  |                 // If we have a specific transparent color
 | ||||||
|  |                 if ($transparency >= 0 && $transparency < $palletsize) { | ||||||
|  |                     // 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 ImageResizeException("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 ImageResizeException("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 ImageResizeException("Unable to disable image alpha blending"); | ||||||
|  |                 } | ||||||
|  |                 if(imagesavealpha($image_resized, true)===false) { | ||||||
|  |                     throw new ImageResizeException("Unable to enable image save alpha"); | ||||||
|  |                 } | ||||||
|  |                 $transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127); | ||||||
|  |                 if($transparent_color===false) { | ||||||
|  |                     throw new ImageResizeException("Unable to allocate transparent color"); | ||||||
|  |                 } | ||||||
|  |                 if(imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color)===false) { | ||||||
|  |                     throw new ImageResizeException("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 ImageResizeException("Unable to copy resized image data to new image"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         $result = false; | ||||||
|  |         switch($output_type) { | ||||||
|  |             case "bmp": | ||||||
|  |                 $result = imagebmp($image_resized, $output_filename, true); | ||||||
|  |                 break; | ||||||
|  |             case "webp": | ||||||
|  |                 $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 ImageResizeException("Failed to save the new image - Unsupported image type: $output_type"); | ||||||
|  |         } | ||||||
|  |         if($result==false) { | ||||||
|  |             throw new ImageResizeException("Failed to save the new image, function returned false when saving type: $output_type"); | ||||||
|  |         } | ||||||
|  |     } finally { | ||||||
|  |         imagedestroy($image); | ||||||
|  |         imagedestroy($image_resized); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function is_animated_gif(String $image_filename) { | ||||||
|  |     $isanigif = 0; | ||||||
|  |     if (($fh = @fopen($image_filename, 'rb'))) { | ||||||
|  |         //check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
 | ||||||
|  |         while (!feof($fh) && $isanigif < 2) { | ||||||
|  |             $chunk = fread($fh, 1024 * 100); | ||||||
|  |             $isanigif += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return ($isanigif == 0); | ||||||
|  | } | ||||||
| @ -124,13 +124,13 @@ class AdminPage extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onPostListBuilding(PostListBuildingEvent $event) |     // public function onPostListBuilding(PostListBuildingEvent $event)
 | ||||||
|     { |     // {
 | ||||||
|         global $user; |     //     global $user;
 | ||||||
|         if ($user->can("manage_admintools") && !empty($event->search_terms)) { |     //     if ($user->can("manage_admintools") && !empty($event->search_terms)) {
 | ||||||
|             $event->add_control($this->theme->dbq_html(Tag::implode($event->search_terms))); |     //         $event->add_control($this->theme->dbq_html(Tag::implode($event->search_terms)));
 | ||||||
|         } |     //     }
 | ||||||
|     } |     // }
 | ||||||
| 
 | 
 | ||||||
|     private function delete_by_query() |     private function delete_by_query() | ||||||
|     { |     { | ||||||
|  | |||||||
							
								
								
									
										270
									
								
								ext/bulk_actions/main.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								ext/bulk_actions/main.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,270 @@ | |||||||
|  | <?php | ||||||
|  | /* | ||||||
|  |  * Name: Bulk Actions | ||||||
|  |  * Author: Matthew Barbour | ||||||
|  |  * License: WTFPL | ||||||
|  |  * Description: Provides query and selection-based bulk action support | ||||||
|  |  * Documentation: Provides bulk action section in list view. Allows performing actions against a set of images based on query or manual selection. | ||||||
|  |  * Based on Mass Tagger by Christian Walde <walde.christian@googlemail.com>, contributions by Shish and Agasa. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BulkActionBlockBuildingEvent extends Event | ||||||
|  | { | ||||||
|  |     /** @var array  */ | ||||||
|  |     public $actions = array(); | ||||||
|  | 
 | ||||||
|  |     public function add_action(String $action, string $button_text, String $confirmation_message = "", String $block = "", int $position = 40) | ||||||
|  |     { | ||||||
|  |         if ($block == null) | ||||||
|  |             $block = ""; | ||||||
|  | 
 | ||||||
|  |         array_push( | ||||||
|  |             $this->actions, | ||||||
|  |             array( | ||||||
|  |                 "block" => $block, | ||||||
|  |                 "confirmation_message" => $confirmation_message, | ||||||
|  |                 "action" => $action, | ||||||
|  |                 "button_text" => $button_text, | ||||||
|  |                 "position" => $position | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class BulkActionEvent extends Event | ||||||
|  | { | ||||||
|  |     /** @var string  */ | ||||||
|  |     public $action; | ||||||
|  |     /** @var array  */ | ||||||
|  |     public $items; | ||||||
|  |     /** @var PageRequestEvent  */ | ||||||
|  |     public $page_request; | ||||||
|  | 
 | ||||||
|  |     function __construct(String $action, PageRequestEvent $pageRequestEvent, array $items) | ||||||
|  |     { | ||||||
|  |         $this->action = $action; | ||||||
|  |         $this->page_request = $pageRequestEvent; | ||||||
|  |         $this->items = $items; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class BulkActions extends Extension | ||||||
|  | { | ||||||
|  |     public function onPostListBuilding(PostListBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         global $config, $page, $user; | ||||||
|  | 
 | ||||||
|  |         if ($user->is_logged_in()) { | ||||||
|  |             $babbe = new BulkActionBlockBuildingEvent(); | ||||||
|  |             send_event($babbe); | ||||||
|  | 
 | ||||||
|  |             if (sizeof($babbe->actions) == 0) | ||||||
|  | 			    return; | ||||||
|  | 
 | ||||||
|  |             usort($babbe->actions, array($this, "sort_blocks")); | ||||||
|  | 
 | ||||||
|  |             $this->theme->display_selector($page, $babbe->actions, Tag::implode($event->search_terms)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user; | ||||||
|  | 
 | ||||||
|  |         if ($user->can("delete_image")) { | ||||||
|  |             $event->add_action("bulk_delete","Delete", "Delete selected images?", "", 10); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->can("bulk_edit_image_tag")) { | ||||||
|  |             $event->add_action("bulk_tag","Tag", "", $this->theme->render_tag_input(), 10); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($user->can("bulk_edit_image_source")) { | ||||||
|  |             $event->add_action("bulk_source","Set Source", "", $this->theme->render_source_input(), 10); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onBulkAction(BulkActionEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user; | ||||||
|  | 
 | ||||||
|  |         switch ($event->action) { | ||||||
|  |             case "bulk_delete": | ||||||
|  |                 if ($user->can("delete_image")) { | ||||||
|  |                     $i = $this->delete_items($event->items); | ||||||
|  |                     flash_message("Deleted $i items"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case "bulk_tag": | ||||||
|  |                 if (!isset($_POST['bulk_tags'])) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if ($user->can("bulk_edit_image_tag")) { | ||||||
|  |                     $tags = $_POST['bulk_tags']; | ||||||
|  |                     $replace = false; | ||||||
|  |                     if (isset($_POST['bulk_tags_replace']) &&  $_POST['bulk_tags_replace'] == "true") { | ||||||
|  |                         $replace = true; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     $i= $this->tag_items($event->items, $tags, $replace); | ||||||
|  |                     flash_message("Tagged $i items"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case "bulk_source": | ||||||
|  |                 if (!isset($_POST['bulk_source'])) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if ($user->can("bulk_edit_image_source")) { | ||||||
|  |                     $source = $_POST['bulk_source']; | ||||||
|  |                     $i = $this->set_source($event->items, $source); | ||||||
|  |                     flash_message("Set source for $i items"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onPageRequest(PageRequestEvent $event) | ||||||
|  |     { | ||||||
|  |         global $page, $user; | ||||||
|  |         if ($event->page_matches("bulk_action") && $user->is_admin()) { | ||||||
|  |             if (!isset($_POST['bulk_action'])) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $action = $_POST['bulk_action']; | ||||||
|  | 
 | ||||||
|  |             $items = []; | ||||||
|  |             if (isset($_POST['bulk_selected_ids']) && $_POST['bulk_selected_ids'] != "") { | ||||||
|  |                 $data = json_decode($_POST['bulk_selected_ids']); | ||||||
|  |                 if (is_array($data)) { | ||||||
|  |                     foreach ($data as $id) { | ||||||
|  |                         if (is_numeric($id)) { | ||||||
|  |                             array_push($items, int_escape($id)); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else if (isset($_POST['bulk_query']) && $_POST['bulk_query'] != "") { | ||||||
|  |                 $query = $_POST['bulk_query']; | ||||||
|  |                 if ($query != null && $query != "") { | ||||||
|  |                     $n = 0; | ||||||
|  |                     $tags = Tag::explode($query); | ||||||
|  |                     while (true) { | ||||||
|  |                         $results = Image::find_image_ids($n, 100, $tags); | ||||||
|  |                         if (count($results) == 0) { | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         reset($results); // rewind to first element in array.
 | ||||||
|  |                         $items = array_merge($items, $results); | ||||||
|  |                         $n += count($results); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (sizeof($items) > 0) { | ||||||
|  |                 reset($items); // rewind to first element in array.
 | ||||||
|  |                 $newEvent = new BulkActionEvent($action, $event, $items); | ||||||
|  |                 send_event($newEvent); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             $page->set_mode("redirect"); | ||||||
|  |             if (!isset($_SERVER['HTTP_REFERER'])) { | ||||||
|  |                 $_SERVER['HTTP_REFERER'] = make_link(); | ||||||
|  |             } | ||||||
|  |             $page->set_redirect($_SERVER['HTTP_REFERER']); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function sort_blocks($a, $b) | ||||||
|  | 	{ | ||||||
|  | 		return $a["position"] - $b["position"]; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private function delete_items(array $items): int | ||||||
|  |     { | ||||||
|  |         $total = 0; | ||||||
|  |         foreach ($items as $id) { | ||||||
|  |             try { | ||||||
|  |                 $image = Image::by_id($id); | ||||||
|  |                 if($image==null) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 send_event(new ImageDeletionEvent($image)); | ||||||
|  |                 $total++; | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 flash_message("Error while removing $id: " . $e->getMessage(), "error"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $total; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function tag_items(array $items, string $tags, bool $replace): int | ||||||
|  |     { | ||||||
|  |         $tags = Tag::explode($tags); | ||||||
|  | 
 | ||||||
|  |         $pos_tag_array = []; | ||||||
|  |         $neg_tag_array = []; | ||||||
|  |         foreach ($tags as $new_tag) { | ||||||
|  |             if (strpos($new_tag, '-') === 0) { | ||||||
|  |                 $neg_tag_array[] = substr($new_tag, 1); | ||||||
|  |             } else { | ||||||
|  |                 $pos_tag_array[] = $new_tag; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $total = 0; | ||||||
|  |         if ($replace) { | ||||||
|  |             foreach ($items as $id) { | ||||||
|  |                 $image = Image::by_id($id); | ||||||
|  |                 if($image==null) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 send_event(new TagSetEvent($image, $tags)); | ||||||
|  |                 $total++; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             foreach ($items as $id) { | ||||||
|  |                 $image = Image::by_id($id); | ||||||
|  |                 if($image==null) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $img_tags = []; | ||||||
|  |                 if (!empty($neg_tag_array)) { | ||||||
|  |                     $img_tags = array_merge($pos_tag_array, $image->get_tag_array()); | ||||||
|  |                     $img_tags = array_diff($img_tags, $neg_tag_array); | ||||||
|  |                 } else { | ||||||
|  |                     $img_tags = array_merge($tags, $image->get_tag_array()); | ||||||
|  |                 } | ||||||
|  |                 send_event(new TagSetEvent($image, $img_tags)); | ||||||
|  |                 $total++; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $total; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function set_source(array $items, String $source): int | ||||||
|  |     { | ||||||
|  |         $total = 0; | ||||||
|  |         foreach ($items as $id) { | ||||||
|  |             try { | ||||||
|  |                 $image = Image::by_id($id); | ||||||
|  |                 if($image==null) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 send_event(new SourceSetEvent($image, $source)); | ||||||
|  |                 $total++; | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 flash_message("Error while setting source for $id: " . $e->getMessage(), "error"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $total; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										197
									
								
								ext/bulk_actions/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								ext/bulk_actions/script.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,197 @@ | |||||||
|  | /*jshint bitwise:true, curly:true, forin:false, noarg:true, noempty:true, nonew:true, undef:true, strict:false, browser:true, jquery:true */ | ||||||
|  | 
 | ||||||
|  | var bulk_selector_active = false; | ||||||
|  | var bulk_selector_initialized = false; | ||||||
|  | var bulk_selector_valid = false; | ||||||
|  | 
 | ||||||
|  | function validate_selections(form, confirmationMessage) { | ||||||
|  |     var queryOnly = false; | ||||||
|  |     if(bulk_selector_active) { | ||||||
|  |         var data = get_selected_items(); | ||||||
|  |         if(data.length==0) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         var query = $(form).find('input[name="bulk_query"]').val(); | ||||||
|  | 
 | ||||||
|  |         if (query == null || query == "") { | ||||||
|  |             return false; | ||||||
|  |         } else { | ||||||
|  |             queryOnly = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     if(confirmationMessage!=null&&confirmationMessage!="") { | ||||||
|  |         return confirm(confirmationMessage); | ||||||
|  |     } else if(queryOnly) { | ||||||
|  |         var action = $(form).find('input[name="submit_button"]').val(); | ||||||
|  | 
 | ||||||
|  |         return confirm("Perform bulk action \"" + action + "\" on all images matching the current search?"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | function activate_bulk_selector () { | ||||||
|  |     set_selected_items([]); | ||||||
|  |     if(!bulk_selector_initialized) { | ||||||
|  |         $("a.shm-thumb").each( | ||||||
|  |             function (index, block) { | ||||||
|  |                 add_selector_button($(block)); | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     $('#bulk_selector_controls').show(); | ||||||
|  |     $('#bulk_selector_activate').hide(); | ||||||
|  |     bulk_selector_active = true; | ||||||
|  |     bulk_selector_initialized = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function deactivate_bulk_selector() { | ||||||
|  |     set_selected_items([]); | ||||||
|  |     $('#bulk_selector_controls').hide(); | ||||||
|  |     $('#bulk_selector_activate').show(); | ||||||
|  |     bulk_selector_active = false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function get_selected_items() { | ||||||
|  |     var data = $('#bulk_selected_ids').val(); | ||||||
|  |     if(data==""||data==null) { | ||||||
|  |         data = []; | ||||||
|  |     } else { | ||||||
|  |         data = JSON.parse(data); | ||||||
|  |     } | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function set_selected_items(items) { | ||||||
|  |     $("a.shm-thumb").removeClass('selected'); | ||||||
|  | 
 | ||||||
|  |     $(items).each( | ||||||
|  |         function(index,item) { | ||||||
|  |             $('a.shm-thumb[data-post-id="' + item + '"]').addClass('selected'); | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     $('input[name="bulk_selected_ids"]').val(JSON.stringify(items)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function select_item(id) { | ||||||
|  |     var data = get_selected_items(); | ||||||
|  |     if(!data.includes(id)) | ||||||
|  |         data.push(id); | ||||||
|  |     set_selected_items(data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function deselect_item(id) { | ||||||
|  |     var data = get_selected_items(); | ||||||
|  |     if(data.includes(id)) | ||||||
|  |         data.splice(data.indexOf(id, 1)); | ||||||
|  |     set_selected_items(data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function toggle_selection( id ) { | ||||||
|  |     var data = get_selected_items(); | ||||||
|  |     console.log(id); | ||||||
|  |     if(data.includes(id)) { | ||||||
|  |         data.splice(data.indexOf(id),1); | ||||||
|  |         set_selected_items(data);             | ||||||
|  |         return false; | ||||||
|  |     } else { | ||||||
|  |         data.push(id); | ||||||
|  |         set_selected_items(data);             | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | function select_all() { | ||||||
|  |     var items = []; | ||||||
|  |     $("a.shm-thumb").each( | ||||||
|  |         function ( index, block ) { | ||||||
|  |             block = $(block); | ||||||
|  |             var id = block.data("post-id"); | ||||||
|  |             items.push(id); | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  |     set_selected_items(items);             | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function select_invert() { | ||||||
|  |     var currentItems = get_selected_items(); | ||||||
|  |     var items = []; | ||||||
|  |     $("a.shm-thumb").each( | ||||||
|  |         function ( index, block ) { | ||||||
|  |             block = $(block); | ||||||
|  |             var id = block.data("post-id"); | ||||||
|  |             if(!currentItems.includes(id)) { | ||||||
|  |                 items.push(id); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  |     set_selected_items(items);             | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function select_none() { | ||||||
|  |     set_selected_items([]);             | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function select_range(start, end) { | ||||||
|  |     var data = get_selected_items(); | ||||||
|  |     var selecting = false; | ||||||
|  |     $("a.shm-thumb").each( | ||||||
|  |         function ( index, block ) { | ||||||
|  |             block = $(block); | ||||||
|  |             var id = block.data("post-id"); | ||||||
|  |             if(id==start) | ||||||
|  |                 selecting = true; | ||||||
|  | 
 | ||||||
|  |             if(selecting) { | ||||||
|  |                 if(!data.includes(id)) | ||||||
|  |                     data.push(id); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if(id==end) { | ||||||
|  |                 selecting = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  |     set_selected_items(data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var last_clicked_item; | ||||||
|  | 
 | ||||||
|  | function add_selector_button($block) {	 | ||||||
|  |     var c = function(e) { | ||||||
|  |         if(!bulk_selector_active) | ||||||
|  |             return true; | ||||||
|  | 
 | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  |         | ||||||
|  |         var id = $block.data("post-id"); | ||||||
|  |         if(e.shiftKey) { | ||||||
|  |             if(last_clicked_item<id) { | ||||||
|  |                 select_range(id, last_clicked_item); | ||||||
|  |             } else { | ||||||
|  |                 select_range(last_clicked_item, id); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             last_clicked_item = id; | ||||||
|  |             toggle_selection(id); | ||||||
|  |         } | ||||||
|  |         return false;  | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     $block.find("A").click(c); | ||||||
|  |     $block.click(c); // sometimes the thumbs *is* the A
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | $(function () { | ||||||
|  | 	// Clear the selection, in case it was autocompleted by the browser.
 | ||||||
|  | 	$('#bulk_selected_ids').val(""); | ||||||
|  | }); | ||||||
							
								
								
									
										10
									
								
								ext/bulk_actions/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								ext/bulk_actions/style.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | .selected { | ||||||
|  | 	outline: 3px solid blue; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .bulk_action { | ||||||
|  | 	margin-top: 8pt; | ||||||
|  | } | ||||||
|  | .bulk_selector_controls table td { | ||||||
|  | 	width: 33%; | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								ext/bulk_actions/theme.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								ext/bulk_actions/theme.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | class BulkActionsTheme extends Themelet | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public function display_selector(Page $page, $actions, $query) | ||||||
|  | 	{ | ||||||
|  | 		global $user; | ||||||
|  | 
 | ||||||
|  | 		$body = "<input type='hidden' name='bulk_selected_ids' id='bulk_selected_ids' />
 | ||||||
|  | 				<input id='bulk_selector_activate' type='button' onclick='activate_bulk_selector();' value='Activate Selector'/> | ||||||
|  | 				<div id='bulk_selector_controls' style='display: none;'> | ||||||
|  | 					<input id='bulk_selector_deactivate' type='button' onclick='deactivate_bulk_selector();' value='Deactivate Selector'/> | ||||||
|  | 					Click on images to mark them. | ||||||
|  | 					<br /> | ||||||
|  | 					<table><tr><td> | ||||||
|  | 					<input id='bulk_selector_select_all' type='button' | ||||||
|  | 						onclick='select_all();' value='All'/> | ||||||
|  | 					</td><td> | ||||||
|  | 					<input id='bulk_selector_select_invert' type='button' | ||||||
|  | 						onclick='select_invert();' value='Invert'/> | ||||||
|  | 					</td><td> | ||||||
|  | 					<input id='bulk)selector_select_none' type='button' | ||||||
|  | 						onclick='select_none();' value='Clear'/> | ||||||
|  | 					</td></tr></table> | ||||||
|  | 		";
 | ||||||
|  | 
 | ||||||
|  | 		$hasQuery = ($query != null && $query != ""); | ||||||
|  | 
 | ||||||
|  | 		if ($hasQuery) { | ||||||
|  | 			$body .= "</div>"; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		foreach ($actions as $action) { | ||||||
|  | 			$body .= "<div class='bulk_action'>" . make_form(make_link("bulk_action"), "POST", False, "", "return validate_selections(this,'" . html_escape($action["confirmation_message"]) . "');") . | ||||||
|  | 				"<input type='hidden' name='bulk_query' value='" . html_escape($query) . "'>" . | ||||||
|  | 				"<input type='hidden' name='bulk_selected_ids' />" . | ||||||
|  | 				"<input type='hidden' name='bulk_action' value='" . $action["action"] . "' />" . | ||||||
|  | 				$action["block"] . | ||||||
|  | 				"<input type='submit' name='submit_button' value='" . $action["button_text"] . "'/>" . | ||||||
|  | 				"</form></div>"; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (!$hasQuery) { | ||||||
|  | 			$body .= "</div>"; | ||||||
|  | 		} | ||||||
|  | 		$block = new Block("Bulk Actions", $body, "left", 30); | ||||||
|  | 		$page->add_block($block); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public function render_tag_input() | ||||||
|  | 	{ | ||||||
|  | 		return "<label><input type='checkbox' style='width:13px;' name='bulk_tags_replace' value='true'/>Replace tags</label>" . | ||||||
|  | 			"<input type='text' name='bulk_tags' required='required' placeholder='Enter tags here' />"; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public function render_source_input() | ||||||
|  | 	{ | ||||||
|  | 		return "<input type='text' name='bulk_source' required='required' placeholder='Enter source here' />"; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -244,8 +244,11 @@ class CronUploader extends Extension | |||||||
|      */ |      */ | ||||||
|     public function process_upload(int $upload_count = 0): bool |     public function process_upload(int $upload_count = 0): bool | ||||||
|     { |     { | ||||||
|         global $config; |         global $config, $database; | ||||||
|  | 
 | ||||||
|         set_time_limit(0); |         set_time_limit(0); | ||||||
|  | 
 | ||||||
|  |         $output_subdir = date('Ymd-His', time())."/"; | ||||||
|         $this->set_dir(); |         $this->set_dir(); | ||||||
|         $this->generate_image_queue(); |         $this->generate_image_queue(); | ||||||
|          |          | ||||||
| @ -262,32 +265,58 @@ class CronUploader extends Extension | |||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Randomize Images
 |         // Randomize Images
 | ||||||
|         shuffle($this->image_queue); |         //shuffle($this->image_queue);
 | ||||||
|  | 
 | ||||||
|  |         $merged = 0; | ||||||
|  |         $added = 0; | ||||||
|  |         $failed = 0; | ||||||
|  | 
 | ||||||
|  |         $failedItems = []; | ||||||
| 
 | 
 | ||||||
|         // Upload the file(s)
 |         // Upload the file(s)
 | ||||||
|         for ($i = 0; $i < $upload_count && sizeof($this->image_queue)>0; $i++) { |         for ($i = 0; $i < $upload_count && sizeof($this->image_queue)>0; $i++) { | ||||||
|             $img = array_pop($this->image_queue); |             $img = array_pop($this->image_queue); | ||||||
|              |              | ||||||
|             try { |             try { | ||||||
|                 $this->add_image($img[0], $img[1], $img[2]); |                 $database->beginTransaction(); | ||||||
|                 $this->move_uploaded($img[0], $img[1], false); |                 $result = $this->add_image($img[0], $img[1], $img[2]); | ||||||
|  |                 $database->commit(); | ||||||
|  |                 $this->move_uploaded($img[0], $img[1], $output_subdir, false); | ||||||
|  |                 if($result==null) { | ||||||
|  |                     $merged++; | ||||||
|  |                 } else { | ||||||
|  |                     $added++; | ||||||
|  |                 } | ||||||
|             } catch (Exception $e) { |             } catch (Exception $e) { | ||||||
|                 $this->move_uploaded($img[0], $img[1], true); |                 $failed++; | ||||||
|  |                 $this->move_uploaded($img[0], $img[1], $output_subdir, true); | ||||||
|  | 				$msgNumber = $this->add_upload_info("(".gettype($e).") ".$e->getMessage()); | ||||||
|  |                 $msgNumber = $this->add_upload_info($e->getTraceAsString()); | ||||||
|                 if (strpos($e->getMessage(), 'SQLSTATE') !== false) { |                 if (strpos($e->getMessage(), 'SQLSTATE') !== false) { | ||||||
|                     // Postgres invalidates the transaction if there is an SQL error,
 |                     // Postgres invalidates the transaction if there is an SQL error,
 | ||||||
|                     // so all subsequence transactions will fail.
 |                     // so all subsequence transactions will fail.
 | ||||||
|                     break; |                     break; | ||||||
|                 } |                 } | ||||||
|  |                 try { | ||||||
|  |                     $database->rollback(); | ||||||
|  |                 } catch (Exception $e) {} | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |         $msgNumber = $this->add_upload_info("Items added: $added"); | ||||||
|  |         $msgNumber = $this->add_upload_info("Items merged: $merged"); | ||||||
|  |         $msgNumber = $this->add_upload_info("Items failed: $failed"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |          | ||||||
|         // Display & save upload log
 |         // Display & save upload log
 | ||||||
|         $this->handle_log(); |         $this->handle_log(); | ||||||
|          |          | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     private function move_uploaded($path, $filename, $corrupt = false) |     private function move_uploaded($path, $filename, $output_subdir, $corrupt = false) | ||||||
|     { |     { | ||||||
|         global $config; |         global $config; | ||||||
|          |          | ||||||
| @ -296,16 +325,17 @@ class CronUploader extends Extension | |||||||
|          |          | ||||||
|         $relativeDir = dirname(substr($path, strlen($this->root_dir) + 7)); |         $relativeDir = dirname(substr($path, strlen($this->root_dir) + 7)); | ||||||
| 
 | 
 | ||||||
|         // Determine which dir to move to
 | 		// Determine which dir to move to
 | ||||||
|         if ($corrupt) { | 		if ($corrupt) { | ||||||
|             // Move to corrupt dir
 | 			// Move to corrupt dir
 | ||||||
|             $newDir .= "/failed_to_upload/".$relativeDir; | 			$newDir .= "/failed_to_upload/".$output_subdir.$relativeDir; | ||||||
|             $info = "ERROR: Image was not uploaded."; | 			$info = "ERROR: Image was not uploaded."; | ||||||
|         } else { | 		} | ||||||
|             $newDir .= "/uploaded/".$relativeDir; | 		else { | ||||||
|             $info = "Image successfully uploaded. "; | 			$newDir .= "/uploaded/".$output_subdir.$relativeDir; | ||||||
|         } | 			$info = "Image successfully uploaded. "; | ||||||
|         $newDir = str_replace("//", "/", $newDir."/"); | 		} | ||||||
|  | 		$newDir = str_replace ( "//", "/", $newDir."/" ); | ||||||
| 
 | 
 | ||||||
|         if (!is_dir($newDir)) { |         if (!is_dir($newDir)) { | ||||||
|             mkdir($newDir, 0775, true); |             mkdir($newDir, 0775, true); | ||||||
| @ -328,7 +358,7 @@ class CronUploader extends Extension | |||||||
|         $metadata = []; |         $metadata = []; | ||||||
|         $metadata ['filename'] = $pathinfo ['basename']; |         $metadata ['filename'] = $pathinfo ['basename']; | ||||||
|         if (array_key_exists('extension', $pathinfo)) { |         if (array_key_exists('extension', $pathinfo)) { | ||||||
|             $metadata['extension'] = $pathinfo['extension']; |             $metadata ['extension'] = $pathinfo ['extension']; | ||||||
|         } |         } | ||||||
|         $metadata ['tags'] = Tag::explode($tags);  |         $metadata ['tags'] = Tag::explode($tags);  | ||||||
|         $metadata ['source'] = null; |         $metadata ['source'] = null; | ||||||
| @ -339,10 +369,13 @@ class CronUploader extends Extension | |||||||
|         $infomsg = ""; // Will contain info message
 |         $infomsg = ""; // Will contain info message
 | ||||||
|         if ($event->image_id == -1) { |         if ($event->image_id == -1) { | ||||||
|             throw new Exception("File type not recognised. Filename: {$filename}"); |             throw new Exception("File type not recognised. Filename: {$filename}"); | ||||||
|  |         } else if ($event->image_id == null) { | ||||||
|  |             $infomsg = "Image merged. Filename: {$filename}"; | ||||||
|         } else { |         } else { | ||||||
|             $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename} - Tags: {$tags}"; |             $infomsg = "Image uploaded. ID: {$event->image_id} - Filename: {$filename}"; | ||||||
|         } |         } | ||||||
|         $msgNumber = $this->add_upload_info($infomsg); |         $msgNumber = $this->add_upload_info($infomsg); | ||||||
|  |         return $event->image_id; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     private function generate_image_queue(): void |     private function generate_image_queue(): void | ||||||
|  | |||||||
| @ -366,8 +366,8 @@ class DanbooruApi extends Extension | |||||||
|         $fileinfo = pathinfo($filename); |         $fileinfo = pathinfo($filename); | ||||||
|         $metadata = []; |         $metadata = []; | ||||||
|         $metadata['filename'] = $fileinfo['basename']; |         $metadata['filename'] = $fileinfo['basename']; | ||||||
|         if (array_key_exists('extension', $pathinfo)) { |         if (array_key_exists('extension', $fileinfo)) { | ||||||
|             $metadata['extension'] = $pathinfo['extension']; |             $metadata['extension'] = $fileinfo['extension']; | ||||||
|         } |         } | ||||||
|         $metadata['tags'] = $posttags; |         $metadata['tags'] = $posttags; | ||||||
|         $metadata['source'] = $source; |         $metadata['source'] = $source; | ||||||
|  | |||||||
| @ -57,6 +57,8 @@ class ET extends Extension | |||||||
|         $info['thumb_quality']	= $config->get_int('thumb_quality'); |         $info['thumb_quality']	= $config->get_int('thumb_quality'); | ||||||
|         $info['thumb_width']	= $config->get_int('thumb_width'); |         $info['thumb_width']	= $config->get_int('thumb_width'); | ||||||
|         $info['thumb_height']	= $config->get_int('thumb_height'); |         $info['thumb_height']	= $config->get_int('thumb_height'); | ||||||
|  |         $info['thumb_scaling']	= $config->get_int('thumb_scaling'); | ||||||
|  |         $info['thumb_type']	    = $config->get_string('thumb_type'); | ||||||
|         $info['thumb_mem']		= $config->get_int("thumb_mem_limit"); |         $info['thumb_mem']		= $config->get_int("thumb_mem_limit"); | ||||||
| 
 | 
 | ||||||
|         $info['stat_images']   = $database->get_one("SELECT COUNT(*) FROM images"); |         $info['stat_images']   = $database->get_one("SELECT COUNT(*) FROM images"); | ||||||
|  | |||||||
| @ -37,10 +37,12 @@ Disk use: {$info['sys_disk']} | |||||||
| 
 | 
 | ||||||
| Thumbnail Generation: | Thumbnail Generation: | ||||||
| Engine: {$info['thumb_engine']} | Engine: {$info['thumb_engine']} | ||||||
|  | Type: {$info['thumb_type']} | ||||||
| Memory: {$info['thumb_mem']} | Memory: {$info['thumb_mem']} | ||||||
| Quality: {$info['thumb_quality']} | Quality: {$info['thumb_quality']} | ||||||
| Width: {$info['thumb_width']} | Width: {$info['thumb_width']} | ||||||
| Height: {$info['thumb_height']} | Height: {$info['thumb_height']} | ||||||
|  | Scaling: {$info['thumb_scaling']} | ||||||
| 
 | 
 | ||||||
| Shimmie stats: | Shimmie stats: | ||||||
| Images: {$info['stat_images']} | Images: {$info['stat_images']} | ||||||
|  | |||||||
| @ -3,14 +3,18 @@ | |||||||
|  * Name: Handle Flash |  * Name: Handle Flash | ||||||
|  * Author: Shish <webmaster@shishnet.org> |  * Author: Shish <webmaster@shishnet.org> | ||||||
|  * Link: http://code.shishnet.org/shimmie2/ |  * Link: http://code.shishnet.org/shimmie2/ | ||||||
|  * Description: Handle Flash files. (No thumbnail is generated for flash files) |  * Description: Handle Flash files. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| class FlashFileHandler extends DataHandlerExtension | class FlashFileHandler extends DataHandlerExtension | ||||||
| { | { | ||||||
|     protected function create_thumb(string $hash): bool |     protected function create_thumb(string $hash): bool | ||||||
|     { |     { | ||||||
|         copy("ext/handle_flash/thumb.jpg", warehouse_path("thumbs", $hash)); |         global $config; | ||||||
|  | 
 | ||||||
|  |         if(!create_thumbnail_ffmpeg($hash)) { | ||||||
|  |             copy("ext/handle_flash/thumb.jpg", warehouse_path("thumbs", $hash)); | ||||||
|  |         } | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -24,13 +24,6 @@ class IcoFileHandler extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onThumbnailGeneration(ThumbnailGenerationEvent $event) |  | ||||||
|     { |  | ||||||
|         if ($this->supported_ext($event->type)) { |  | ||||||
|             $this->create_thumb($event->hash); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function onDisplayingImage(DisplayingImageEvent $event) |     public function onDisplayingImage(DisplayingImageEvent $event) | ||||||
|     { |     { | ||||||
|         global $page; |         global $page; | ||||||
| @ -88,8 +81,10 @@ class IcoFileHandler extends Extension | |||||||
|         $inname  = warehouse_path("images", $hash); |         $inname  = warehouse_path("images", $hash); | ||||||
|         $outname = warehouse_path("thumbs", $hash); |         $outname = warehouse_path("thumbs", $hash); | ||||||
| 
 | 
 | ||||||
|         $w = $config->get_int("thumb_width"); |         $tsize = get_thumbnail_size_scaled($width, $height); | ||||||
|         $h = $config->get_int("thumb_height"); |         $w = $tsize[0]; | ||||||
|  |         $h = $tsise[1]; | ||||||
|  |          | ||||||
|         $q = $config->get_int("thumb_quality"); |         $q = $config->get_int("thumb_quality"); | ||||||
|         $mem = $config->get_int("thumb_mem_limit") / 1024 / 1024; // IM takes memory in MB
 |         $mem = $config->get_int("thumb_mem_limit") / 1024 / 1024; // IM takes memory in MB
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,14 +3,14 @@ | |||||||
|  * Name: Handle Pixel |  * Name: Handle Pixel | ||||||
|  * Author: Shish <webmaster@shishnet.org> |  * Author: Shish <webmaster@shishnet.org> | ||||||
|  * Link: http://code.shishnet.org/shimmie2/ |  * Link: http://code.shishnet.org/shimmie2/ | ||||||
|  * Description: Handle JPEG, PNG, GIF, etc files |  * Description: Handle JPEG, PNG, GIF, WEBP, etc files | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| class PixelFileHandler extends DataHandlerExtension | class PixelFileHandler extends DataHandlerExtension | ||||||
| { | { | ||||||
|     protected function supported_ext(string $ext): bool |     protected function supported_ext(string $ext): bool | ||||||
|     { |     { | ||||||
|         $exts = ["jpg", "jpeg", "gif", "png"]; |         $exts = ["jpg", "jpeg", "gif", "png", "webp"]; | ||||||
|         $ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext; |         $ext = (($pos = strpos($ext, '?')) !== false) ? substr($ext, 0, $pos) : $ext; | ||||||
|         return in_array(strtolower($ext), $exts); |         return in_array(strtolower($ext), $exts); | ||||||
|     } |     } | ||||||
| @ -39,7 +39,7 @@ class PixelFileHandler extends DataHandlerExtension | |||||||
| 
 | 
 | ||||||
|     protected function check_contents(string $tmpname): bool |     protected function check_contents(string $tmpname): bool | ||||||
|     { |     { | ||||||
|         $valid = [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG]; |         $valid = [IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_WEBP]; | ||||||
|         if (!file_exists($tmpname)) { |         if (!file_exists($tmpname)) { | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| @ -54,15 +54,6 @@ class PixelFileHandler extends DataHandlerExtension | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected function create_thumb(string $hash): bool |     protected function create_thumb(string $hash): bool | ||||||
|     { |  | ||||||
|         $outname = warehouse_path("thumbs", $hash); |  | ||||||
|         if (file_exists($outname)) { |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|         return $this->create_thumb_force($hash); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected function create_thumb_force(string $hash): bool |  | ||||||
|     { |     { | ||||||
|         global $config; |         global $config; | ||||||
| 
 | 
 | ||||||
| @ -77,7 +68,7 @@ class PixelFileHandler extends DataHandlerExtension | |||||||
|                 $ok = $this->make_thumb_gd($inname, $outname); |                 $ok = $this->make_thumb_gd($inname, $outname); | ||||||
|                 break; |                 break; | ||||||
|             case 'convert': |             case 'convert': | ||||||
|                 $ok = $this->make_thumb_convert($inname, $outname); |                 $ok = create_thumbnail_convert($hash); | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -98,100 +89,31 @@ class PixelFileHandler extends DataHandlerExtension | |||||||
| 		", 20);
 | 		", 20);
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // IM thumber {{{
 |     // GD thumber {{{
 | ||||||
|     private function make_thumb_convert(string $inname, string $outname): bool |     private function make_thumb_gd(string $inname, string $outname): bool | ||||||
|     { |     { | ||||||
|         global $config; |         global $config; | ||||||
| 
 | 
 | ||||||
|         $w = $config->get_int("thumb_width"); |         try { | ||||||
|         $h = $config->get_int("thumb_height"); |             $info = getimagesize($inname); | ||||||
|         $q = $config->get_int("thumb_quality"); |             $tsize = get_thumbnail_size_scaled($info[0], $info[1]); | ||||||
|         $convert = $config->get_string("thumb_convert_path"); |             $image = image_resize_gd($inname, $info, $tsize[0], $tsize[1],  | ||||||
| 
 |                 $outname, $config->get_string('thumb_type'),$config->get_int('thumb_quality')); | ||||||
|         //  ffff imagemagick fails sometimes, not sure why
 |         } catch(InsufficientMemoryException $e) { | ||||||
|         //$format = "'%s' '%s[0]' -format '%%[fx:w] %%[fx:h]' info:";
 |         	$tsize = get_thumbnail_max_size_scaled(); | ||||||
|         //$cmd = sprintf($format, $convert, $inname);
 | 			$thumb = imagecreatetruecolor($tsize[0], min($tsize[1], 64)); | ||||||
|         //$size = shell_exec($cmd);
 |             $white = imagecolorallocate($thumb, 255, 255, 255); | ||||||
|         //$size = explode(" ", trim($size));
 |             $black = imagecolorallocate($thumb, 0, 0, 0); | ||||||
|         $size = getimagesize($inname); |             imagefill($thumb, 0, 0, $white); | ||||||
|         if ($size[0] > $size[1]*5) { |             log_warning("handle_pixel","Insufficient memory while creating thumbnail: ".$e->getMessage()); | ||||||
|             $size[0] = $size[1]*5; |             imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black); | ||||||
|         } |             return true; | ||||||
|         if ($size[1] > $size[0]*5) { |         } catch(Exception $e) { | ||||||
|             $size[1] = $size[0]*5; |             log_error("handle_pixel","Error while creating thumbnail: ".$e->getMessage()); | ||||||
|         } |             return false; | ||||||
| 
 |  | ||||||
|         // running the call with cmd.exe requires quoting for our paths
 |  | ||||||
|         $format = '"%s" "%s[0]" -extent %ux%u -flatten -strip -thumbnail %ux%u -quality %u jpg:"%s"'; |  | ||||||
|         $cmd = sprintf($format, $convert, $inname, $size[0], $size[1], $w, $h, $q, $outname); |  | ||||||
|         $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); |  | ||||||
| 
 |  | ||||||
|         log_debug('handle_pixel', "Generating thumbnail with command `$cmd`, returns $ret"); |  | ||||||
| 
 |  | ||||||
|         if ($config->get_bool("thumb_optim", false)) { |  | ||||||
|             exec("jpegoptim $outname", $output, $ret); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|     // }}}
 |     // }}}
 | ||||||
|     // GD thumber {{{
 |  | ||||||
|     private function make_thumb_gd(string $inname, string $outname): bool |  | ||||||
|     { |  | ||||||
|         global $config; |  | ||||||
|         $thumb = $this->get_thumb($inname); |  | ||||||
|         $ok = imagejpeg($thumb, $outname, $config->get_int('thumb_quality')); |  | ||||||
|         imagedestroy($thumb); |  | ||||||
|         return $ok; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private function get_thumb(string $tmpname) |  | ||||||
|     { |  | ||||||
|         global $config; |  | ||||||
| 
 |  | ||||||
|         $info = getimagesize($tmpname); |  | ||||||
|         $width = $info[0]; |  | ||||||
|         $height = $info[1]; |  | ||||||
| 
 |  | ||||||
|         $memory_use = (filesize($tmpname)*2) + ($width*$height*4) + (4*1024*1024); |  | ||||||
|         $memory_limit = get_memory_limit(); |  | ||||||
| 
 |  | ||||||
|         if ($memory_use > $memory_limit) { |  | ||||||
|             $w = $config->get_int('thumb_width'); |  | ||||||
|             $h = $config->get_int('thumb_height'); |  | ||||||
|             $thumb = imagecreatetruecolor($w, min($h, 64)); |  | ||||||
|             $white = imagecolorallocate($thumb, 255, 255, 255); |  | ||||||
|             $black = imagecolorallocate($thumb, 0, 0, 0); |  | ||||||
|             imagefill($thumb, 0, 0, $white); |  | ||||||
|             imagestring($thumb, 5, 10, 24, "Image Too Large :(", $black); |  | ||||||
|             return $thumb; |  | ||||||
|         } else { |  | ||||||
|             if ($width > $height*5) { |  | ||||||
|                 $width = $height*5; |  | ||||||
|             } |  | ||||||
|             if ($height > $width*5) { |  | ||||||
|                 $height = $width*5; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             $image = imagecreatefromstring(file_get_contents($tmpname)); |  | ||||||
|             $tsize = get_thumbnail_size($width, $height); |  | ||||||
| 
 |  | ||||||
|             $thumb = imagecreatetruecolor($tsize[0], $tsize[1]); |  | ||||||
|             imagecopyresampled( |  | ||||||
|                 $thumb, |  | ||||||
|                 $image, |  | ||||||
|                 0, |  | ||||||
|                 0, |  | ||||||
|                 0, |  | ||||||
|                 0, |  | ||||||
|                 $tsize[0], |  | ||||||
|                 $tsize[1], |  | ||||||
|                 $width, |  | ||||||
|                 $height |  | ||||||
|                     ); |  | ||||||
|             return $thumb; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     // }}}
 |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,12 +3,12 @@ | |||||||
|  * Name: Handle SVG |  * Name: Handle SVG | ||||||
|  * Author: Shish <webmaster@shishnet.org> |  * Author: Shish <webmaster@shishnet.org> | ||||||
|  * Link: http://code.shishnet.org/shimmie2/ |  * Link: http://code.shishnet.org/shimmie2/ | ||||||
|  * Description: Handle static SVG files. (No thumbnail is generated for SVG files) |  * Description: Handle static SVG files.  | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| use enshrined\svgSanitize\Sanitizer; | use enshrined\svgSanitize\Sanitizer; | ||||||
| 
 | 
 | ||||||
| class SVGFileHandler extends Extension | class SVGFileHandler extends DataHandlerExtension | ||||||
| { | { | ||||||
|     public function onDataUpload(DataUploadEvent $event) |     public function onDataUpload(DataUploadEvent $event) | ||||||
|     { |     { | ||||||
| @ -32,13 +32,12 @@ class SVGFileHandler extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onThumbnailGeneration(ThumbnailGenerationEvent $event) |     protected function create_thumb(string $hash): bool | ||||||
|     { |     { | ||||||
|         if ($this->supported_ext($event->type)) { |         if(!create_thumbnail_convert($hash)) { | ||||||
|             $hash = $event->hash; |  | ||||||
| 
 |  | ||||||
|             copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash)); |             copy("ext/handle_svg/thumb.jpg", warehouse_path("thumbs", $hash)); | ||||||
|         } |         } | ||||||
|  |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onDisplayingImage(DisplayingImageEvent $event) |     public function onDisplayingImage(DisplayingImageEvent $event) | ||||||
| @ -68,13 +67,13 @@ class SVGFileHandler extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function supported_ext(string $ext): bool |     protected function supported_ext(string $ext): bool | ||||||
|     { |     { | ||||||
|         $exts = ["svg"]; |         $exts = ["svg"]; | ||||||
|         return in_array(strtolower($ext), $exts); |         return in_array(strtolower($ext), $exts); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function create_image_from_data(string $filename, array $metadata): Image |     protected function create_image_from_data(string $filename, array $metadata): Image | ||||||
|     { |     { | ||||||
|         $image = new Image(); |         $image = new Image(); | ||||||
| 
 | 
 | ||||||
| @ -92,7 +91,7 @@ class SVGFileHandler extends Extension | |||||||
|         return $image; |         return $image; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function check_contents(string $file): bool |     protected function check_contents(string $file): bool | ||||||
|     { |     { | ||||||
|         if (!file_exists($file)) { |         if (!file_exists($file)) { | ||||||
|             return false; |             return false; | ||||||
|  | |||||||
| @ -55,62 +55,16 @@ class VideoFileHandler extends DataHandlerExtension | |||||||
|      */ |      */ | ||||||
|     protected function create_thumb(string $hash): bool |     protected function create_thumb(string $hash): bool | ||||||
|     { |     { | ||||||
|         global $config; |  | ||||||
| 
 |  | ||||||
|         $ok = false; |         $ok = false; | ||||||
| 
 | 
 | ||||||
|         $ffmpeg = $config->get_string("thumb_ffmpeg_path"); |         $ok = create_thumbnail_ffmpeg($hash); | ||||||
|         $inname  = warehouse_path("images", $hash); |  | ||||||
|         $outname = warehouse_path("thumbs", $hash); |  | ||||||
| 
 |  | ||||||
|         $orig_size = $this->video_size($inname); |  | ||||||
|         $scaled_size = get_thumbnail_size($orig_size[0], $orig_size[1]); |  | ||||||
|         $cmd = escapeshellcmd(implode(" ", [ |  | ||||||
|             escapeshellarg($ffmpeg), |  | ||||||
|             "-y", "-i", escapeshellarg($inname), |  | ||||||
|             "-vf", "scale={$scaled_size[0]}:{$scaled_size[1]}", |  | ||||||
|             "-ss", "00:00:00.0", |  | ||||||
|             "-f", "image2", |  | ||||||
|             "-vframes", "1", |  | ||||||
|             escapeshellarg($outname), |  | ||||||
|         ])); |  | ||||||
| 
 |  | ||||||
|         exec($cmd, $output, $ret); |  | ||||||
| 
 |  | ||||||
|         if ((int)$ret == (int)0) { |  | ||||||
|             $ok = true; |  | ||||||
|             log_error('handle_video', "Generating thumbnail with command `$cmd`, returns $ret"); |  | ||||||
|         } else { |  | ||||||
|             log_debug('handle_video', "Generating thumbnail with command `$cmd`, returns $ret"); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         return $ok; |         return $ok; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected function video_size(string $filename) |     protected function video_size(string $filename): array | ||||||
|     { |     { | ||||||
|         global $config; |         return video_size($filename); | ||||||
|         $ffmpeg = $config->get_string("thumb_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('handle_video', "Getting video size with `$cmd`, returns $output -- $size[0], $size[1]"); |  | ||||||
|         return $size; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected function supported_ext(string $ext): bool |     protected function supported_ext(string $ext): bool | ||||||
|  | |||||||
| @ -19,7 +19,9 @@ class ImageIO extends Extension | |||||||
|         global $config; |         global $config; | ||||||
|         $config->set_default_int('thumb_width', 192); |         $config->set_default_int('thumb_width', 192); | ||||||
|         $config->set_default_int('thumb_height', 192); |         $config->set_default_int('thumb_height', 192); | ||||||
|  |         $config->set_default_int('thumb_scaling', 100); | ||||||
|         $config->set_default_int('thumb_quality', 75); |         $config->set_default_int('thumb_quality', 75); | ||||||
|  |         $config->set_default_string('thumb_type', 'jpg'); | ||||||
|         $config->set_default_int('thumb_mem_limit', parse_shorthand_int('8MB')); |         $config->set_default_int('thumb_mem_limit', parse_shorthand_int('8MB')); | ||||||
|         $config->set_default_string('thumb_convert_path', 'convert'); |         $config->set_default_string('thumb_convert_path', 'convert'); | ||||||
| 
 | 
 | ||||||
| @ -137,8 +139,15 @@ class ImageIO extends Extension | |||||||
|         $thumbers['Built-in GD'] = "gd"; |         $thumbers['Built-in GD'] = "gd"; | ||||||
|         $thumbers['ImageMagick'] = "convert"; |         $thumbers['ImageMagick'] = "convert"; | ||||||
| 
 | 
 | ||||||
|  |         $thumb_types = []; | ||||||
|  |         $thumb_types['JPEG'] = "jpg"; | ||||||
|  |         $thumb_types['WEBP'] = "webp"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         $sb = new SetupBlock("Thumbnailing"); |         $sb = new SetupBlock("Thumbnailing"); | ||||||
|         $sb->add_choice_option("thumb_engine", $thumbers, "Engine: "); |         $sb->add_choice_option("thumb_engine", $thumbers, "Engine: "); | ||||||
|  |         $sb->add_label("<br>"); | ||||||
|  |         $sb->add_choice_option("thumb_type", $thumb_types, "Filetype: "); | ||||||
| 
 | 
 | ||||||
|         $sb->add_label("<br>Size "); |         $sb->add_label("<br>Size "); | ||||||
|         $sb->add_int_option("thumb_width"); |         $sb->add_int_option("thumb_width"); | ||||||
| @ -148,6 +157,10 @@ class ImageIO extends Extension | |||||||
|         $sb->add_int_option("thumb_quality"); |         $sb->add_int_option("thumb_quality"); | ||||||
|         $sb->add_label(" % quality "); |         $sb->add_label(" % quality "); | ||||||
| 
 | 
 | ||||||
|  |         $sb->add_label("<br>High-DPI scaling "); | ||||||
|  |         $sb->add_int_option("thumb_scaling"); | ||||||
|  |         $sb->add_label("%"); | ||||||
|  | 
 | ||||||
|         if ($config->get_string("thumb_engine") == "convert") { |         if ($config->get_string("thumb_engine") == "convert") { | ||||||
|             $sb->add_label("<br>ImageMagick Binary: "); |             $sb->add_label("<br>ImageMagick Binary: "); | ||||||
|             $sb->add_text_option("thumb_convert_path"); |             $sb->add_text_option("thumb_convert_path"); | ||||||
| @ -240,7 +253,13 @@ class ImageIO extends Extension | |||||||
|         if (!is_null($image)) { |         if (!is_null($image)) { | ||||||
|             $page->set_mode("data"); |             $page->set_mode("data"); | ||||||
|             if ($type == "thumb") { |             if ($type == "thumb") { | ||||||
|                 $page->set_type("image/jpeg"); | 				$ext = $config->get_string("thumb_type"); | ||||||
|  | 				if (array_key_exists($ext, MIME_TYPE_MAP)) { | ||||||
|  | 					$page->set_type(MIME_TYPE_MAP[$ext]); | ||||||
|  | 				} else { | ||||||
|  | 					$page->set_type("image/jpeg"); | ||||||
|  | 				} | ||||||
|  |                  | ||||||
|                 $file = $image->get_thumb_filename(); |                 $file = $image->get_thumb_filename(); | ||||||
|             } else { |             } else { | ||||||
|                 $page->set_type($image->get_mime_type()); |                 $page->set_type($image->get_mime_type()); | ||||||
| @ -259,6 +278,9 @@ class ImageIO extends Extension | |||||||
|                 $page->set_data(""); |                 $page->set_data(""); | ||||||
|             } else { |             } else { | ||||||
|                 $page->add_http_header("Last-Modified: $gmdate_mod"); |                 $page->add_http_header("Last-Modified: $gmdate_mod"); | ||||||
|  | 				if ($type != "thumb") { | ||||||
|  | 					$page->add_http_header("Content-Disposition: inline; filename=".$image->get_nice_image_name()); | ||||||
|  | 				} | ||||||
|                 $page->set_data(file_get_contents($file)); |                 $page->set_data(file_get_contents($file)); | ||||||
|                  |                  | ||||||
|                 if ($config->get_int("image_expires")) { |                 if ($config->get_int("image_expires")) { | ||||||
| @ -301,6 +323,7 @@ class ImageIO extends Extension | |||||||
|             and have it stored in a 'replaced images' list that could be |             and have it stored in a 'replaced images' list that could be | ||||||
|             inspected later by an admin? |             inspected later by an admin? | ||||||
|         */ |         */ | ||||||
|  | 
 | ||||||
|         log_debug("image", "Removing image with hash ".$existing->hash); |         log_debug("image", "Removing image with hash ".$existing->hash); | ||||||
|         $existing->remove_image_only(); // Actually delete the old image file from disk
 |         $existing->remove_image_only(); // Actually delete the old image file from disk
 | ||||||
|          |          | ||||||
| @ -319,6 +342,9 @@ class ImageIO extends Extension | |||||||
|                 ] |                 ] | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |         /* Generate new thumbnail */ | ||||||
|  |         send_event(new ThumbnailGenerationEvent($image->hash, strtolower($image->ext))); | ||||||
|  | 
 | ||||||
|         log_info("image", "Replaced Image #{$id} with ({$image->hash})"); |         log_info("image", "Replaced Image #{$id} with ({$image->hash})"); | ||||||
|     } |     } | ||||||
|     // }}} end replace
 |     // }}} end replace
 | ||||||
|  | |||||||
| @ -11,6 +11,6 @@ class QRImage extends Extension | |||||||
| { | { | ||||||
|     public function onDisplayingImage(DisplayingImageEvent $event) |     public function onDisplayingImage(DisplayingImageEvent $event) | ||||||
|     { |     { | ||||||
|         $this->theme->links_block(make_http(make_link('image/'.$event->image->id.'.jpg'))); |         $this->theme->links_block(make_http(make_link('image/'.$event->image->id.'.'.$event->image->ext))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -73,13 +73,13 @@ class Ratings extends Extension | |||||||
|         $event->panel->add_block($sb); |         $event->panel->add_block($sb); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     public function onPostListBuilding(PostListBuildingEvent $event) |     // public function onPostListBuilding(PostListBuildingEvent $event)
 | ||||||
|     { |     // {
 | ||||||
|         global $user; |     //     global $user;
 | ||||||
|         if ($user->is_admin() && !empty($event->search_terms)) { |     //     if ($user->is_admin() && !empty($event->search_terms)) {
 | ||||||
|             $this->theme->display_bulk_rater(Tag::implode($event->search_terms)); |     //         $this->theme->display_bulk_rater(Tag::implode($event->search_terms));
 | ||||||
|         } |     //     }
 | ||||||
|     } |     // }
 | ||||||
| 
 | 
 | ||||||
|      |      | ||||||
|     public function onDisplayingImage(DisplayingImageEvent $event) |     public function onDisplayingImage(DisplayingImageEvent $event) | ||||||
| @ -143,6 +143,65 @@ class Ratings extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function onTagTermParse(TagTermParseEvent $event) | ||||||
|  |     { | ||||||
|  |         $matches = []; | ||||||
|  | 
 | ||||||
|  |         if (preg_match("/^rating[=|:](?:([sqeu]+)|(safe|questionable|explicit|unknown))$/D", strtolower($event->term), $matches) && $event->parse) { | ||||||
|  |             $ratings = $matches[1] ? $matches[1] : $matches[2][0]; | ||||||
|  |             $ratings = array_intersect(str_split($ratings), str_split(Ratings::get_user_privs($user))); | ||||||
|  | 
 | ||||||
|  |             $rating = $ratings[0]; | ||||||
|  |              | ||||||
|  |             $image = Image::by_id($event->id); | ||||||
|  | 
 | ||||||
|  |             $re = new RatingSetEvent($image, $rating); | ||||||
|  | 
 | ||||||
|  |             send_event($re); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!empty($matches)) { | ||||||
|  |             $event->metatag = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user; | ||||||
|  | 
 | ||||||
|  |         if ($user->is_admin()) { | ||||||
|  |             $event->add_action("bulk_rate","Set Rating","",$this->theme->get_selection_rater_html("bulk_rating")); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onBulkAction(BulkActionEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user; | ||||||
|  | 
 | ||||||
|  |         switch($event->action) { | ||||||
|  |             case "bulk_rate": | ||||||
|  |                 if (!isset($_POST['bulk_rating'])) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if ($user->is_admin()) { | ||||||
|  |                     $rating = $_POST['bulk_rating']; | ||||||
|  |                     $total = 0; | ||||||
|  |                     foreach ($event->items as $id) { | ||||||
|  |                         $image = Image::by_id($id); | ||||||
|  |                         if($image==null) { | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  |          | ||||||
|  |                         send_event(new RatingSetEvent($image, $rating)); | ||||||
|  | 						$total++; | ||||||
|  |                     } | ||||||
|  |                     flash_message("Rating set for $total items"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function onPageRequest(PageRequestEvent $event) |     public function onPageRequest(PageRequestEvent $event) | ||||||
|     { |     { | ||||||
|         global $user, $page; |         global $user, $page; | ||||||
| @ -271,8 +330,17 @@ class Ratings extends Extension | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if ($config->get_int("ext_ratings2_version") < 3) { |         if ($config->get_int("ext_ratings2_version") < 3) { | ||||||
|             $database->Execute("ALTER TABLE images CHANGE rating rating CHAR(1) NOT NULL DEFAULT 'u'"); |             $database->Execute("UPDATE images SET rating = 'u' WHERE rating is null"); | ||||||
|             $config->set_int("ext_ratings2_version", 3); |             switch($database->get_driver_name()) { | ||||||
|  |                 case "mysql": | ||||||
|  |                     $database->Execute("ALTER TABLE images CHANGE rating rating CHAR(1) NOT NULL DEFAULT 'u'"); | ||||||
|  |                     break; | ||||||
|  |                 case "pgsql": | ||||||
|  |                     $database->Execute("ALTER TABLE images ALTER COLUMN rating SET DEFAULT 'u'"); | ||||||
|  |                     $database->Execute("ALTER TABLE images ALTER COLUMN rating SET NOT NULL"); | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  | 			$config->set_int("ext_ratings2_version", 3); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -45,4 +45,13 @@ class RatingsTheme extends Themelet | |||||||
| 		";
 | 		";
 | ||||||
|         $page->add_block(new Block("List Controls", $html, "left")); |         $page->add_block(new Block("List Controls", $html, "left")); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function get_selection_rater_html(String $id = "select_rating") { | ||||||
|  |         return "<select name='".$id."'>
 | ||||||
|  | 					<option value='s'>Safe</option> | ||||||
|  | 					<option value='q'>Questionable</option> | ||||||
|  | 					<option value='e'>Explicit</option> | ||||||
|  | 					<option value='u'>Unrated</option> | ||||||
|  | 				</select>";
 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,14 +15,24 @@ | |||||||
| 
 | 
 | ||||||
| class RegenThumb extends Extension | class RegenThumb extends Extension | ||||||
| { | { | ||||||
|  |     public function regenerate_thumbnail($image, $force = true): string | ||||||
|  |     { | ||||||
|  |         global $database; | ||||||
|  |         $event = new ThumbnailGenerationEvent($image->hash, $image->ext, $force); | ||||||
|  |         send_event($event); | ||||||
|  |         $database->cache->delete("thumb-block:{$image->id}"); | ||||||
|  |         return $event->generated; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function onPageRequest(PageRequestEvent $event) |     public function onPageRequest(PageRequestEvent $event) | ||||||
|     { |     { | ||||||
|         global $database, $page, $user; |         global $database, $page, $user; | ||||||
| 
 | 
 | ||||||
|         if ($event->page_matches("regen_thumb/one") && $user->can("delete_image") && isset($_POST['image_id'])) { |         if ($event->page_matches("regen_thumb/one") && $user->can("delete_image") && isset($_POST['image_id'])) { | ||||||
|             $image = Image::by_id(int_escape($_POST['image_id'])); |             $image = Image::by_id(int_escape($_POST['image_id'])); | ||||||
|             send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true)); | 
 | ||||||
|             $database->cache->delete("thumb-block:{$image->id}"); |             $this->regenerate_thumbnail($image); | ||||||
|  | 
 | ||||||
|             $this->theme->display_results($page, $image); |             $this->theme->display_results($page, $image); | ||||||
|         } |         } | ||||||
|         if ($event->page_matches("regen_thumb/mass") && $user->can("delete_image") && isset($_POST['tags'])) { |         if ($event->page_matches("regen_thumb/mass") && $user->can("delete_image") && isset($_POST['tags'])) { | ||||||
| @ -30,8 +40,7 @@ class RegenThumb extends Extension | |||||||
|             $images = Image::find_images(0, 10000, $tags); |             $images = Image::find_images(0, 10000, $tags); | ||||||
| 
 | 
 | ||||||
|             foreach ($images as $image) { |             foreach ($images as $image) { | ||||||
|                 send_event(new ThumbnailGenerationEvent($image->hash, $image->ext, true)); |                 $this->regenerate_thumbnail($image); | ||||||
|                 $database->cache->delete("thumb-block:{$image->id}"); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             $page->set_mode("redirect"); |             $page->set_mode("redirect"); | ||||||
| @ -47,11 +56,156 @@ class RegenThumb extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onPostListBuilding(PostListBuildingEvent $event) |     // public function onPostListBuilding(PostListBuildingEvent $event)
 | ||||||
|  |     // {
 | ||||||
|  |     //     global $user;
 | ||||||
|  |     //     if ($user->can("delete_image") && !empty($event->search_terms)) {
 | ||||||
|  |     //         $event->add_control($this->theme->mtr_html(Tag::implode($event->search_terms)));
 | ||||||
|  |     //     }
 | ||||||
|  |     // }
 | ||||||
|  | 
 | ||||||
|  |     public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) | ||||||
|     { |     { | ||||||
|         global $user; |         global $user; | ||||||
|         if ($user->can("delete_image") && !empty($event->search_terms)) { | 
 | ||||||
|             $event->add_control($this->theme->mtr_html(Tag::implode($event->search_terms))); |         if ($user->can("delete_image")) { | ||||||
|  |             $event->add_action("bulk_regen","Regen Thumbnails","",$this->theme->bulk_html()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onBulkAction(BulkActionEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user; | ||||||
|  | 
 | ||||||
|  |         switch($event->action) { | ||||||
|  |             case "bulk_regen": | ||||||
|  |                 if ($user->can("delete_image")) { | ||||||
|  |                     $force = true; | ||||||
|  |                     if(isset($_POST["bulk_regen_thumb_missing_only"]) | ||||||
|  |                         &&$_POST["bulk_regen_thumb_missing_only"]=="true")  | ||||||
|  |                     { | ||||||
|  |                         $force=false; | ||||||
|  |                     } | ||||||
|  |      | ||||||
|  |                     $total = 0; | ||||||
|  |                     foreach ($event->items as $id) { | ||||||
|  |                         $image = Image::by_id($id); | ||||||
|  |                         if($image==null) { | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         if($this->regenerate_thumbnail($image, $force)) { | ||||||
|  |                             $total++; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     flash_message("Regenerated thumbnails for $total items"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function onAdminBuilding(AdminBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         $this->theme->display_admin_block(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onAdminAction(AdminActionEvent $event) { | ||||||
|  |         global $database; | ||||||
|  | 
 | ||||||
|  |         switch($event->action) { | ||||||
|  |             case "regen_thumbs": | ||||||
|  |             $event->redirect = true; | ||||||
|  |                 $force = false; | ||||||
|  |                 if(isset($_POST["regen_thumb_force"])&&$_POST["regen_thumb_force"]=="true") { | ||||||
|  |                     $force=true; | ||||||
|  |                 } | ||||||
|  |                 $limit = 1000; | ||||||
|  |                 if(isset($_POST["regen_thumb_limit"])&&is_numeric($_POST["regen_thumb_limit"])) { | ||||||
|  |                     $limit=intval($_POST["regen_thumb_limit"]); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $type = ""; | ||||||
|  |                 if(isset($_POST["regen_thumb_limit"])) { | ||||||
|  |                     $type = $_POST["regen_thumb_type"]; | ||||||
|  |                 } | ||||||
|  |                 $images = $this->get_images($type); | ||||||
|  |                  | ||||||
|  |                 $i = 0; | ||||||
|  |                 foreach ($images as $image) { | ||||||
|  |                     if(!$force) { | ||||||
|  |                         $path = warehouse_path("thumbs", $image["hash"], false); | ||||||
|  |                         if(file_exists($path)) { | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     $event = new ThumbnailGenerationEvent($image["hash"], $image["ext"], $force); | ||||||
|  |                     send_event($event); | ||||||
|  |                     if($event->generated) { | ||||||
|  |                         $i++; | ||||||
|  |                     } | ||||||
|  |                     if($i>=$limit) { | ||||||
|  |                         break; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 flash_message("Re-generated $i thumbnails"); | ||||||
|  |                 break; | ||||||
|  |             case "delete_thumbs": | ||||||
|  |                 $event->redirect = true; | ||||||
|  | 
 | ||||||
|  |                 if(isset($_POST["delete_thumb_type"])&&$_POST["delete_thumb_type"]!="") { | ||||||
|  |                     $images = $this->get_images($_POST["delete_thumb_type"]); | ||||||
|  |                  | ||||||
|  |                     $i = 0; | ||||||
|  |                     foreach ($images as $image) { | ||||||
|  |                         $outname = warehouse_path("thumbs", $image["hash"]); | ||||||
|  |                         if(file_exists($outname)) { | ||||||
|  |                             unlink($outname); | ||||||
|  |                             $i++; | ||||||
|  |                         }   | ||||||
|  |                     } | ||||||
|  |                     flash_message("Deleted $i thumbnails for ".$_POST["delete_thumb_type"]." images"); | ||||||
|  |                 } else { | ||||||
|  |                     $dir = "data/thumbs/"; | ||||||
|  |                     $this->remove_dir_recursively($dir); | ||||||
|  |                     flash_message("Deleted all thumbnails");     | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function get_images(String $ext = null)  | ||||||
|  |     { | ||||||
|  |         global $database; | ||||||
|  | 
 | ||||||
|  |         $query = "SELECT hash, ext FROM images"; | ||||||
|  |         $args = []; | ||||||
|  |         if($ext!=null&&$ext!="") { | ||||||
|  |             $query .= " WHERE ext = :ext"; | ||||||
|  |             $args["ext"] = $ext; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $database->get_all($query, $args); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function remove_dir_recursively($dir)  | ||||||
|  |     { | ||||||
|  |         if (is_dir($dir)) { | ||||||
|  |             $objects = scandir($dir); | ||||||
|  |             foreach ($objects as $object) { | ||||||
|  |                     if ($object != "." && $object != "..") { | ||||||
|  |                     if (filetype($dir."/".$object) == "dir") { | ||||||
|  |                         $this->remove_dir_recursively($dir."/".$object);  | ||||||
|  |                     } else { | ||||||
|  |                         unlink   ($dir."/".$object); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             reset($objects); | ||||||
|  |             rmdir($dir); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -37,4 +37,48 @@ class RegenThumbTheme extends Themelet | |||||||
| 		";
 | 		";
 | ||||||
|         return $html; |         return $html; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function bulk_html() { | ||||||
|  |         return "<label><input type='checkbox' name='bulk_regen_thumb_missing_only' id='bulk_regen_thumb_missing_only' style='width:13px' value='true' />Only missing thumbs</label>"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function display_admin_block() | ||||||
|  |     { | ||||||
|  |         global $page, $database; | ||||||
|  | 
 | ||||||
|  |         $types = []; | ||||||
|  |         $results = $database->get_all("SELECT ext, count(*) count FROM images group by ext"); | ||||||
|  |         foreach ($results as $result) { | ||||||
|  |             array_push($types,"<option value='".$result["ext"]."'>".$result["ext"]." (".$result["count"].")</option>"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $html = " | ||||||
|  |             Will only regenerate missing thumbnails, unless force is selected. Force will override the limit and will likely take a very long time to process. | ||||||
|  | 			<p>".make_form(make_link("admin/regen_thumbs"))." | ||||||
|  | 				<table class='form'> | ||||||
|  |                 <tr><th><label for='regen_thumb_force'>Force</label></th><td><input type='checkbox' name='regen_thumb_force' id='regen_thumb_force' value='true' /></td></tr> | ||||||
|  |                 <tr><th><label for='regen_thumb_limit'>Limit</label></th><td><input type='number' name='regen_thumb_limit' id='regen_thumb_limit' value='1000' /></td></tr> | ||||||
|  |                 <tr><th><label for='regen_thumb_type'>Type</label></th><td> | ||||||
|  |                     <select name='regen_thumb_type' id='regen_thumb_type' value='1000'> | ||||||
|  |                         <option value=''>All</option> | ||||||
|  |                         ".implode($types)." | ||||||
|  |                     </select> | ||||||
|  |                 </td></tr> | ||||||
|  |                 <tr><td colspan='2'><input type='submit' value='Regenerate Thumbnails'></td></tr> | ||||||
|  | 				</table> | ||||||
|  | 			</form></p> | ||||||
|  | 			<p>".make_form(make_link("admin/delete_thumbs"),"POST",False, "","return confirm('Are you sure you want to delete all thumbnails?')")." | ||||||
|  | 				<table class='form'> | ||||||
|  |                     <tr><th><label for='delete_thumb_type'>Type</label></th><td> | ||||||
|  |                         <select name='delete_thumb_type' id='delete_thumb_type' value='1000'> | ||||||
|  |                             <option value=''>All</option> | ||||||
|  |                             ".implode($types)." | ||||||
|  |                         </select> | ||||||
|  |                     </td></tr> | ||||||
|  | 					<tr><td colspan='2'><input type='submit' value='Delete Thumbnails'></td></tr> | ||||||
|  | 				</table> | ||||||
|  |             </form></p> | ||||||
|  |             		";
 | ||||||
|  |         $page->add_block(new Block("Regen Thumbnails", $html)); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -11,25 +11,20 @@ | |||||||
|  * Documentation: |  * Documentation: | ||||||
|  *  This extension allows admins to resize images. |  *  This extension allows admins to resize images. | ||||||
|  */ |  */ | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * This class is just a wrapper around SCoreException. |  | ||||||
|  */ |  | ||||||
| class ImageResizeException extends SCoreException |  | ||||||
| { |  | ||||||
|     public $error; |  | ||||||
| 
 |  | ||||||
|     public function __construct(string $error) |  | ||||||
|     { |  | ||||||
|         $this->error = $error; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  *	This class handles image resize requests. |  *	This class handles image resize requests. | ||||||
|  */ |  */ | ||||||
| class ResizeImage extends Extension | class ResizeImage extends Extension | ||||||
| { | { | ||||||
|  |     /** | ||||||
|  |      * Needs to be after the data processing extensions | ||||||
|  |      */ | ||||||
|  |     public function get_priority(): int | ||||||
|  |     { | ||||||
|  |         return 55; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     public function onInitExt(InitExtEvent $event) |     public function onInitExt(InitExtEvent $event) | ||||||
|     { |     { | ||||||
|         global $config; |         global $config; | ||||||
| @ -69,7 +64,7 @@ class ResizeImage extends Extension | |||||||
| 
 | 
 | ||||||
|         $image_obj = Image::by_id($event->image_id); |         $image_obj = Image::by_id($event->image_id); | ||||||
| 
 | 
 | ||||||
|         if ($config->get_bool("resize_upload") == true && ($image_obj->ext == "jpg" || $image_obj->ext == "png" || $image_obj->ext == "gif")) { |         if ($config->get_bool("resize_upload") == true && ($image_obj->ext == "jpg" || $image_obj->ext == "png" || $image_obj->ext == "gif" || $image_obj->ext == "webp")) { | ||||||
|             $width = $height = 0; |             $width = $height = 0; | ||||||
| 
 | 
 | ||||||
|             if ($config->get_int("resize_default_width") !== 0) { |             if ($config->get_int("resize_default_width") !== 0) { | ||||||
| @ -159,11 +154,6 @@ class ResizeImage extends Extension | |||||||
|      |      | ||||||
|     // Private functions
 |     // Private functions
 | ||||||
|     /* ----------------------------- */ |     /* ----------------------------- */ | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This function could be made much smaller by using the ImageReplaceEvent |  | ||||||
|      * ie: Pretend that we are replacing the image with a resized copy. |  | ||||||
|      */ |  | ||||||
|     private function resize_image(Image $image_obj, int $width, int $height) |     private function resize_image(Image $image_obj, int $width, int $height) | ||||||
|     { |     { | ||||||
|         global $database; |         global $database; | ||||||
| @ -174,134 +164,42 @@ class ResizeImage extends Extension | |||||||
|          |          | ||||||
|         $hash = $image_obj->hash; |         $hash = $image_obj->hash; | ||||||
|         $image_filename  = warehouse_path("images", $hash); |         $image_filename  = warehouse_path("images", $hash); | ||||||
|         $info = getimagesize($image_filename); |  | ||||||
|         /* Get the image file type */ |  | ||||||
|         $pathinfo = pathinfo($image_obj->filename); |  | ||||||
|         $filetype = strtolower($pathinfo['extension']); |  | ||||||
| 
 | 
 | ||||||
|  |         $info = getimagesize($image_filename); | ||||||
|         if (($image_obj->width != $info[0]) || ($image_obj->height != $info[1])) { |         if (($image_obj->width != $info[0]) || ($image_obj->height != $info[1])) { | ||||||
|             throw new ImageResizeException("The current image size does not match what is set in the database! - Aborting Resize."); |             throw new ImageResizeException("The current image size does not match what is set in the database! - Aborting Resize."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         $memory_use = $this->calc_memory_use($info); |  | ||||||
|         $memory_limit = get_memory_limit(); |  | ||||||
|         if ($memory_use > $memory_limit) { |  | ||||||
|             throw new ImageResizeException("The image is too large to resize given the memory limits. ($memory_use > $memory_limit)"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         list($new_height, $new_width) = $this->calc_new_size($image_obj, $width, $height); |         list($new_height, $new_width) = $this->calc_new_size($image_obj, $width, $height); | ||||||
| 
 | 
 | ||||||
|         /* Attempt to load the image */ |  | ||||||
|         switch ($info[2]) { |  | ||||||
|           case IMAGETYPE_GIF:   $image = imagecreatefromgif($image_filename);   break; |  | ||||||
|           case IMAGETYPE_JPEG:  $image = imagecreatefromjpeg($image_filename);  break; |  | ||||||
|           case IMAGETYPE_PNG:   $image = imagecreatefrompng($image_filename);   break; |  | ||||||
|           default: |  | ||||||
|             throw new ImageResizeException("Unsupported image type (Only GIF, JPEG, and PNG are supported)."); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Handle transparent images
 |  | ||||||
|          |  | ||||||
|         $image_resized = imagecreatetruecolor($new_width, $new_height); |  | ||||||
|          |  | ||||||
|         if ($info[2] == IMAGETYPE_GIF) { |  | ||||||
|             $transparency = imagecolortransparent($image); |  | ||||||
| 
 |  | ||||||
|             // If we have a specific transparent color
 |  | ||||||
|             if ($transparency >= 0) { |  | ||||||
|                 // 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']); |  | ||||||
| 
 |  | ||||||
|                 // Completely fill the background of the new image with allocated color.
 |  | ||||||
|                 imagefill($image_resized, 0, 0, $transparency); |  | ||||||
| 
 |  | ||||||
|                 // Set the background color for new image to transparent
 |  | ||||||
|                 imagecolortransparent($image_resized, $transparency); |  | ||||||
|             } |  | ||||||
|         } elseif ($info[2] == IMAGETYPE_PNG) { |  | ||||||
|             //
 |  | ||||||
|             // More info here:  http://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
 |  | ||||||
|             //
 |  | ||||||
|             imagealphablending($image_resized, false); |  | ||||||
|             imagesavealpha($image_resized, true); |  | ||||||
|             $transparent_color = imagecolorallocatealpha($image_resized, 255, 255, 255, 127); |  | ||||||
|             imagefilledrectangle($image_resized, 0, 0, $new_width, $new_height, $transparent_color); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Actually resize the image.
 |  | ||||||
|         imagecopyresampled($image_resized, $image, 0, 0, 0, 0, $new_width, $new_height, $image_obj->width, $image_obj->height); |  | ||||||
|          |  | ||||||
|         /* Temp storage while we resize */ |         /* Temp storage while we resize */ | ||||||
|         $tmp_filename = tempnam("/tmp", 'shimmie_resize'); |         $tmp_filename = tempnam("/tmp", 'shimmie_resize'); | ||||||
|         if (empty($tmp_filename)) { |         if (empty($tmp_filename)) { | ||||||
|             throw new ImageResizeException("Unable to save temporary image file."); |             throw new ImageResizeException("Unable to save temporary image file."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         /* Output to the same format as the original image */ |         image_resize_gd($image_filename, $info, $new_width, $new_height, $tmp_filename); | ||||||
|         switch ($info[2]) { | 
 | ||||||
|           case IMAGETYPE_GIF:   imagegif($image_resized, $tmp_filename);    break; |         $new_image = new Image(); | ||||||
|           case IMAGETYPE_JPEG:  imagejpeg($image_resized, $tmp_filename);   break; |         $new_image->hash = md5_file($tmp_filename); | ||||||
|           case IMAGETYPE_PNG:   imagepng($image_resized, $tmp_filename);    break; |         $new_image->filesize = filesize($tmp_filename); | ||||||
|           default: |         $new_image->filename = 'resized-'.$image_obj->filename; | ||||||
|             throw new ImageResizeException("Failed to save the new image - Unsupported image type."); |         $new_image->width = $new_width; | ||||||
|         } |         $new_image->height = $new_height; | ||||||
|  |         $new_image->ext = $image_obj->ext; | ||||||
| 
 | 
 | ||||||
|         /* Move the new image into the main storage location */ |         /* Move the new image into the main storage location */ | ||||||
|         $new_hash = md5_file($tmp_filename); |         $target = warehouse_path("images", $new_image->hash); | ||||||
|         $new_size = filesize($tmp_filename); |  | ||||||
|         $target = warehouse_path("images", $new_hash); |  | ||||||
|         if (!@copy($tmp_filename, $target)) { |         if (!@copy($tmp_filename, $target)) { | ||||||
|             throw new ImageResizeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); |             throw new ImageResizeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); | ||||||
|         } |         } | ||||||
|         $new_filename = 'resized-'.$image_obj->filename; |  | ||||||
|          |          | ||||||
|         /* Remove temporary file */ |         /* Remove temporary file */ | ||||||
|         @unlink($tmp_filename); |         @unlink($tmp_filename); | ||||||
| 
 | 
 | ||||||
|         /* Delete original image and thumbnail */ |         send_event(new ImageReplaceEvent($image_obj->id, $new_image)); | ||||||
|         log_debug("image", "Removing image with hash ".$hash); |  | ||||||
|         $image_obj->remove_image_only(); |  | ||||||
|          |          | ||||||
|         /* Generate new thumbnail */ |         log_info("resize", "Resized Image #{$image_obj->id} - New hash: {$new_image->hash}"); | ||||||
|         send_event(new ThumbnailGenerationEvent($new_hash, $filetype)); |  | ||||||
|          |  | ||||||
|         /* Update the database */ |  | ||||||
|         $database->Execute(" |  | ||||||
| 			UPDATE images SET filename = :filename, filesize = :filesize, hash = :hash, width = :width, height = :height |  | ||||||
| 			WHERE id = :id |  | ||||||
| 		", [
 |  | ||||||
|             "filename"=>$new_filename, "filesize"=>$new_size, "hash"=>$new_hash, |  | ||||||
|             "width"=>$new_width, "height"=>$new_height,	"id"=>$image_obj->id |  | ||||||
|         ]); |  | ||||||
|          |  | ||||||
|         log_info("resize", "Resized Image #{$image_obj->id} - New hash: {$new_hash}"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 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 |  | ||||||
|      */ |  | ||||||
|     private 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; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -106,11 +106,6 @@ class RotateImage extends Extension | |||||||
|      |      | ||||||
|     // Private functions
 |     // Private functions
 | ||||||
|     /* ----------------------------- */ |     /* ----------------------------- */ | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * This function could be made much smaller by using the ImageReplaceEvent |  | ||||||
|      * ie: Pretend that we are replacing the image with a rotated copy. |  | ||||||
|      */ |  | ||||||
|     private function rotate_image(int $image_id, int $deg) |     private function rotate_image(int $image_id, int $deg) | ||||||
|     { |     { | ||||||
|         global $database; |         global $database; | ||||||
| @ -129,24 +124,10 @@ class RotateImage extends Extension | |||||||
|         if (file_exists($image_filename)==false) { |         if (file_exists($image_filename)==false) { | ||||||
|             throw new ImageRotateException("$image_filename does not exist."); |             throw new ImageRotateException("$image_filename does not exist."); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|         $info = getimagesize($image_filename); |         $info = getimagesize($image_filename); | ||||||
|         /* Get the image file type */ |  | ||||||
|         $pathinfo = pathinfo($image_obj->filename); |  | ||||||
|         $filetype = strtolower($pathinfo['extension']); |  | ||||||
|          |          | ||||||
|         /* |         $memory_use =calc_memory_use ($info); | ||||||
|             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 |  | ||||||
|         */ |  | ||||||
|         $memory_use = ($info[0] * $info[1] * ($info['bits'] / 8) * $info['channels'] * 2.5) / 1024; |  | ||||||
|         $memory_limit = get_memory_limit(); |         $memory_limit = get_memory_limit(); | ||||||
|          |          | ||||||
|         if ($memory_use > $memory_limit) { |         if ($memory_use > $memory_limit) { | ||||||
| @ -182,7 +163,21 @@ class RotateImage extends Extension | |||||||
|         } |         } | ||||||
|         */ |         */ | ||||||
| 
 | 
 | ||||||
|         $image_rotated = imagerotate($image, $deg, 0); |         $background_color = 0; | ||||||
|  |         switch($info[2]){ | ||||||
|  |             case IMAGETYPE_PNG: | ||||||
|  |             case IMAGETYPE_WEBP: | ||||||
|  |                 $background_color = imagecolorallocatealpha($image, 0, 0, 0, 127); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         if($background_color===false) { | ||||||
|  |             throw new ImageRotateException("Unable to allocate transparent color"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $image_rotated = imagerotate($image, $deg, $background_color); | ||||||
|  |         if($image_rotated===false) { | ||||||
|  |             throw new ImageRotateException("Image rotate failed"); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         /* Temp storage while we rotate */ |         /* Temp storage while we rotate */ | ||||||
|         $tmp_filename = tempnam(ini_get('upload_tmp_dir'), 'shimmie_rotate'); |         $tmp_filename = tempnam(ini_get('upload_tmp_dir'), 'shimmie_rotate'); | ||||||
| @ -191,49 +186,42 @@ class RotateImage extends Extension | |||||||
|         } |         } | ||||||
|          |          | ||||||
|         /* Output to the same format as the original image */ |         /* Output to the same format as the original image */ | ||||||
|  |         $result = false; | ||||||
|         switch ($info[2]) { |         switch ($info[2]) { | ||||||
|           case IMAGETYPE_GIF:   imagegif($image_rotated, $tmp_filename);    break; |           case IMAGETYPE_GIF:   $result = imagegif($image_rotated, $tmp_filename);      break; | ||||||
|           case IMAGETYPE_JPEG:  imagejpeg($image_rotated, $tmp_filename);   break; |           case IMAGETYPE_JPEG:  $result = imagejpeg($image_rotated, $tmp_filename);     break; | ||||||
|           case IMAGETYPE_PNG:   imagepng($image_rotated, $tmp_filename);    break; |           case IMAGETYPE_PNG:   $result = imagepng($image_rotated, $tmp_filename,9);    break; | ||||||
|  |           case IMAGETYPE_WEBP:  $result = imagewebp($image_rotated, $tmp_filename);     break; | ||||||
|  |           case IMAGETYPE_BMP:   $result = imagebmp($image_rotated, $tmp_filename,true); break; | ||||||
|           default: |           default: | ||||||
|             throw new ImageRotateException("Unsupported image type."); |             throw new ImageRotateException("Unsupported image type."); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if($result===false) { | ||||||
|  |             throw new ImageRotateException("Could not save image: ".$tmp_filename); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         list($new_width, $new_height) = getimagesize($tmp_filename); | ||||||
|  | 
 | ||||||
|  |         $new_image = new Image(); | ||||||
|  |         $new_image->hash = md5_file($tmp_filename); | ||||||
|  |         $new_image->filesize = filesize($tmp_filename); | ||||||
|  |         $new_image->filename = 'rotated-'.$image_obj->filename; | ||||||
|  |         $new_image->width = $new_width; | ||||||
|  |         $new_image->height = $new_height; | ||||||
|  |         $new_image->ext = $image_obj->ext; | ||||||
|  | 
 | ||||||
|         /* Move the new image into the main storage location */ |         /* Move the new image into the main storage location */ | ||||||
|         $new_hash = md5_file($tmp_filename); |         $target = warehouse_path("images", $new_image->hash); | ||||||
|         $new_size = filesize($tmp_filename); |  | ||||||
|         $target = warehouse_path("images", $new_hash); |  | ||||||
|         if (!@copy($tmp_filename, $target)) { |         if (!@copy($tmp_filename, $target)) { | ||||||
|             throw new ImageRotateException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); |             throw new ImageRotateException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); | ||||||
|         } |         } | ||||||
|         $new_filename = 'rotated-'.$image_obj->filename; |  | ||||||
|          |  | ||||||
|         list($new_width, $new_height) = getimagesize($target); |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|         /* Remove temporary file */ |         /* Remove temporary file */ | ||||||
|         @unlink($tmp_filename); |         @unlink($tmp_filename); | ||||||
| 
 | 
 | ||||||
|         /* Delete original image and thumbnail */ |         send_event(new ImageReplaceEvent($image_id, $new_image)); | ||||||
|         log_debug("image", "Removing image with hash ".$hash); |  | ||||||
|         $image_obj->remove_image_only(); |  | ||||||
| 
 | 
 | ||||||
|         /* Generate new thumbnail */ |         log_info("rotate", "Rotated Image #{$image_id} - New hash: {$new_image->hash}"); | ||||||
|         send_event(new ThumbnailGenerationEvent($new_hash, $filetype)); |  | ||||||
|          |  | ||||||
|         /* Update the database */ |  | ||||||
|         $database->Execute( |  | ||||||
|             "UPDATE images SET 
 |  | ||||||
| 					filename = :filename, filesize = :filesize,	hash = :hash, width = :width, height = :height |  | ||||||
| 				WHERE  |  | ||||||
| 					id = :id |  | ||||||
| 				",
 |  | ||||||
|             [ |  | ||||||
|                     "filename"=>$new_filename, "filesize"=>$new_size, "hash"=>$new_hash, |  | ||||||
|                     "width"=>$new_width, "height"=>$new_height,	"id"=>$image_id |  | ||||||
|                 ] |  | ||||||
|         ); |  | ||||||
|          |  | ||||||
|         log_info("rotate", "Rotated Image #{$image_id} - New hash: {$new_hash}"); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -179,13 +179,13 @@ class TagEdit extends Extension | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function onPostListBuilding(PostListBuildingEvent $event) |     // public function onPostListBuilding(PostListBuildingEvent $event)
 | ||||||
|     { |     // {
 | ||||||
|         global $user; |     //     global $user;
 | ||||||
|         if ($user->can("bulk_edit_image_source") && !empty($event->search_terms)) { |     //     if ($user->can("bulk_edit_image_source") && !empty($event->search_terms)) {
 | ||||||
|             $event->add_control($this->theme->mss_html(Tag::implode($event->search_terms))); |     //         $event->add_control($this->theme->mss_html(Tag::implode($event->search_terms)));
 | ||||||
|         } |     //     }
 | ||||||
|     } |     // }
 | ||||||
| 
 | 
 | ||||||
|     public function onImageInfoSet(ImageInfoSetEvent $event) |     public function onImageInfoSet(ImageInfoSetEvent $event) | ||||||
|     { |     { | ||||||
|  | |||||||
							
								
								
									
										459
									
								
								ext/transcode/main.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										459
									
								
								ext/transcode/main.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,459 @@ | |||||||
|  | <?php | ||||||
|  | /* | ||||||
|  |  * Name: Transcode Image | ||||||
|  |  * Author: Matthew Barbour <matthew@darkholme.net> | ||||||
|  |  * Description: Allows admins to automatically and manually transcode images. | ||||||
|  |  * License: MIT | ||||||
|  |  * Version: 1.0 | ||||||
|  |  * Documentation: | ||||||
|  |  *	Can transcode on-demand and automatically on upload. Config screen allows choosing an output format for each of the supported input formats. | ||||||
|  |  *  Supports GD and ImageMagick. Both support bmp, gif, jpg, png, and webp as inputs, and jpg, png, and lossy webp as outputs.  | ||||||
|  |  *  ImageMagick additionally supports tiff and psd inputs, and webp lossless output. | ||||||
|  |  *  If and image is uanble to be transcoded for any reason, the upload will continue unaffected. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  |  /* | ||||||
|  |  * This is used by the image transcoding code when there is an error while transcoding | ||||||
|  |  */ | ||||||
|  | class ImageTranscodeException extends SCoreException{ } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TranscodeImage extends Extension | ||||||
|  | { | ||||||
|  |     const CONVERSION_ENGINES = [ | ||||||
|  |         "GD" => "gd", | ||||||
|  |         "ImageMagick" => "convert", | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const ENGINE_INPUT_SUPPORT = [ | ||||||
|  |         "gd" => [ | ||||||
|  |             "bmp", | ||||||
|  |             "gif", | ||||||
|  |             "jpg", | ||||||
|  |             "png", | ||||||
|  |             "webp", | ||||||
|  |         ], | ||||||
|  |         "convert" => [ | ||||||
|  |             "bmp", | ||||||
|  |             "gif", | ||||||
|  |             "jpg", | ||||||
|  |             "png", | ||||||
|  |             "psd", | ||||||
|  |             "tiff", | ||||||
|  |             "webp", | ||||||
|  |         ] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const ENGINE_OUTPUT_SUPPORT = [ | ||||||
|  |         "gd" => [ | ||||||
|  |             "jpg", | ||||||
|  |             "png", | ||||||
|  |             "webp-lossy", | ||||||
|  |         ], | ||||||
|  |         "convert" => [ | ||||||
|  |             "jpg", | ||||||
|  |             "png", | ||||||
|  |             "webp-lossy", | ||||||
|  |             "webp-lossless", | ||||||
|  |         ] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const LOSSLESS_FORMATS = [ | ||||||
|  |         "webp-lossless", | ||||||
|  |         "png", | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const INPUT_FORMATS = [ | ||||||
|  |         "BMP" => "bmp", | ||||||
|  |         "GIF" => "gif", | ||||||
|  |         "JPG" => "jpg", | ||||||
|  |         "PNG" => "png", | ||||||
|  |         "PSD" => "psd", | ||||||
|  |         "TIFF" => "tiff", | ||||||
|  |         "WEBP" => "webp", | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const FORMAT_ALIASES = [ | ||||||
|  |         "tif" => "tiff", | ||||||
|  |         "jpeg" => "jpg", | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     const OUTPUT_FORMATS = [ | ||||||
|  |         "" => "", | ||||||
|  |         "JPEG (lossy)" => "jpg", | ||||||
|  |         "PNG (lossless)" => "png", | ||||||
|  |         "WEBP (lossy)" => "webp-lossy", | ||||||
|  |         "WEBP (lossless)" => "webp-lossless", | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Needs to be after upload, but before the processing extensions | ||||||
|  |      */ | ||||||
|  |     public function get_priority(): int | ||||||
|  |     { | ||||||
|  |         return 45; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public function onInitExt(InitExtEvent $event) | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  |         $config->set_default_bool('transcode_enabled', true); | ||||||
|  |         $config->set_default_bool('transcode_upload', false); | ||||||
|  |         $config->set_default_string('transcode_engine', "gd"); | ||||||
|  |         $config->set_default_int('transcode_quality', 80); | ||||||
|  | 
 | ||||||
|  |         foreach(array_values(self::INPUT_FORMATS) as $format) { | ||||||
|  |             $config->set_default_string('transcode_upload_'.$format, ""); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user, $config; | ||||||
|  | 
 | ||||||
|  |         if ($user->is_admin() && $config->get_bool("resize_enabled")) { | ||||||
|  |             $engine = $config->get_string("transcode_engine"); | ||||||
|  |             if($this->can_convert_format($engine,$event->image->ext)) { | ||||||
|  |                 $options = $this->get_supported_output_formats($engine, $event->image->ext); | ||||||
|  |                 $event->add_part($this->theme->get_transcode_html($event->image, $options)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public function onSetupBuilding(SetupBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  | 
 | ||||||
|  |         $engine = $config->get_string("transcode_engine"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         $sb = new SetupBlock("Image Transcode"); | ||||||
|  |         $sb->add_bool_option("transcode_enabled", "Allow transcoding images: "); | ||||||
|  |         $sb->add_bool_option("transcode_upload", "<br>Transcode on upload: "); | ||||||
|  |         $sb->add_choice_option('transcode_engine',self::CONVERSION_ENGINES,"<br />Transcode engine: "); | ||||||
|  |         foreach(self::INPUT_FORMATS as $display=>$format) { | ||||||
|  |             if(in_array($format, self::ENGINE_INPUT_SUPPORT[$engine])) { | ||||||
|  |                 $outputs = $this->get_supported_output_formats($engine, $format); | ||||||
|  |                 $sb->add_choice_option('transcode_upload_'.$format,$outputs,"<br />$display to: "); | ||||||
|  |             }             | ||||||
|  |         } | ||||||
|  |         $sb->add_int_option("transcode_quality", "<br/>Lossy format quality: "); | ||||||
|  |         $event->panel->add_block($sb); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onDataUpload(DataUploadEvent $event) | ||||||
|  |     { | ||||||
|  |         global $config, $page; | ||||||
|  | 
 | ||||||
|  |          if ($config->get_bool("transcode_upload") == true) { | ||||||
|  |             $ext = strtolower($event->type); | ||||||
|  | 
 | ||||||
|  |             $ext = $this->clean_format($ext); | ||||||
|  | 
 | ||||||
|  |             if($event->type=="gif"&&is_animated_gif($event->tmpname)) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if(in_array($ext, array_values(self::INPUT_FORMATS))) { | ||||||
|  |                 $target_format = $config->get_string("transcode_upload_".$ext); | ||||||
|  |                 if(empty($target_format)) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 try { | ||||||
|  |                     $new_image = $this->transcode_image($event->tmpname, $ext, $target_format); | ||||||
|  |                     $event->set_type($this->determine_ext($target_format)); | ||||||
|  |                     $event->set_tmpname($new_image); | ||||||
|  |                 } catch(Exception $e) { | ||||||
|  |                     log_error("transcode","Error while performing upload transcode: ".$e->getMessage()); | ||||||
|  |                     // We don't want to interfere with the upload process, 
 | ||||||
|  |                     // so if something goes wrong the untranscoded image jsut continues
 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }  | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public function onPageRequest(PageRequestEvent $event) | ||||||
|  |     { | ||||||
|  |         global $page, $user; | ||||||
|  | 
 | ||||||
|  |         if ($event->page_matches("transcode") && $user->is_admin()) { | ||||||
|  |              $image_id = int_escape($event->get_arg(0)); | ||||||
|  |              if (empty($image_id)) { | ||||||
|  |                  $image_id = isset($_POST['image_id']) ? int_escape($_POST['image_id']) : null; | ||||||
|  |              } | ||||||
|  |              // Try to get the image ID
 | ||||||
|  |              if (empty($image_id)) { | ||||||
|  |                  throw new ImageTranscodeException("Can not resize Image: No valid Image ID given."); | ||||||
|  |              } | ||||||
|  |              $image_obj = Image::by_id($image_id); | ||||||
|  |              if (is_null($image_obj)) { | ||||||
|  |                  $this->theme->display_error(404, "Image not found", "No image in the database has the ID #$image_id"); | ||||||
|  |              } else { | ||||||
|  |                  if (isset($_POST['transcode_format'])) { | ||||||
|  |                      | ||||||
|  |                      try { | ||||||
|  |                         $this->transcode_and_replace_image($image_obj, $_POST['transcode_format']); | ||||||
|  |                         $page->set_mode("redirect"); | ||||||
|  |                         $page->set_redirect(make_link("post/view/".$image_id)); | ||||||
|  |                      } catch (ImageTranscodeException $e) { | ||||||
|  |                          $this->theme->display_transcode_error($page, "Error Transcoding", $e->getMessage()); | ||||||
|  |                      } | ||||||
|  |                  } | ||||||
|  |              } | ||||||
|  |          } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |      | ||||||
|  |     public function onBulkActionBlockBuilding(BulkActionBlockBuildingEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user, $config; | ||||||
|  | 
 | ||||||
|  |         $engine = $config->get_string("transcode_engine"); | ||||||
|  | 
 | ||||||
|  |         if ($user->is_admin()) { | ||||||
|  |             $event->add_action("bulk_transcode","Transcode","",$this->theme->get_transcode_picker_html($this->get_supported_output_formats($engine))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function onBulkAction(BulkActionEvent $event) | ||||||
|  |     { | ||||||
|  |         global $user, $database; | ||||||
|  | 
 | ||||||
|  |         switch($event->action) { | ||||||
|  |             case "bulk_transcode": | ||||||
|  |                 if (!isset($_POST['transcode_format'])) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if ($user->is_admin()) { | ||||||
|  |                     $format = $_POST['transcode_format']; | ||||||
|  |                     $total = 0; | ||||||
|  |                     foreach ($event->items as $id) { | ||||||
|  |                         try { | ||||||
|  |                             $database->beginTransaction(); | ||||||
|  |                             $image = Image::by_id($id); | ||||||
|  |                             if($image==null) { | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  |                              | ||||||
|  |                             $this->transcode_and_replace_image($image, $format); | ||||||
|  |                             // If a subsequent transcode fails, the database need to have everything about the previous transcodes recorded already,
 | ||||||
|  |                             // otherwise the image entries will be stuck pointing to missing image files
 | ||||||
|  |                             $database->commit(); | ||||||
|  |                             $total++; | ||||||
|  |                         } catch(Exception $e) { | ||||||
|  |                             log_error("transcode", "Error while bulk transcode on item $id to $format: ".$e->getMessage()); | ||||||
|  |                             try { | ||||||
|  |                                 $database->rollback(); | ||||||
|  |                             } catch (Exception $e) {} | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     flash_message("Transcoded  $total items"); | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function clean_format($format): ?string { | ||||||
|  |         if(array_key_exists($format, self::FORMAT_ALIASES)) { | ||||||
|  |             return self::FORMAT_ALIASES[$format]; | ||||||
|  |         } | ||||||
|  |         return $format; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function can_convert_format($engine, $format): bool  | ||||||
|  |     { | ||||||
|  |         $format = $this->clean_format($format); | ||||||
|  |         if(!in_array($format, self::ENGINE_INPUT_SUPPORT[$engine])) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function get_supported_output_formats($engine, ?String $omit_format = null): array  | ||||||
|  |     { | ||||||
|  |         $omit_format = $this->clean_format($omit_format); | ||||||
|  |         $output = []; | ||||||
|  |         foreach(self::OUTPUT_FORMATS as $key=>$value) { | ||||||
|  |             if($value=="") { | ||||||
|  |                 $output[$key] = $value; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             if(in_array($value, self::ENGINE_OUTPUT_SUPPORT[$engine])  | ||||||
|  |                 &&(empty($omit_format)||$omit_format!=$this->determine_ext($value))) { | ||||||
|  |                 $output[$key] = $value; | ||||||
|  |             }             | ||||||
|  |         } | ||||||
|  |         return $output; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private function determine_ext(String $format): String  | ||||||
|  |     { | ||||||
|  |         switch($format) { | ||||||
|  |             case "webp-lossless": | ||||||
|  |             case "webp-lossy": | ||||||
|  |                 return "webp"; | ||||||
|  |             default: | ||||||
|  |                 return $format; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function transcode_and_replace_image(Image $image_obj, String $target_format) | ||||||
|  |     { | ||||||
|  |         $target_format = $this->clean_format($target_format); | ||||||
|  |         $original_file = warehouse_path("images", $image_obj->hash); | ||||||
|  | 
 | ||||||
|  |         $tmp_filename = $this->transcode_image($original_file, $image_obj->ext, $target_format); | ||||||
|  |          | ||||||
|  |         $new_image = new Image(); | ||||||
|  |         $new_image->hash = md5_file($tmp_filename); | ||||||
|  |         $new_image->filesize = filesize($tmp_filename); | ||||||
|  |         $new_image->filename = $image_obj->filename; | ||||||
|  |         $new_image->width = $image_obj->width; | ||||||
|  |         $new_image->height = $image_obj->height; | ||||||
|  |         $new_image->ext = $this->determine_ext($target_format); | ||||||
|  | 
 | ||||||
|  |         /* Move the new image into the main storage location */ | ||||||
|  |         $target = warehouse_path("images", $new_image->hash); | ||||||
|  |         if (!@copy($tmp_filename, $target)) { | ||||||
|  |             throw new ImageTranscodeException("Failed to copy new image file from temporary location ({$tmp_filename}) to archive ($target)"); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         /* Remove temporary file */ | ||||||
|  |         @unlink($tmp_filename); | ||||||
|  | 
 | ||||||
|  |         send_event(new ImageReplaceEvent($image_obj->id, $new_image)); | ||||||
|  | 
 | ||||||
|  |     }     | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     private function transcode_image(String $source_name, String $source_format, string $target_format): string | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  | 
 | ||||||
|  |         if($source_format==$this->determine_ext($target_format)) { | ||||||
|  |             throw new ImageTranscodeException("Source and target formats are the same: ".$source_format); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $engine = $config->get_string("transcode_engine"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         if(!$this->can_convert_format($engine,$source_format)) { | ||||||
|  |             throw new ImageTranscodeException("Engine $engine does not support input format $source_format"); | ||||||
|  |         } | ||||||
|  |         if(!in_array($target_format, self::ENGINE_OUTPUT_SUPPORT[$engine])) { | ||||||
|  |             throw new ImageTranscodeException("Engine $engine does not support output format $target_format"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         switch($engine) { | ||||||
|  |             case "gd": | ||||||
|  |                 return $this->transcode_image_gd($source_name, $source_format, $target_format); | ||||||
|  |             case "convert": | ||||||
|  |                 return $this->transcode_image_convert($source_name, $source_format, $target_format); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function transcode_image_gd(String $source_name, String $source_format, string $target_format): string | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  |              | ||||||
|  |         $q = $config->get_int("transcode_quality"); | ||||||
|  | 
 | ||||||
|  |         $tmp_name = tempnam("/tmp", "shimmie_transcode"); | ||||||
|  | 
 | ||||||
|  |         $image = imagecreatefromstring(file_get_contents($source_name)); | ||||||
|  |         try { | ||||||
|  |             $result = false; | ||||||
|  |             switch($target_format) { | ||||||
|  |                 case "webp-lossy": | ||||||
|  |                     $result = imagewebp($image, $tmp_name, $q); | ||||||
|  |                     break; | ||||||
|  |                 case "png": | ||||||
|  |                     $result = imagepng($image, $tmp_name, 9); | ||||||
|  |                     break; | ||||||
|  |                 case "jpg": | ||||||
|  |                     // In case of alpha channels
 | ||||||
|  |                     $width = imagesx($image); | ||||||
|  |                     $height = imagesy($image); | ||||||
|  |                     $new_image = imagecreatetruecolor($width, $height); | ||||||
|  |                     if($new_image===false) { | ||||||
|  |                         throw new ImageTranscodeException("Could not create image with dimensions $width x $height"); | ||||||
|  |                     } | ||||||
|  |                     try{ | ||||||
|  |                         $black = imagecolorallocate($new_image,  0, 0, 0); | ||||||
|  |                         if($black===false) { | ||||||
|  |                             throw new ImageTranscodeException("Could not allocate background color"); | ||||||
|  |                         } | ||||||
|  |                         if(imagefilledrectangle($new_image, 0, 0, $width, $height, $black)===false) { | ||||||
|  |                             throw new ImageTranscodeException("Could not fill background color"); | ||||||
|  |                         } | ||||||
|  |                         if(imagecopy($new_image, $image, 0, 0, 0, 0, $width, $height)===false) { | ||||||
|  |                             throw new ImageTranscodeException("Could not copy source image to new image"); | ||||||
|  |                         } | ||||||
|  |                         $result = imagejpeg($new_image, $tmp_name, $q); | ||||||
|  |                     } finally { | ||||||
|  |                         imagedestroy($new_image); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |             if($result===false) { | ||||||
|  |                 throw new ImageTranscodeException("Error while transcoding ".$source_name." to ".$target_format); | ||||||
|  |             } | ||||||
|  |             return $tmp_name; | ||||||
|  |         } finally { | ||||||
|  |             imagedestroy($image); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function transcode_image_convert(String $source_name, String $source_format, string $target_format): string | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  |              | ||||||
|  |         $q = $config->get_int("transcode_quality"); | ||||||
|  |         $convert = $config->get_string("thumb_convert_path"); | ||||||
|  | 
 | ||||||
|  |         if($convert==null||$convert=="")  | ||||||
|  |         { | ||||||
|  |             throw new ImageTranscodeException("ImageMagick path not configured"); | ||||||
|  |         } | ||||||
|  |         $ext = $this->determine_ext($target_format); | ||||||
|  | 
 | ||||||
|  |         $args = " -flatten "; | ||||||
|  |         $bg = "none"; | ||||||
|  |         switch($target_format) { | ||||||
|  |             case "webp-lossless": | ||||||
|  |                 $args .= '-define webp:lossless=true'; | ||||||
|  |                 break; | ||||||
|  |             case "webp-lossy": | ||||||
|  |                 $args .= ''; | ||||||
|  |                 break; | ||||||
|  |             case "png": | ||||||
|  |                 $args .= '-define png:compression-level=9'; | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 $bg = "black"; | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         $tmp_name = tempnam("/tmp", "shimmie_transcode"); | ||||||
|  | 
 | ||||||
|  |         $format = '"%s" %s -quality %u -background %s "%s"  %s:"%s"'; | ||||||
|  |         $cmd = sprintf($format, $convert, $args, $q, $bg, $source_name, $ext, $tmp_name); | ||||||
|  |         $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); | ||||||
|  | 
 | ||||||
|  |         log_debug('transcode', "Transcoding with command `$cmd`, returns $ret"); | ||||||
|  | 
 | ||||||
|  |         if($ret!==0) { | ||||||
|  |             throw new ImageTranscodeException("Transcoding failed with command ".$cmd); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $tmp_name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								ext/transcode/theme.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								ext/transcode/theme.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | class TranscodeImageTheme extends Themelet | ||||||
|  | { | ||||||
|  |     /* | ||||||
|  |      * Display a link to resize an image | ||||||
|  |      */ | ||||||
|  |     public function get_transcode_html(Image $image, array $options) | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  | 
 | ||||||
|  |         $html = " | ||||||
|  | 			".make_form(make_link("transcode/{$image->id}"), 'POST')." | ||||||
|  |                 <input type='hidden' name='image_id' value='{$image->id}'> | ||||||
|  |                 ".$this->get_transcode_picker_html($options)." | ||||||
|  | 				<br><input id='transcodebutton' type='submit' value='Transcode'> | ||||||
|  | 			</form> | ||||||
|  | 		";
 | ||||||
|  |          | ||||||
|  |         return $html; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public function get_transcode_picker_html(array $options) { | ||||||
|  |         $html = "<select id='transcode_format'  name='transcode_format' required='required' >"; | ||||||
|  |         foreach($options as $display=>$value) { | ||||||
|  |             $html .= "<option value='$value'>$display</option>"; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $html."</select>"; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function display_transcode_error(Page $page, string $title, string $message) | ||||||
|  |     { | ||||||
|  |         $page->set_title("Transcode Image"); | ||||||
|  |         $page->set_heading("Transcode Image"); | ||||||
|  |         $page->add_block(new NavBlock()); | ||||||
|  |         $page->add_block(new Block($title, $message)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -18,9 +18,15 @@ class DataUploadEvent extends Event | |||||||
|     /** @var string */ |     /** @var string */ | ||||||
|     public $hash; |     public $hash; | ||||||
|     /** @var string */ |     /** @var string */ | ||||||
|     public $type; |     public $type = ""; | ||||||
|     /** @var int */ |     /** @var int */ | ||||||
|     public $image_id = -1; |     public $image_id = -1; | ||||||
|  |     /** @var bool */ | ||||||
|  |     public $handled = false; | ||||||
|  |     /** @var bool */ | ||||||
|  |     public $merged = false; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Some data is being uploaded. |      * Some data is being uploaded. | ||||||
| @ -29,21 +35,39 @@ class DataUploadEvent extends Event | |||||||
|      */ |      */ | ||||||
|     public function __construct(string $tmpname, array $metadata) |     public function __construct(string $tmpname, array $metadata) | ||||||
|     { |     { | ||||||
|  |         global $config; | ||||||
|  | 
 | ||||||
|         assert(file_exists($tmpname)); |         assert(file_exists($tmpname)); | ||||||
|         assert(is_string($metadata["filename"])); |         assert(is_string($metadata["filename"])); | ||||||
|         assert(is_string($metadata["extension"])); |  | ||||||
|         assert(is_array($metadata["tags"])); |         assert(is_array($metadata["tags"])); | ||||||
|         assert(is_string($metadata["source"]) || is_null($metadata["source"])); |         assert(is_string($metadata["source"]) || is_null($metadata["source"])); | ||||||
| 
 | 
 | ||||||
|         $this->tmpname = $tmpname; |  | ||||||
| 
 |  | ||||||
|         $this->metadata = $metadata; |         $this->metadata = $metadata; | ||||||
|  | 
 | ||||||
|  |         $this->set_tmpname($tmpname); | ||||||
|  | 
 | ||||||
|  |         if($config->get_bool("upload_use_mime")) { | ||||||
|  |             $this->set_type(get_extension_from_mime($tmpname)); | ||||||
|  |         } else { | ||||||
|  |             if(array_key_exists('extension',$metadata)&&!empty($metadata['extension'])) { | ||||||
|  |                 $this->type = strtolower($metadata['extension']); | ||||||
|  |             } else { | ||||||
|  |                 throw new UploadException("Could not determine extension for file ".$metadata["filename"]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function set_type(String $type) { | ||||||
|  |         $this->type = strtolower($type); | ||||||
|  |         $this->metadata["extension"] = $this->type; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function set_tmpname(String $tmpname) { | ||||||
|  |         $this->tmpname = $tmpname; | ||||||
|         $this->metadata['hash'] = md5_file($tmpname); |         $this->metadata['hash'] = md5_file($tmpname); | ||||||
|         $this->metadata['size'] = filesize($tmpname); |         $this->metadata['size'] = filesize($tmpname); | ||||||
| 
 |  | ||||||
|         // useful for most file handlers, so pull directly into fields
 |         // useful for most file handlers, so pull directly into fields
 | ||||||
|         $this->hash = $this->metadata['hash']; |         $this->hash = $this->metadata['hash']; | ||||||
|         $this->type = strtolower($metadata['extension']); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -76,6 +100,7 @@ class Upload extends Extension | |||||||
|         $config->set_default_int('upload_size', parse_shorthand_int('1MB')); |         $config->set_default_int('upload_size', parse_shorthand_int('1MB')); | ||||||
|         $config->set_default_int('upload_min_free_space', parse_shorthand_int('100MB')); |         $config->set_default_int('upload_min_free_space', parse_shorthand_int('100MB')); | ||||||
|         $config->set_default_bool('upload_tlsource', true); |         $config->set_default_bool('upload_tlsource', true); | ||||||
|  |         $config->set_default_bool('upload_use_mime', false); | ||||||
| 
 | 
 | ||||||
|         $this->is_full = false; |         $this->is_full = false; | ||||||
| 
 | 
 | ||||||
| @ -108,6 +133,7 @@ class Upload extends Extension | |||||||
|         $sb->add_label("<i>PHP Limit = ".ini_get('upload_max_filesize')."</i>"); |         $sb->add_label("<i>PHP Limit = ".ini_get('upload_max_filesize')."</i>"); | ||||||
|         $sb->add_choice_option("transload_engine", $tes, "<br/>Transload: "); |         $sb->add_choice_option("transload_engine", $tes, "<br/>Transload: "); | ||||||
|         $sb->add_bool_option("upload_tlsource", "<br/>Use transloaded URL as source if none is provided: "); |         $sb->add_bool_option("upload_tlsource", "<br/>Use transloaded URL as source if none is provided: "); | ||||||
|  |         $sb->add_bool_option("upload_use_mime", "<br/>Use mime type to determine file types: "); | ||||||
|         $event->panel->add_block($sb); |         $event->panel->add_block($sb); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -320,9 +346,6 @@ class Upload extends Extension | |||||||
|                  |                  | ||||||
|                 $event = new DataUploadEvent($file['tmp_name'], $metadata); |                 $event = new DataUploadEvent($file['tmp_name'], $metadata); | ||||||
|                 send_event($event); |                 send_event($event); | ||||||
|                 if ($event->image_id == -1) { |  | ||||||
|                     throw new UploadException("File type not recognised"); |  | ||||||
|                 } |  | ||||||
|                 $page->add_http_header("X-Shimmie-Image-ID: ".int_escape($event->image_id)); |                 $page->add_http_header("X-Shimmie-Image-ID: ".int_escape($event->image_id)); | ||||||
|             } catch (UploadException $ex) { |             } catch (UploadException $ex) { | ||||||
|                 $this->theme->display_upload_error( |                 $this->theme->display_upload_error( | ||||||
|  | |||||||
| @ -219,7 +219,7 @@ class UploadTheme extends Themelet | |||||||
|         $html .= ' (Drag & drop onto your bookmarks toolbar, then click when looking at an image)'; |         $html .= ' (Drag & drop onto your bookmarks toolbar, then click when looking at an image)'; | ||||||
| 
 | 
 | ||||||
|         // Bookmarklet checks if shimmie supports ext. If not, won't upload to site/shows alert saying not supported.
 |         // Bookmarklet checks if shimmie supports ext. If not, won't upload to site/shows alert saying not supported.
 | ||||||
|         $supported_ext = "jpg jpeg gif png"; |         $supported_ext = "jpg jpeg gif png webp"; | ||||||
|         if (class_exists("FlashFileHandler")) { |         if (class_exists("FlashFileHandler")) { | ||||||
|             $supported_ext .= " swf"; |             $supported_ext .= " swf"; | ||||||
|         } |         } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user