Jacek Kowalski
4 hours ago 6b9278a5d57091fce636dbb640b380cfb990db6c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
declare(strict_types=1);
 
namespace App\Service;
 
/**
 * Scales covers down to JPEG thumbnails. Smaller images are never enlarged.
 */
class CoverThumbnailer {
    private const int JPEG_QUALITY = 85;
 
    /**
     * Sizes are twice the display size, for high-density screens.
     *
     * @var array<string, array{int, int}> maximum width and height by size name
     */
    public const array SIZES = [
        'list' => [150, 225],
        'details' => [600, 900],
    ];
 
    /**
     * @param string $data the image file's contents
     * @param callable(string): string $targetPath gives the file for a size name
     *
     * @throws CoverProcessingException
     */
    public function createThumbnails(string $data, callable $targetPath): void {
        $image = $data === '' ? false : @imagecreatefromstring($data);
 
        if ($image === false) {
            throw new CoverProcessingException('The file could not be read as an image.');
        }
 
        $image = self::applyExifOrientation($image, $data);
 
        foreach (self::SIZES as $name => [$maxWidth, $maxHeight]) {
            if (!imagejpeg(self::scale($image, $maxWidth, $maxHeight), $targetPath($name), self::JPEG_QUALITY)) {
                throw new CoverProcessingException('The thumbnail could not be written.');
            }
        }
    }
 
    private static function scale(\GdImage $image, int $maxWidth, int $maxHeight): \GdImage {
        $width = imagesx($image);
        $height = imagesy($image);
        $ratio = min(1, $maxWidth / $width, $maxHeight / $height);
        $targetWidth = max(1, (int)round($width * $ratio));
        $targetHeight = max(1, (int)round($height * $ratio));
 
        // JPEG has no transparency, so transparent PNG and WebP covers go on a white background.
        $thumbnail = imagecreatetruecolor($targetWidth, $targetHeight);
        imagefill($thumbnail, 0, 0, (int)imagecolorallocate($thumbnail, 255, 255, 255));
        imagecopyresampled($thumbnail, $image, 0, 0, 0, 0, $targetWidth, $targetHeight, $width, $height);
 
        return $thumbnail;
    }
 
    /**
     * Phones store the rotation of a photo as metadata, which thumbnails would otherwise lose.
     */
    private static function applyExifOrientation(\GdImage $image, string $data): \GdImage {
        if (\IMAGETYPE_JPEG !== (@getimagesizefromstring($data)[2] ?? null)) {
            return $image;
        }
 
        $orientation = @exif_read_data('data://image/jpeg;base64,' . base64_encode($data))['Orientation'] ?? 1;
 
        if (\in_array($orientation, [2, 4, 5, 7], true)) {
            imageflip($image, \IMG_FLIP_HORIZONTAL);
        }
 
        $angle = match ($orientation) {
            3, 4 => 180,
            5, 8 => 90,
            6, 7 => 270,
            default => 0,
        };
 
        return $angle === 0 ? $image : imagerotate($image, $angle, 0);
    }
}