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

harmim / images / #1409

pending completion
#1409

push

harmim
Fix tests coverage in GitHub Actions

203 of 276 relevant lines covered (73.55%)

0.74 hits per line

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

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

3
declare(strict_types=1);
4

5
/**
6
 * @author Dominik Harmim <harmim6@gmail.com>
7
 */
8

9
namespace Harmim\Images;
10

11
use Nette;
12

13

14
class ImageStorage
15
{
16
        use Nette\SmartObject;
17

18

19
        public const RETURN_ORIG = 'return_orig',
20
                RETURN_COMPRESSED = 'return_compressed';
21

22
        public const RESIZE_SHRINK_ONLY = 'shrink_only',
23
                RESIZE_STRETCH = 'stretch',
24
                RESIZE_FIT = 'fit',
25
                RESIZE_FILL = 'fill',
26
                RESIZE_EXACT = 'exact',
27
                RESIZE_FILL_EXACT = 'fill_exact';
28

29
        private const RESIZE_FLAGS = [
30
                self::RESIZE_SHRINK_ONLY => Nette\Utils\Image::SHRINK_ONLY,
31
                self::RESIZE_STRETCH => Nette\Utils\Image::STRETCH,
32
                self::RESIZE_FIT => Nette\Utils\Image::FIT,
33
                self::RESIZE_FILL => Nette\Utils\Image::FILL,
34
                self::RESIZE_EXACT => Nette\Utils\Image::EXACT,
35
        ];
36

37
        private array $config;
38

39
        private array $types = [];
40

41
        private string $baseDir;
42

43
        private string $placeholder;
44

45
        private string $origDir;
46

47
        private string $compressionDir;
48

49

50
        public function __construct(array $config)
1✔
51
        {
52
                if ($config['types'] && is_array($config['types'])) {
1✔
53
                        $this->types = $config['types'];
1✔
54
                }
55
                $this->config = $config;
1✔
56
                $this->baseDir = $config['wwwDir'] . DIRECTORY_SEPARATOR . $config['imagesDir'];
1✔
57
                $this->placeholder = $config['wwwDir'] . DIRECTORY_SEPARATOR . $config['placeholder'];
1✔
58
                $this->origDir = $this->baseDir . DIRECTORY_SEPARATOR . $config['origDir'];
1✔
59
                $this->compressionDir = $this->baseDir . DIRECTORY_SEPARATOR . $config['compressionDir'];
1✔
60
        }
1✔
61

62

63
        /**
64
         * @param Nette\Http\FileUpload $file
65
         * @return string
66
         *
67
         * @throws Nette\Utils\ImageException
68
         * @throws Nette\IOException
69
         */
70
        public function saveUpload(Nette\Http\FileUpload $file): string
1✔
71
        {
72
                if ($file->isOk()) {
1✔
73
                        return $this->saveImage((string) $file->getName(), (string) $file->getTemporaryFile());
1✔
74
                }
75

76
                throw new Nette\IOException($file->getError());
×
77
        }
78

79

80
        /**
81
         * @param string $name
82
         * @param string $path
83
         * @return string
84
         *
85
         * @throws Nette\Utils\ImageException
86
         */
87
        public function saveImage(string $name, string $path): string
1✔
88
        {
89
                $file = new Nette\Http\FileUpload([
1✔
90
                        'name' => $name,
1✔
91
                        'size' => 0,
1✔
92
                        'tmp_name' => $path,
1✔
93
                        'error' => UPLOAD_ERR_OK,
1✔
94
                ]);
95

96
                $fileName = $this->getUniqueFileName($file->getName());
1✔
97
                $origPath = $this->getOrigPath($fileName);
1✔
98

99
                $file->move($origPath);
1✔
100

101
                try {
102
                        $this->createImage($origPath, $this->getCompressionPath($fileName));
1✔
103
                } catch (Nette\Utils\ImageException $e) {
×
104
                        Nette\Utils\FileSystem::delete($origPath);
×
105
                        throw $e;
×
106
                }
107

108
                return $fileName;
1✔
109
        }
110

111

112
        /**
113
         * @param string $file
114
         * @param string[] $excludedTypes
115
         * @return void
116
         */
117
        public function deleteImage(string $file, array $excludedTypes = []): void
118
        {
119
                if (!$excludedTypes) {
×
120
                        Nette\Utils\FileSystem::delete($this->getOrigPath($file));
×
121
                        Nette\Utils\FileSystem::delete($this->getCompressionPath($file));
×
122
                }
123

124
                foreach ($this->types as $key => $value) {
×
125
                        if (!$excludedTypes || !in_array($key, $excludedTypes, true)) {
×
126
                                Nette\Utils\FileSystem::delete($this->getDestPath($file, ['type' => $key]));
×
127
                        }
128
                }
129

130
                if (is_readable($this->baseDir)) {
×
131
                        $excludedFolders = array_keys($this->types) + [
×
132
                                $this->config['origDir'],
×
133
                                $this->config['compressionDir'],
×
134
                        ];
135

136
                        foreach (Nette\Utils\Finder::find($this->getSubDir($file) . DIRECTORY_SEPARATOR . $file)
×
137
                                ->from($this->baseDir)
×
138
                                ->exclude(...$excludedFolders) as $file) {
×
139
                                Nette\Utils\FileSystem::delete($file->getRealPath());
×
140
                        }
141
                }
142
        }
143

144

145
        /**
146
         * @param string $file
147
         * @param string|null $type
148
         * @param array $options
149
         * @return string|null
150
         *
151
         * @throws Nette\Utils\ImageException
152
         */
153
        public function getImageLink(string $file, ?string $type = null, array $options = []): ?string
154
        {
155
                if ($type !== null) {
×
156
                        $options['type'] = $type;
×
157
                }
158

159
                return ($image = $this->getImage($file, $options)) ? (string) $image : null;
×
160
        }
161

162

163
        /**
164
         * @internal
165
         *
166
         * @param string $file
167
         * @param array $args
168
         * @return Image|null
169
         *
170
         * @throws Nette\Utils\ImageException
171
         */
172
        public function getImage(string $file, array $args = []): ?Image
1✔
173
        {
174
                $options = $this->getOptions($args);
1✔
175
                $srcPath = $this->getCompressionPath($file);
1✔
176

177
                if (!$file || !is_readable($srcPath)) {
1✔
178
                        return $this->getPlaceholderImage($options);
1✔
179
                }
180

181
                $destPath = $this->getDestPath($file, $options);
1✔
182

183
                if (is_readable($destPath)) {
1✔
184
                        [$width, $height] = getimagesize($destPath);
1✔
185
                } elseif ($image = $this->createImage($srcPath, $destPath, $options)) {
1✔
186
                        [$width, $height] = $image;
1✔
187
                } else {
188
                        return $this->getPlaceholderImage($options);
×
189
                }
190

191
                return new Image($this->createRelativeWWWPath($destPath), (int) $width, (int) $height);
1✔
192
        }
193

194

195
        /**
196
         * @internal
197
         *
198
         * @param string $fileName
199
         * @return string
200
         */
201
        public function getSubDir(string $fileName): string
1✔
202
        {
203
                return (string) (ord(substr($fileName, 0, 1)) % 42);
1✔
204
        }
205

206

207
        /**
208
         * @internal
209
         *
210
         * @param array $args
211
         * @return array
212
         */
213
        public function getOptions(array $args = []): array
1✔
214
        {
215
                $type = [];
1✔
216

217
                if (
218
                        !empty($args['type'])
1✔
219
                        && array_key_exists($args['type'], $this->types)
1✔
220
                        && is_array($this->types[$args['type']])
1✔
221
                ) {
222
                        $type = $this->types[$args['type']];
1✔
223
                }
224

225
                return $args + $type + $this->config;
1✔
226
        }
227

228

229
        /**
230
         * @param string $srcPath
231
         * @param string $destPath
232
         * @param array $options
233
         * @return array
234
         *
235
         * @throws Nette\Utils\ImageException
236
         */
237
        private function createImage(string $srcPath, string $destPath, array $options = []): array
1✔
238
        {
239
                if (!$options) {
1✔
240
                        $options = $this->config;
1✔
241
                }
242

243
                try {
244
                        $type = null;
1✔
245
                        $image = Nette\Utils\Image::fromFile($srcPath);
1✔
246

247
                        Nette\Utils\FileSystem::createDir(dirname($destPath));
1✔
248

249
                        $this->transformImage($image, $options, $srcPath, $type);
1✔
250

251
                        $image->sharpen()->save($destPath, $options['compression'] ?: null, $type);
1✔
252

253
                        return [$image->getWidth(), $image->getHeight()];
1✔
254

255
                } catch (\Throwable $e) {
×
256
                        throw new Nette\Utils\ImageException($e->getMessage(), $e->getCode(), $e);
×
257
                }
258
        }
259

260

261
        private function transformImage(Nette\Utils\Image &$image, array $options, string $srcPath, ?int &$type): void
1✔
262
        {
263
                $resizeFlags = static::RESIZE_FLAGS[static::RESIZE_FIT];
1✔
264

265
                if (!empty($options['transform'])) {
1✔
266
                        if (strpos($options['transform'], '|') !== false) {
1✔
267
                                $resizeFlags = 0;
×
268

269
                                foreach (explode('|', $options['transform']) as $flag) {
×
270
                                        if (isset(static::RESIZE_FLAGS[$flag])) {
×
271
                                                $resizeFlags |= static::RESIZE_FLAGS[$flag];
×
272
                                        } elseif ($flag === static::RESIZE_FILL_EXACT) {
×
273
                                                $this->transformFillExact($image, $options, $srcPath, $type);
×
274

275
                                                return;
×
276
                                        }
277
                                }
278
                        } elseif (isset(static::RESIZE_FLAGS[$options['transform']])) {
1✔
279
                                $resizeFlags = static::RESIZE_FLAGS[$options['transform']];
1✔
280
                        } elseif ($options['transform'] === static::RESIZE_FILL_EXACT) {
×
281
                                $this->transformFillExact($image, $options, $srcPath, $type);
×
282

283
                                return;
×
284
                        }
285
                }
286

287
                $image->resize($options['width'], $options['height'], $resizeFlags);
1✔
288
        }
1✔
289

290

291
        private function transformFillExact(Nette\Utils\Image &$image, array $options, string $srcPath, ?int &$type): void
292
        {
293
                if ($this->isTransparentPng($srcPath)) {
×
294
                        $color = Nette\Utils\Image::rgb(255, 255, 255, 127);
×
295
                } else {
296
                        $color = Nette\Utils\Image::rgb(255, 255, 255);
×
297
                }
298

299
                $blank = Nette\Utils\Image::fromBlank($options['width'], $options['height'], $color);
×
300
                $image->resize($options['width'], null);
×
301
                $image->resize(null, $options['height']);
×
302
                $blank->place(
×
303
                        $image,
×
304
                        (int) ($options['width'] / 2 - $image->getWidth() / 2),
×
305
                        (int) ($options['height'] / 2 - $image->getHeight() / 2),
×
306
                );
307
                $image = $blank;
×
308
                $type = Nette\Utils\Image::PNG;
×
309
        }
310

311

312
        private function isTransparentPng(string $path): bool
313
        {
314
                $type = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path);
×
315
                if ($type !== image_type_to_mime_type(IMAGETYPE_PNG)) {
×
316
                        return false;
×
317
                }
318

319
                $image = imagecreatefrompng($path);
×
320
                $width = imagesx($image);
×
321
                $height = imagesy($image);
×
322

323
                for ($x = 0; $x < $width; $x++) {
×
324
                        for ($y = 0; $y < $height; $y++) {
×
325
                                if ((imagecolorat($image, $x, $y) & 0x7F00_0000) >> 24) {
×
326
                                        return true;
×
327
                                }
328
                        }
329
                }
330

331
                return false;
×
332
        }
