<?php define( 'PHOTON__ALLOW_ANY_EXTENSION', 1 ); define( 'PHOTON__ALLOW_QUERY_STRINGS', 2 ); require dirname( __FILE__ ) . '/plugin.php'; if ( file_exists( dirname( __FILE__ ) . '/../config.php' ) ) require dirname( __FILE__ ) . '/../config.php'; else if ( file_exists( dirname( __FILE__ ) . '/config.php' ) ) require dirname( __FILE__ ) . '/config.php'; // Explicit Configuration $allowed_functions = apply_filters( 'allowed_functions', array( // 'q' => RESERVED // 'zoom' => global resolution multiplier (argument filter) 'h' => 'setheight', // done 'w' => 'setwidth', // done 'crop' => 'crop', // done 'resize' => 'resize_and_crop', // done 'fit' => 'fit_in_box', // done 'lb' => 'letterbox', // done 'ulb' => 'unletterbox', // compat 'filter' => 'filter', // compat 'brightness' => 'brightness', // compat 'contrast' => 'contrast', // compat 'colorize' => 'colorize', // compat 'smooth' => 'smooth', // compat ) ); unset( $allowed_functions['q'] ); $allowed_types = apply_filters( 'allowed_types', array( 'gif', 'jpg', 'jpeg', 'png', ) ); $disallowed_file_headers = apply_filters( 'disallowed_file_headers', array( '8BPS', ) ); // Expects a trailing slash $tmpdir = apply_filters( 'tmpdir', '/tmp/' ); $remote_image_max_size = apply_filters( 'remote_image_max_size', 55 * 1024 * 1024 ); /* 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 ); require dirname( __FILE__ ) . '/libjpeg.php'; // Implicit configuration if ( file_exists( '/usr/local/bin/optipng' ) ) define( 'OPTIPNG', '/usr/local/bin/optipng' ); else define( 'OPTIPNG', false ); if ( file_exists( '/usr/local/bin/jpegoptim' ) ) define( 'JPEGOPTIM', '/usr/local/bin/jpegoptim' ); else define( 'JPEGOPTIM', false ); /** * zoom - ( "zoom" function via the uri ) - Intended for improving visuals * on high pixel ratio devices and browsers when zoomed in. No zoom in crop. * * Valid zoom levels are 1,1.5,2-10. */ function zoom( $arguments, $function_name, $image ) { static $zoom; if ( !isset( $zoom ) ) { if ( isset( $_GET['zoom'] ) ) { $zoom = floatval( $_GET['zoom'] ); // Clamp to 1-10 $zoom = max( 1, $zoom ); $zoom = min( 10, $zoom ); if ( $zoom < 2 ) { // Round UP to the nearest half $zoom = ceil( $zoom * 2 ) / 2; } else { // Round UP to the nearest integer $zoom = ceil( $zoom ); } } else { $zoom = false; } } if ( $zoom <= 1 ) return $arguments; $w = $image->getimagewidth(); $h = $image->getimageheight(); switch ( $function_name ) { case 'setheight' : case 'setwidth' : $new_arguments = $arguments * $zoom; if ( substr( $arguments, -1 ) == '%' ) $new_arguments .= '%'; break; case 'fit_in_box' : case 'resize_and_crop' : list( $width, $height ) = explode( ',', $arguments ); $new_width = $width * $zoom; $new_height = $height * $zoom; // Avoid dimensions larger than original. while ( ( $new_width > $w || $new_height > $h ) && $zoom > 1 ) { // Step down to the next lower zoom level. if ( $zoom > 2 ) { $zoom -= 1; } else { $zoom -= 0.5; } $new_width = $width * $zoom; $new_height = $height * $zoom; } $new_arguments = "$new_width,$new_height"; break; default : $new_arguments = $arguments; } return $new_arguments; } add_filter( 'arguments', 'zoom', 10, 3 ); /** * crop - ("crop" function via the uri) - crop an image * * @param (resource)image the source gd image resource * @param (string)args "x,y,w,h" widh each csv column being /^[0-9]+(px)?$/ * all values in percentages by default, but can be set * to absolute pixel values by specifying px ie 25px * * @return (resource)image the resulting image gd resource * **/ function crop( &$image, $args ) { $args = explode( ',', $args ); $w = $image->getimagewidth(); $h = $image->getimageheight(); if ( substr( $args[2], -2 ) == 'px' ) $new_w = max( 0, min( $w, intval( $args[2] ) ) ); else $new_w = round( $w * abs( intval( $args[2] ) ) / 100 ); if ( substr( $args[3], -2 ) == 'px' ) $new_h = max( 0, min( $h, intval( $args[3] ) ) ); else $new_h = round( $h * abs( intval( $args[3] ) ) / 100 ); if ( substr( $args[0], -2 ) == 'px' ) $s_x = intval( $args[0] ); else $s_x = round( $w * abs( intval( $args[0] ) ) / 100 ); if ( substr( $args[1], -2 ) == 'px' ) $s_y = intval( $args[1] ); else $s_y = round( $h * abs( intval( $args[1] ) ) / 100 ); $image->cropimage( $new_w, $new_h, $s_x, $s_y ); } /** * setheight - ( "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 % * @param boolean $upscale Whether to allow upscaling or not, defaults to not allowing. * * @return (resource) the resulting gs image resource **/ function setheight( &$image, $args, $upscale = false ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); if ( substr( $args, -1 ) == '%' ) $new_height = round( $h * abs( intval( $args ) ) / 100 ); else $new_height = intval( $args ); // New height can't be calculated, then bail if ( ! $new_height ) return; // 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 if ( $new_height > $h && $upscale && $new_height > PHOTON__UPSCALE_MAX_PIXELS ) return; $ratio = $h / $new_height; $new_w = round( $w / $ratio ); $new_h = round( $h / $ratio ); $s_x = $s_y = 0; $image->scaleimage( $new_w, $new_h ); } /** * setwidth - ( "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 % * @param boolean $upscale Whether to allow upscaling or not, defaults to not allowing. * * @return (resource) the resulting gs image resource **/ function setwidth( &$image, $args, $upscale = false ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); if ( substr( $args, -1 ) == '%' ) $new_width = round( $w * abs( intval( $args ) ) / 100 ); else $new_width = intval( $args ); // New width can't be calculated, then bail if ( ! $new_width ) return; // 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 if ( $new_width > $w && $upscale && $new_width > PHOTON__UPSCALE_MAX_PIXELS ) return; $ratio = $w / $new_width; $new_w = round( $w / $ratio ); $new_h = round( $h / $ratio ); $s_x = $s_y = 0; $image->scaleimage( $new_w, $new_h ); } /** * fit_in_box - ( "fit" function via the uri ) - resize the image to fit it in the dimensions provided, maintaining its aspect ratio * * @param (resource)image the source gd image resource * @param (string)args "(int),(int)" width and height of the box * * @return (resource) the resulting gs image resource **/ function fit_in_box( &$image, $args ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); list( $end_w, $end_h ) = explode( ',', $args ); $end_w = abs( intval( $end_w ) ); $end_h = abs( intval( $end_h ) ); if ( ( $w == $end_w && $h == $end_h ) || ! $end_w || ! $end_h || ( $w < $end_w && $h < $end_h ) ) { return; } $image->scaleimage( $end_w, $end_h, true ); } /** * 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 * * @param (resource) image the source gd image resource * @param (string) args "w,h" width,height in pixels * * @return (resource)image the resulting image gd resource * **/ function resize_and_crop( &$image, $args ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); list( $end_w, $end_h ) = explode( ',', $args ); $end_w = (int) $end_w; $end_h = (int) $end_h; if ( 0 == $end_w || 0 == $end_h ) 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 ); // If we need to crop off the sides elseif ( $ratio_orig > $ratio_end ) { setheight( $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 ); $y = floor( ( $image->getimageheight() - $end_h ) / 2 ); crop( $image, "0px,{$y}px,{$end_w}px,{$end_h}px" ); } } /** * unletterbox - ("ulb" function via the uri) - originally by Demitrious K. * * Removes black letterboxing bands from the top and bottom of an image * * $param (resource) img the source gd image resource * $param (string) args true is the only acceptable argument * * @return (resource)image the resulting image gd resource **/ function unletterbox( &$img, $args ) { if ( 'true' !== $args ) return $img; gmagick_to_gd( $img ); // rgb values averaged per pixel, and then those averaged for the entire row $max_value_considered_black = 3; $width = imagesx( $img ); $height = imagesy( $img ); $first_nonblack_line = null; for( $h=0; $h < $height; $h++ ) { $line_value = 0; for( $w=0; $w < $width; $w++ ) { $rgb = imagecolorat( $img, $w, $h ); $r = ( $rgb >> 16 ) & 0xFF; $g = ( $rgb >> 8 ) & 0xFF; $b = $rgb & 0xFF; $line_value += round( ( $r + $g + $b ) / 3 ); } if ( round( $line_value/$width ) > $max_value_considered_black ) { $first_nonblack_line = $h + 1; break; } } if ( ! $first_nonblack_line ) { gd_to_gmagick( $img ); return; } $last_nonblack_line = null; for( $h = $height - 1; $h >= 0; $h-- ) { $line_value = 0; for( $w=0; $w < $width; $w++ ) { $rgb = imagecolorat( $img, $w, $h ); $r = ( $rgb >> 16 ) & 0xFF; $g = ( $rgb >> 8 ) & 0xFF; $b = $rgb & 0xFF; $line_value += round( ( $r + $g + $b ) / 3 ); } if ( round( $line_value / $width ) > $max_value_considered_black ) { $last_nonblack_line = $h; break; } } if ( ! $last_nonblack_line || $last_nonblack_line <= $first_nonblack_line ) { gd_to_gmagick( $img ); return; } $args = implode( ',', array( '0px', $first_nonblack_line . 'px', $width . 'px', ( $last_nonblack_line - $first_nonblack_line ) . 'px', ) ); gd_to_gmagick( $img ); crop( $img, $args ); } // {{{ filter($image,$filter) /** * Box resizes an image and fills the background with black * * @param object $image * @param array $args */ function letterbox( &$image, $args ) { $w = $image->getimagewidth(); $h = $image->getimageheight(); list( $end_w, $end_h ) = explode( ',', $args ); $end_w = abs( intval( $end_w ) ); $end_h = abs( intval( $end_h ) ); if ( ( $w == $end_w && $h == $end_h ) || ! $end_w || ! $end_h || ( $w < $end_w && $h < $end_h ) ) { return; } $image->scaleimage( $end_w, $end_h, true ); $new_w = $image->getimagewidth(); $new_h = $image->getimageheight(); $border_h = round( ( $end_h - $new_h ) / 2 ); $border_w = round( ( $end_w - $new_w ) / 2 ); if ( $border_h > PHOTON__UPSCALE_MAX_PIXELS || $border_w > PHOTON__UPSCALE_MAX_PIXELS ) { return; } $image->borderimage('#000', $border_w, $border_h ); // Since we create the borders with rounded values // we have to chop any excessive pixels off. $crop_x = $border_w * 2 + $new_w - $end_w; $crop_y = $border_h * 2 + $new_h - $end_h; if ( $crop_x || $crop_y ) $image->cropimage( $end_w, $end_h, $crop_x, $crop_y ); } /** * filter - ("filter" via the uri) - originally by Alex M. * * Performs various filters on the image such as grayscale * This is only for filters that accept no args * * @param resource $image The source GD image resource * @param string $filter The filter name * @return resource The resulting GD imageresource **/ function filter( &$image, $filter ) { $args = explode( ',', $filter ); $filter = array_shift( $args ); gmagick_to_gd( $image ); switch ( $filter ) { case 'negate': do_action( 'bump_stats', 'filter_negate' ); imagefilter( $image, IMG_FILTER_NEGATE ); break; case 'grayscale': case 'greyscale': do_action( 'bump_stats', 'filter_grayscale' ); imagefilter( $image, IMG_FILTER_GRAYSCALE ); break; case 'sepia': do_action( 'bump_stats', 'filter_sepia' ); imagefilter( $image, IMG_FILTER_GRAYSCALE ); imagefilter( $image, IMG_FILTER_COLORIZE, 90, 60, 40 ); break; case 'edgedetect': do_action( 'bump_stats', 'filter_edgedetect' ); imagefilter( $image, IMG_FILTER_EDGEDETECT ); break; case 'emboss': do_action( 'bump_stats', 'filter_emboss' ); imagefilter( $image, IMG_FILTER_EMBOSS ); break; case 'blurgaussian': do_action( 'bump_stats', 'filter_blurgaussian' ); imagefilter( $image, IMG_FILTER_GAUSSIAN_BLUR ); break; case 'blurselective': do_action( 'bump_stats', 'filter_blurselective' ); imagefilter( $image, IMG_FILTER_SELECTIVE_BLUR ); break; case 'meanremoval': do_action( 'bump_stats', 'filter_meanremoval' ); imagefilter( $image, IMG_FILTER_MEAN_REMOVAL ); break; } gd_to_gmagick( $image ); } // }}} /** * brightness - ("brightness" via the uri) - originally by Alex M. * * Adjusts image brightness (-255 through 255) * * @param resource $original The source GD image resource * @param resource $brightness The brightness adjustment value * @return resource The resulting GD imageresource **/ function brightness( &$image, $brightness ) { $brightness = (int) $brightness; gmagick_to_gd( $image ); imagefilter( $image, IMG_FILTER_BRIGHTNESS, $brightness ); gd_to_gmagick( $image ); } /** * contrast - ("contrast" via the uri) - originally by Alex M. * * Adjusts image contrast (-100 through 100) * * @param resource $original The source GD image resource * @param resource $contrast The contrast adjustment value * @return resource The resulting GD imageresource **/ function contrast( &$image, $contrast ) { $contrast = (int) $contrast; gmagick_to_gd( $image ); imagefilter( $image, IMG_FILTER_CONTRAST, $contrast * -1 ); // Make +value increase contrast gd_to_gmagick( $image ); } /** * colorize - ("colorize" via the uri) - originally by Alex M. * * Hues the image to a certain color: red,green,blue * * @param resource $original The source GD image resource * @param resource $colors A comma seperated rgb value (255,255,255 = white) * @return resource The resulting GD imageresource **/ function colorize( &$image, $colors ) { $colors = explode( ',', $colors ); $color = array_map( 'intval', $colors ); $red = ( !empty($color[0]) ) ? $color[0] : 0; $green = ( !empty($color[1]) ) ? $color[1] : 0; $blue = ( !empty($color[2]) ) ? $color[2] : 0; gmagick_to_gd( $image ); imagefilter( $image, IMG_FILTER_COLORIZE, $red, $green, $blue ); gd_to_gmagick( $image ); } /** * smooth - ("smooth" via the uri) - originally by Alex M. * * Adjusts image smoothness * * @param resource $original The source GD image resource * @param resource $smoothness The smoothness adjustment value * @return resource The resulting GD imageresource **/ function smooth( &$image, $smoothness ) { gmagick_to_gd( $image ); imagefilter( $image, IMG_FILTER_SMOOTH, (float) $smoothness ); gd_to_gmagick( $image ); } function httpdie( $code='404 Not Found', $message='Error: 404 Not Found' ) { $numerical_error_code = preg_replace( '/[^\\d]/', '', $code ); do_action( 'bump_stats', "http_error-$numerical_error_code" ); header( 'HTTP/1.1 ' . $code ); die( $message ); } function gmagick_to_gd( &$image ) { global $type; if ( $type == "jpeg" ) $image->setcompressionquality( 100 ); $image = imagecreatefromstring( $image->getimageblob() ); } function gd_to_gmagick( &$image ) { global $type; ob_start(); switch( $type ) { case 'gif': imagegif( $image, null ); break; case 'png': imagepng( $image, null, 0 ); break; default: imagejpeg( $image, null, 100 ); break; } $image = new Gmagick(); $image->readimageblob( ob_get_clean() ); } function fetch_raw_data( $url, $timeout = 10, $connect_timeout = 2 ) { $ch = curl_init( $url ); curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $connect_timeout ); curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout ); curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true ); curl_setopt( $ch, CURLOPT_MAXREDIRS, 3 ); curl_setopt( $ch, CURLOPT_USERAGENT, 'Photon/1.0' ); curl_setopt( $ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); curl_setopt( $ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); curl_setopt( $ch, CURLOPT_WRITEFUNCTION, function( $curl_handle, $data ) { global $raw_data, $raw_data_size, $remote_image_max_size; $data_size = strlen( $data ); $raw_data .= $data; $raw_data_size += $data_size; if ( $raw_data_size > $remote_image_max_size ) httpdie( '400 Bad Request', "You can only process images up to $remote_image_max_size bytes." ); return $data_size; } ); return curl_exec( $ch ); } function do_a_filter( $function_name, $arguments ) { global $image, $allowed_functions, $type; if ( ! isset( $allowed_functions[$function_name] ) ) return; $function_name = $allowed_functions[$function_name]; if ( function_exists( $function_name ) && is_callable( $function_name ) ) { do_action( 'bump_stats', $function_name ); if ( 'gif' == $type ) { /* allowed functions have identical names as private members of the Gif_Image class */ $image->add_function( $function_name, $arguments ); } else { $arguments = apply_filters( 'arguments', $arguments, $function_name, $image ); $function_name( $image, $arguments ); } } } function photon_cache_headers( $expires=63115200 ) { 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' ); } $parsed = parse_url( $_SERVER['REQUEST_URI'] ); $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' : '' ) . '://'; 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 ( $origin_domain_exception & PHOTON__ALLOW_QUERY_STRINGS ) { $url .= '?' . preg_replace( '/#.*$/', '', (string) $parsed['query'] ); unset( $parsed['query'] ); } else { httpdie( '400 Bad Request', "Sorry, the parameters you provided were not valid" ); } } if ( false === filter_var( $url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED ) ) httpdie( '400 Bad Request', "Sorry, the parameters you provided were not valid" ); $raw_data = ''; $raw_data_size = 0; $fetched = fetch_raw_data( $url ); if ( ! $fetched || empty( $raw_data ) ) httpdie( '504 Gateway Timeout', 'We cannot complete this request, remote data could not be fetched' ); foreach ( $disallowed_file_headers as $file_header ) { if ( substr( $raw_data, 0, strlen( $file_header ) ) == $file_header ) httpdie( '400 Bad Request', 'Error 0002. The type of image you are trying to process is not allowed.' ); } if ( 'GIF' === substr( $raw_data, 0, 3 ) ) { require dirname( __FILE__ ) . '/libgif.php'; $image = new Gif_Image( $raw_data ); $type = 'gif'; } else { try { $image = new Gmagick(); $image->readimageblob( $raw_data ); $type = strtolower( $image->getimageformat() ); } catch ( GmagickException $e ) { httpdie( '400 Bad Request', 'We cannot complete this request, remote data was invalid' ); } } if ( ! in_array( $type, $allowed_types ) ) httpdie( '400 Bad Request', 'Error 0003. The type of image you are trying to process is not allowed.' ); if ( $type == 'jpeg' ) $quality = get_jpeg_quality( $raw_data, $raw_data_size ); else $quality = 90; unset( $raw_data ); try { // Run through all uri supplied functions which are valid and allowed foreach( $_GET as $function_name => $arguments ) { if ( !is_array( $arguments ) ) $arguments = array( $arguments ); foreach ( $arguments as $argument ) { if ( $argument === apply_filters( $function_name, $argument ) ) do_a_filter( $function_name, $argument ); } } 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" ); clearstatcache(); $save = $og - filesize( $tmp ); 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(); } 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" ); clearstatcache(); $save = $og - filesize( $tmp ); 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; } } catch ( GmagickException $e ) { httpdie( '400 Bad Request', "Sorry, the parameters you provided were not valid" ); }