From 5300ffadf1c7faf3f505a585f2223fb6448b86bf Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Sat, 3 Aug 2013 20:34:59 -0400 Subject: 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']); } } -- cgit v1.3 From b67fc7d54e3fbde2f9e117a961998c5f8440679a Mon Sep 17 00:00:00 2001 From: Michael Foster Date: Sat, 3 Aug 2013 20:50:37 -0400 Subject: Only use `identify` if we have to --- inc/image.php | 15 ++++++++++++--- post.php | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/inc/image.php b/inc/image.php index 1dac4724..92e74277 100644 --- a/inc/image.php +++ b/inc/image.php @@ -11,7 +11,7 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { class Image { public $src, $format, $image, $size; - public function __construct($src, $format = false) { + public function __construct($src, $format = false, $size = false) { global $config; $this->src = $src; @@ -28,7 +28,7 @@ class Image { } } - $this->image = new $classname($this); + $this->image = new $classname($this, $size); if (!$this->image->valid()) { $this->delete(); @@ -122,10 +122,15 @@ class ImageBase extends ImageGD { return (bool)$this->image; } - public function __construct($img) { + public function __construct($img, $size = false) { if (method_exists($this, 'init')) $this->init(); + if ($size && $size[0] > 0 && $size[1] > 0) { + $this->width = $size[0]; + $this->height = $size[1]; + } + if ($img !== false) { $this->src = $img->src; $this->from(); @@ -244,6 +249,10 @@ class ImageConvert extends ImageBase { $this->temp = false; } public function from() { + if ($this->width > 0 && $this->height > 0) { + $this->image = true; + return; + } $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]; diff --git a/post.php b/post.php index 905110d4..434bc7e2 100644 --- a/post.php +++ b/post.php @@ -464,7 +464,7 @@ if (isset($_POST['delete'])) { } // create image object - $image = new Image($upload, $post['extension']); + $image = new Image($upload, $post['extension'], $size); if ($image->size->width > $config['max_width'] || $image->size->height > $config['max_height']) { $image->delete(); error($config['error']['maxsize']); -- cgit v1.3 From e59c404ac27b1fcf8ebdcc6c8369e53c50615073 Mon Sep 17 00:00:00 2001 From: Czterooki Date: Fri, 2 Aug 2013 22:04:59 -0400 Subject: Roach style, very dirty --- stylesheets/img/bghead.png | Bin 0 -> 3465 bytes stylesheets/img/roachtlo.png | Bin 0 -> 35442 bytes stylesheets/roach.css | 256 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 stylesheets/img/bghead.png create mode 100644 stylesheets/img/roachtlo.png create mode 100644 stylesheets/roach.css diff --git a/stylesheets/img/bghead.png b/stylesheets/img/bghead.png new file mode 100644 index 00000000..94202009 Binary files /dev/null and b/stylesheets/img/bghead.png differ diff --git a/stylesheets/img/roachtlo.png b/stylesheets/img/roachtlo.png new file mode 100644 index 00000000..f3a9cefc Binary files /dev/null and b/stylesheets/img/roachtlo.png differ diff --git a/stylesheets/roach.css b/stylesheets/roach.css new file mode 100644 index 00000000..ab5ab6c1 --- /dev/null +++ b/stylesheets/roach.css @@ -0,0 +1,256 @@ +html, body { + background:#FFFEED !important; + background-image: url('img/bghead.png'), url('img/roachtlo.png') !important; + background-repeat: repeat-x, no-repeat !important; + background-attachment: fixed, fixed !important; + background-position:top, right bottom !important; + color:#835B36 !important; + font-family: sans-serif !important; + font-size: 90% !important; +} +a { + color:#888 !important; + text-decoration: none !important; +} +a:hover { + color:#000 !important; +} +.reflink a:hover{ + font-weight: bold !important; +} +input, textarea { +background-color: #fff !important; +color: #000 !important; +border: 1px solid #CCC !important; +} +input:focus, textarea:focus +{ +background-color: #fff !important; +} +.adminbar { + text-align:right !important; + clear:both !important; + float:right !important; + font-size: 13px !important; +} +.navbar { + font-size: 13px !important; +} +header { + clear:both !important; + text-align:center !important; + font-size:1.7em !important; + color:#455736 !important; + width:100% !important; +} +header div.subtitle, h1 { + color:#455736 !important; +} +div.banner { + background:#F3DBA7 !important; + text-align:center !important; + padding:2px !important; + color:#835B36 !important; + width:100% !important; + margin: 0 !important; + font-weight: normal !important; + font-size: 90% !important; +} +.catalogmode { + background:#0040E0 !important; + text-align:center !important; + padding:2px !important; + color:#FFFFFF !important; + width:100% !important; +} +.postarea { + background:inherit !important; +} +.rules { + /*font-size:0.7em !important;*/ + width: 468px !important; + font-size: 10px !important; + font-family: sans-serif !important; +} +.rules li { + margin-left: 1em !important; + /*text-indent: 0em !important;*/ +} +form table tr th, form table th, .postblock { + background:#CDC8A3 !important; + color:#455736 !important; + font-weight:800 !important; + text-align: left !important; + padding-left: 3px !important; + padding-right: 3px !important; + border: none !important; +} +.footer { + text-align:center !important; + font-size:12px !important; + font-family:serif !important; +} +.passvalid { + background:#F3DBA7 !important; + text-align:center !important; + width:100% !important; + color:#ffffff !important; +} +.dellist { + font-weight: bold !important; + text-align:center !important; +} +.delbuttons { + text-align:center !important; + padding-bottom:4px !important; + +} +.managehead { + background:#AAAA66 !important; + color:#400000 !important; + padding:0px !important; +} +.postlists { + background:#FFFFFF !important; + width:100% !important; + padding:0px !important; + color:#FFF !important; +} +.row1 { + background:#EEEECC !important; + color:#FFF !important; +} +.row2 { + background:#DDDDAA !important; + color:#FFF !important; +} +.unkfunc { + background:inherit !important; + color:#789922 !important; +} +.filesize { + text-decoration:none !important; +} +.filetitle { + background:inherit !important; + font-size:1.2em !important; + color:#6D9E55 !important; + font-weight:800 !important; +} +.postername { + color:#117743 !important; + font-weight:bold !important; +} +.postertrip { + color:#117743 !important; +} +.oldpost { + color:#6D9E55 !important; + font-weight:800 !important; +} +.omittedposts { + color:#707070 !important; +} + +.reply { + background: rgba(210,140,0,0.3) !important; + color: #835B36 !important; + -moz-border-radius: 5px !important; + -webkit-border-radius: 5px !important; + padding: 5px !important; + border-style: none !important; +} +.post-hover { + background: rgba(241,219,165,1) !important; + border-style: none !important; + border: 1px dashed #000000 !important; +} +.replyhl { + background: #F0C0B0 !important; + color: #FFF !important; +} +.doubledash { + vertical-align:top !important; + clear:both !important; + float:left !important; +} +.replytitle { + font-size: 1.0em !important; + color:#6D9E55 !important; + font-weight:800 !important; +} +.commentpostername { + color:#117743 !important; + font-weight:800 !important; +} +.thumbnailmsg { + font-size: small !important; + color:#FFF !important; +} +.abbrev { + color:#707070 !important; +} +.highlighted { + background:#F3DBA7 !important; + color:#707070 !important; + border: 2px dashed #707070 !important; +} +#watchedthreads { + background-color: #F3DBa7 !important; +} + +.hidethread { + background: transparent url('./icons/blueplus/icons.gif') -32px -16px no-repeat !important; +} + +.unhidethread { + background: transparent url('./icons/blueplus/icons.gif') -48px 0px no-repeat !important; +} + +.watchthread { + background: transparent url('./icons/blueplus/icons.gif') -32px 0px no-repeat !important; +} + +.expandthread { + background: transparent url('./icons/blueplus/icons.gif') 0px -16px no-repeat !important; +} + +.quickreply { + background: transparent url('./icons/blueplus/icons.gif') 0px 0px no-repeat !important; +} + +.hidewatchedthreads { + background: transparent url('./icons/blueplus/icons.gif') -48px -16px no-repeat !important; +} + +.refreshwatchedthreads { + background: transparent url('./icons/blueplus/icons.gif') -16px -16px no-repeat !important; +} + +.restorewatchedthreads { + background: transparent url('./icons/blueplus/icons.gif') -16px 0px no-repeat !important; +} +.reflinkpreview { + background-color: #F3DBA7 !important; +} +div.pages { +background: none; +border-right: none; +border-bottom: none; +display: inline; +padding: 4px; +border: 4px double #000; +border-collapse: separate; +border-spacing: 2px; +border-color: gray; +color: #835B36 !important; +} +hr { +border-top: 1px solid #835B36 !important; +} +.desktop-style div.boardlist:nth-child(1) { + background: none repeat scroll 0 0 #FFFFFF !important; +} +.desktop-style div.boardlist:nth-child(1):hover { + background: none repeat scroll 0 0 #FFFFFF; +} \ No newline at end of file -- cgit v1.3