ImageUtils::imageCreateFromString()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace WebThumbnailer\Utils;
6
7
use WebThumbnailer\Exception\ImageConvertException;
8
use WebThumbnailer\Exception\NotAnImageException;
9
use WebThumbnailer\WebThumbnailer;
10
11
/**
12
 * Util class to manipulate GD images.
13
 */
14
class ImageUtils
15
{
16
    /**
17
     * Generate a clean PNG thumbnail from given image resource.
18
     *
19
     * It makes sure the downloaded image is really an image,
20
     * doesn't contain funny stuff, and it resize it to a standard size.
21
     * Resizing conserves proportions.
22
     *
23
     * @param string   $imageStr  Source image.
24
     * @param string   $target    Path where the generated thumb will be saved.
25
     * @param int      $maxWidth  Max width for the generated thumb.
26
     * @param int      $maxHeight Max height for the generated thumb.
27
     * @param bool     $crop      Will crop the image to a fixed size if true. Height AND width must be provided.
28
     *
29
     * @throws NotAnImageException   The given resource isn't an image.
30
     * @throws ImageConvertException Another error occured.
31
     */
32
    public static function generateThumbnail(
33
        string $imageStr,
34
        string $target,
35
        int $maxWidth,
36
        int $maxHeight,
37
        bool $crop = false,
38
        string $resizeMode = WebThumbnailer::RESAMPLE
39
    ): void {
40
        if (!touch($target)) {
41
            throw new ImageConvertException('Target file is not writable.');
42
        }
43
44
        if ($crop && ($maxWidth == 0 || $maxHeight == 0)) {
45
            throw new ImageConvertException('Both width and height must be provided for cropping');
46
        }
47
48
        if ($maxWidth < 0 || $maxHeight < 0) {
49
            throw new ImageConvertException('Height and width must be zero or positive');
50
        }
51
52
        $sourceImg = static::imageCreateFromString($imageStr);
53
        if ($sourceImg === false) {
54
            throw new NotAnImageException();
55
        }
56
57
        $originalWidth = imagesx($sourceImg);
58
        $originalHeight = imagesy($sourceImg);
59
        if ($maxWidth > $originalWidth) {
60
            $maxWidth = $originalWidth;
61
        }
62
        if ($maxHeight > $originalHeight) {
63
            $maxHeight = $originalHeight;
64
        }
65
66
        list($finalWidth, $finalHeight) = static::calcNewSize(
67
            $originalWidth,
68
            $originalHeight,
69
            $maxWidth,
70
            $maxHeight,
71
            $crop
72
        );
73
74
        $targetImg = imagecreatetruecolor($finalWidth, $finalHeight);
75
        if ($targetImg === false) {
76
            throw new ImageConvertException('Could not generate the thumbnail from source image.');
77
        }
78
79
        $resizeFunction = $resizeMode === WebThumbnailer::RESIZE ? 'imagecopyresized' : 'imagecopyresampled';
80
        if (
81
            !$resizeFunction(
82
                $targetImg,
83
                $sourceImg,
84
                0,
85
                0,
86
                0,
87
                0,
88
                $finalWidth,
89
                $finalHeight,
90
                $originalWidth,
91
                $originalHeight
92
            )
93
        ) {
94
            static::imageDestroy($sourceImg);
95
            static::imageDestroy($targetImg);
0 ignored issues
show
Bug introduced by
It seems like $targetImg can also be of type GdImage; however, parameter $image of WebThumbnailer\Utils\ImageUtils::imageDestroy() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

95
            static::imageDestroy(/** @scrutinizer ignore-type */ $targetImg);
Loading history...
96
97
            throw new ImageConvertException('Could not generate the thumbnail from source image.');
98
        }
99
100
        if ($crop) {
101
            $targetImg = imagecrop($targetImg, [
102
                'x' => $finalWidth >= $finalHeight ? (int) floor(($finalWidth - $maxWidth) / 2) : 0,
103
                'y' => $finalHeight <= $finalWidth ? (int) floor(($finalHeight - $maxHeight) / 2) : 0,
104
                'width' => $maxWidth,
105
                'height' => $maxHeight
106
            ]);
107
        }
108
109
        if (false === $targetImg) {
110
            throw new ImageConvertException('Could not generate the thumbnail.');
111
        }
112
113
        imagedestroy($sourceImg);
114
        imagejpeg($targetImg, $target);
115
        imagedestroy($targetImg);
116
    }
117
118
    /**
119
     * Calculate image new size to keep proportions depending on actual image size
120
     * and max width/height settings.
121
     *
122
     * @param int  $originalWidth  Image original width
123
     * @param int  $originalHeight Image original height
124
     * @param int  $maxWidth       Target image maximum width
125
     * @param int  $maxHeight      Target image maximum height
126
     * @param bool $crop           Is cropping enabled
127
     *
128
     * @return int[] [final width, final height]
129
     *
130
     * @throws ImageConvertException At least maxwidth or maxheight needs to be defined
131
     */
132
    public static function calcNewSize(
133
        int $originalWidth,
134
        int $originalHeight,
135
        int $maxWidth,
136
        int $maxHeight,
137
        bool $crop
138
    ): array {
139
        if (empty($maxHeight) && empty($maxWidth)) {
140
            throw new ImageConvertException('At least maxwidth or maxheight needs to be defined.');
141
        }
142
        $diffWidth = !empty($maxWidth) ? $originalWidth - $maxWidth : false;
143
        $diffHeight = !empty($maxHeight) ? $originalHeight - $maxHeight : false;
144
145
        if (
146
            ($diffHeight === false && $diffWidth !== false)
147
            || ($diffWidth > $diffHeight && !$crop)
148
            || ($diffWidth < $diffHeight && $crop)
149
        ) {
150
            $finalWidth = $maxWidth;
151
            $finalHeight = $originalHeight * ($finalWidth / $originalWidth);
152
        } else {
153
            $finalHeight = $maxHeight;
154
            $finalWidth = $originalWidth * ($finalHeight / $originalHeight);
155
        }
156
157
        return [(int) floor($finalWidth), (int) floor($finalHeight)];
158
    }
159
160
    /**
161
     * Check if a file extension is an image.
162
     *
163
     * @param string $ext file extension.
164
     *
165
     * @return bool true if it's an image extension, false otherwise.
166
     */
167
    public static function isImageExtension(string $ext): bool
168
    {
169
        $supportedImageFormats = ['png', 'jpg', 'jpeg', 'svg'];
170
        return in_array($ext, $supportedImageFormats);
171
    }
172
173
    /**
174
     * Check if a string is an image.
175
     *
176
     * @param string $content String to check.
177
     *
178
     * @return bool True if the content is image, false otherwise.
179
     */
180
    public static function isImageString(string $content): bool
181
    {
182
        return static::imageCreateFromString($content) !== false;
183
    }
184
185
    /**
186
     * With custom error handlers, @ does not stop the warning to being thrown.
187
     *
188
     * @param string $content
189
     *
190
     * @return resource|false
191
     */
192
    protected static function imageCreateFromString(string $content)
193
    {
194
        try {
195
            return @imagecreatefromstring($content);
0 ignored issues
show
Bug Best Practice introduced by
The expression return @imagecreatefromstring($content) also could return the type GdImage which is incompatible with the documented return type false|resource.
Loading history...
196
        } catch (\Throwable $e) {
197
            // Avoid raising PHP exceptions here with custom error handler, we want to raise our own.
198
        }
199
200
        return false;
201
    }
202
203
    /**
204
     * With custom error handlers, @ does not stop the warning to being thrown.
205
     *
206
     * @param resource $image
207
     *
208
     * @return bool
209
     */
210
    // resource can't be type hinted:
211
    // phpcs:ignore Gskema.Sniffs.CompositeCodeElement.FqcnMethodSniff
212
    protected static function imageDestroy($image): bool
213
    {
214
        try {
215
            return @imagedestroy($image);
216
        } catch (\Throwable $e) {
217
            // Avoid raising PHP exceptions here with custom error handler, we want to raise our own.
218
        }
219
220
        return false;
221
    }
222
}
223