mime, self::SUPPORTED_MIMES)) { return self::DEFAULT_HASH; } if ($media->thumbnail_path == null) { return self::DEFAULT_HASH; } $file = $path ?: storage_path('app/'.$media->thumbnail_path); if (! is_file($file)) { return self::DEFAULT_HASH; } $contents = file_get_contents($file); if ($contents === false || $contents === '') { return self::DEFAULT_HASH; } return self::fromBinary($contents) ?? self::DEFAULT_HASH; } /** * Hash raw image bytes, or return null when they cannot be decoded. * * Decoding goes through the configured Intervention driver instead of calling * GD directly, for two reasons. A vips (or imagick) host is not required to * ship ext-gd at all, and there the old imagecreatefromstring() call was an * undefined function: an \Error, which slipped past every catch (\Exception) * above it and failed the thumbnail job. And the driver that wrote the * thumbnail is the one guaranteed to be able to read it back (webp included). * * GD stays as a fallback when it is installed, so a misconfigured driver * degrades to the previous behaviour instead of to the default hash. */ public static function fromBinary(string $contents): ?string { foreach (self::candidateDrivers() as $driver) { try { $pixels = self::samplePixels($contents, $driver); if (! $pixels) { continue; } $blurhash = BlurhashEngine::encode($pixels, self::COMPONENTS_X, self::COMPONENTS_Y); return strlen($blurhash) > 191 ? null : $blurhash; } catch (Throwable $e) { continue; } } return null; } protected static function candidateDrivers(): array { $drivers = [config('image.driver', 'vips')]; if (function_exists('imagecreatefromstring')) { $drivers[] = 'gd'; } return array_values(array_unique($drivers)); } /** * Decode, downscale and flatten the image, then return it as rows of [r, g, b]. */ protected static function samplePixels(string $contents, string $driver): array { $image = ImageDriverManager::createImageManager([ 'decodeAnimation' => false, ], $driver)->decodeBinary($contents); $image = $image->scaleDown(self::SAMPLE_MAX, self::SAMPLE_MAX); // Transparent pixels have to be composited onto the background before they // are read, because only RGB is sampled. GD happened to leave white behind // fully transparent pixels after a resize, libvips leaves black, so without // this every transparent PNG hashes to a dark placeholder under vips. $image = $image->fillTransparentAreas(); $native = $image->core()->native(); return match (true) { $native instanceof VipsImage => self::pixelsFromVips($native), $native instanceof Imagick => self::pixelsFromImagick($native), $native instanceof GdImage => self::pixelsFromGd($native), default => throw new RuntimeException('Unsupported image driver for blurhash'), }; } protected static function pixelsFromVips(VipsImage $image): array { // The vips driver keeps the source colourspace (a CMYK jpg thumbnail stays // CMYK), so normalise to 8-bit sRGB before reading the first three bands. if ($image->interpretation !== Interpretation::SRGB) { $image = $image->colourspace(Interpretation::SRGB); } // Already opaque after fillTransparentAreas(), this only drops the alpha band. if ($image->hasAlpha()) { $image = $image->flatten(['background' => [255, 255, 255]]); } if ($image->bands > 3) { $image = $image->extract_band(0, ['n' => 3]); } if ($image->format !== BandFormat::UCHAR) { $image = $image->cast(BandFormat::UCHAR); } $width = $image->width; $height = $image->height; $bands = $image->bands; return self::pixelsFromBytes($image->writeToMemory(), $width, $height, $bands); } protected static function pixelsFromImagick(Imagick $image): array { if ($image->getImageColorspace() !== Imagick::COLORSPACE_SRGB) { $image->transformImageColorspace(Imagick::COLORSPACE_SRGB); } $width = $image->getImageWidth(); $height = $image->getImageHeight(); $values = $image->exportImagePixels(0, 0, $width, $height, 'RGB', Imagick::PIXEL_CHAR); $pixels = []; $i = 0; for ($y = 0; $y < $height; $y++) { $row = []; for ($x = 0; $x < $width; $x++) { $row[] = [$values[$i], $values[$i + 1], $values[$i + 2]]; $i += 3; } $pixels[] = $row; } return $pixels; } protected static function pixelsFromGd(GdImage $image): array { $width = imagesx($image); $height = imagesy($image); $pixels = []; for ($y = 0; $y < $height; $y++) { $row = []; for ($x = 0; $x < $width; $x++) { $colors = imagecolorsforindex($image, imagecolorat($image, $x, $y)); $row[] = [$colors['red'], $colors['green'], $colors['blue']]; } $pixels[] = $row; } return $pixels; } protected static function pixelsFromBytes(string $bytes, int $width, int $height, int $bands): array { if ($width < 1 || $height < 1 || strlen($bytes) < $width * $height * $bands) { throw new RuntimeException('Unexpected pixel buffer size for blurhash'); } $pixels = []; $i = 0; for ($y = 0; $y < $height; $y++) { $row = []; for ($x = 0; $x < $width; $x++) { $r = ord($bytes[$i]); $row[] = $bands >= 3 ? [$r, ord($bytes[$i + 1]), ord($bytes[$i + 2])] : [$r, $r, $r]; $i += $bands; } $pixels[] = $row; } return $pixels; } }