self::MAX_BYTES ) { return null; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local media library file. $data = file_get_contents( $path ); if ( false === $data || strlen( $data ) < 16 ) { return null; } $image = null; if ( "\xFF\xD8" === substr( $data, 0, 2 ) ) { $image = self::jpeg( $data ); } elseif ( "\x89PNG\r\n\x1A\n" === substr( $data, 0, 8 ) ) { $image = self::png( $data, $deflate ); } return $image ? $image : self::via_gd( $data ); } /** * A JPEG as a DCT-encoded image. * * @param string $data File contents. * @return array|null */ private static function jpeg( $data ) { $len = strlen( $data ); $p = 2; while ( $p + 9 < $len ) { if ( "\xFF" !== $data[ $p ] ) { return null; } $marker = ord( $data[ $p + 1 ] ); $size = ( ord( $data[ $p + 2 ] ) << 8 ) | ord( $data[ $p + 3 ] ); if ( in_array( $marker, array( 0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF ), true ) ) { $bits = ord( $data[ $p + 4 ] ); $h = ( ord( $data[ $p + 5 ] ) << 8 ) | ord( $data[ $p + 6 ] ); $w = ( ord( $data[ $p + 7 ] ) << 8 ) | ord( $data[ $p + 8 ] ); $ch = ord( $data[ $p + 9 ] ); if ( ! $w || ! $h || 8 !== $bits ) { return null; } if ( 1 === $ch ) { $cs = '/DeviceGray'; } elseif ( 4 === $ch ) { // Adobe CMYK JPEGs are stored inverted. $cs = '/DeviceCMYK /Decode [1 0 1 0 1 0 1 0]'; } else { $cs = '/DeviceRGB'; } return array( 'w' => $w, 'h' => $h, 'dict' => '/ColorSpace ' . $cs . ' /BitsPerComponent 8 /Filter /DCTDecode', 'data' => $data, 'smask' => null, 'smask_params' => '', ); } $p += 2 + $size; } return null; } /** * A PNG as a Flate-encoded image with PNG predictors. * * @param string $data File contents. * @param bool $deflate Whether zlib is available (needed for transparency). * @return array|null */ private static function png( $data, $deflate ) { $p = 8; $len = strlen( $data ); $ihdr = null; $plte = ''; $idat = ''; while ( $p + 8 <= $len ) { $n = unpack( 'N', substr( $data, $p, 4 ) )[1]; $type = substr( $data, $p + 4, 4 ); $body = substr( $data, $p + 8, $n ); if ( 'IHDR' === $type ) { $ihdr = unpack( 'Nw/Nh/Cbits/Ccolor/Ccomp/Cfilter/Cinterlace', $body ); } elseif ( 'PLTE' === $type ) { $plte = $body; } elseif ( 'IDAT' === $type ) { $idat .= $body; } elseif ( 'IEND' === $type ) { break; } $p += 12 + $n; } if ( ! $ihdr || '' === $idat || $ihdr['interlace'] || $ihdr['bits'] > 8 || ! $ihdr['w'] || ! $ihdr['h'] ) { return null; } $w = (int) $ihdr['w']; $h = (int) $ihdr['h']; $bits = (int) $ihdr['bits']; $color = (int) $ihdr['color']; $parms = static function ( $colors ) use ( $bits, $w ) { return ' /DecodeParms << /Predictor 15 /Colors ' . $colors . ' /BitsPerComponent ' . $bits . ' /Columns ' . $w . ' >>'; }; switch ( $color ) { case 0: $cs = '/DeviceGray'; $colors = 1; break; case 2: $cs = '/DeviceRGB'; $colors = 3; break; case 3: // Palette transparency (tRNS) would need decoding, so the image is drawn opaque. if ( '' === $plte ) { return null; } $cs = '[/Indexed /DeviceRGB ' . ( strlen( $plte ) / 3 - 1 ) . ' <' . bin2hex( $plte ) . '>]'; $colors = 1; break; case 4: case 6: return $deflate ? self::png_alpha( $idat, $w, $h, 6 === $color ) : null; default: return null; } return array( 'w' => $w, 'h' => $h, 'dict' => '/ColorSpace ' . $cs . ' /BitsPerComponent ' . $bits . ' /Filter /FlateDecode' . $parms( $colors ), 'data' => $idat, 'smask' => null, 'smask_params' => '', ); } /** * Splits an 8-bit PNG with an alpha channel into colour and soft mask. * * Each row keeps its PNG filter byte. PNG filters work per channel (each * byte refers to the same channel of the pixel before or above), so the * separated channels still decode correctly with the same predictors. * * @param string $idat Compressed image data. * @param int $w Width. * @param int $h Height. * @param bool $rgb RGBA (true) or grey + alpha (false). * @return array|null */ private static function png_alpha( $idat, $w, $h, $rgb ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Corrupt data just means "no logo". $raw = @gzuncompress( $idat ); if ( false === $raw ) { return null; } $bpp = $rgb ? 4 : 2; $cbpp = $bpp - 1; $row = 1 + $w * $bpp; $color = ''; $alpha = ''; if ( strlen( $raw ) < $row * $h ) { return null; } for ( $y = 0; $y < $h; $y++ ) { $line = substr( $raw, $y * $row, $row ); $color .= $line[0]; $alpha .= $line[0]; $pixels = substr( $line, 1 ); for ( $x = 0; $x < $w; $x++ ) { $color .= substr( $pixels, $x * $bpp, $cbpp ); $alpha .= $pixels[ $x * $bpp + $cbpp ]; } } $parms = static function ( $colors ) use ( $w ) { return ' /Filter /FlateDecode /DecodeParms << /Predictor 15 /Colors ' . $colors . ' /BitsPerComponent 8 /Columns ' . $w . ' >>'; }; return array( 'w' => $w, 'h' => $h, 'dict' => '/ColorSpace ' . ( $rgb ? '/DeviceRGB' : '/DeviceGray' ) . ' /BitsPerComponent 8' . $parms( $cbpp ), 'data' => gzcompress( $color ), 'smask' => gzcompress( $alpha ), 'smask_params' => $parms( 1 ), ); } /** * Converts any image GD can read into a JPEG on white. * * @param string $data File contents. * @return array|null */ private static function via_gd( $data ) { if ( ! function_exists( 'imagecreatefromstring' ) || ! function_exists( 'imagejpeg' ) ) { return null; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Unsupported formats return false. $src = @imagecreatefromstring( $data ); if ( ! $src ) { return null; } $w = imagesx( $src ); $h = imagesy( $src ); $dst = imagecreatetruecolor( $w, $h ); imagefill( $dst, 0, 0, imagecolorallocate( $dst, 255, 255, 255 ) ); imagecopy( $dst, $src, 0, 0, 0, 0, $w, $h ); ob_start(); imagejpeg( $dst, null, 90 ); $jpeg = (string) ob_get_clean(); unset( $src, $dst ); return '' !== $jpeg ? self::jpeg( $jpeg ) : null; } }