'image/webp' ) ); if ( $class && false !== stripos( $class, 'imagick' ) && class_exists( 'Imagick' ) ) { $engine = 'imagick'; } elseif ( $class && false !== stripos( $class, 'gd' ) && function_exists( 'imagewebp' ) && function_exists( 'imagecreatefromstring' ) ) { $engine = 'gd'; } return $engine; } /** * Open an image. * * @param string $path File. * @return array|WP_Error Handle: [engine, resource, width, height, alpha]. */ public static function open( $path ) { $engine = self::engine(); if ( '' === $engine || ! is_readable( $path ) ) { return new WP_Error( 'spinix_io_no_engine', 'fallback' ); } try { if ( 'imagick' === $engine ) { $im = new Imagick(); $im->readImage( $path ); if ( $im->getNumberImages() > 1 ) { $im = $im->coalesceImages(); $im->setIteratorIndex( 0 ); $first = $im->getImage(); $im->clear(); $im = $first; } if ( Imagick::COLORSPACE_CMYK === $im->getImageColorspace() ) { $im->clear(); return new WP_Error( 'spinix_io_cmyk', 'fallback' ); } // Prepare once: drop metadata (keep the colour profile), set WebP output. $profiles = $im->getImageProfiles( 'icc', true ); $im->stripImage(); if ( ! empty( $profiles['icc'] ) ) { $im->profileImage( 'icc', $profiles['icc'] ); } $im->setImageFormat( 'webp' ); $im->setOption( 'webp:method', '4' ); return array( 'engine' => 'imagick', 'im' => $im, 'w' => $im->getImageWidth(), 'h' => $im->getImageHeight(), ); } wp_raise_memory_limit( 'image' ); $data = Spinix_Image_Optimization_Filesystem::read( $path ); $im = false === $data ? false : @imagecreatefromstring( $data ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a broken upload must fail quietly; the error is reported in the Images list. unset( $data ); if ( ! $im ) { return new WP_Error( 'spinix_io_decode', __( 'File is not an image.', 'spinix-image-optimization' ) ); } if ( ! imageistruecolor( $im ) ) { imagepalettetotruecolor( $im ); } imagealphablending( $im, false ); imagesavealpha( $im, true ); return array( 'engine' => 'gd', 'im' => $im, 'w' => imagesx( $im ), 'h' => imagesy( $im ), ); } catch ( Exception $e ) { return new WP_Error( 'spinix_io_decode', $e->getMessage() ); } } /** * Free a handle. * * @param array $h Handle. */ public static function close( $h ) { if ( ! is_array( $h ) || empty( $h['im'] ) ) { return; } if ( 'imagick' === $h['engine'] ) { $h['im']->clear(); } elseif ( PHP_VERSION_ID < 80000 ) { imagedestroy( $h['im'] ); // GD images are freed automatically on PHP 8+. } } /** * Encode to WebP in memory. * * @param array $h Handle. * @param int $q Quality. * @return string|false */ private static function webp( $h, $q ) { if ( 'imagick' === $h['engine'] ) { $h['im']->setImageCompressionQuality( (int) $q ); return $h['im']->getImageBlob(); } // GD can only write WebP to a file or to the output: capture it in our own // buffer, which is always closed again, even if encoding fails. $level = ob_get_level(); $ok = false; $blob = ''; ob_start(); try { $ok = imagewebp( $h['im'], null, $q ); } finally { while ( ob_get_level() > $level ) { $blob = (string) ob_get_clean() . $blob; } } return $ok ? $blob : false; } /** * Encode a handle to a WebP file, searching the quality in memory. * * @param array $h Handle. * @param string $dest Destination path. * @param int $quality Starting quality. * @param int $floor Lowest quality allowed (0 = no search). * @param float $target Target bytes per pixel. * @return array|WP_Error {path, width, height, quality, bytes} */ public static function save( $h, $dest, $quality, $floor, $target ) { try { $pixels = max( 1, $h['w'] * $h['h'] ); $q = (int) $quality; $blob = self::webp( $h, $q ); if ( false === $blob || '' === $blob ) { return new WP_Error( 'spinix_io_encode', __( 'WebP file could not be written.', 'spinix-image-optimization' ) ); } // Quality search: predict the step from how far we are from the target // (WebP size grows roughly exponentially with quality), max 3 extra encodes. $tries = 0; while ( $floor > 0 && $q > $floor && $tries < 3 ) { $bytes = strlen( $blob ); $limit = $pixels * $target; if ( $bytes <= $limit || ( $bytes - $limit ) <= 2048 ) { break; } $step = (int) ceil( log( $bytes / $limit ) / 0.022 ); $q = max( $floor, $q - max( 4, min( 30, $step ) ) ); $next = self::webp( $h, $q ); if ( false === $next || '' === $next ) { break; } $blob = $next; ++$tries; } if ( ! Spinix_Image_Optimization_Filesystem::write( $dest, $blob ) ) { return new WP_Error( 'spinix_io_write', __( 'WebP file could not be written.', 'spinix-image-optimization' ) ); } return array( 'path' => $dest, 'width' => $h['w'], 'height' => $h['h'], 'quality' => $q, 'bytes' => strlen( $blob ), ); } catch ( Exception $e ) { return new WP_Error( 'spinix_io_encode', $e->getMessage() ); } } }