Settings > Advanced > Image Resizer private $quality = 85; // default quality for jpeg/webp private $sharpen = true; // sharpen images, only applies when resampling down private $smart_crop = false; // crops image based on image entropy private $progressive = false; // progressive for jpeg/webp private $copy_icc_profile = false; // copy ICC color profile for jpeg private $allowed_width = array(100, 200, 320, 480, 640, 800, 1024, 1280, 1600, 4096, 8192, 16384); // default allowed width requests. Includes sizes for panorama. private $max_memory = 128; // max memory allowance when assigning MORE memory than in ini_get('memory_limit') // private vars private $min_resize_factor = .9; // minimum difference between resize and original for resampling to take place private $memory_tweak = 2; // tweak factor assigned to estimating required memory to resize/crop images. private $convert_legacy_cache = true; // convert and move image cache from X3.28.0 and earlier private $legacy_quality = 90; // default for converting legacy image cache (must match q from legacy cache) // object vars function object_vars(){ $debug = get_object_vars($this); foreach (array('x3_root', 'abs_path', 'cache_dir', 'cache_path') as $key) if(isset($debug[$key])) $debug[$key] = str_replace($_SERVER['DOCUMENT_ROOT'], '***', $debug[$key]); return highlight_string('
' . $this->object_vars(); $this->msg('Error', $msg, $code); } // debug private function debug(){ $render_path = $this->x3_root . DIRECTORY_SEPARATOR . 'render'; $this->render_exists = is_dir($render_path); if($this->render_exists) { $this->render_is_writeable = is_writable($render_path); $this->cache_dir_exists = is_dir($this->cache_dir); if($this->cache_dir_exists){ $this->cache_dir_is_writeable = is_writable($this->cache_dir); $this->cache_file_exists = is_file($this->cache_path); if($this->cache_file_exists) $this->cache_file_is_writeable = is_writable($this->cache_path); } } $msg = $this->object_vars(); $this->msg('Debug', $msg, 200); } private function msg($title, $msg, $code = 400){ header('Expires: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); if(function_exists('http_response_code')) http_response_code($code); exit('

' . $title . '

' . $msg); } // get parameters private function get_request(){ $request_array = explode('/render/', strtok($_SERVER['REQUEST_URI'], '?'), 2); $request = count($request_array) === 2 && !empty($request_array[1]) ? array_filter(explode('/', $request_array[1], 2)) : false; // error / must be params+path if(empty($request) || count($request) !== 2) $this->error('Invalid request parameters.'); // params $p = array(); foreach (explode('-', $request[0]) as $item) { $val = substr($item, 1); if($val) $p[$item[0]] = $val; // if $val, must be key also } // error / must have either w or c if(!isset($p['w']) && !isset($p['c'])) $this->error('Must specify either w[width] or c[crop].'); // get width $width = isset($p['w']) ? (int) $p['w'] : false; // get crop $crop = isset($p['c']) ? preg_split('/(\.|:)/', $p['c']) : false; if($crop) { $ratio = count($crop) === 2 ? array_filter(array_map('intval', $crop)) : array(); if(count($ratio) !== 2) $this->error('Crop must be in a valid width:height format with values larger than 0.'); // round crop ratio down to lowest full numbers (4.2 -> 2.1, 3.9 -> 1.3, etc) function gcd($a, $b) { return $b ? gcd($b, $a % $b) : $a; } $gcd_num = array_reduce($ratio, 'gcd'); if($gcd_num != 1) foreach ($ratio as $key => $val) $ratio[$key] = $val / $gcd_num; $crop_ratio = $ratio[0] / $ratio[1]; if($crop_ratio > 10 || $crop_ratio < .1) $this->error('Crop ratio must be 10 or lower.'); } // set request values $this->request = array( 'width' => $width ?: false, 'crop_ratio' => $crop ? $crop_ratio : false, 'request' => implode('-', array_filter(array($width ? 'w' . $width : 0, $crop ? 'c' . $ratio[0] . '.' . $ratio[1] : 0))) ); // set paths $this->x3_root = PHP_MAJOR_VERSION >= 7 ? dirname(__DIR__, 3) : dirname(dirname(dirname(__DIR__))); $this->x3_url_path = str_replace($_SERVER['DOCUMENT_ROOT'], '', $this->x3_root); //$this->rel_path = trim(urldecode($request[1]), '/'); $this->rel_path = trim(rawurldecode($request[1]), '/'); $this->content_path = '/content/' . $this->rel_path; //$abs_path = $this->x3_root . $this->content_path; $this->abs_path = realpath($this->x3_root . $this->content_path); // <- follows symlinks if(!$this->abs_path || !is_file($this->abs_path) || strpos(dirname($this->rel_path), ':') || preg_match('/(\.\.|<|>)/', $this->rel_path)) $this->error('Invalid path or file does not exist ' . $this->rel_path . '', 404); } // set debug private function set_debug() { $request_uri = $_SERVER['REQUEST_URI']; $this->debug = (bool) strpos($request_uri, '?debug'); $this->force = $this->debug ? (bool) strpos($request_uri, '&force') : false; if($this->force) $this->debug = false; } // set cache path and get cache private function get_cache(){ $this->cache_path = $this->x3_root . DIRECTORY_SEPARATOR . 'render' . DIRECTORY_SEPARATOR . $this->request['request'] . DIRECTORY_SEPARATOR . $this->rel_path; // return if debug or force if($this->debug || $this->force) return; // serve from cache if exists if(file_exists($this->cache_path)) $this->serve_image($this->cache_path, 'Cache'); } // get source image data private function get_source(){ $types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png'); if(defined('IMAGETYPE_WEBP')) $types[IMAGETYPE_WEBP] = 'webp'; $this->info = getimagesize($this->abs_path, $extra); $this->iptc = is_array($extra) && isset($extra['APP13']) ? iptcparse($extra['APP13']) : null; $this->source = !empty($this->info) && is_array($this->info) && count($this->info) > 3 ? array_filter(array( 'width' => (int) $this->info[0], 'height' => (int) $this->info[1], 'aspect' => $this->info[0] > 0 && $this->info[1] > 0 ? (float) $this->info[0] / $this->info[1] : false, 'type' => (int) $this->info[2], 'type_name' => $this->get_prop_val($this->info[2], $types), 'mime' => isset($this->info['mime']) && is_string($this->info['mime']) && substr($this->info['mime'], 0, 6) === 'image/' ? $this->info['mime'] : false )) : array(); if(count($this->source) !== 6) $this->error('Invalid image ' . $this->rel_path . '', 400); } // get config private function get_config() { $path = $this->x3_root . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.user.json'; $content = is_file($path) ? file_get_contents($path) : false; $config = $content ? json_decode($content, TRUE) : false; $r = !empty($config) && isset($config['back']['image_resizer']) ? $config['back']['image_resizer'] : false; if(!empty($r)){ if(isset($r['default_quality'])) { $this->quality = $r['default_quality']; $this->legacy_quality = $r['default_quality']; } if(isset($r['progressive']) && in_array($this->source['type'], array(2, 18))) $this->progressive = $r['progressive']; if(isset($r['copy_icc_profile']) && $this->source['type'] === 2) $this->copy_icc_profile = $r['copy_icc_profile']; if(isset($r['sharpen'])) $this->sharpen = $r['sharpen']; if(isset($r['smart_crop'])) $this->smart_crop = $r['smart_crop']; if(isset($r['allowed_width'])) { $this->allowed_width = $r['allowed_width'] == '0' ? array() : array_unique(array_merge($this->allowed_width, array_filter(array_map('intval', explode(',', $r['allowed_width']))))); sort($this->allowed_width, SORT_NUMERIC); } if(isset($r['max_memory'])) $this->max_memory = (int) $r['max_memory']; if($this->debug) $this->config = $r; } if(!$this->debug && $this->request['width'] && !empty($this->allowed_width) && !in_array($this->request['width'], $this->allowed_width)) $this->error('Invalid width request ' . $this->request['width'] . '. Allowed widths:

