From 5300ffadf1c7faf3f505a585f2223fb6448b86bf Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Sat, 3 Aug 2013 20:34:59 -0400 Subject: [PATCH] Better image processing. Add support for GraphicsMagick (a fork of ImageMagick) and `exiftool` (for stripping EXIF metadata quickly). --- inc/config.php | 30 ++++++++++++++++++++++++------ inc/functions.php | 27 ++++++++++++++++++++++++--- inc/image.php | 55 ++++++++++++++++++++++++++++++++++++++++++------------- post.php | 21 ++++++++++++++------- 4 files changed, 104 insertions(+), 29 deletions(-) diff --git a/inc/config.php b/inc/config.php index 15af9661..45fa957c 100644 --- a/inc/config.php +++ b/inc/config.php @@ -63,6 +63,10 @@ // Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system. // Requires safe_mode to be disabled. $config['dns_system'] = false; + + // When executing most command-line tools (such as `convert` for ImageMagick image processing), add this + // to the environment path (seperated by :). + $config['shell_path'] = '/usr/local/bin'; /* * ==================== @@ -487,7 +491,11 @@ * 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in * PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. * - * 'convert+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) + * 'gm' GraphicsMagick (`gm`) is a fork of ImageMagick with many improvements. It is more + * efficient and gets thumbnailing done using fewer resources. + * + * 'convert+gifscale' + * OR 'gm+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) * instead of `convert` for resizing GIFs. It's faster and resulting animated * thumbnails have less artifacts than if resized with ImageMagick. */ @@ -496,10 +504,24 @@ // Command-line options passed to ImageMagick when using `convert` for thumbnailing. Don't touch the // placement of "%s" and "%d". - $config['convert_args'] = '-background transparent %s -strip -thumbnail %dx%d -quality 65'; + $config['convert_args'] = '-size %dx%d %s -thumbnail %dx%d +profile "*" %s'; // Strip EXIF metadata from JPEG files. $config['strip_exif'] = false; + // Use the command-line `exiftool` tool to strip EXIF metadata without decompressing/recompressing JPEGs. + // Ignored when $config['redraw_image'] is true. + $config['strip_with_exiftool'] = false; + + // Redraw the image to strip any excess data (commonly ZIP archives) WARNING: This might strip the + // animation of GIFs, depending on the chosen thumbnailing method. It also requires recompressing + // the image, so more processing power is required. + $config['redraw_image'] = false; + + // Automatically correct the orientation of JPEG files using -auto-orient in `convert`. This only works + // when `convert` or `gm` is selected for thumbnailing. Again, requires more processing power because + // this basically does the same thing as $config['redraw_image']. (If $config['redraw_image'] is enabled, + // this value doesn't matter as $config['redraw_image'] attempts to correct orientation too.) + $config['convert_auto_orient'] = false; // Regular expression to check for an XSS exploit with IE 6 and 7. To disable, set to false. // Details: https://github.com/savetheinternet/Tinyboard/issues/20 @@ -562,10 +584,6 @@ // Display image identification links using regex.info/exif, TinEye and Google Images. $config['image_identification'] = false; - // Redraw the image to strip any excess data (commonly ZIP archives) WARNING: This might strip the - // animation of GIFs, depending on the chosen thumbnailing method. - $config['redraw_image'] = false; - /* * ==================== * Board settings diff --git a/inc/functions.php b/inc/functions.php index 309546ef..8b2c4c7b 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -79,7 +79,7 @@ function loadConfig() { if ($config['debug']) { if (!isset($debug)) { - $debug = array('sql' => array(), 'purge' => array(), 'cached' => array(), 'write' => array()); + $debug = array('sql' => array(), 'exec' => array(), 'purge' => array(), 'cached' => array(), 'write' => array()); $debug['start'] = microtime(true); } } @@ -848,7 +848,7 @@ function bumpThread($id) { if ($config['try_smarter']) $build_pages[] = thread_find_page($id); - + $query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :time WHERE `id` = :id AND `thread` IS NULL", $board['uri'])); $query->bindValue(':time', time(), PDO::PARAM_INT); $query->bindValue(':id', $id, PDO::PARAM_INT); @@ -1282,7 +1282,7 @@ function buildIndex() { for ($page = 1; $page <= $config['max_pages']; $page++) { $filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page)); - + if ($config['try_smarter'] && isset($build_pages) && count($build_pages) && !in_array($page, $build_pages) && is_file($filename)) continue; $content = index($page); @@ -1928,3 +1928,24 @@ function DNS($host) { return $ip_addr; } + +function shell_exec_error($command) { + global $config, $debug; + + if ($config['debug']) + $start = microtime(true); + + $return = trim(shell_exec('PATH="' . escapeshellcmd($config['shell_path']) . ':$PATH";' . $command . ' 2>&1 && echo "TB_SUCCESS"')); + $return = preg_replace('/TB_SUCCESS$/', '', $return); + + if ($config['debug']) { + $time = round((microtime(true) - $start) * 1000, 2) . 'ms'; + $debug['exec'][] = array( + 'command' => $command, + 'time' => '~' . $time, + 'response' => $return ? $return : null + ); + } + + return $return === 'TB_SUCCESS' ? false : $return; +} diff --git a/inc/image.php b/inc/image.php index 88f735dd..1dac4724 100644 --- a/inc/image.php +++ b/inc/image.php @@ -19,7 +19,7 @@ class Image { if ($config['thumb_method'] == 'imagick') { $classname = 'ImageImagick'; - } elseif ($config['thumb_method'] == 'convert' || $config['thumb_method'] == 'convert+gifsicle') { + } elseif (in_array($config['thumb_method'], array('convert', 'convert+gifsicle', 'gm', 'gm+gifsicle'))) { $classname = 'ImageConvert'; } else { $classname = 'Image' . strtoupper($this->format); @@ -44,8 +44,6 @@ class Image { public function resize($extension, $max_width, $max_height) { global $config; - - $gifsicle = false; if ($config['thumb_method'] == 'imagick') { $classname = 'ImageImagick'; @@ -54,6 +52,13 @@ class Image { } elseif ($config['thumb_method'] == 'convert+gifsicle') { $classname = 'ImageConvert'; $gifsicle = true; + } elseif ($config['thumb_method'] == 'gm') { + $classname = 'ImageConvert'; + $gm = true; + } elseif ($config['thumb_method'] == 'gm+gifsicle') { + $classname = 'ImageConvert'; + $gm = true; + $gifsicle = true; } else { $classname = 'Image' . strtoupper($extension); if (!class_exists($classname)) { @@ -81,7 +86,6 @@ class Image { $height = $max_height; } - $thumb->gifsicle = $gifsicle; $thumb->_resize($this->image->image, $width, $height); return $thumb; @@ -227,15 +231,20 @@ class ImageImagick extends ImageBase { class ImageConvert extends ImageBase { - public $width, $height, $temp, $gifsicle; + public $width, $height, $temp, $gm = false, $gifsicle = false; public function init() { global $config; + if ($config['thumb_method'] == 'gm' || $config['thumb_method'] == 'gm+gifsicle') + $this->gm = true; + if ($config['thumb_method'] == 'convert+gifsicle' || $config['thumb_method'] == 'gm+gifsicle') + $this->gifsicle = true; + $this->temp = false; } - public function from() { - $size = trim(shell_exec('identify -format "%w %h" ' . escapeshellarg($this->src . '[0]'))); + public function from() { + $size = shell_exec_error(($this->gm ? 'gm ' : '') . 'identify -format "%w %h" ' . escapeshellarg($this->src . '[0]')); if (preg_match('/^(\d+) (\d+)$/', $size, $m)) { $this->width = $m[1]; $this->height = $m[2]; @@ -251,9 +260,17 @@ class ImageConvert extends ImageBase { if (!$this->temp) { if ($config['strip_exif']) { - shell_exec('convert ' . escapeshellarg($this->src) . ' -auto-orient -strip ' . escapeshellarg($src)); + if($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' . + escapeshellarg($this->src) . ' -auto-orient -strip ' . escapeshellarg($src))) { + $this->destroy(); + error('Failed to resize image!', null, $error); + } } else { - shell_exec('convert ' . escapeshellarg($this->src) . ' -auto-orient ' . escapeshellarg($src)); + if($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' . + escapeshellarg($this->src) . ' -auto-orient ' . escapeshellarg($src))) { + $this->destroy(); + error('Failed to resize image!', null, $error); + } } } else { rename($this->temp, $src); @@ -289,13 +306,25 @@ class ImageConvert extends ImageBase { escapeshellarg($this->temp) . '2>&1 &&echo $?') !== '0') || !file_exists($this->temp)) error('Failed to resize image!', null, $error); } else { - if (trim($error = shell_exec('convert ' . sprintf($config['convert_args'], '', $this->width, $this->height) . ' ' . - escapeshellarg($this->src) . ' ' . escapeshellarg($this->temp) . ' 2>&1 &&echo $?')) !== '0' || !file_exists($this->temp)) + if ($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' . + sprintf($config['convert_args'], + $this->width, + $this->height, + escapeshellarg($this->src), + $this->width, + $this->height, + escapeshellarg($this->temp))) || !file_exists($this->temp)) error('Failed to resize image!', null, $error); } } else { - if (trim($error = shell_exec('convert ' . sprintf($config['convert_args'], '-flatten', $this->width, $this->height) . ' ' . - escapeshellarg($this->src . '[0]') . " " . escapeshellarg($this->temp) . ' 2>&1 &&echo $?')) !== '0' || !file_exists($this->temp)) + if ($error = shell_exec_error(($this->gm ? 'gm ' : '') . 'convert ' . + sprintf($config['convert_args'], + $this->width, + $this->height, + escapeshellarg($this->src . '[0]'), + $this->width, + $this->height, + escapeshellarg($this->temp))) || !file_exists($this->temp)) error('Failed to resize image!', null, $error); } } diff --git a/post.php b/post.php index 65e85bcc..905110d4 100644 --- a/post.php +++ b/post.php @@ -447,14 +447,17 @@ if (isset($_POST['delete'])) { } - if ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg') { + if ($config['convert_auto_orient'] && ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg')) { // The following code corrects the image orientation. // Currently only works with the 'convert' option selected but it could easily be expanded to work with the rest if you can be bothered. if (!($config['redraw_image'] || ($config['strip_exif'] && ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg')))) { - if ($config['thumb_method'] == 'convert' || $config['thumb_method'] == 'convert+gifsicle') { + if (in_array($config['thumb_method'], array('convert', 'convert+gifsicle', 'gm', 'gm+gifsicle'))) { $exif = exif_read_data($upload); + $gm = in_array($config['thumb_method'], array('gm', 'gm+gifsicle')); if (isset($exif['Orientation']) && $exif['Orientation'] != 1) { - shell_exec('convert ' . escapeshellarg($upload) . ' -auto-orient ' . escapeshellarg($upload)); + if($error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' . + escapeshellarg($upload) . ' -auto-orient ' . escapeshellarg($upload))) + error('Could not auto-orient image!', null, $error); } } } @@ -462,7 +465,6 @@ if (isset($_POST['delete'])) { // create image object $image = new Image($upload, $post['extension']); - if ($image->size->width > $config['max_width'] || $image->size->height > $config['max_height']) { $image->delete(); error($config['error']['maxsize']); @@ -503,8 +505,13 @@ if (isset($_POST['delete'])) { } if ($config['redraw_image'] || ($config['strip_exif'] && ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg'))) { - $image->to($post['file']); - $dont_copy_file = true; + if (!$config['redraw_image'] && $config['strip_with_exiftool']) { + if($error = shell_exec_error('exiftool -q -all= ' . escapeshellarg($upload))) + error('Could not strip EXIF metadata!', null, $error); + } else { + $image->to($post['file']); + $dont_copy_file = true; + } } $image->destroy(); } else { @@ -518,7 +525,7 @@ if (isset($_POST['delete'])) { } if (!isset($dont_copy_file) || !$dont_copy_file) { - if (!@move_uploaded_file($_FILES['file']['tmp_name'], $post['file'])) + if (!@move_uploaded_file($upload, $post['file'])) error($config['error']['nomove']); } }