333

334

335
        private function getPlaceholderImage(array $options): ?Image
1✔
336
        {
337
                if (!is_readable($this->placeholder)) {
1✔
338
                        return null;
×
339
                }
340

341
                return new Image(
1✔
342
                        $this->createRelativeWWWPath($this->placeholder),
1✔
343
                        (int) $options['width'],
1✔
344
                        (int) $options['height'],
1✔
345
                );
346
        }
347

348

349
        private function createRelativeWWWPath(string $path): string
1✔
350
        {
351
                return substr($path, strlen($this->config['wwwDir']));
1✔
352
        }
353

354

355
        private function getCompressionPath(string $fileName): string
1✔
356
        {
357
                return
358
                        $this->compressionDir
1✔
359
                        . DIRECTORY_SEPARATOR
1✔
360
                        . $this->getSubDir($fileName)
1✔
361
                        . DIRECTORY_SEPARATOR
1✔
362
                        . $fileName;
1✔
363
        }
364

365

366
        private function getOrigPath(string $fileName): string
1✔
367
        {
368
                return $this->origDir . DIRECTORY_SEPARATOR . $this->getSubDir($fileName) . DIRECTORY_SEPARATOR . $fileName;
1✔
369
        }
370

371

372
        private function getDestPath(string $fileName, array $options = []): string
1✔
373
        {
374
                if (!$options) {
1✔
375
                        $options = $this->config;
×
376
                }
377

378
                if (!empty($options[static::RETURN_ORIG])) {
1✔
379
                        return $this->getOrigPath($fileName);
1✔
380
                } elseif (!empty($options[static::RETURN_COMPRESSED])) {
1✔
381
                        return $this->getCompressionPath($fileName);
1✔
382
                } elseif (!empty($options['destDir'])) {
1✔
383
                        $destDir = $options['destDir'];
×
384
                } elseif (!empty($options['type']) && array_key_exists($options['type'], $this->types)) {
1✔
385
                        $destDir = $options['type'];
1✔
386
                } else {
387
                        $destDir = "w{$options['width']}h{$options['height']}";
1✔
388
                }
389

390
                return
391
                        $this->baseDir
1✔
392
                        . DIRECTORY_SEPARATOR
1✔
393
                        . $destDir
1✔
394
                        . DIRECTORY_SEPARATOR
1✔
395
                        . $this->getSubDir($fileName)
1✔
396
                        . DIRECTORY_SEPARATOR
1✔
397
                        . $fileName;
1✔
398
        }
399

400

401
        private function getUniqueFileName(string $fileName): string
1✔
402
        {
403
                do {
404
                        $fileName = $this->getRandomFileName($fileName);
1✔
405
                } while (file_exists($this->getOrigPath($fileName)));
1✔
406

407
                return $fileName;
1✔
408
        }
409

410

411
        private function getRandomFileName(string $fileName): string
1✔
412
        {
413
                $name = Nette\Utils\Random::generate();
1✔
414

415
                return
416
                        $name
417
                        . substr(md5($name), -5)
1✔
418
                        . substr(str_shuffle(md5($fileName)), -5)
1✔
419
                        . '.'
1✔
420
                        . pathinfo($fileName, PATHINFO_EXTENSION);
1✔
421
        }
422
}
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