• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

harmim / images / #1415

pending completion
#1415

push

harmim
Use PHPStan and coding standard

377 of 377 new or added lines in 7 files covered. (100.0%)

447 of 525 relevant lines covered (85.14%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.97
/src/ImageStorage.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Harmim\Images;
6

7
use Harmim;
8
use Nette;
9

10

11
readonly class ImageStorage
12
{
13
        private Harmim\Images\Config\Config $config;
14

15
        private string $baseDir;
16

17
        private string $placeholder;
18

19
        private string $origDir;
20

21
        private string $compressionDir;
22

23

24
        /**
25
         * @param  array<string, mixed>  $config
26
         */
27
        public function __construct(array $config)
1✔
28
        {
29
                $this->config = Harmim\Images\Config\Config::fromArray($config);
1✔
30
                $this->baseDir =
1✔
31
                        $this->config->wwwDir
1✔
32
                        . DIRECTORY_SEPARATOR
1✔
33
                        . $this->config->imagesDir;
1✔
34
                $this->placeholder =
1✔
35
                        $this->config->wwwDir
1✔
36
                        . DIRECTORY_SEPARATOR
1✔
37
                        . $this->config->placeholder;
1✔
38
                $this->origDir =
1✔
39
                        $this->baseDir . DIRECTORY_SEPARATOR . $this->config->origDir;
1✔
40
                $this->compressionDir =
1✔
41
                        $this->baseDir
1✔
42
                        . DIRECTORY_SEPARATOR
1✔
43
                        . $this->config->compressionDir;
1✔
44
        }
1✔
45

46

47
        /**
48
         * @throws Nette\Utils\ImageException
49
         * @throws Nette\IOException
50
         */
51
        public function saveUpload(Nette\Http\FileUpload $file): string
1✔
52
        {
53
                if ($file->isOk()) {
1✔
54
                        return $this->saveImage(
1✔
55
                                $file->getSanitizedName(),
1✔
56
                                $file->getTemporaryFile(),
1✔
57
                        );
58
                }
59

60
                throw new Nette\IOException(
×
61
                        sprintf('Upload error code: %d.', $file->getError()),
×
62
                );
63
        }
64

65

66
        /**
67
         * @throws Nette\Utils\ImageException
68
         */
69
        public function saveImage(string $name, string $path): string
1✔
70
        {
71
                $file = new Nette\Http\FileUpload([
1✔
72
                        'name' => $name,
1✔
73
                        'size' => 0,
1✔
74
                        'tmp_name' => $path,
1✔
75
                        'error' => UPLOAD_ERR_OK,
1✔
76
                ]);
77

78
                $fileName = $this->getUniqueFileName($file->getSanitizedName());
1✔
79
                $origPath = $this->getOrigPath($fileName);
1✔
80

81
                $file->move($origPath);
1✔
82

83
                try {
84
                        $this->createImage($origPath, $this->getCompressionPath($fileName));
1✔
85
                } catch (Nette\Utils\ImageException $e) {
×
86
                        Nette\Utils\FileSystem::delete($origPath);
×
87
                        throw $e;
×
88
                }
89

90
                return $fileName;
1✔
91
        }
92

93

94
        /**
95
         * @param  list<string>  $excludedTypes
96
         */
97
        public function deleteImage(string $file, array $excludedTypes = []): void
98
        {
99
                if (count($excludedTypes) === 0) {
×
100
                        Nette\Utils\FileSystem::delete($this->getOrigPath($file));
×
101
                        Nette\Utils\FileSystem::delete($this->getCompressionPath($file));
×
102
                }
103

104
                foreach (array_keys($this->config->types) as $type) {
×
105
                        if (
106
                                count($excludedTypes) === 0
×
107
                                || !in_array($type, $excludedTypes, strict: true)
×
108
                        ) {
109
                                Nette\Utils\FileSystem::delete(
×
110
                                        $this->getDestPath($file, ['type' => $type]),
×
111
                                );
112
                        }
113
                }
114

115
                if (is_readable($this->baseDir)) {
×
116
                        $excludedFolders = array_keys($this->config->types) + [
×
117
                                $this->config->origDir,
×
118
                                $this->config->compressionDir,
×
119
                        ];
120

121
                        foreach (Nette\Utils\Finder::find(
×
122
                                $this->getSubDir($file) . DIRECTORY_SEPARATOR . $file,
×
123
                        )->from($this->baseDir)->exclude(...$excludedFolders) as $f) {
×
124
                                Nette\Utils\FileSystem::delete($f->getRealPath());
×
125
                        }
126
                }
127
        }
128

129

130
        /**
131
         * @param  array<string, mixed>  $options
132
         * @throws Nette\Utils\ImageException
133
         */
134
        public function getImageLink(
1✔
135
                string $file,
136
                ?string $type = null,
137
                array $options = [],
138
        ): ?string
139
        {
140
                if ($type !== null) {
1✔
141
                        $options['type'] = $type;
×
142
                }
143

144
                return ($image = $this->getImage($file, $options)) === null
1✔
145
                        ? null
×
146
                        : (string) $image;
1✔
147
        }
148

149

150
        /**
151
         * @internal
152
         * @param  array<string, mixed>  $options
153
         * @throws Nette\Utils\ImageException
154
         */
155
        final public function getImage(string $file, array $options = []): ?Image
1✔
156
        {
157
                $config = $this->getConfig($options);
1✔
158
                $srcPath = $this->getCompressionPath($file);
1✔
159

160
                if ($file === '' || !is_readable($srcPath)) {
1✔
161
                        return $this->getPlaceholderImage($config);
1✔
162
                }
163

164
                $destPath = $this->getDestPath($file, $config);
1✔
165

166
                if (
167
                        is_readable($destPath)
1✔
168
                        && ($size = getimagesize($destPath)) !== false
1✔
169
                ) {
170
                        [$width, $height] = $size;
1✔
171
                } else {
172
                        [$width, $height] = $this->createImage(
1✔
173
                                $srcPath,
1✔
174
                                $destPath,
175
                                $config,
176
                        );
177
                }
178

179
                return new Image(
1✔
180
                        $this->createRelativeWWWPath($destPath),
1✔
181
                        $width,
182
                        $height,
183
                );
184
        }
185

186

187
        /**
188
         * @internal
189
         */
190
        final public function getSubDir(string $fileName): string
1✔
191
        {
192
                return (string) (ord(substr($fileName, 0, 1)) % 42);
1✔
193
        }
194

195

196
        /**
197
         * @internal
198
         * @param  array<string, mixed>  $options
199
         */
200
        final public function getConfig(
1✔
201
                array $options = [],
202
        ): Harmim\Images\Config\Config
203
        {
204
                return $this->config->mergeWithOptions($options);
1✔
205
        }
206

207

208
        /**
209
         * @return array{int, int}
210
         * @throws Nette\Utils\ImageException
211
         */
212
        private function createImage(
1✔
213
                string $srcPath,
214
                string $destPath,
215
                Harmim\Images\Config\Config $config = null,
216
        ): array
217
        {
218
                if ($config === null) {
1✔
219
                        $config = $this->config;
1✔
220
                }
221

222
                try {
223
                        $type = null;
1✔
224
                        $image = Nette\Utils\Image::fromFile($srcPath);
1✔
225
                        Nette\Utils\FileSystem::createDir(dirname($destPath));
1✔
226
                        $this->transformImage($image, $config, $srcPath, $type);
1✔
227
                        $image->sharpen()->save($destPath, $config->compression, $type);
1✔
228

229
                        return [$image->getWidth(), $image->getHeight()];
1✔
230

231
                } catch (\Throwable $e) {
×
232
                        throw new Nette\Utils\ImageException(
×
233
                                $e->getMessage(),
×
234
                                $e->getCode(),
×
235
                                $e,
236
                        );
237
                }
238
        }
239

240

241
        /**
242
         * @param-out  Nette\Utils\Image  $image
243
         * @param-out  ?int  $type
244
         */
245
        private function transformImage(
1✔
246
                Nette\Utils\Image &$image,
247
                Harmim\Images\Config\Config $config,
248
                string $srcPath,
249
                ?int &$type,
250
        ): void
251
        {
252
                $resizeFlags = Resize::OrSmaller->flag();
1✔
253

254
                if (is_array($config->transform) && count($config->transform) > 1) {
1✔
255
                        foreach ($config->transform as $resize) {
×
256
                                if ($resize === Resize::Exact) {
×
257
                                        $this->transformExact($image, $config, $srcPath, $type);
×
258
                                        return;
×
259
                                } elseif ($resize instanceof Resize) {
×
260
                                        $resizeFlags |= $resize->flag();
×
261
                                }
262
                        }
263
                } else {
264
                        $resize = is_array($config->transform)
1✔
265
                                ? $config->transform[0]
×
266
                                : $config->transform;
1✔
267

268
                        if ($resize === Resize::Exact) {
1✔
269
                                $this->transformExact($image, $config, $srcPath, $type);
×
270
                                return;
×
271
                        } elseif ($resize instanceof Resize) {
1✔
272
                                $resizeFlags = $resize->flag();
1✔
273
                        }
274
                }
275

276
                assert(in_array(
1✔
277
                        $resizeFlags,
1✔
278
                        [
279
                                Nette\Utils\Image::ShrinkOnly,
1✔
280
                                Nette\Utils\Image::Stretch,
1✔
281
                                Nette\Utils\Image::OrSmaller,
1✔
282
                                Nette\Utils\Image::OrBigger,
1✔
283
                                Nette\Utils\Image::Cover,
1✔
284
                        ],
285
                        strict: true,
1✔
286
                ));
287
                $image->resize($config->width, $config->height, $resizeFlags);
1✔
288
        }
1✔
289

290

291
        /**
292
         * @param-out  Nette\Utils\Image  $image
293
         * @param-out  ?int  $type
294
         */
295
        private function transformExact(
296
                Nette\Utils\Image &$image,
297
                Harmim\Images\Config\Config $config,
298
                string $srcPath,
299
                ?int &$type,
300
        ): void
301
        {
302
                if ($this->isTransparentPng($srcPath)) {
×
303
                        $color = Nette\Utils\Image::rgb(255, 255, 255, 127);
×
304
                } else {
305
                        $color = Nette\Utils\Image::rgb(255, 255, 255);
×
306
                }
307

308
                $blank = Nette\Utils\Image::fromBlank(
×
309
                        $config->width,
×
310
                        $config->height,
×
311
                        $color,
312
                );
313
                $image->resize($config->width, null);
×
314
                $image->resize(null, $config->height);
×
315
                $blank->place(
×
316
                        $image,
×
317
                        (int) ($config->width / 2 - $image->getWidth() / 2),
×
318
                        (int) ($config->height / 2 - $image->getHeight() / 2),
×
319
                );
320
                $image = $blank;
×
321
                $type = Nette\Utils\Image::PNG;
×
322
        }
323

324

325
        private function isTransparentPng(string $path): bool
326
        {
327
                if (($finfo = finfo_open(FILEINFO_MIME_TYPE)) === false) {
×
328
                        return false;
×
329
                }
330

331
                if (
332
                        finfo_file($finfo, $path) !== image_type_to_mime_type(IMAGETYPE_PNG)
×
333
                ) {
334
                        return false;
×
335
                }
336

337
                if (($image = imagecreatefrompng($path)) === false) {
×
338
                        return false;
×
339
                }
340

341
                $width = imagesx($image);
×
342
                $height = imagesy($image);
×
343

344
                for ($x = 0; $x < $width; $x++) {
×
345
                        for ($y = 0; $y < $height; $y++) {
×
346
                                $color = imagecolorat($image, $x, $y);
×
347
                                if ($color !== false && ($color & 0x7F00_0000 >> 24) !== 0) {
×
348
                                        return true;
×
349
                                }
350
                        }
351
                }
352

353
                return false;
×
354
        }
355

356

357
        private function getPlaceholderImage(
1✔
358
                Harmim\Images\Config\Config $config,
359
        ): ?Image
360
        {
361
                if (!is_readable($this->placeholder)) {
1✔
362
                        return null;
×
363
                }
364

365
                return new Image(
1✔
366
                        $this->createRelativeWWWPath($this->placeholder),
1✔
367
                        $config->width,
1✔
368
                        $config->height,
1✔
369
                );
370
        }
371

372

373
        private function createRelativeWWWPath(string $path): string
1✔
374
        {
375
                return substr($path, strlen($this->config->wwwDir));
1✔
376
        }
377

378

379
        private function getCompressionPath(string $fileName): string
1✔
380
        {
381
                return $this->compressionDir
1✔
382
                        . DIRECTORY_SEPARATOR
1✔
383
                        . $this->getSubDir($fileName)
1✔
384
                        . DIRECTORY_SEPARATOR
1✔
385
                        . $fileName;
1✔
386
        }
387

388

389
        private function getOrigPath(string $fileName): string
1✔
390
        {
391
                return $this->origDir
1✔
392
                        . DIRECTORY_SEPARATOR
1✔
393
                        . $this->getSubDir($fileName)
1✔
394
                        . DIRECTORY_SEPARATOR
1✔
395
                        . $fileName;
1✔
396
        }
397

398

399
        /**
400
         * @param  array<string, mixed>|Harmim\Images\Config\Config  $config
401
         */
402
        private function getDestPath(
1✔
403
                string $fileName,
404
                array|Harmim\Images\Config\Config $config,
405
        ): string
406
        {
407
                if (is_array($config)) {
1✔
408
                        if (count($config) === 0) {
×
409
                                $config = $this->config;
×
410
                        } else {
411
                                $config = $this->getConfig($config);
×
412
                        }
413
                }
414

415
                if ($config->orig !== null && $config->orig) {
1✔
416
                        return $this->getOrigPath($fileName);
1✔
417
                } elseif ($config->compressed !== null && $config->compressed) {
1✔
418
                        return $this->getCompressionPath($fileName);
1✔
419
                } elseif ($config->destDir !== null && $config->destDir !== '') {
1✔
420
                        $destDir = $config->destDir;
×
421
                } elseif ($config->type !== null) {
1✔
422
                        $destDir = $config->type;
1✔
423
                } else {
424
                        $destDir = sprintf('w%dh%d', $config->width, $config->height);
1✔
425
                }
426

427
                return $this->baseDir
1✔
428
                        . DIRECTORY_SEPARATOR
1✔
429
                        . $destDir
1✔
430
                        . DIRECTORY_SEPARATOR
1✔
431
                        . $this->getSubDir($fileName)
1✔
432
                        . DIRECTORY_SEPARATOR
1✔
433
                        . $fileName;
1✔
434
        }
435

436

437
        private function getUniqueFileName(string $fileName): string
1✔
438
        {
439
                do {
440
                        $fileName = $this->getRandomFileName($fileName);
1✔
441
                } while (file_exists($this->getOrigPath($fileName)));
1✔
442

443
                return $fileName;
1✔
444
        }
445

446

447
        private function getRandomFileName(string $fileName): string
1✔
448
        {
449
                $name = Nette\Utils\Random::generate();
1✔
450

451
                return $name
452
                        . substr(md5($name), -5)
1✔
453
                        . substr(str_shuffle(md5($fileName)), -5)
1✔
454
                        . '.'
1✔
455
                        . pathinfo($fileName, PATHINFO_EXTENSION);
1✔
456
        }
457
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc