_JPG_MAX_QUALITY = defined( 'JPG_MAX_QUALITY' ) ? JPG_MAX_QUALITY : 100; $this->_PNG_MAX_QUALITY = defined( 'PNG_MAX_QUALITY' ) ? PNG_MAX_QUALITY : 100; $this->_PNG_ZLIB_COMPRESSION_LEVEL = defined( 'PNG_ZLIB_COMPRESSION_LEVEL' ) ? PNG_ZLIB_COMPRESSION_LEVEL : 2; $this->_PNG_COMPRESSION_FILTER = defined( 'PNG_COMPRESSION_FILTER' ) ? PNG_COMPRESSION_FILTER : 1; $this->_WEBP_MAX_QUALITY = defined( 'WEBP_MAX_QUALITY' ) ? WEBP_MAX_QUALITY : 100; $this->_AVIF_MAX_QUALITY = defined( 'AVIF_MAX_QUALITY' ) ? AVIF_MAX_QUALITY : 100; $this->_AVIF_ENCODING_ENABLED = defined( 'AVIF_ENCODING_ENABLED' ) ? AVIF_ENCODING_ENABLED : false; $this->_UPSCALE_MAX_PIXELS = defined( 'UPSCALE_MAX_PIXELS' ) ? UPSCALE_MAX_PIXELS : 1024; $this->_UPSCALE_MAX_PIXELS_GIF = defined( 'UPSCALE_MAX_PIXELS_GIF' ) ? UPSCALE_MAX_PIXELS_GIF : 1024; $this->_IMAGE_MAX_WIDTH = defined( 'IMAGE_MAX_WIDTH' ) ? IMAGE_MAX_WIDTH : 20000; $this->_IMAGE_MAX_HEIGHT = defined( 'IMAGE_MAX_HEIGHT' ) ? IMAGE_MAX_HEIGHT : 20000; $this->_WEBP_LOSSLESS_COMPRESSION_EFFORT = defined( 'WEBP_LOSSLESS_COMPRESSION_EFFORT' ) ? WEBP_LOSSLESS_COMPRESSION_EFFORT : 0; // Allow global image optimization disabling for server load management $this->_DISABLE_IMAGE_OPTIMIZATIONS = defined( 'DISABLE_IMAGE_OPTIMIZATIONS' ) ? DISABLE_IMAGE_OPTIMIZATIONS : false; if ( ! $this->_DISABLE_IMAGE_OPTIMIZATIONS ) { // External defines should be set to the local file path of the utilities $this->_PNGCRUSH = defined( 'PNGCRUSH' ) ? PNGCRUSH : false; $this->_PNGQUANT = defined( 'PNGQUANT' ) ? PNGQUANT : false; $this->_OPTIPNG = defined( 'OPTIPNG' ) ? OPTIPNG : false; $this->_JPEGTRAN = defined( 'JPEGTRAN' ) ? JPEGTRAN : false; $this->_JPEGOPTIM = defined( 'JPEGOPTIM' ) ? JPEGOPTIM : false; } // c2pa feature is only enabled when all three are set. $this->_C2PATOOL = defined( 'C2PATOOL' ) ? C2PATOOL : false; $this->_C2PA_CERT_PATH = defined( 'C2PA_CERT_PATH' ) ? C2PA_CERT_PATH : false; $this->_C2PA_KEY_PATH = defined( 'C2PA_KEY_PATH' ) ? C2PA_KEY_PATH : false; // Whether the class will process all the 'w' and 'h' query arguments or just the first one. // For example, if the query args are 'w=100&h=150' then, without chaining, only the first 'w' parameter // change is applied, however with chaining enabled, first the width would be applied and then the height. $this->_ALLOW_DIMS_CHAINING = defined( 'ALLOW_DIMS_CHAINING' ) ? ALLOW_DIMS_CHAINING : false; $this->supported_formats = $this->get_supported_formats(); } function __destruct() { ; } public function __set( $name, $value ) { $this->$name = $value; } public function __get( $name ) { return $this->$name; } private function send_headers( $file_size = null ) { if ( $file_size ) header( 'Content-Length: ' . $file_size ); header( 'Content-Type: ' . $this->mime_type ); if ( $this->send_last_modified_header ) { header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', time() ) . ' GMT' ); } if ( $this->image_max_age ) { header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $this->image_max_age ) . ' GMT' ); header( 'Cache-Control: public, max-age=' . $this->image_max_age ); } if ( $this->canonical_url ) { header( 'Link: <' . $this->canonical_url . '>; rel="canonical"' ); } if ( $this->send_nosniff_header ) { header( 'X-Content-Type-Options: nosniff', false ); } if ( $this->send_etag_header && $file_size ) { header( 'ETag: "' . substr( md5( $file_size . '.' . time() ), 0, 16 ) . '"', false ); } if ( 0 < $this->bytes_saved ) { header( 'X-Bytes-Saved: ' . $this->bytes_saved ); } if ( $this->_DISABLE_IMAGE_OPTIMIZATIONS ) { header( "X-Optim-Disabled: true" ); } // The Vary header must be sent, as images often get converted into other formats header( 'Vary: Accept' ); if ( false !== $this->dpr_header_value ) { header( 'Content-DPR: ' . $this->dpr_header_value ); } } private function replace_file_if_smaller( $file, $candidate ) { clearstatcache(); $file_size = filesize( $file ); $candidate_size = filesize( $candidate ); if ( $candidate_size < $file_size ) { copy( $candidate, $file ); return true; } return false; } private function silent_unlink( $file ) { @unlink( $file ); } private function pngcrush( $file ) { $transformed = tempnam( '/dev/shm/', 'crush-post-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $transformed ); $cmd = $this->_PNGCRUSH . ' ' . escapeshellarg( $file ) . ' ' . escapeshellarg( $transformed ) . ' 2>&1'; exec( $cmd, $o, $e ); if ( $e === 0 ) { return $this->replace_file_if_smaller( $file, $transformed ); } return false; } private function optipng( $file, $flags = '' ) { $transformed = tempnam( '/dev/shm/', 'opti-post-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $transformed ); register_shutdown_function( array( $this, 'silent_unlink' ), $transformed . '.bak' ); $cmd = $this->_OPTIPNG; $cmd .= " $flags -clobber -force -out " . escapeshellarg( $transformed ) . ' ' . escapeshellarg( $file ) . ' >/dev/null 2>&1'; exec( $cmd, $o, $e ); if ( $e === 0 ){ return $this->replace_file_if_smaller( $file, $transformed ); } return false; } private function pngquant( $file, $flags = '' ) { $transformed = tempnam( '/dev/shm/', 'quant-post' ); register_shutdown_function( array( $this, 'silent_unlink' ), $transformed ); $cmd = $this->_PNGQUANT; $cmd .= " $flags -f -o " . escapeshellarg( $transformed ) . ' ' . escapeshellarg( $file ); exec( $cmd, $o, $e ); if ( $e === 0 ){ return $this->replace_file_if_smaller( $file, $transformed ); } return false; } private function jpegtran( $file ) { $transformed = tempnam( '/dev/shm/', 'tran-post-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $transformed ); $cmd = $this->_JPEGTRAN . ' -copy all -optimize -progressive -outfile ' . escapeshellarg( $transformed ) . ' ' . escapeshellarg( $file ) . ' 2>&1'; exec( $cmd, $o, $e ); if ( $e === 0 ) { return $this->replace_file_if_smaller( $file, $transformed ); } return false; } private function get_supported_formats() { if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { $accept = explode( ',', $_SERVER['HTTP_ACCEPT'] ); } else { $accept = array(); } $formats = array( 'jpeg', 'png', 'gif_indirect', 'gif' ); if ( ( isset( $_GET['webp'] ) && 1 == intval( $_GET['webp'] ) ) || in_array( 'image/webp', $accept, true ) ) { $formats[] = 'webp'; } if ( $this->_AVIF_ENCODING_ENABLED && ( ( isset( $_GET['avif'] ) && 1 == intval( $_GET['avif'] ) ) || in_array( 'image/avif', $accept, true ) ) ) { $formats[] = 'avif'; } return $formats; } private function jpegoptim( $file ) { $transformed = tempnam( '/dev/shm/', 'jopt-post-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $transformed ); copy( $file, $transformed ); $cmd = $this->_JPEGOPTIM . ' -T0.0 --all-progressive'; $cmd .= ' -p ' . escapeshellarg( $transformed ); exec( $cmd, $o, $e ); if ( $e === 0 ) { return $this->replace_file_if_smaller( $file, $transformed ); } return false; } private function c2pa_signing_enabled() { return $this->_C2PATOOL && $this->_C2PA_CERT_PATH && $this->_C2PA_KEY_PATH; } private function should_sign_c2pa() { return $this->c2pa_signing_enabled() && $this->c2pa_manifest_detected; } private function detect_c2pa_manifest() { // Embedded manifest stores only // JPEG, AVIF and GIF use cheap byte scans, false positives possible switch ( $this->image_format ) { case 'jpg': case 'jpeg': case 'avif': return false !== strpos( $this->image_data, 'c2pa' ); case 'png': return in_array( 'caBX', $this->get_png_chunks(), true ); case 'webp': return in_array( 'C2PA', $this->get_webp_chunks(), true ); case 'gif': return false !== strpos( $this->image_data, 'C2PA_GIF' ); } return false; } private function get_png_chunks() { $chunks = array(); $len = strlen( $this->image_data ); // Skip the 8-byte signature, then walk the length/type/data/crc chunks for ( $offset = 8; $offset + 8 <= $len; ) { $chunk_size = unpack( 'Nsize', substr( $this->image_data, $offset, 4 ) )['size']; $chunks[] = substr( $this->image_data, $offset + 4, 4 ); $offset += $chunk_size + 12; } return $chunks; } private function get_c2pa_actions() { $action_map = array( 'crop' => 'c2pa.cropped', 'crop_offset' => 'c2pa.cropped', 'unletterbox' => 'c2pa.cropped', 'resize_and_crop' => 'c2pa.resized', 'fit_in_box' => 'c2pa.resized.proportional', 'set_width' => 'c2pa.resized.proportional', 'set_height' => 'c2pa.resized.proportional', 'letterbox' => 'c2pa.resized', 'brightness' => 'c2pa.adjustedColor', 'contrast' => 'c2pa.adjustedColor', 'colorize' => 'c2pa.adjustedColor', 'smooth' => 'c2pa.filtered', ); $action_names = array(); foreach ( $this->processed as $function_name ) { if ( isset( $action_map[ $function_name ] ) ) { $action_names[] = $action_map[ $function_name ]; } else if ( 0 === strpos( $function_name, 'filter_' ) ) { $action_names[] = 'c2pa.filtered'; } else { $action_names[] = 'c2pa.edited'; } } // Signed outputs are always re-encoded, even when no transformation was requested $action_names[] = 'c2pa.transcoded'; // Exhaustive list of actions that don't need to declare a digitalSourceType $action_requires_digital_source = array( // Inception 'c2pa.created' => false, 'c2pa.opened' => false, // Non-editorial 'c2pa.converted' => false, 'c2pa.published' => false, 'c2pa.repackaged' => false, 'c2pa.transcoded' => false, 'c2pa.resized.proportional' => false, 'c2pa.enhanced' => false, 'c2pa.edited.metadata' => false, 'c2pa.watermarked' => false, 'c2pa.watermarked.bound' => false, 'c2pa.watermarked.unbound' => false, 'c2pa.redacted' => false, // Perceptible, but exempted by the Conformance Program 'c2pa.placed' => false, ); $actions = array(); foreach ( array_unique( $action_names ) as $action ) { $entry = array( 'action' => $action ); if ( $action_requires_digital_source[ $action ] ?? true ) { // IPTC term for non-AI algorithmic processing $entry['digitalSourceType'] = 'http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced'; } $actions[] = $entry; } return $actions; } private function maybe_sign_c2pa( $file, $format ) { if ( ! $this->should_sign_c2pa() ) { return; } // c2patool relies on file extensions to determine formats $base_parent = tempnam( '/dev/shm/', 'c2pa-parent-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $base_parent ); $parent = $base_parent . '.' . $this->image_format; symlink( $base_parent, $parent ); register_shutdown_function( array( $this, 'silent_unlink' ), $parent ); file_put_contents( $parent, $this->image_data ); // Detection false positives get signed as well, with the manifest-less // input recorded as the parent ingredient $manifest_file = tempnam( '/dev/shm/', 'c2pa-manifest-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $manifest_file ); $manifest = array( 'claim_generator_info' => array( array( 'name' => 'Photon', 'specVersion' => '2.4.0' ) ), 'alg' => defined( 'C2PA_SIGN_ALG' ) ? C2PA_SIGN_ALG : 'es256', 'sign_cert' => $this->_C2PA_CERT_PATH, 'private_key' => $this->_C2PA_KEY_PATH, 'assertions' => array( array( 'label' => 'c2pa.actions', 'created' => true, 'data' => array( 'allActionsIncluded' => true, 'actions' => $this->get_c2pa_actions(), ), ), ), ); if ( defined( 'C2PA_TSA_URL' ) && C2PA_TSA_URL ) { $manifest['ta_url'] = C2PA_TSA_URL; } file_put_contents( $manifest_file, json_encode( $manifest ) ); $source = $file . '.' . $format; $signed = $file . '-signed.' . $format; register_shutdown_function( array( $this, 'silent_unlink' ), $source ); register_shutdown_function( array( $this, 'silent_unlink' ), $signed ); symlink( $file, $source ); $cmd = $this->_C2PATOOL . ' ' . escapeshellarg( $source ) . ' -m ' . escapeshellarg( $manifest_file ) . ' -p ' . escapeshellarg( $parent ) . ' --settings ' . escapeshellarg( __DIR__ . '/c2pa-settings.json' ) . ' -f -o ' . escapeshellarg( $signed ) . ' 2>&1'; exec( $cmd, $o, $e ); clearstatcache(); if ( 0 !== $e || ! file_exists( $signed ) || ! filesize( $signed ) ) { do_action( 'bump_stats', 'c2pa_sign_fail' ); if ( function_exists( 'httpdie' ) ) { httpdie( '500 Internal Server Error', 'Unable to process the image.' ); } else { header( 'HTTP/1.1 500 Internal Server Error' ); die( 'Unable to process the image.' ); } } rename( $signed, $file ); do_action( 'bump_stats', 'c2pa_signed' ); } private function get_orientation() { if ( 'Gif_Image' === get_class( $this->image ) ) { $orientation = 0; } else { $orientation = $this->image->getimageorientation(); } switch( $orientation ) { case 3: case 4: $degrees = 180; break; case 5: case 6: $degrees = 90; break; case 7: case 8: $degrees = 270; break; default: $degrees = 0; break; } $flopped = in_array( $orientation, array( 2, 4, 5, 7 ), true ); return array( 'degrees' => $degrees, 'flopped' => $flopped ); } private function is_strip_effective( $strip ) { return ! isset( $strip ) || 'none' !== $strip; } private function is_metadata_being_stripped() { return $this->is_strip_effective( $_GET['strip'] ?? null ); } private function maybe_strip_metadata( $strip = null ) { if ( ! isset( $strip ) ) { $strip = $_GET['strip'] ?? null; } if ( ! $this->is_strip_effective( $strip ) ) { return; } // Strip profiles while maintaining digital source data $this->image->setimageprofile( 'EXIF', null, true ); $this->image->setimageprofile( 'XMP', null, true ); $this->image->setimageprofile( 'IPTC', null, true ); } private function maybe_hardcode_exif_orientation( $strip = null ) { if ( ! isset( $strip ) ) { $strip = $_GET['strip'] ?? null; } if ( true === $this->orientation_hardcoded || ! $this->is_strip_effective( $strip ) ) { return; } $this->image->autoorientimage( Photon_OpenCV::ORIENTATION_UNDEFINED ); $this->orientation_hardcoded = true; $this->inverted_axis = false; $this->image_width = $this->image->getimagewidth(); $this->image_height = $this->image->getimageheight(); } private function compress_and_send_png() { $output = tempnam( '/dev/shm/', 'pre-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $output ); // If the image color was originally PALETTE and it has been changed, then change it back // to PALETTE, but only if PNGQUANT is disabled (otherwise the final file size is larger) if ( ! $this->_PNGQUANT && Photon_OpenCV::IMGTYPE_PALETTE == $this->image_type && Photon_OpenCV::IMGTYPE_PALETTE != $this->image->getimagetype() ) { $this->image->setimagetype( Photon_OpenCV::IMGTYPE_PALETTE ); } if ( $this->quality ) { $this->quality = min( max( intval( $this->quality ), 20 ), $this->_PNG_MAX_QUALITY ); } else { $this->quality = $this->_PNG_MAX_QUALITY; } $this->mime_type = 'image/png'; $this->maybe_hardcode_exif_orientation(); $this->maybe_strip_metadata(); // From the official documentation http://www.graphicsmagick.org/wand/magick_wand.html // `the quality value sets the zlib compression level (quality / 10) and filter-type (quality % 10)` $this->image->setcompressionquality( $this->_PNG_ZLIB_COMPRESSION_LEVEL * 10 + $this->_PNG_COMPRESSION_FILTER ); if ( $this->quality_explicitly_set ) { $this->image->forcereencode(); } $this->image->setimageformat( 'PNG' ); $file_modified = $this->image->blobrequiresreencoding(); $this->image->writeimage( $output ); $o_size = filesize( $output ); if ( $this->_PNGQUANT ) { $file_modified = $this->pngquant( $output, "--speed 5 --quality={$this->quality}-100" ) || $file_modified; if ( $this->_OPTIPNG ) { $file_modified = $this->optipng( $output, '-o1' ) || $file_modified; } } else if ( $this->_PNGCRUSH ) { $file_modified = $this->pngcrush( $output ) || $file_modified; } else if ( $this->_OPTIPNG ) { $file_modified = $this->optipng( $output ) || $file_modified; } // Untouched files retain their original manifests if ( $file_modified ) { $this->maybe_sign_c2pa( $output, 'png' ); } if ( $this->send_bytes_saved ) { clearstatcache(); $n_size = filesize( $output ); $this->bytes_saved = $o_size - $n_size; } else { $n_size = filesize( $output ); } $this->send_headers( $n_size ); readfile( $output ); } private function compress_and_send_jpeg() { $output = tempnam( '/dev/shm/', 'pre-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $output ); if ( $this->quality ) { $this->quality = min( max( intval( $this->quality ), 20 ), $this->_JPG_MAX_QUALITY ); } else { $this->quality = $this->_JPG_MAX_QUALITY; } $this->mime_type = 'image/jpeg'; $this->lossy_compression_used = true; // Hardcode orientation if it's being stripped $this->maybe_hardcode_exif_orientation(); $this->maybe_strip_metadata(); $this->image->setimageformat( 'JPEG' ); $this->image->setcompressionquality( $this->quality ); if ( $this->quality_explicitly_set ) { $this->image->forcereencode(); } $file_modified = $this->image->blobrequiresreencoding(); $this->image->writeimage( $output ); $o_size = filesize( $output ); if ( $this->_JPEGOPTIM ) { $file_modified = $this->jpegoptim( $output ) || $file_modified; } else if ( $this->_JPEGTRAN ) { $file_modified = $this->jpegtran( $output ) || $file_modified; } // Untouched files retain their original manifests if ( $file_modified ) { $this->maybe_sign_c2pa( $output, 'jpeg' ); } if ( $this->send_bytes_saved ) { clearstatcache(); $n_size = filesize( $output ); $this->bytes_saved = $o_size - $n_size; } else { $n_size = filesize( $output ); } $this->send_headers( $n_size ); readfile( $output ); } private function compress_and_send_avif() { $output = tempnam( '/dev/shm/', 'pre-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $output ); if ( $this->quality ) { $this->quality = min( max( intval( $this->quality ), 20 ), $this->_AVIF_MAX_QUALITY ); } else { $this->quality = $this->_AVIF_MAX_QUALITY; } // Always hardcode exif orientation, as browsers don't support it // and the underlying library has trouble adding metadata to it $this->maybe_hardcode_exif_orientation( 'all' ); $this->maybe_strip_metadata( 'all' ); $this->image->setimageformat( 'AVIF' ); $this->image->setcompressionquality( $this->quality ); if ( $this->quality_explicitly_set ) { $this->image->forcereencode(); } if ( $this->lossless_input ) { $this->image->setimageoption( 'AVIF', 'lossless', 'true' ); } else { $this->lossy_compression_used = true; } $file_modified = $this->image->blobrequiresreencoding(); $this->image->writeimage( $output ); // Untouched files retain their original manifests if ( $file_modified ) { $this->maybe_sign_c2pa( $output, 'avif' ); } $o_size = strlen( $this->image_data ); $n_size = filesize( $output ); $this->bytes_saved = $o_size - $n_size; $this->mime_type = 'image/avif'; $this->send_headers( filesize( $output ) ); readfile( $output ); } private function directly_compress_and_send_gif() { $output = tempnam( '/dev/shm/', 'pre-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $output ); // Always hardcode exif orientation, as browsers don't support it $this->maybe_hardcode_exif_orientation( 'all' ); $this->maybe_strip_metadata(); $this->image->setimageformat( 'GIF' ); $file_modified = $this->image->blobrequiresreencoding(); $this->image->writeimage( $output ); // Untouched files retain their original manifests if ( $file_modified ) { $this->maybe_sign_c2pa( $output, 'gif' ); } $this->mime_type = 'image/gif'; $this->send_headers( filesize( $output ) ); readfile( $output ); } private function try_to_compress_and_send_webp() { // Maximum dimensions defined in the official specs if ( $this->image->getimagewidth() > 16383 || $this->image->getimageheight() > 16383 ) { return false; } // Since webp conversion is memory hungry, allow clients to decide whether to use it based on image dimensions. $allowed = apply_filters( 'webp_dimensions_cutoff', true, $this->image->getimagewidth(), $this->image->getimageheight() ); if ( $allowed !== true ) { return false; } $output = tempnam( '/dev/shm/', 'pre-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $output ); $requested_quality = intval( $this->quality ); if ( $requested_quality ) { $this->quality = min( max( $requested_quality, 20 ), $this->_WEBP_MAX_QUALITY ); } else { $this->quality = $this->_WEBP_MAX_QUALITY; } // Always hardcode exif orientation, as browsers don't support it $this->maybe_hardcode_exif_orientation( 'all' ); $this->maybe_strip_metadata(); $this->image->setimageformat( 'WEBP' ); if ( $this->lossless_input || 100 === $requested_quality ) { // Some encoders (e.g. libwebpmux) look at the quality value to determine how much effort to put into compressing the lossless image $this->image->setcompressionquality( $this->_WEBP_LOSSLESS_COMPRESSION_EFFORT ); $this->image->setimageoption( 'WEBP', 'lossless', 'true' ); } else { $this->image->setcompressionquality( $this->quality ); $this->image->setimageoption( 'WEBP', 'lossless', 'false' ); $this->lossy_compression_used = true; } if ( $this->quality_explicitly_set ) { $this->image->forcereencode(); } if ( ! $this->_DISABLE_IMAGE_OPTIMIZATIONS ) { $this->image->setimageoption( 'WEBP', 'minimize_size', 'true' ); } $file_modified = $this->image->blobrequiresreencoding(); try { $this->image->writeimage( $output ); } catch ( Exception $e ) { do_action( 'bump_stats', 'direct_webp_write_fail' ); return false; } // Untouched files retain their original manifests if ( $file_modified ) { $this->maybe_sign_c2pa( $output, 'webp' ); } $o_size = strlen( $this->image_data ); $n_size = filesize( $output ); $this->bytes_saved = $o_size - $n_size; $this->mime_type = 'image/webp'; $this->send_headers( filesize( $output ) ); readfile( $output ); return true; } private function process_and_send_gif() { // The original data is still needed as the c2pa parent file if ( ! $this->should_sign_c2pa() ) { unset( $this->image_data ); } if ( ! $this->image->process_image_functions( $this->_UPSCALE_MAX_PIXELS_GIF ) ) { if ( function_exists( 'imageresize_graceful_fail' ) ) { imageresize_graceful_fail(); } else if ( function_exists( 'httpdie' ) ) { httpdie( '400 Bad Request', 'Sorry, the parameters you provided were not valid' ); } else { header( 'HTTP/1.1 400 Bad Request' ); die( 'Sorry, the parameters you provided were not valid' ); } } // The GIF class does it's own accounting for what processing functions // were successful in it and the Image_Effect class. So we use it's array. $this->processed = $this->image->get_processed(); if ( $this->should_sign_c2pa() ) { $output = tempnam( '/dev/shm/', 'pre-' ); register_shutdown_function( array( $this, 'silent_unlink' ), $output ); file_put_contents( $output, $this->image->get_image_blob() ); $this->maybe_sign_c2pa( $output, 'gif' ); $this->send_headers( filesize( $output ) ); readfile( $output ); return; } $this->send_headers(); echo $this->image->get_image_blob(); } private function load_jpeg_info() { $jpeg = new Jpeg_Image(); $this->jpeg_details = $jpeg->get_jpeg_details( $this->image_data ); } private function valid_request( $new_w, $new_h ) { // Just serve the original image if: // the requested dims are the same as the source image and we are not changing quality // or // one or both of the requested image dims are too small // or // we have upscaled without permission (upscale == false) or have overstepped the upscale limits if ( ( $new_w == $this->image_width && $new_h == $this->image_height && ! ( 'image/jpeg' == $this->mime_type && isset( $_GET['quality'] ) ) ) || ( $new_w < 3 || $new_h < 3 ) || ( $new_w > $this->image_width && $new_h > $this->image_height && ( ! $this->upscale || $new_w > $this->_UPSCALE_MAX_PIXELS || $new_h > $this->_UPSCALE_MAX_PIXELS ) ) ) { return false; } return true; } private function get_memory_limit() { $mem_limit = ini_get( 'memory_limit' ); if ( preg_match( '/^[0-9]+G$/', $mem_limit ) ) return intval( str_replace( 'G', '', $mem_limit ) ) * 1024 * 1024 * 1024; else if ( preg_match( '/^[0-9]+M$/', $mem_limit ) ) return intval( str_replace( 'M', '', $mem_limit ) ) * 1024 * 1024; else if ( preg_match( '/^[0-9]+K$/', $mem_limit ) ) return intval( str_replace( 'K', '', $mem_limit ) ) * 1024; else if ( 0 < intval( $mem_limit ) ) return intval( $mem_limit ); else return 128 * 1024 * 1024; // default setting for the PHP memory limit } private function acceptable_dimensions() { if ( 30 > strlen( $this->image_data ) ) return false; $w = $h = 0; if ( 'GIF8' === substr( $this->image_data, 0, 4 ) ) { $w = ord( substr( $this->image_data, 7, 1 ) ) << 8; $w = $w | ord( substr( $this->image_data, 6, 1 ) ); $h = ord( substr( $this->image_data, 9, 1 ) ) << 8; $h = $h | ord( substr( $this->image_data, 8, 1 ) ); } else if ( "PNG\r\n\032\n" == substr( $this->image_data, 1, 7 ) && ( 'IHDR' == substr( $this->image_data, 12, 4 ) ) ) { $w = ord( substr( $this->image_data, 16, 1 ) ) << 8; $w = ( $w | ord( substr( $this->image_data, 17, 1 ) ) ) << 8; $w = ( $w | ord( substr( $this->image_data, 18, 1 ) ) ) << 8; $w = $w | ord( substr( $this->image_data, 19, 1 ) ); $h = ord( substr( $this->image_data, 20, 1 ) ) << 8; $h = ( $h | ord( substr( $this->image_data, 21, 1 ) ) ) << 8; $h = ( $h | ord( substr( $this->image_data, 22, 1 ) ) ) << 8; $h = $h | ord( substr( $this->image_data, 23, 1 ) ); } else if ( 'RIFF' === substr( $this->image_data, 0, 4 ) ) { switch ( substr( $this->image_data, 8, 8 ) ) { case 'WEBPVP8 ': $size = unpack( 'vw/vh', substr( $this->image_data, 26, 4 ) ); $w = $size['w'] & 0x3fff; $h = $size['h'] & 0x3fff; break; case 'WEBPVP8X': $size = unpack( 'Cw_low/vw_high/Ch_low/vh_high', substr( $this->image_data, 24, 6 ) ); $w = ( ( $size['w_high'] << 8 ) | $size['w_low'] ) + 1; $h = ( ( $size['h_high'] << 8 ) | $size['h_low'] ) + 1; break; case 'WEBPVP8L': $bits = unpack( 'Vsize', substr( $this->image_data, 21, 4 ) ); $w = ( $bits['size'] & 0x3fff ) + 1; $h = ( ( $bits['size'] >> 14 ) & 0x3fff ) + 1; break; default: $w = 0; $h = 0; break; } } else { $this->load_jpeg_info(); if ( isset( $this->jpeg_details['x'] ) ) $w = $this->jpeg_details['x']; if ( isset( $this->jpeg_details['y'] ) ) $h = $this->jpeg_details['y']; } return ( $this->_IMAGE_MAX_WIDTH > $w && $this->_IMAGE_MAX_HEIGHT > $h ); } private function get_webp_chunks() { $chunks = array(); $len = strlen( $this->image_data ); for ( $offset = 12; $offset < $len - 8; ) { $chunks[] = substr( $this->image_data, $offset, 4 ); $chunk_size = ord( $this->image_data[ $offset + 4 ] ) | ( ord( $this->image_data[ $offset + 5 ] ) << 8 ) | ( ord( $this->image_data[ $offset + 6 ] ) << 16 ) | ( ord( $this->image_data[ $offset + 7 ] ) << 24 ); $offset += $chunk_size + 8; } return $chunks; } private function is_lossless_webp( $chunks ) { return in_array( 'VP8L', $chunks, true ); } private function is_animated_webp( $chunks ) { // Imperfect check. The ANIM chunk should be ignored if the animation // flag is not set. This is, however, good enough, as encoders should be // able to handle single frame animations, though there may be advantages // in explicitly handling these via the static workflow. return in_array( 'ANIM', $chunks, true ); } public function load_image() { if ( ! $this->image_data || ! $this->acceptable_dimensions() ) return false; if ( 'GIF' === substr( $this->image_data, 0, 3 ) && ( $this->_DISABLE_IMAGE_OPTIMIZATIONS || ! in_array( 'webp', $this->supported_formats, true ) ) ) { // Only use the indirect flow if we aren't going to try to convert it to WebP $this->image = new Gif_Image( $this->image_data ); $this->image->disable_zoom(); // zooming is handled in this class if ( ! $this->send_etag_header ) $this->image->disable_etag_header(); $this->image_format = 'gif'; $this->mime_type = 'image/gif'; $this->image_width = $this->image->get_image_width(); $this->image_height = $this->image->get_image_height(); $this->animated_input = true; } else { try { $this->image = new Photon_OpenCV(); $this->image->readimageblob( $this->image_data, true ); } catch ( Exception $e ) { return false; } $this->image_format = strtolower( $this->image->getimageformat() ); if ( in_array( $this->image_format, array( 'jpg', 'jpeg' ) ) ) { $this->mime_type = 'image/jpeg'; if ( ! isset( $this->jpeg_details ) ) $this->load_jpeg_info(); } elseif ( 'png' == $this->image_format ) { $this->mime_type = 'image/png'; $this->lossless_input = true; } elseif ( 'bmp' == $this->image_format ) { $this->mime_type = 'image/bmp'; $this->lossless_input = true; } elseif ( 'gif' == $this->image_format ) { $this->mime_type = 'image/gif'; $this->lossless_input = true; $this->animated_input = true; } elseif ( 'webp' == $this->image_format ) { $this->mime_type = 'image/webp'; $webp_chunks = $this->get_webp_chunks(); $this->lossless_input = $this->is_lossless_webp( $webp_chunks ); $this->animated_input = $this->is_animated_webp( $webp_chunks ); } elseif ( 'avif' == $this->image_format ) { $this->mime_type = 'image/avif'; $this->lossless_input = false; $this->animated_input = false; } else { $this->mime_type = 'application/octet-stream'; } $this->image_width = $this->image->getimagewidth(); $this->image_height = $this->image->getimageheight(); $this->image_type = $this->image->getimagetype(); $this->image_has_transparency = in_array( $this->image_type, array( Photon_OpenCV::IMGTYPE_GRAYSCALEMATTE, Photon_OpenCV::IMGTYPE_TRUECOLORMATTE, Photon_OpenCV::IMGTYPE_PALETTEMATTE, Photon_OpenCV::IMGTYPE_COLORSEPARATIONMATTE, ) ); $orientation = $this->get_orientation(); if ( 90 === $orientation['degrees'] || 270 === $orientation['degrees'] ) { $this->inverted_axis = true; } } if ( $this->c2pa_signing_enabled() ) { $this->c2pa_manifest_detected = $this->detect_c2pa_manifest(); } return true; } /** * @return array */ public function get_transformation_args() { $retA = array(); foreach( $_GET as $arg => $val ) { if ( ! is_numeric( $arg ) && in_array( $arg, self::$allowed_functions, true ) && ! is_array( $val ) ) { $retA[ $arg ] = $val; } } return $retA; } public function process_image() { $this->upscale = isset( $_GET['upscale'] ) && '1' == $_GET['upscale']; if ( isset( $_GET['quality'] ) && 0 != intval( $_GET['quality'] ) ) { $this->quality = intval( $_GET['quality'] ); $this->quality_explicitly_set = true; } if ( isset( $_GET['allow_lossy'] ) && 1 == intval( $_GET['allow_lossy'] ) ) { $this->allow_lossy = true; } $args = $this->get_transformation_args(); // This type of crop is always processed first and the rest of the // arguments are processed in the order that they were specified in if ( isset( $args['crop'] ) && '1' == $args['crop'] ) { $requested_w = ( ! empty( $args['w'] ) ) ? max( 0, intval( $args['w'] ) ) : null; $requested_h = ( ! empty( $args['h'] ) ) ? max( 0, intval( $args['h'] ) ) : null; if ( isset( $args['w'] ) ) unset( $args['w'] ); if ( isset( $args['h'] ) ) unset( $args['h'] ); if ( ( $requested_w || $requested_h ) && $this->crop( $requested_w, $requested_h ) ) $this->processed[] = 'crop'; unset( $args['crop'] ); } reset( $args ); while ( 0 < count( $args ) ) { // get the next argument to process $arg = key( $args ); $val = current( $args ); switch ( $arg ) { case 'crop': if ( ! is_array( $val ) && strpos( $val, ',' ) ) { // 1st-style crop (crop=x,y,w,h) $params = explode( ',', $val ); if ( 4 == count( $params ) && $this->crop_offset( $params[0], $params[1], $params[2], $params[3] ) ) $this->processed[] = 'crop_offset'; } else if ( '1' == $val ) { // 2nd-style crop (w=X&h=Y&crop=1) $requested_w = ( ! empty( $args['w'] ) ) ? max( 0, intval( $args['w'] ) ) : null; $requested_h = ( ! empty( $args['h'] ) ) ? max( 0, intval( $args['h'] ) ) : null; if ( isset( $args['w'] ) ) unset( $args['w'] ); if ( isset( $args['h'] ) ) unset( $args['h'] ); if ( ( $requested_w || $requested_h ) && $this->crop( $requested_w, $requested_h ) ) $this->processed[] = 'crop'; } unset( $args['crop'] ); break; case 'crop_offset': // explicit 1st-style crop (crop=x,y,w,h) $params = explode( ',', $val ); unset( $args['crop_offset'] ); if ( 4 == count( $params ) && $this->crop_offset( $params[0], $params[1], $params[2], $params[3] ) ) $this->processed[] = 'crop_offset'; break; case 'resize': $s_params = $this->apply_zoom( 'resize', $val ); unset( $args['resize'] ); $params = explode( ',', $s_params ); if ( 2 == count( $params ) && $this->resize( $params[0], $params[1] ) ) $this->processed[] = 'resize_and_crop'; break; case 'fit': $s_params = $this->apply_zoom( 'fit', $val ); unset( $args['fit'] ); $params = explode( ',', $s_params ); if ( 2 == count( $params ) && $this->fit( $params[0], $params[1] ) ) $this->processed[] = 'fit_in_box'; break; case 'w': $requested_w = ( ! empty( $val ) ) ? max( 0, intval( $val ) ) : null; unset( $args['w'] ); if ( $requested_w ) { $w = $this->apply_zoom( 'set_width', $requested_w ); if ( $this->set_width( $w ) ) { $this->processed[] = 'set_width'; if ( ! $this->_ALLOW_DIMS_CHAINING && isset( $args['h'] ) ) unset( $args['h'] ); } } break; case 'h': $requested_h = ( ! empty( $val ) ) ? max( 0, intval( $val ) ) : null; unset( $args['h'] ); if ( $requested_h ) { $h = $this->apply_zoom( 'set_height', $requested_h ); if ( $this->set_height( $h ) ) { $this->processed[] = 'set_height'; if ( ! $this->_ALLOW_DIMS_CHAINING && isset( $args['w'] ) ) unset( $args['w'] ); } } break; case 'brightness': case 'colorize': case 'contrast': case 'filter': case 'smooth': // Disabled for animated images if ( 'Gif_Image' === get_class( $this->image ) ) { $this->image->add_function( $arg, $val ); } else if ( ! $this->animated_input ) { // Due to the fact that these functions require memory intensive conversions back and forth // between the backend class and GD, we only allow them to be executed if the (roughly estimated) // memory requirement for executing them will be less than the current script memory limit // smooth requires at worst around 2.5 times more memory, in addition to the 1.7 for the working set. $est_gd_size = $this->image_width * $this->image_height * 4 * 1.7 * 2.5; if ( memory_get_usage( true ) + $est_gd_size < $this->get_memory_limit() ) { // Image_Effect relies on GD, which strips and ignores EXIF $this->maybe_hardcode_exif_orientation( 'all' ); $effect = new Image_Effect( 'optimized', $this->image_format ); if ( method_exists( $effect, $arg ) ) { $effect->$arg( $this->image, $val ); if ( is_array( $effect->processed ) ) $this->processed = array_merge( $this->processed, $effect->processed ); } } } unset( $args[$arg] ); break; case 'lb': // letterboxing is not available for animated images if ( ! $this->animated_input && $this->letterbox( $val ) ) { $this->processed[] = 'letterbox'; } unset( $args['lb'] ); break; case 'ulb': // unletterboxing is not available for animated images if ( ! $this->animated_input && 'true' === $val ) { // Due to the fact that these functions require memory intensive conversions back and forth // between the backend class and GD, we only allow unletterboxing if the (roughly estimated) memory // requirement will be less than the script memory limit $est_gd_size = $this->image_width * $this->image_height * 4 * 2.5; if ( memory_get_usage( true ) + $est_gd_size < $this->get_memory_limit() && $this->unletterbox() ) { $this->processed[] = 'unletterbox'; } } unset( $args['ulb'] ); break; default: error_log( "Error: an unknown arg made it through: $arg" ); unset( $args[ $arg ] ); break; } } // If user requested a quality < 100, enforce lossy compression $requested_quality = intval( $this->quality ); $should_use_quality = $requested_quality > 0 && $requested_quality < 100; if ( $should_use_quality || $this->allow_lossy ) { $this->lossless_input = false; } if ( 'Gif_Image' === get_class( $this->image ) ) { if ( 0 == count( $this->processed ) ) { // Unset the Gif_Image object to free some memory for the `echo` below unset( $this->image ); $this->send_headers( strlen( $this->image_data ) ); echo $this->image_data; } else { $this->process_and_send_gif(); } } else { // GraphicsMagick ocassionally incorrectly converts GRAYSCALEMATTE images // to GRAYSCALE on write, losing transparency information. if ( Photon_OpenCV::IMGTYPE_GRAYSCALEMATTE === $this->image->getimagetype() ) { $this->image->setimagetype( Photon_OpenCV::IMGTYPE_TRUECOLORMATTE ); } // If lossy, try to keep the original quality if ( 'image/jpeg' === $this->mime_type && ! isset( $this->quality ) && ! empty( $this->jpeg_details['q'] ) ) { $this->quality = $this->jpeg_details['q']; } if ( ! $this->animated_input && ! $this->_DISABLE_IMAGE_OPTIMIZATIONS && in_array( 'avif', $this->supported_formats, true ) ) { $this->compress_and_send_avif(); return; } // WebP encoding time is comparable to other other lossless formats, // therefore it is only considered an optimization if lossy is an option if ( ! $this->_DISABLE_IMAGE_OPTIMIZATIONS || 'image/webp' === $this->mime_type || $this->lossless_input ) { // WebP conversions can fail or be rejected (e.g. they violate size limits), fallback to traditional formats if ( in_array( 'webp', $this->supported_formats, true ) ) { if ( $this->try_to_compress_and_send_webp() ) { return; } } } if ( $this->animated_input && in_array( 'gif', $this->supported_formats, true ) ) { // For simplicitly, proper animation fallback from WebP only happens if direct GIF encoding is supported $this->directly_compress_and_send_gif(); } elseif ( $this->lossless_input || $this->image_has_transparency ) { $this->compress_and_send_png(); } else { $this->compress_and_send_jpeg(); } } } public function crop( $width, $height ) { if ( $this->inverted_axis ) { $tmp = $width; $width = $height; $height = $tmp; } // crop the largest possible portion of the original image $aspect_ratio = $this->image_width / $this->image_height; $new_w = min( $width, $this->image_width ); $new_h = min( $height, $this->image_height ); if ( ! $new_w ) $new_w = intval( $new_h * $aspect_ratio ); if ( ! $new_h ) $new_h = intval( $new_w / $aspect_ratio ); $size_ratio = max( $new_w / $this->image_width, $new_h / $this->image_height ); $crop_w = min( ceil( $new_w / $size_ratio ), $this->image_width ); $crop_h = min( ceil( $new_h / $size_ratio ), $this->image_height ); $s_x = round( ( $this->image_width - $crop_w ) / 2 ); $s_y = round( ( $this->image_height - $crop_h ) / 2 ); // checks params and skips this transformation if it is found to be out of bounds if ( ! $this->valid_request( $new_w, $new_h ) ) return false; if ( 'Gif_Image' === get_class( $this->image ) ) { $this->image->add_function( 'resize_and_crop', ( $width . ',' . $height ) ); return true; } $this->image->cropimage( $crop_w, $crop_h, $s_x, $s_y ); if ( $this->image_has_transparency ) $this->image->resizeimage( $new_w, $new_h, Photon_OpenCV::FILTER_LANCZOS, 1.0 ); else $this->image->scaleimage( $new_w, $new_h ); $this->image_width = $new_w; $this->image_height = $new_h; return true; } public function crop_offset( $x, $y, $width, $height ) { if ( $this->inverted_axis ) { $tmp = $x; $x = $y; $y = $tmp; $tmp = $width; $width = $height; $height = $tmp; } // crop using the offsets if ( substr( $width, -2 ) == 'px' ) $new_w = max( 0, min( $this->image_width, intval( $width ) ) ); else $new_w = round( $this->image_width * abs( intval( $width ) ) / 100 ); if ( substr( $height, -2 ) == 'px' ) $new_h = max( 0, min( $this->image_height, intval( $height ) ) ); else $new_h = round( $this->image_height * abs( intval( $height ) ) / 100 ); if ( substr( $x, -2 ) == 'px' ) $s_x = intval( $x ); else $s_x = round( $this->image_width * abs( intval( $x ) ) / 100 ); if ( substr( $y, -2 ) == 'px' ) $s_y = intval( $y ); else $s_y = round( $this->image_height * abs( intval( $y ) ) / 100 ); // check we haven't overstepped any boundaries if ( $s_x >= $this->image_width ) $s_x = 0; if ( $s_y >= $this->image_height ) $s_y = 0; if ( $new_w > $this->image_width - $s_x ) $new_w = $this->image_width - $s_x; if ( $new_h > $this->image_height - $s_y ) $new_h = $this->image_height - $s_y; $orientation = $this->get_orientation(); switch ( $orientation['degrees'] ) { case 90: $flipped_x = false; $flipped_y = ! $orientation['flopped']; break; case 180: $flipped_x = ! $orientation['flopped']; $flipped_y = true; break; case 270: $flipped_x = true; $flipped_y = $orientation['flopped']; break; default: $flipped_x = $orientation['flopped']; $flipped_y = false; break; } if ( $flipped_x ) { $s_x = $this->image_width - $s_x - $new_w; } if ( $flipped_y ) { $s_y = $this->image_height - $s_y - $new_h; } // checks params and skips this transformation if it is found to be out of bounds if ( ! $this->valid_request( $new_w, $new_h ) ) return false; if ( 'Gif_Image' === get_class( $this->image ) ) { $this->image->add_function( 'crop_offset', ( $s_x . ',' . $s_y . ',' . $new_w . ',' . $new_h ) ); return true; } $this->image->cropimage( $new_w, $new_h, $s_x, $s_y ); $this->image_width = $new_w; $this->image_height = $new_h; return true; } public function resize( $width, $height ) { if ( $this->inverted_axis ) { $tmp = $width; $width = $height; $height = $tmp; } $new_w = intval( $width ); $new_h = intval( $height ); if ( 0 >= $new_w || 0 >= $new_h || ( ( $new_w > $this->image_width ) && ( $new_w > $this->_UPSCALE_MAX_PIXELS ) ) || ( ( $new_h > $this->image_height ) && ( $new_h > $this->_UPSCALE_MAX_PIXELS ) ) ) { return false; } if ( 'Gif_Image' === get_class( $this->image ) ) { // the GIF class processes internally $this->image->add_function( 'resize_and_crop', ( $new_w . ',' . $new_h ) ); return true; } $this->upscale = true; // check dims and skip this transformation if they are found to be out of bounds if ( ! $this->valid_request( $new_w, $new_h ) ) { return false; } $ratio_orig = $this->image_width / $this->image_height; $ratio_end = $new_w / $new_h; if ( $ratio_orig > $ratio_end ) { // If we need to crop off the sides $crop_w = max( 1, round( $this->image_height * $ratio_end ) ); $crop_h = $this->image_height; } else { // If we need to crop off the top/bottom $crop_w = $this->image_width; $crop_h = max( 1, round( $this->image_width / $ratio_end ) ); } $s_x = floor( ( $this->image_width - $crop_w ) / 2 ); $s_y = floor( ( $this->image_height - $crop_h ) / 2 ); $this->image->cropimage( $crop_w, $crop_h, $s_x, $s_y ); if ( $this->image_has_transparency ) $this->image->resizeimage( $new_w, $new_h, Photon_OpenCV::FILTER_LANCZOS, 1.0 ); else $this->image->scaleimage( $new_w, $new_h ); $this->image_width = $new_w; $this->image_height = $new_h; return true; } public function fit( $width, $height ) { if ( $this->inverted_axis ) { $tmp = $width; $width = $height; $height = $tmp; } $new_w = $requested_w = abs( intval( $width ) ); $new_h = $requested_h = abs( intval( $height ) ); // we do not allow both new width and height to be larger at the same time if ( ! $requested_w || ! $requested_h || ( $this->image_width < $requested_w && $this->image_height < $requested_h ) ) { return false; } if ( 'Gif_Image' === get_class( $this->image ) ) { // the GIF class processes internally $this->image->add_function( 'fit_in_box', ( $requested_w . ',' . $requested_h ) ); return true; } $original_aspect = $this->image_width / $this->image_height; $new_aspect = $requested_w / $requested_h; if ( $original_aspect >= $new_aspect ) { $new_h = $requested_h; $new_w = round( $this->image_width / ( $this->image_height / $requested_h ) ); // check we haven't overstepped the width if ( $new_w > $requested_w ) { $new_w = $requested_w; $new_h = round( $this->image_height / ( $this->image_width / $requested_w ) ); } } else { $new_w = $requested_w; $new_h = round( $this->image_height / ( $this->image_width / $requested_w ) ); // check we haven't overstepped the height if ( $new_h > $requested_h ) { $new_h = $requested_h; $new_w = round( $this->image_width / ( $this->image_height / $requested_h ) ); } } // checks params and skips this transformation if it is found to be out of bounds if ( ! $this->valid_request( $new_w, $new_h ) ) return false; if ( $this->image_has_transparency ) $this->image->resizeimage( $new_w, $new_h, Photon_OpenCV::FILTER_LANCZOS, 1.0, true ); else $this->image->scaleimage( $new_w, $new_h, true ); $this->image_width = $new_w; $this->image_height = $new_h; return true; } public function set_width( $width ) { $width = intval( $width ); if ( $this->inverted_axis ) { $ratio = $this->image_height / $width; } else { $ratio = $this->image_width / $width; } if ( 0 == $ratio ) return false; $new_w = intval( $this->image_width / $ratio ); $new_h = intval( $this->image_height / $ratio ); // checks params and skips this transformation if it is found to be out of bounds if ( ! $this->valid_request( $new_w, $new_h ) ) return false; if ( 'Gif_Image' === get_class( $this->image ) ) { $this->image->add_function( 'set_width', $new_w ); return true; } if ( $this->image_has_transparency ) $this->image->resizeimage( $new_w, $new_h, Photon_OpenCV::FILTER_LANCZOS, 1.0 ); else $this->image->scaleimage( $new_w, $new_h ); $this->image_width = $new_w; $this->image_height = $new_h; return true; } public function set_height( $height ) { $height = intval( $height ); if ( $this->inverted_axis ) { $ratio = $this->image_width / $height; } else { $ratio = $this->image_height / $height; } if ( 0 == $ratio ) return false; $new_w = intval( $this->image_width / $ratio ); $new_h = intval( $this->image_height / $ratio ); // checks params and skips this transformation if it is found to be out of bounds if ( ! $this->valid_request( $new_w, $new_h ) ) return false; if ( 'Gif_Image' === get_class( $this->image ) ) { $this->image->add_function( 'set_height', $new_h ); return true; } if ( $this->image_has_transparency ) $this->image->resizeimage( $new_w, $new_h, Photon_OpenCV::FILTER_LANCZOS, 1.0 ); else $this->image->scaleimage( $new_w, $new_h ); $this->image_width = $new_w; $this->image_height = $new_h; return true; } function client_hint_dpr() { if ( true === $this->use_client_hints ) { if ( true === array_key_exists( 'HTTP_DPR', $_SERVER ) ) { return floatval( $_SERVER['HTTP_DPR'] ); } } return false; } function determine_zoom() { $hint = $this->client_hint_dpr(); if ( false !== $hint ) { return $hint; } if ( isset( $_GET['zoom'] ) ) { return floatval( $_GET['zoom'] ); } return 1; } function apply_zoom( $function_name, $arguments ) { $zoom = $this->determine_zoom(); // Treat zoom < 1 and === 1 the same: return early // (effectively 1x zoom) if ( $zoom <= 1 ) return $arguments; // Make sure that zoom is effectively never more than 10. $zoom = min( 10, $zoom ); if ( $zoom > 3 ) { // Round UP to the nearest 0.5 $zoom = ceil( $zoom * 2 ) / 2; } if ( false !== $this->client_hint_dpr() ) { // If we are responding to a request which provided a DPR // hint then the client is expecting a Content-DPR header // in the response $this->dpr_header_value = $zoom; } switch ( $function_name ) { case 'set_height' : case 'set_width' : $new_arguments = $arguments * $zoom; break; case 'fit' : case 'resize' : list( $width, $height ) = array_map( 'intval', explode( ',', $arguments ) ); $new_width = $width * $zoom; $new_height = $height * $zoom; // Avoid dimensions larger than original. while ( ( $new_width > $this->image_width || $new_height > $this->image_height ) && $zoom > 1 ) { // Step down in increments until we have valid dims if ( $zoom > 3 ) { $zoom -= 0.5; } else { $zoom -= 0.1; } $new_width = $width * $zoom; $new_height = $height * $zoom; } $new_arguments = "$new_width,$new_height"; break; default : $new_arguments = $arguments; } return $new_arguments; } private function unletterbox() { // GD cannot handle exif, hardcode it early $this->maybe_hardcode_exif_orientation( 'all' ); $img_effect = new Image_Effect( 'optimized', $this->image_format ); $img_effect->optimized_to_gd( $this->image ); // confirm the image is letterboxed and then sample the color // and use that color to search for the dims we need to crop $lb_red = -1; $lb_green = -1; $lb_blue = -1; for ( $w = 0; $w < imagesx( $this->image ); $w++ ) { $rgb = imagecolorat( $this->image, $w, 0 ); $r = ( $rgb >> 16 ) & 0xFF; $g = ( $rgb >> 8 ) & 0xFF; $b = $rgb & 0xFF; if ( -1 == $lb_red ) { $lb_red = $r; } else if ( $lb_red > $r + 1 || $lb_red < $r - 1 ) { $lb_red = -1; break; } if ( -1 == $lb_green ) { $lb_green = $g; } else if ( $lb_green > $g + 1 || $lb_green < $g - 1 ) { $lb_green = -1; break; } if ( -1 == $lb_blue ) { $lb_blue = $b; } else if ( $lb_blue > $b + 1 || $lb_blue < $b - 1 ) { $lb_blue = -1; break; } } if ( 0 > $lb_red || 0 > $lb_green || 0 > $lb_blue ) { $img_effect->gd_to_optimized( $this->image ); return false; } $first_unmatched_line = -1; for ( $h = 1; $h < imagesy( $this->image ); $h++ ) { $tr = $tg = $tb = 0; for ( $w = 0; $w < imagesx( $this->image ); $w++ ) { $rgb = imagecolorat( $this->image, $w, $h ); $r = ( $rgb >> 16 ) & 0xFF; $g = ( $rgb >> 8 ) & 0xFF; $b = $rgb & 0xFF; $tr += $r; $tg += $g; $tb += $b; } $ar = $tr / $w; $ag = $tg / $w; $ab = $tb / $w; if ( $lb_red > $ar + 10 || $lb_red < $ar - 10 ) { $first_unmatched_line = $h; break; } if ( $lb_green > $ag + 10 || $lb_green < $ag - 10 ) { $first_unmatched_line = $h; break; } if ( $lb_blue > $ab + 10 || $lb_blue < $ab - 10 ) { $first_unmatched_line = $h; break; } } if ( 0 > $first_unmatched_line ) { $img_effect->gd_to_optimized( $this->image ); return false; } $last_unmatched_line = -1; for ( $h = imagesy( $this->image ) - 1; $h >= 0; $h-- ) { $tr = $tg = $tb = 0; for( $w = 0; $w < imagesx( $this->image ); $w++ ) { $rgb = imagecolorat( $this->image, $w, $h ); $r = ( $rgb >> 16 ) & 0xFF; $g = ( $rgb >> 8 ) & 0xFF; $b = $rgb & 0xFF; $tr += $r; $tg += $g; $tb += $b; } $ar = $tr / $w; $ag = $tg / $w; $ab = $tb / $w; if ( $lb_red > $ar + 10 || $lb_red < $ar - 10 ) { $last_unmatched_line = $h + 1; break; } if ( $lb_green > $ag + 10 || $lb_green < $ag - 10 ) { $last_unmatched_line = $h + 1; break; } if ( $lb_blue > $ab + 10 || $lb_blue < $ab - 10 ) { $last_unmatched_line = $h + 1; break; } } $img_effect->gd_to_optimized( $this->image ); if ( 0 > $last_unmatched_line || $last_unmatched_line <= $first_unmatched_line ) { return false; } return $this->crop_offset( '0px', $first_unmatched_line . 'px', $this->image_width . 'px', ( $last_unmatched_line - $first_unmatched_line ) . 'px' ); } function letterbox( $args ) { $arg_arr = explode( ',', $args ); if ( 2 > count( $arg_arr ) ) return false; $end_w = abs( intval( $arg_arr[0] ) ); $end_h = abs( intval( $arg_arr[1] ) ); if ( $this->inverted_axis ) { $tmp = $end_w; $end_w = $end_h; $end_h = $tmp; } if ( 3 == count( $arg_arr ) ) { $color = $arg_arr[2]; if ( false === strpos( $color, '#' ) ) $color = '#' . $color; if ( ! preg_match( '/^#[0-9a-fA-F]{6}$/', $color ) ) $color = '#000'; } else { $color = '#000'; } if ( ( $this->image_width == $end_w && $this->image_height == $end_h ) || ! $end_w || ! $end_h || ( $this->image_width < $end_w && $this->image_height < $end_h ) ) { return false; } if ( $this->image_has_transparency ) $this->image->resizeimage( $end_w, $end_h, Photon_OpenCV::FILTER_LANCZOS, 1.0, true ); else $this->image->scaleimage( $end_w, $end_h, true ); $new_w = $this->image->getimagewidth(); $new_h = $this->image->getimageheight(); $border_h = round( ( $end_h - $new_h ) / 2 ); $border_w = round( ( $end_w - $new_w ) / 2 ); if ( $border_h > $this->_UPSCALE_MAX_PIXELS || $border_w > $this->_UPSCALE_MAX_PIXELS ) { return false; } $this->image->borderimage( $color, $border_w, $border_h ); $this->image_width = $end_w; $this->image_height = $end_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 ) $this->image->cropimage( $end_w, $end_h, $crop_x, $crop_y ); return true; } } // class Image_Processor } // class_exists