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
![]() |
|||
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
|
|||
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 |