From 0d51e25883101fbb9089147a892fd3cd75313fea Mon Sep 17 00:00:00 2001 From: Jan Grewe <jan@faked.org> Date: Sun, 24 Jan 2016 15:12:58 +0100 Subject: [PATCH] update from upstream --- index.php | 214 +++++++++++++++++++++++++++++--------- libgif.php | 291 ++++++++++++++++++++++++++++++++++++---------------- libjpeg.php | 74 +++++++++++-- 3 files changed, 426 insertions(+), 153 deletions(-) diff --git a/index.php b/index.php index f9bef56..96e05a2 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,8 @@ <?php -define( 'PHOTON__ALLOW_ANY_EXTENSION', 1 ); -define( 'PHOTON__ALLOW_QUERY_STRINGS', 2 ); +define( 'PHOTON__ALLOW_QUERY_STRINGS', 1 ); +define( 'PHOTON__DEFAULT_MAX_QUALITY', 89 ); +define( 'PHOTON__PNG_MAX_QUALITY', 80 ); require dirname( __FILE__ ) . '/plugin.php'; if ( file_exists( dirname( __FILE__ ) . '/../config.php' ) ) @@ -13,8 +14,10 @@ else if ( file_exists( dirname( __FILE__ ) . '/config.php' ) ) $allowed_functions = apply_filters( 'allowed_functions', array( // 'q' => RESERVED // 'zoom' => global resolution multiplier (argument filter) - 'h' => 'setheight', // done - 'w' => 'setwidth', // done +// 'quality' => sets the quality of JPEG images during processing +// 'strip => strips JPEG images of exif, icc or all "extra" data (params: info,color,all) + 'h' => 'set_height', // done + 'w' => 'set_width', // done 'crop' => 'crop', // done 'resize' => 'resize_and_crop', // done 'fit' => 'fit_in_box', // done @@ -47,24 +50,37 @@ $remote_image_max_size = apply_filters( 'remote_image_max_size', 55 * 1024 * 102 /* Array of domains exceptions * Keys are domain name * Values are bitmasks with the following options: - * PHOTON__ALLOW_ANY_EXTENSION: Allow any extension (including none) in the path of the URL * PHOTON__ALLOW_QUERY_STRINGS: Append the string found in the 'q' query string parameter as the query string of the remote URL */ $origin_domain_exceptions = apply_filters( 'origin_domain_exceptions', array() ); // You can override this by defining it in config.php if ( ! defined( 'PHOTON__UPSCALE_MAX_PIXELS' ) ) - define( 'PHOTON__UPSCALE_MAX_PIXELS', 1000 ); + define( 'PHOTON__UPSCALE_MAX_PIXELS', 2000 ); + +// Allow smaller upscales for GIFs, compared to the other image types +if ( ! defined( 'PHOTON__UPSCALE_MAX_PIXELS_GIF' ) ) + define( 'PHOTON__UPSCALE_MAX_PIXELS_GIF', 1000 ); require dirname( __FILE__ ) . '/libjpeg.php'; // Implicit configuration -if ( file_exists( '/usr/local/bin/optipng' ) ) +if ( file_exists( '/usr/local/bin/optipng' ) && ! defined( 'DISABLE_IMAGE_OPTIMIZATIONS' ) ) define( 'OPTIPNG', '/usr/local/bin/optipng' ); else define( 'OPTIPNG', false ); -if ( file_exists( '/usr/local/bin/jpegoptim' ) ) +if ( file_exists( '/usr/local/bin/pngquant' ) && ! defined( 'DISABLE_IMAGE_OPTIMIZATIONS' ) ) + define( 'PNGQUANT', '/usr/local/bin/pngquant' ); +else + define( 'PNGQUANT', false ); + +if ( file_exists( '/usr/local/bin/cwebp' ) && ! defined( 'DISABLE_IMAGE_OPTIMIZATIONS' ) ) + define( 'CWEBP', '/usr/local/bin/cwebp' ); +else + define( 'CWEBP', false ); + +if ( file_exists( '/usr/local/bin/jpegoptim' ) && ! defined( 'DISABLE_IMAGE_OPTIMIZATIONS' ) ) define( 'JPEGOPTIM', '/usr/local/bin/jpegoptim' ); else define( 'JPEGOPTIM', false ); @@ -103,8 +119,8 @@ function zoom( $arguments, $function_name, $image ) { $h = $image->getimageheight(); switch ( $function_name ) { - case 'setheight' : - case 'setwidth' : + case 'set_height' : + case 'set_width' : $new_arguments = $arguments * $zoom; if ( substr( $arguments, -1 ) == '%' ) $new_arguments .= '%'; @@ -176,7 +192,7 @@ function crop( &$image, $args ) { } /** - * setheight - ( "h" function via the uri ) - resize the image to an explicit height, maintaining its aspect ratio + * set_height - ( "h" function via the uri ) - resize the image to an explicit height, maintaining its aspect ratio * * @param (resource)image the source gd image resource * @param (string)args "/^[0-9]+%?$/" the new height in pixels, or as a percentage if suffixed with an % @@ -184,7 +200,7 @@ function crop( &$image, $args ) { * * @return (resource) the resulting gs image resource **/ -function setheight( &$image, $args, $upscale = false ) { +function set_height( &$image, $args, $upscale = false ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); @@ -199,7 +215,7 @@ function setheight( &$image, $args, $upscale = false ) { // New height is greater than original image, but we don't have permission to upscale if ( $new_height > $h && ! $upscale ) return; - // Sane limit when upscaling, defaults to 1000 + // Sane limit when upscaling if ( $new_height > $h && $upscale && $new_height > PHOTON__UPSCALE_MAX_PIXELS ) return; @@ -213,7 +229,7 @@ function setheight( &$image, $args, $upscale = false ) { } /** - * setwidth - ( "w" function via the uri ) - resize the image to an explicit width, maintaining its aspect ratio + * set_width - ( "w" function via the uri ) - resize the image to an explicit width, maintaining its aspect ratio * * @param (resource)image the source gd image resource * @param (string)args "/^[0-9]+%?$/" the new width in pixels, or as a percentage if suffixed with an % @@ -221,7 +237,7 @@ function setheight( &$image, $args, $upscale = false ) { * * @return (resource) the resulting gs image resource **/ -function setwidth( &$image, $args, $upscale = false ) { +function set_width( &$image, $args, $upscale = false ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); @@ -236,7 +252,7 @@ function setwidth( &$image, $args, $upscale = false ) { // New height is greater than original image, but we don't have permission to upscale if ( $new_width > $w && ! $upscale ) return; - // Sane limit when upscaling, defaults to 1000 + // Sane limit when upscaling if ( $new_width > $w && $upscale && $new_width > PHOTON__UPSCALE_MAX_PIXELS ) return; @@ -278,7 +294,7 @@ function fit_in_box( &$image, $args ) { /** * resize_and_crop - ("resize" function via the uri) - originally by Alex M. * - * Differs from setwidth, setheight, and crop in that you provide a width/height and it resizes to that and then crops off excess + * Differs from set_width, set_height, and crop in that you provide a width/height and it resizes to that and then crops off excess * * @param (resource) image the source gd image resource * @param (string) args "w,h" width,height in pixels @@ -298,23 +314,29 @@ function resize_and_crop( &$image, $args ) { if ( 0 == $end_w || 0 == $end_h ) return; + if ( $end_w > $w && $end_w > PHOTON__UPSCALE_MAX_PIXELS ) + return; + + if ( $end_h > $h && $end_h > PHOTON__UPSCALE_MAX_PIXELS ) + return; + $ratio_orig = $w / $h; $ratio_end = $end_w / $end_h; // If the original and new images are proportional (no cropping needed), just do a standard resize if ( $ratio_orig == $ratio_end ) - setwidth( $image, $end_w, true ); + set_width( $image, $end_w, true ); // If we need to crop off the sides elseif ( $ratio_orig > $ratio_end ) { - setheight( $image, $end_h, true ); + set_height( $image, $end_h, true ); $x = floor( ( $image->getimagewidth() - $end_w ) / 2 ); crop( $image, "{$x}px,0px,{$end_w}px,{$end_h}px" ); } // If we need to crop off the top/bottom elseif ( $ratio_orig < $ratio_end ) { - setwidth( $image, $end_w, true ); + set_width( $image, $end_w, true ); $y = floor( ( $image->getimageheight() - $end_h ) / 2 ); crop( $image, "0px,{$y}px,{$end_w}px,{$end_h}px" ); } @@ -640,10 +662,102 @@ function do_a_filter( $function_name, $arguments ) { } } -function photon_cache_headers( $expires=63115200 ) { +function photon_cache_headers( $image_url, $content_type = 'image/jpeg', $expires = 63115200 ) { + header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', time() ) . ' GMT' ); header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $expires ) . ' GMT' ); header( 'Cache-Control: public, max-age='.$expires ); header( 'X-Content-Type-Options: nosniff' ); + header( 'Link: <' . $image_url . '>; rel="canonical"' ); + header( 'Content-Type: ' . $content_type ); + // animated webp images are not much smaller than GIFs and take an age to convert, + // ignore them and do not affect caching with the Vary header. + if ( 'image/gif' != $content_type ) + header( 'Vary: Accept' ); +} + +function serve_file( $url, $content_type, $filename, $bytes_saved ) { + photon_cache_headers( $url, $content_type ); + $filesize = filesize( $filename ); + header( 'ETag: "' . substr( md5( $filesize . '.' . time() ), 0, 16 ) . '"' ); + header( 'Content-Length: ' . $filesize ); + if ( defined( 'DISABLE_IMAGE_OPTIMIZATIONS' ) ) { + header( "X-Optim-Disabled: true" ); + } else { + header( 'X-Bytes-Saved: ' . $bytes_saved ); + } + $fp = fopen( $filename, 'r' ); + register_shutdown_function( 'unlink', $filename ); + fpassthru( $fp ); +} + +/** + * Compresses the PNG image using the best option available + * + * @param string $filename The file name of the temp image to compress + * @param int $quality The requested quality of the image + * + * @return string The content type of the final image (png or webp) + **/ +function compress_image_png( $filename, $quality ) { + $content_type = 'image/png'; + if ( isset( $_GET['quality'] ) && 100 == intval( $_GET['quality'] ) ) { + if ( false !== OPTIPNG ) { + exec( OPTIPNG . " $filename" ); + } else if ( false !== CWEBP && ( defined( 'CWEBP_PNG' ) && true === CWEBP_PNG ) && + isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) ) { + $content_type = 'image/webp'; + exec( CWEBP . " -quiet --lossless $filename -o $filename" ); + } + } else { + if ( false !== CWEBP && ( defined( 'CWEBP_PNG' ) && true === CWEBP_PNG ) && + isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) ) { + $content_type = 'image/webp'; + exec( CWEBP . " -quiet -q $quality -alpha_q 100 $filename -o $filename" ); + } else if ( false !== PNGQUANT ) { + exec( PNGQUANT . ' --speed 5 --quality=' . $quality . "-100 -f -o $filename $filename" ); + if ( false !== OPTIPNG ) exec( OPTIPNG . " -o1 $filename" ); + } else { + if ( false !== OPTIPNG ) exec( OPTIPNG . " $filename" ); + } + } + return $content_type; +} + +/** + * Compresses the JPEG image using the best option available + * + * @param string $filename The file name of the temp image to compress + * @param resource $image The GraphicsMagick image resource + * @param int $quality The requested quality of the image + * + * @return string The content type of the final image (jpeg or webp) + **/ +function compress_image_jpg( $filename, $image, $quality ) { + $content_type = 'image/jpeg'; + // Convert to WEBP if the following hold: + // 1. WEBP image processing for JPEGs is enabled. + // 2. The client is advertising support for WEBP in the accept header. + // 3. The requested quality is not 100%, as we need to honour sending images at their original + // visual quality and WEBP images at high quality are larger than their JPEG counterparts. + if ( false !== CWEBP && ( defined( 'CWEBP_JPEG' ) && true === CWEBP_JPEG ) && + isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) && + ! ( isset( $_GET['quality'] ) && 100 == intval( $_GET['quality'] ) ) ) { + $content_type = 'image/webp'; + exifrotate( $filename, $image, true ); + if ( Gmagick::IMGTYPE_GRAYSCALE == $image->getimagetype() ) { + exec( CWEBP . " --lossless -quiet -m 2 -q $quality -o $filename $filename" ); + } else { + exec( CWEBP . " -quiet -m 2 -q $quality -o $filename $filename" ); + } + } else if ( false !== JPEGOPTIM ) { + $strip = false; + if ( isset( $_GET['strip'] ) ) { + $strip = $_GET['strip']; + exifrotate( $filename, $image, $strip ); + } + jpegoptim( $filename, $strip ); + } + return $content_type; } $parsed = parse_url( $_SERVER['REQUEST_URI'] ); @@ -651,22 +765,19 @@ $exploded = explode( '/', $_SERVER['REQUEST_URI'] ); $origin_domain = strtolower( $exploded[1] ); $origin_domain_exception = array_key_exists( $origin_domain, $origin_domain_exceptions ) ? $origin_domain_exceptions[$origin_domain] : 0; -$scheme = 'http' . ( array_key_exists( 'ssl', $_GET ) ? 's' : '' ) . '://'; +$scheme = 'http' . ( array_key_exists( 'HTTPS', $_SERVER ) ? 's' : '' ) . '://'; parse_str( ( empty( $parsed['query'] ) ? '' : $parsed['query'] ), $_GET ); $ext = strtolower( pathinfo( $parsed['path'], PATHINFO_EXTENSION ) ); -if ( ! in_array( $ext, $allowed_types ) && !( $origin_domain_exception & PHOTON__ALLOW_ANY_EXTENSION ) ) - httpdie( '400 Bad Request', 'Error 0001. The type of image you are trying to process is not allowed.' ); - $url = $scheme . substr( $parsed['path'], 1 ); $url = preg_replace( '/#.*$/', '', $url ); $url = apply_filters( 'url', $url ); -if ( isset( $parsed['query'] ) ) { +if ( isset( $_GET['q'] ) ) { if ( $origin_domain_exception & PHOTON__ALLOW_QUERY_STRINGS ) { - $url .= '?' . preg_replace( '/#.*$/', '', (string) $parsed['query'] ); - unset( $parsed['query'] ); + $url .= '?' . preg_replace( '/#.*$/', '', (string) $_GET['q'] ); + unset( $_GET['q'] ); } else { httpdie( '400 Bad Request', "Sorry, the parameters you provided were not valid" ); } @@ -690,6 +801,13 @@ if ( 'GIF' === substr( $raw_data, 0, 3 ) ) { require dirname( __FILE__ ) . '/libgif.php'; $image = new Gif_Image( $raw_data ); $type = 'gif'; + if ( 0 === strlen( $_SERVER['QUERY_STRING'] ) ) { + do_action( 'bump_stats', 'image_gif' ); + photon_cache_headers( $url, 'image/gif' ); + header( 'ETag: "' . substr( md5( $raw_data_size . '.' . time() ), 0, 16 ) . '"' ); + header( 'Content-Length: ' . $raw_data_size ); + die( $raw_data ); + } } else { try { $image = new Gmagick(); @@ -705,10 +823,17 @@ if ( ! in_array( $type, $allowed_types ) ) if ( $type == 'jpeg' ) $quality = get_jpeg_quality( $raw_data, $raw_data_size ); +else if ( $type == 'png' ) + $quality = PHOTON__PNG_MAX_QUALITY; else - $quality = 90; + $quality = PHOTON__DEFAULT_MAX_QUALITY; unset( $raw_data ); +if ( isset( $_GET['quality'] ) ) + $quality = min( max( intval( $_GET['quality'] ), 20 ), $quality ); +else + $quality = min( PHOTON__DEFAULT_MAX_QUALITY, $quality ); + try { // Run through all uri supplied functions which are valid and allowed foreach( $_GET as $function_name => $arguments ) { @@ -722,50 +847,37 @@ try { switch ( $type ) { case 'png': - do_action( 'bump_stats', 'image_png' ); - header( 'Content-Type: image/png' ); $image->setcompressionquality( $quality ); $tmp = tempnam( $tmpdir, 'OPTIPNG-' ); $image->write( $tmp ); $og = filesize( $tmp ); - exec( OPTIPNG . " $tmp" ); + $content_type = compress_image_png( $tmp, $quality ); clearstatcache(); $save = $og - filesize( $tmp ); + serve_file( $url, $content_type, $tmp, $save ); + do_action( 'bump_stats', 'image_png' . ( 'image/webp' == $content_type ? '_as_webp' : '' ) ); do_action( 'bump_stats', 'png_bytes_saved', $save ); - $fp = fopen( $tmp, 'r' ); - photon_cache_headers(); - header( 'Content-Length: ' . filesize( $tmp ) ); - header( 'X-Bytes-Saved: ' . $save ); - unlink( $tmp ); - fpassthru( $fp ); break; case 'gif': do_action( 'bump_stats', 'image_gif' ); - if ( $image->process_image() ) { - photon_cache_headers(); - header( 'Content-Type: image/gif' ); - echo $image->get_imageblob(); + if ( $image->process_image_functions( PHOTON__UPSCALE_MAX_PIXELS_GIF ) ) { + photon_cache_headers( $url, 'image/gif' ); + echo $image->get_image_blob(); } else { httpdie( '400 Bad Request', "Sorry, the parameters you provided were not valid" ); } break; default: - do_action( 'bump_stats', 'image_jpeg' ); - header( 'Content-Type: image/jpeg' ); $image->setcompressionquality( $quality ); $tmp = tempnam( $tmpdir, 'JPEGOPTIM-' ); $image->write( $tmp ); $og = filesize( $tmp ); - exec( JPEGOPTIM . " --all-progressive -p $tmp" ); + $content_type = compress_image_jpg( $tmp, $image, $quality ); clearstatcache(); $save = $og - filesize( $tmp ); + serve_file( $url, $content_type, $tmp, $save ); + do_action( 'bump_stats', 'image_jpeg' . ( 'image/webp' == $content_type ? '_as_webp' : '' ) ); do_action( 'bump_stats', 'jpg_bytes_saved', $save ); - $fp = fopen( $tmp, 'r' ); - photon_cache_headers(); - header( 'Content-Length: ' . filesize( $tmp ) ); - header( 'X-Bytes-Saved: ' . $save ); - unlink( $tmp ); - fpassthru( $fp ); break; } diff --git a/libgif.php b/libgif.php index e0921b4..90a05d2 100644 --- a/libgif.php +++ b/libgif.php @@ -1,6 +1,7 @@ <?php /* - * Classes to handle the cropping, resizing and manipulation of animated GIF image files. + * Classes to manipulate animated GIF images. + * Maintained at: https://code.trac.wordpress.org/browser/photon/libgif.php */ if ( ! class_exists( 'Gif_Frame' ) ) { @@ -18,22 +19,22 @@ if ( ! class_exists( 'Gif_Frame' ) ) { private $_image; function __construct( $lc_mod, $palette, $image, $head, $box_dims, $gr_mod ) { - $this->pos_x = $box_dims[0]; - $this->pos_y = $box_dims[1]; - $this->width = $box_dims[2]; - $this->height = $box_dims[3]; + $this->pos_x = $box_dims[0]; + $this->pos_y = $box_dims[1]; + $this->width = $box_dims[2]; + $this->height = $box_dims[3]; - $this->lc_mod = $lc_mod; - $this->gr_mod = $gr_mod; - $this->palette = $palette; + $this->lc_mod = $lc_mod; + $this->gr_mod = $gr_mod; + $this->palette = $palette; if ( strlen( $gr_mod ) == 8 ) - $this->transp = ord( $gr_mod[3] ) & 1 ? 1 : 0; + $this->transp = ord( $gr_mod[3] ) & 1 ? 1 : 0; else - $this->transp = 0; + $this->transp = 0; - $this->head = $head; - $this->image = $image; + $this->head = $head; + $this->image = $image; } public function __set( $name, $value ) { @@ -70,6 +71,7 @@ if ( ! class_exists( 'Gif_Image' ) ) { private $crop_width = 0; private $crop_height = 0; private $crop = false; + private $fit = false; private $resize_ratio = Array( 0, 0 ); private $frame_count = 0; private $au = 0; @@ -82,8 +84,10 @@ if ( ! class_exists( 'Gif_Image' ) ) { private $dl_frms = Array(); private $pre_process_actions = Array(); private $post_process_actions = Array(); + private $upscale_max_pixels = 1000; + private $zoom_enabled = true; - private static $pre_actions = Array( 'setheight', 'setwidth', 'crop', 'resize_and_crop', 'fit_in_box' ); + private static $pre_actions = Array( 'set_height', 'set_width', 'crop', 'crop_offset', 'resize_and_crop', 'fit_in_box' ); const optimize = true; @@ -99,17 +103,18 @@ if ( ! class_exists( 'Gif_Image' ) ) { function __construct( $gif_data ) { $this->gif = $gif_data; $this->max_len = strlen( $gif_data ); - if ( $this->max_len < 14 ) - httpdie( '400 Bad Request', "unable to process the image data" ); - + if ( $this->max_len < 14 ) { + $this->error_and_die( '400 Bad Request', 'unable to process the image data' ); + } $this->gif_header = $this->get_bytes(13); $this->parse_header(); $this->parse_frames(); $buffer_add = ''; while ( self::GIF_BLOCK_END != ord( $this->gif[ $this->ptr ] ) ) { - if ( $this->ptr >= $this->max_len ) - httpdie( '400 Bad Request', "unable to process the image data" ); + if ( $this->ptr >= $this->max_len ) { + $this->error_and_die( '400 Bad Request', 'unable to process the image data' ); + } switch ( ord( $this->gif[ $this->ptr + 1 ] ) ) { case self::GIF_EXT_COMMENT: $sum = 2; @@ -180,8 +185,9 @@ if ( ! class_exists( 'Gif_Image' ) ) { // invalid start header found, increment by 1 byte to 'sync' to the next header $this->get_bytes( 1 ); } - if ( $this->ptr >= $this->max_len ) - httpdie( '400 Bad Request', "unable to process the image header" ); + if ( $this->ptr >= $this->max_len ) { + $this->error_and_die( '400 Bad Request', 'unable to process the image data' ); + } } $this->g_mod = $buff; } @@ -198,8 +204,9 @@ if ( ! class_exists( 'Gif_Image' ) ) { $this->frame_count--; continue 2; } - if ( $this->ptr >= $this->max_len ) - httpdie( '400 Bad Request', "unable to process the image frames" ); + if ( $this->ptr >= $this->max_len ) { + $this->error_and_die( '400 Bad Request', 'unable to process the image data' ); + } switch ( ord( $this->gif[ $this->ptr + 1 ] ) ) { case self::GIF_EXT_GRAPHIC_CONTROL: $this->gn_fld[] = $this->gif[ $this->ptr + 3 ]; @@ -306,6 +313,17 @@ if ( ! class_exists( 'Gif_Image' ) ) { return chr( $int & 255 ) . chr( ( $int & 0xFF00 ) >> 8 ); } + private function error_and_die( $result = '400 Bad Request', $message = '' ) { + if ( function_exists( 'imageresize_graceful_fail' ) ) { + imageresize_graceful_fail(); + } else if ( function_exists( 'httpdie' ) ) { + httpdie( $result, $message ); + } else { + header( "HTTP/1.1 $result" ); + die( $message ); + } + } + private function filter( &$image, $filter ) { $args = explode( ',', $filter ); $filter = array_shift( $args ); @@ -461,14 +479,19 @@ if ( ! class_exists( 'Gif_Image' ) ) { } else { $offset_x = 0; $offset_y = 0; - $n_width = round( $this->frame_array[$index]->width * $this->resize_ratios[0] ) ? : 1; - $n_height = round( $this->frame_array[$index]->height * $this->resize_ratios[1] ) ? : 1; + if ( $this->fit ) { + $n_width = $this->new_width; + $n_height = $this->new_height; + } else { + $n_width = round( $this->frame_array[$index]->width * $this->resize_ratios[0] ) ? : 1; + $n_height = round( $this->frame_array[$index]->height * $this->resize_ratios[1] ) ? : 1; + } $s_width = $this->frame_array[$index]->width; $s_height = $this->frame_array[$index]->height; } - if ( 0 == $n_width ) $n_width = 1; - if ( 0 == $n_height ) $n_height = 1; + if ( 0 >= $n_width ) $n_width = 1; + if ( 0 >= $n_height ) $n_height = 1; if ( 0 >= $s_width ) $s_width = 1; if ( 0 >= $s_height ) $s_height = 1; @@ -522,28 +545,26 @@ if ( ! class_exists( 'Gif_Image' ) ) { $hd = $offset = 13 + pow( 2, ( ord( $str_img[10] ) & 7 ) + 1 ) * 3; $palet = ''; $i_hd = 0; - $m_off = 0; for ( $i = 13; $i < $offset; $i++ ) $palet .= $str_img[$i]; $str_max_len = strlen( $str_img ); - if ( $this->frame_array[$index]->transp ) { - while ( self::GIF_EXT_GRAPHIC_CONTROL != ord( $str_img[ $offset + $m_off ] ) ) { - $m_off++; - if ( ( $offset + $m_off + 4 ) > $str_max_len ) - httpdie( '400 Bad Request', "unable to reprocess the image frame" ); - } - $str_img[ $offset + $m_off + 2 ] = $this->gn_fld[ $index ]; - $str_img[ $offset + $m_off + 3 ] = $this->dl_frmf[ $index ]; - $str_img[ $offset + $m_off + 4 ] = $this->dl_frms[ $index ]; - } - while ( self::GIF_BLOCK_IMAGE_DESCRIPTOR != ord( $str_img[ $offset ] ) ) { - $offset++; - $i_hd++; - if ( ( $offset + 9 ) > $str_max_len ) - httpdie( '400 Bad Request', "unable to reprocess the image frame" ); + if ( self::GIF_EXT_GRAPHIC_CONTROL == ord( $str_img[ $offset + 1 ] ) && + $this->frame_array[$index]->transp ) { + $str_img[ $offset + 3 ] = $this->gn_fld[ $index ]; + $str_img[ $offset + 4 ] = $this->dl_frmf[ $index ]; + $str_img[ $offset + 5 ] = $this->dl_frms[ $index ]; + } + $sum = 2; + while ( 0x00 != ( $lc_i = ord( $str_img[ $offset + $sum ] ) ) ) + $sum += $lc_i + 1; + $offset += ( $sum + 1 ); + $i_hd += ( $sum + 1 ); + if ( ( $offset + 10 ) > $str_max_len ) { + $this->error_and_die( '400 Bad Request', 'unable to reprocess the image frame' ); + } } $str_img[ $offset + 1 ] = $this->frame_array[ $index ]->off_xy[0]; @@ -551,14 +572,14 @@ if ( ! class_exists( 'Gif_Image' ) ) { $str_img[ $offset + 3 ] = $this->frame_array[ $index ]->off_xy[2]; $str_img[ $offset + 4 ] = $this->frame_array[ $index ]->off_xy[3]; $str_img[ $offset + 9 ] = chr( $str_img[ $offset + 9 ] | 0x80 | ( ord( $str_img[10] ) & 0x7 ) ); - $ms1 = substr( $str_img, $hd, $i_hd + 10 ); + $ms1 = substr( $str_img, $hd, $i_hd + 10 ); $ms1 = $this->frame_array[$index]->gr_mod . $ms1; return $ms1 . $palet . substr( substr( $str_img, $offset + 10 ), 0, -1 ); } - private function setheight( $args, $upscale = false ) { + private function set_height( $args, $upscale = false ) { if ( substr( $args, -1 ) == '%' ) $this->new_height = round( $this->int_h * abs( intval( $args ) ) / 100 ); else @@ -572,7 +593,7 @@ if ( ! class_exists( 'Gif_Image' ) ) { return; } // sane limit when upscaling, defaults to 1000 - if ( $this->new_height > $this->int_h && $upscale && $this->new_height > PHOTON__UPSCALE_MAX_PIXELS ) { + if ( $this->new_height > $this->int_h && $upscale && $this->new_height > $this->upscale_max_pixels ) { // if the sizes are too big, then we serve the original size $this->new_width = $this->int_w; $this->new_height = $this->int_h; @@ -587,7 +608,7 @@ if ( ! class_exists( 'Gif_Image' ) ) { $this->crop_height = $this->new_height; } - private function setwidth( $args, $upscale = false ) { + private function set_width( $args, $upscale = false ) { if ( '%' == substr( $args, -1 ) ) $this->new_width = round( $this->int_w * abs( intval( $args ) ) / 100 ); else @@ -602,7 +623,7 @@ if ( ! class_exists( 'Gif_Image' ) ) { } // Sane limit when upscaling, defaults to 1000 - if ( $this->new_width > $this->int_w && $upscale && $this->new_width > PHOTON__UPSCALE_MAX_PIXELS ) { + if ( $this->new_width > $this->int_w && $upscale && $this->new_width > $this->upscale_max_pixels ) { // if the sizes are too big, then we serve the original size $this->new_width = $this->int_w; $this->new_height = $this->int_h; @@ -640,8 +661,8 @@ if ( ! class_exists( 'Gif_Image' ) ) { // check that if we have made it bigger than the images original size, that we remain with bounds if ( $this->new_width >= $this->int_w && $this->new_height >= $this->int_h ) { - if ( ( $this->new_width > PHOTON__UPSCALE_MAX_PIXELS ) || - ( $this->new_height > PHOTON__UPSCALE_MAX_PIXELS ) ) { + if ( ( $this->new_width > $this->upscale_max_pixels ) || + ( $this->new_height > $this->upscale_max_pixels ) ) { $this->new_width = $this->int_w; $this->new_height = $this->int_h; } @@ -656,7 +677,6 @@ if ( ! class_exists( 'Gif_Image' ) ) { $this->new_height = $this->int_h; return; } - list( $end_w, $end_h ) = explode( ',', $args ); $end_w = abs( intval( $end_w ) ); @@ -672,21 +692,22 @@ if ( ! class_exists( 'Gif_Image' ) ) { if ( $original_aspect >= $new_aspect ) { $this->new_height = $end_h; - $this->new_width = $this->int_w / ( $this->int_h / $end_h ); + $this->new_width = round( $this->int_w / ( $this->int_h / $end_h ) ); // check we haven't overstepped the width if ( $this->new_width > $end_w ) { - $this->new_height = $this->new_height - round( ( $this->new_width - $end_w ) * $new_aspect ); $this->new_width = $end_w; + $this->new_height = round( $this->int_h / ( $this->int_w / $end_w ) ); } } else { $this->new_width = $end_w; - $this->new_height = $this->int_h / ( $this->int_w / $end_w ); + $this->new_height = round( $this->int_h / ( $this->int_w / $end_w ) ); // check we haven't overstepped the height if ( $this->new_height > $end_h ) { - $this->new_width = $this->new_width - round( ( $this->new_height - $end_h ) * $new_aspect ); $this->new_height = $end_h; + $this->new_width = round( $this->int_w / ( $this->int_h / $end_h ) ); } } + $this->fit = true; } } @@ -697,7 +718,6 @@ if ( ! class_exists( 'Gif_Image' ) ) { $this->new_height = $this->int_h; return; } - $args = explode( ',', $args ); // if we don't have the correct number of args, default @@ -732,6 +752,32 @@ if ( ! class_exists( 'Gif_Image' ) ) { $this->crop = true; } + private function crop_offset( $args ) { + // if the args are malformed, default to the original size + if ( false === strpos( $args, ',' ) ) { + $this->new_width = $this->int_w; + $this->new_height = $this->int_h; + return; + } + $args = explode( ',', $args ); + + // if we don't have the correct number of args, default + if ( count( $args ) != 4 ) { + $this->new_width = $this->int_w; + $this->new_height = $this->int_h; + return; + } + + $this->crop_width = max( 0, min( $this->int_w, intval( $args[2] ) ) ); + $this->crop_height = max( 0, min( $this->int_h, intval( $args[3] ) ) ); + $this->s_x = intval( $args[0] ); + $this->s_y = intval( $args[1] ); + + $this->new_width = $this->crop_width; + $this->new_height = $this->crop_height; + $this->crop = true; + } + private function resize_and_crop( $args ) { // if the args are malformed, default to the original size if ( false === strpos( $args, ',' ) ) { @@ -756,39 +802,77 @@ if ( ! class_exists( 'Gif_Image' ) ) { // If the original and new images are proportional (no cropping needed), just do a standard resize if ( $ratio_orig == $ratio_end ) { - $this->setwidth( $end_w, true ); + $this->set_width( $end_w, true ); } else { - $aspect_ratio = $this->int_w / $this->int_h; - - if ( $end_w >= $this->int_w && $end_h >= $this->int_h ) { - $this->new_width = max( $end_w, $this->int_w ); - $this->new_height = max( $end_h, $this->int_h ); - if ( ( $this->new_width > PHOTON__UPSCALE_MAX_PIXELS ) || - ( $this->new_height > PHOTON__UPSCALE_MAX_PIXELS ) ) { - $this->new_width = $this->int_w; - $this->new_height = $this->int_h; - } - } else { - $this->new_width = $end_w; - $this->new_height = $end_h; - } + if ( $end_w >= $this->int_w && $end_h >= $this->int_h ) { + $this->new_width = max( $end_w, $this->int_w ); + $this->new_height = max( $end_h, $this->int_h ); + } else { + $this->new_width = $end_w; + $this->new_height = $end_h; + } + + if ( ! $this->new_width ) + $this->new_width = intval( $this->new_height * $ratio_orig ); + if ( ! $this->new_height ) + $this->new_height = intval( $this->new_width / $ratio_orig ); + + // Check if the width or height are too large, if they are then default to original size + if ( ( ( $this->new_width > $this->int_w ) && ( $this->new_width > $this->upscale_max_pixels ) ) || + ( $this->new_height > $this->int_h && ( $this->new_height > $this->upscale_max_pixels ) ) ) { + $this->new_width = $this->int_w; + $this->new_height = $this->int_h; + return; + } + + $size_ratio = max( $this->new_width / $this->int_w, $this->new_height / $this->int_h ); + $this->crop_width = min( ceil( $this->new_width / $size_ratio ), $this->int_w ); + $this->crop_height = min( ceil( $this->new_height / $size_ratio ), $this->int_h ); + + $this->s_x = round( ( $this->int_w - $this->crop_width ) / 2 ); + $this->s_y = round( ( $this->int_h - $this->crop_height ) / 2 ); + $this->crop = true; + } + } - if ( ! $this->new_width ) - $this->new_width = intval( $this->new_height * $aspect_ratio ); - if ( ! $this->new_height ) - $this->new_height = intval( $this->new_width / $aspect_ratio ); + public function process_image( $new_w, $new_h, $crop, $s_x, $s_y, $crop_w, $crop_h ) { + // if the gif image has an invalid size for either value, do not process it + if ( 1 > $this->int_w || 1 > $this->int_h ) + return false; - $size_ratio = max( $this->new_width / $this->int_w, $this->new_height / $this->int_h ); - $this->crop_width = min( ceil( $this->new_width / $size_ratio ), $this->int_w ); - $this->crop_height = min( ceil( $this->new_height / $size_ratio ), $this->int_h ); + $this->new_width = $new_w; + $this->new_height = $new_h; + $this->crop = $crop; + $this->s_x = $s_x; + $this->s_y = $s_y; + $this->crop_width = $crop_w; + $this->crop_height = $crop_h; + + // we fail if the image size is too small + if ( 1 > $this->new_width || 1 > $this->new_height ) + return false; + + if ( $this->crop ) { + $this->resize_ratios[0] = $this->new_width / $this->crop_width; + $this->resize_ratios[1] = $this->new_height / $this->crop_height; + } else { + $this->resize_ratios[0] = $this->new_width / $this->int_w; + $this->resize_ratios[1] = $this->new_height / $this->int_h; + } - $this->s_x = round( ( $this->int_w - $this->crop_width ) / 2 ); - $this->s_y = round( ( $this->int_h - $this->crop_height ) / 2 ); - $this->crop = true; + $this->image_data = ''; + for ( $i = 0; $i < $this->frame_count; $i++ ) { + $this->image_data .= $this->repack_frame( $this->process_frame( $this->get_frame_image( $i ), $i ), $i ); + $this->frame_array[ $i ] = null; } + + return true; } - public function process_image() { + public function process_image_functions( $upscale_max_pixels ) { + if ( isset( $upscale_max_pixels ) ) + $this->upscale_max_pixels = $upscale_max_pixels; + // if the gif image has an invalid size for either value, do not process it if ( 1 > $this->int_w || 1 > $this->int_h ) return false; @@ -796,7 +880,7 @@ if ( ! class_exists( 'Gif_Image' ) ) { // we need at least one action to perform otherwise we should just send the original if ( 0 == count( $this->pre_process_actions ) ) { $this->pre_process_actions[] = Array ( - 'func_name' => 'setWidth', + 'func_name' => 'set_width', 'params' => $this->int_w, ); } @@ -805,15 +889,13 @@ if ( ! class_exists( 'Gif_Image' ) ) { // do the pre-processing functions foreach ( $this->pre_process_actions as $action ) { $this->$action[ 'func_name' ]( $action[ 'params' ] ); - if ( 'crop' == $action[ 'func_name' ] ) + if ( 'crop' == $action[ 'func_name' ] || 'crop_offset' == $action[ 'func_name' ] ) $cropped = true; } - // zoom functionality is not supported with the 'crop' function - if ( ! $cropped ) { - // check if zoom needs to be run, and run if neccessary - if ( isset( $_GET['zoom'] ) ) - $this->zoom( $_GET['zoom'] ); + // zoom functionality is not supported with the 'crop-style' functions + if ( ! $cropped && isset( $_GET['zoom'] ) && $this->zoom_enabled ) { + $this->zoom( $_GET['zoom'] ); } // we fail if the image size is too small @@ -829,13 +911,15 @@ if ( ! class_exists( 'Gif_Image' ) ) { } $this->image_data = ''; - for ( $i = 0; $i < $this->frame_count; $i++ ) + for ( $i = 0; $i < $this->frame_count; $i++ ) { $this->image_data .= $this->repack_frame( $this->process_frame( $this->get_frame_image( $i ), $i ), $i ); + $this->frame_array[ $i ] = null; + } return true; } - public function get_imageblob() { + public function get_image_blob() { $gm = $this->gif_header; $gm[10] = $gm[10] & 0x7F; $i_bytes = $this->int_raw( round( ( $this->crop ? $this->crop_width : $this->int_w ) * $this->resize_ratios[0] ) ? : 1 ); @@ -845,6 +929,8 @@ if ( ! class_exists( 'Gif_Image' ) ) { $gm[8] = $i_bytes[0]; $gm[9] = $i_bytes[1]; + $this->image_data = $gm . $this->g_mod . $this->image_data; + $con = ''; if ( strlen( $this->g_mode ) ) $con = $this->g_mode . "\x3B"; @@ -854,7 +940,14 @@ if ( ! class_exists( 'Gif_Image' ) ) { if ( ! $this->au ) $con = "\x21\xFE\x0Eautomattic_inc\x00" . $con; - return $gm . $this->g_mod . $this->image_data . ( iconv_strlen( $con ) >= 19 ? $con : "\x21" ); + $this->image_data .= ( strlen( $con ) >= 19 ? $con : "\x21" ); + + if ( ! headers_sent() ) { + header( 'ETag: "' . substr( md5( strlen( $this->image_data ) . '.' . time() ), 0, 16 ) . '"' ); + header( 'Content-Length: ' . strlen( $this->image_data ) ); + } + + return $this->image_data; } public function add_function( $function_name, $arguments ) { @@ -870,5 +963,21 @@ if ( ! class_exists( 'Gif_Image' ) ) { ); } } + + public function get_frame_count() { + return $this->frame_count; + } + + public function get_image_width() { + return intval( $this->int_w ); + } + + public function get_image_height() { + return intval( $this->int_h ); + } + + public function disable_zoom() { + $this->zoom_enabled = false; + } } } diff --git a/libjpeg.php b/libjpeg.php index 9eb6826..64fbbf5 100644 --- a/libjpeg.php +++ b/libjpeg.php @@ -1,6 +1,6 @@ <?php -function get_jpeg_header_data( &$buff, $buff_len, $want=null ) { +function get_jpeg_header_data( &$buff, $buff_len, $want=null ) { $data = buffer_read( $buff, $buff_len, 2, true ); // Read the first two characters // Check that the first two characters are 0xFF 0xDA (SOI - Start of image) if ( $data != "\xFF\xD8" ) { @@ -13,13 +13,13 @@ function get_jpeg_header_data( &$buff, $buff_len, $want=null ) { // NO FF found - close file and return - JPEG is probably corrupted return false; } - // Cycle through the file until, one of: + // Cycle through the file until, one of: // 1) an EOI (End of image) marker is hit, // 2) we have hit the compressed image data (no more headers are allowed after data) // 3) or end of file is hit $headerdata = array(); $hit_compressed_image_data = FALSE; - while ( ( $data{1} != "\xD9" ) && ( !$hit_compressed_image_data) && ( $data != '' ) ) { + while ( ( $data{1} != "\xD9" ) && ( !$hit_compressed_image_data) && ( $data != '' ) ) { // Found a segment to look at. // Check that the segment marker is not a Restart marker - restart markers don't have size or data after them if ( ( ord($data{1}) < 0xD0 ) || ( ord($data{1}) > 0xD7 ) ) { @@ -32,11 +32,11 @@ function get_jpeg_header_data( &$buff, $buff_len, $want=null ) { $segdata = buffer_read( $buff, $buff_len, $decodedsize['size'] - 2 ); // Store the segment information in the output array if ( !$want || $want == ord($data{1}) ) { - $headerdata[] = (object)array( + $headerdata[] = (object)array( "SegType" => ord($data{1}), "SegName" => $GLOBALS[ "JPEG_Segment_Names" ][ ord($data{1}) ], "SegDesc" => $GLOBALS[ "JPEG_Segment_Descriptions" ][ ord($data{1}) ], - "SegData" => $segdata + "SegData" => $segdata ); } } @@ -172,7 +172,7 @@ $GLOBALS[ "JPEG_Segment_Descriptions" ] = array( * and how it handles identify -verbose $filename to present you with a Quality * number. This number should be considered approximate. It's essentially * based upon the numbers used to perform compression on the original image - * data... + * data... * * See: http://www.obrador.com/essentialjpeg/headerinfo.htm * See: http://www.impulseadventure.com/photo/jpeg-quantization.html @@ -192,7 +192,7 @@ function get_jpeg_quality( &$buff, $buff_len = null ) { 143, 139, 132, 128, 125, 119, 115, 108, 104, 99, 94, 90, 84, 79, 74, 70, 64, 59, 55, 49, 45, 40, 34, 30, 25, 20, 15, 11, 6, 4, - 0 + 0 ), // hash 'sums' => array ( 32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104, @@ -239,18 +239,18 @@ function get_jpeg_quality( &$buff, $buff_len = null ) { ), // sums ), // single ); // tables - + if ( ! isset( $buff_len ) ) $buff_len = strlen( $buff ); $headers = get_jpeg_header_data( $buff, $buff_len, 0xDB ); if ( !is_array( $headers ) || !count( $headers ) ) return 100; - $header = $headers[0]; + $header = $headers[0]; $quality = 0; if ( strlen($header->SegData) > 128 ) { $entry = array( 0 => array(), 1 => array() ); - foreach ( str_split( substr( $header->SegData, 1, 64) ) as $chr ) + foreach ( str_split( substr( $header->SegData, 1, 64) ) as $chr ) $entry[0][] = ord($chr); foreach ( str_split( substr( $header->SegData, -64) ) as $chr ) $entry[1][] = ord($chr); @@ -270,8 +270,60 @@ function get_jpeg_quality( &$buff, $buff_len = null ) { for( $i = 0; $i <= 100; $i++ ) { if ( ( $qvalue < $tables[$table]['hash'][$i] ) && ( $sum < $tables[$table]['sums'][$i] ) ) continue; - return $i; + return min( $i+1, 100 ); } return 100; // go with a safe value } +function exifrotate( $file, $image, $strip ) { + if ( ! function_exists( 'exif_read_data' ) ) + return; + + if ( ! in_array( $strip, array( 'all', 'info' ) ) ) + return; + + $exif = @exif_read_data( $file ); + + if ( ! isset( $exif[ 'Orientation' ] ) ) + return; + + $degrees = 0; + switch( $exif[ 'Orientation' ] ) { + case 3: + $degrees = 180; + break; + case 6: + $degrees = 90; + break; + case 8: + $degrees = 270; + break; + } + + if ( $degrees ) { + $image->rotateImage( 'black', $degrees ); + // We need to write again, since we wrote it earlier to read EXIF data + $image->write( $file ); + } +} + +function jpegoptim( $file, $strip = false ) { + if ( false === JPEGOPTIM ) + return; + + $cmd = JPEGOPTIM . ' -T0.0 --all-progressive'; + switch ( $strip ) { + case 'all': + $cmd .= ' -f --strip-all'; + break; + case 'info': + $cmd .= ' -f --strip-com --strip-exif --strip-iptc'; + break; + case 'color': + $cmd .= ' -f --strip-icc'; + break; + } + $cmd .= " -p $file"; + exec( $cmd ); +} + -- GitLab