[' . implode(', ', $this->allowed_width) . ']'); } // calculate render private function get_render(){ $src_width = $this->source['width']; $src_height = $this->source['height']; $src_aspect = $this->source['aspect']; $request_width = $this->request['width'] ?: $src_width; $request_aspect = $this->request['crop_ratio'] ?: $src_aspect; $resize = $request_width < $src_width * $this->min_resize_factor; $crop = $src_aspect != $request_aspect; $crop_factor = $src_aspect / $request_aspect; $crop_width = $crop_factor > 1 ? $src_width / $crop_factor : $src_width; $crop_height = $crop_factor < 1 ? $src_height * $crop_factor : $src_height; $dst_w = $request_width > $crop_width * $this->min_resize_factor ? $crop_width : $request_width; $dst_h = $dst_w / $request_aspect; $resample = $crop_width > $dst_w;// && $crop_height > $dst_h; if(!$resample) $this->sharpen = false; $resize_width = $crop_factor > 1 ? $dst_w * $crop_factor : $dst_w; $resize_height = $crop_factor < 1 ? $dst_h / $crop_factor : $dst_h; $this->render = array( 'crop' => $crop, 'crop_factor' => $crop_factor, 'original' => !$crop && !$resize, 'crop_width' => (int) round($crop_width), 'crop_height' => (int) round($crop_height), 'dst_w' => (int) round($dst_w), 'dst_h' => (int) round($dst_h), 'resize_width' => (int) round($resize_width), 'resize_height' => (int) round($resize_height), 'resample' => $resample, 'center_crop' => $crop ? array( $crop_factor > 1 ? (int) round(($resize_width - $dst_w) / 2) : 0, $crop_factor < 1 ? (int) round(($resize_height - $dst_h) / 2) : 0 ) : false ); } // set cache dir private function set_cache_dir($retry = false){ $this->cache_dir = dirname($this->cache_path); // create cache_dir if(!$this->debug && !file_exists($this->cache_dir) && !@mkdir($this->cache_dir, 0777, true)) { // if we get to here, something is wrong. // 1. Either cannot write (wrong permissions) // 2. Or mkdir already processed by another request running simultaneously. Check again. if($retry) $this->error('Failed to create cache directory ' . $this->cache_dir, 500); usleep(200000); // wait 0.2 seconds $this->set_cache_dir(true); } } // sharpen (auto) private function sharpen($image){ $final = sqrt($this->render['dst_w'] * $this->render['dst_h']) * (750.0 / sqrt($this->render['crop_width'] * $this->render['crop_height'])); $result = max(round(52 + -0.27810650887573124 * $final + .00047337278106508946 * $final * $final), 0); if(!imageconvolution($image, array( array(-1, -2, -1), array(-2, $result + 12, -2), array(-1, -2, -1) ), $result, 0)) $this->error('Failed to sharpen image.', 500); } // get non-empty prop val private function get_prop_val($name, $array){ return isset($array[$name]) && !empty($array[$name]) ? $array[$name] : false; } // get mime. A bit overkill, but won't fail. private function get_mime($path){ // mime should always match source (if assigned) if(isset($this->source['mime'])) return $this->source['mime']; // mime_content_type if(function_exists('mime_content_type')){ $mime = mime_content_type($path); if($mime && strtok($mime, '/') == 'image') return $mime; } // finfo_open if(function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $path); finfo_close($finfo); if($mime && strtok($mime, '/') == 'image') return $mime; } // mime from extension $mime_ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if($mime_ext && in_array($mime_ext, array('jpg', 'jpeg', 'webp', 'png', 'gif'))) return 'image/' . str_replace('jpg', 'jpeg', $mime_ext); // last resort assume jpeg return 'image/jpeg'; } // serve image private function serve_image($path, $msg = '', $data = false){ // header("Last-Modified: $lastModified"); header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 315360000) . ' GMT'); header('Cache-Control: public, max-age=315360000, s-max-age=315360000'); header('content-type: ' . $this->get_mime($path)); header('content-length: ' . ($data ? strlen($data) : filesize($path))); $request_time = $this->get_prop_val('REQUEST_TIME_FLOAT', $_SERVER); $info = implode(', ', array_filter(array( isset($this->render) ? $this->source['width'] . 'x' . $this->source['height'] . ' => ' . $this->render['dst_w'] . 'x' . $this->render['dst_h'] : false, round(memory_get_peak_usage() / 1048576, 1) . 'M', $request_time ? round((microtime(true) - $request_time) * 1000) . 'ms' : false ))); header('X3-Resizer: ' . $msg . ' [' . $info . ']'); if($data) { echo $data; } else if(!readfile($path)){ header('content-type: text/html'); $url_path = str_replace($_SERVER['DOCUMENT_ROOT'], '', $path); $this->error('Could not read file ' . $url_path . '', 404); } exit; } // copy ICC profile private function copyICCProfile(){ require __DIR__ . '/icc/class.jpeg_icc.php'; try { $o = new JPEG_ICC(); $o->LoadFromJPEG($this->abs_path); $o->SaveToJPEG($this->cache_path); } catch (Exception $e) { // on fail, continue // $this->error('Failed to copy ICC color profile to ' . $this->cache_path, 500); } } // set memory private function set_memory(){ // get $limit = function_exists('ini_get') ? (int) @ini_get('memory_limit') : 0; if($limit < 1 || $limit >= $this->max_memory) return; // destination resize/smart_crop or crop $dst_w = $this->render['resample'] || $this->smart_crop ? $this->render['resize_width'] : $this->render['dst_w']; $dst_h = $this->render['resample'] || $this->smart_crop ? $this->render['resize_height'] : $this->render['dst_h']; // memory required / (src + resize) * tweak / MB $src_area = $this->source['width'] * $this->source['height']; $dst_area = $dst_w * $dst_h; $mb = 1048576; $desired = 2 * $mb + ($src_area * 3 + $dst_area * 3) * $this->memory_tweak; if($this->sharpen) $desired += $dst_area * 4 / ($src_area/$dst_area); $desired = (int) ceil($desired / $mb); $new = min($desired, $this->max_memory); // set ini $changed = $new > $limit ? (bool) @ini_set('memory_limit', $new . 'M') : false; // debug if($this->debug) $this->memory = array('limit' => $limit . 'M', 'desired' => $desired . 'M', 'new' => $new . 'M', 'changed' => $changed); } // convert and serve from legacy cache (X3.28.0 and earlier) private function legacy_cache(){ // return if(!$this->convert_legacy_cache || $this->debug || $this->force) return; // legacy cache dir $legacy_cache_dir = $this->x3_root . DIRECTORY_SEPARATOR . '_cache' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'rendered'; // return if(!is_dir($legacy_cache_dir)) return; // array to match legacy cache file names in _cache/images/rendered directory $arr = array( 'path' => '../../..' . $this->content_path, 'width' => (float) $this->render['resize_width'], 'height' => (float) $this->render['resize_height'], 'cropWidth' => $this->render['crop'] ? (float) $this->render['dst_w'] : null, 'cropHeight' => $this->render['crop'] ? (float) $this->render['dst_h'] : null, 'iptc' => $this->iptc, 'quality' => (int) $this->legacy_quality, 'progressive' => true, 'background' => null, 'cropper' => 'centered' ); // legacy cache path $legacy_cache_path = $legacy_cache_dir . DIRECTORY_SEPARATOR . md5($arr['path'] . serialize($arr)); // return if legacy cache does not exist if(!is_file($legacy_cache_path)) return; // attempt to move and rename legacy cache file to new cache path if(!rename($legacy_cache_path, $this->cache_path)) $this->error('Failed to move legacy cache from ' . $legacy_cache_path . ' to ' . $this->cache_path, 500); // success, serve converted image $this->serve_image($this->cache_path, 'Legacy cache converted'); } // construct function __construct() { // get request parameters from url $this->get_request(); // set debug $this->set_debug(); // set cache path and get cache if exists $this->get_cache(); // get source; $this->get_source(); // get config $this->get_config(); // get render $this->get_render(); // serve original image if appropriate if(!$this->debug && $this->render['original']) $this->serve_image($this->abs_path, 'Original'); // cache dir $this->set_cache_dir(); // legacy cache $this->legacy_cache(); // set memory $this->set_memory(); // debug if($this->debug) $this->debug(); // IMAGE // create src image (resize or crop required) $image = call_user_func('imagecreatefrom' . $this->source['type_name'], $this->abs_path); if(!$image) $this->error('Failed to call imagecreatefrom' . $this->source['type_name'] . '()', 500); // resize required if($this->render['resample']){ $resized_image = imagecreatetruecolor($this->render['resize_width'], $this->render['resize_height']); if(!$resized_image || !imagecopyresampled($resized_image, $image, 0, 0, 0, 0, $this->render['resize_width'], $this->render['resize_height'], $this->source['width'], $this->source['height'])) $this->error('Failed to resize image.', 500); imagedestroy($image); $image = $resized_image; unset($resized_image); } // crop if($this->render['crop']){ // smart crop if($this->smart_crop) { require __DIR__ . '/smart_crop.php'; $cropxy = (new smart_crop($image))->get_resized($this->render['dst_w'], $this->render['dst_h']); // fallback to center_crop if smart_crop fails (could be requested crop is same as aspect, or too small image) list($crop_x, $crop_y) = $cropxy ?: $this->render['center_crop']; } else { list($crop_x, $crop_y) = $this->render['center_crop']; } // cropit! $cropped_image = imagecreatetruecolor($this->render['dst_w'], $this->render['dst_h']); if(!$cropped_image || !imagecopy($cropped_image, $image, 0, 0, $crop_x, $crop_y, $this->render['dst_w'], $this->render['dst_h'])) $this->error('Failed to crop image.', 500); imagedestroy($image); $image = $cropped_image; unset($cropped_image); } // sharpen if($this->sharpen) $this->sharpen($image); // progressive / interlace if($this->progressive && !imageinterlace($image, 1)) $this->error('Failed to interlace (progressive) image.', 500); // change quality to format defaults if png or gif if(!in_array($this->source['type'], array(2, 18))) $this->quality = $this->source['type'] == 1 ? null : -1; // if copy ICC profile, save to cache before copying ICC profile, then serve from cached file if($this->copy_icc_profile) { if(!call_user_func('image' . $this->source['type_name'], $image, $this->cache_path, $this->quality)) $this->error('Failed to create image.', 500); imagedestroy($image); $data = false; $this->copyICCProfile(); // create in memory, save data to cache, server from memory } else { ob_start(NULL); if(!call_user_func('image' . $this->source['type_name'], $image, null, $this->quality)) $this->error('Failed to create image.', 500); imagedestroy($image); $data = ob_get_contents(); ob_end_clean(); if(!file_put_contents($this->cache_path, $data)) $this->error('Failed to save cache ' . $this->cache_path, 500); } // serve image, from data or cache_path $this->serve_image($this->cache_path, 'Rendered and cached', $data); } } // new resizer new resizer;