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

harmim / images / #1407

pending completion
#1407

push

harmim
`Nette\Utils\Image::place` does not accept floats

2 of 2 new or added lines in 1 file covered. (100.0%)

190 of 274 relevant lines covered (69.34%)

0.69 hits per line

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

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

3
declare(strict_types=1);
4

5
/**
6
 * @author Dominik Harmim <harmim6@gmail.com>
7
 * @copyright Copyright (c) 2017 Dominik Harmim
8
 */
9

10
namespace Harmim\Images;
11

12
use Nette;
13

14

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

19

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

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

35
        /**
36
         * @var array
37
         */
38
        private $config;
39

40
        /**
41
         * @var array
42
         */
43
        private $types = [];
44

45
        /**
46
         * @var string
47
         */
48
        private $baseDir;
49

50
        /**
51
         * @var string
52
         */
53
        private $placeholder;
54

55
        /**
56
         * @var string
57
         */
58
        private $origDir;
59

60
        /**
61
         * @var string
62
         */
63
        private $compressionDir;
64

65

66
        public function __construct(array $config)
1✔
67
        {
68
                if ($config['types'] && is_array($config['types'])) {
1✔
69
                        $this->types = $config['types'];
1✔
70
                }
71
                $this->config = $config;
1✔
72
                $this->baseDir = $config['wwwDir'] . DIRECTORY_SEPARATOR . $config['imagesDir'];
1✔
73
                $this->placeholder = $config['wwwDir'] . DIRECTORY_SEPARATOR . $config['placeholder'];
1✔
74
                $this->origDir = $this->baseDir . DIRECTORY_SEPARATOR . $config['origDir'];
1✔
75
                $this->compressionDir = $this->baseDir . DIRECTORY_SEPARATOR . $config['compressionDir'];
1✔
76
        }
1✔
77

78

79
        public function saveUpload(Nette\Http\FileUpload $file): string
1✔
80
        {
81
                if ($file->isOk()) {
1✔
82
                        return $this->saveImage((string) $file->getName(), (string) $file->getTemporaryFile());
1✔
83
                }
84

85
                throw new Nette\IOException($file->getError());
×
86
        }
87

88

89
        public function saveImage(string $name, string $path): string
1✔
90
        {
91
                $file = new Nette\Http\FileUpload([
1✔
92
                        'name' => $name,
1✔
93
                        'size' => 0,
1✔
94
                        'tmp_name' => $path,
1✔
95
                        'error' => UPLOAD_ERR_OK,
1✔
96
                ]);
97

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

101
                $file->move($origPath);
1✔
102

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

110
                return $fileName;
1✔
111
        }
112

113

114
        /**
115
         * @param string|IImage|mixed $fileName
116
         * @param array $excludedTypes
117
         * @return void
118
         */
119
        public function deleteImage($fileName, array $excludedTypes = []): void
×
120
        {
121
                $fileName = $this->resolveFileName($fileName);
×
122

123
                if (!$excludedTypes) {
×
124
                        Nette\Utils\FileSystem::delete($this->getOrigPath($fileName));
×
125
                        Nette\Utils\FileSystem::delete($this->getCompressionPath($fileName));
×
126
                }
127

128
                foreach ($this->types as $key => $value) {
×
129
                        if (!$excludedTypes || !in_array($key, $excludedTypes, true)) {
×
130
                                Nette\Utils\FileSystem::delete($this->getDestPath($fileName, ['type' => $key]));
×
131
                        }
132
                }
133

134
                if (is_readable($this->baseDir)) {
×
135
                        $excludedFolders = array_keys($this->types) + [
×
136
                                        $this->origDir,
×
137
                                        $this->compressionDir,
×
138
                                ];
139

140
                        /** @var \SplFileInfo $file */
141
                        foreach (Nette\Utils\Finder::find($this->getSubDir($fileName) . '/' . $fileName)
×
142
                                ->from($this->baseDir)
×
143
                                ->exclude($excludedFolders) as $file) {
×
144
                                Nette\Utils\FileSystem::delete($file->getRealPath());
×
145
                        }
146
                }
147
        }
148

149

150
        /**
151
         * @param string|IImage|mixed $fileName
152
         * @param string|null $type
153
         * @param array $options
154
         * @return string|null
155
         */
156
        public function getImageLink($fileName, ?string $type = null, array $options = []): ?string
×
157
        {
158
                if ($type !== null) {
×
159
                        $options['type'] = $type;
×
160
                }
161

162
                $image = $this->getImage($this->resolveFileName($fileName), $options);
×
163

164
                return $image ? (string) $image : null;
×
165
        }
166

167

168
        /**
169
         * @internal
170
         * @param string|IImage|mixed $fileName
171
         * @param array $args
172
         * @return Image|null
173
         */
174
        public function getImage($fileName, array $args = []): ?Image
1✔
175
        {
176
                $fileName = $this->resolveFileName($fileName);
1✔
177

178
                $options = $this->getOptions($args);
1✔
179
                $srcPath = $this->getCompressionPath($fileName);
1✔
180

181
                if (!$fileName || !is_readable($srcPath)) {
1✔
182
                        return $this->getPlaceholderImage($options);
1✔
183
                }
184

185
                $destPath = $this->getDestPath($fileName, $options);
1✔
186

187
                if (is_readable($destPath)) {
1✔
188
                        [$width, $height] = getimagesize($destPath);
1✔
189

190
                } elseif ($image = $this->createImage($srcPath, $destPath, $options)) {
1✔
191
                        [$width, $height] = $image;
1✔
192

193
                } else {
194
                        return $this->getPlaceholderImage($options);
×
195
                }
196

197
                return new Image($this->createRelativeWWWPath($destPath), (int) $width, (int) $height);
1✔
198
        }
199

200

201
        /**
202
         * @internal
203
         * @param string $fileName
204
         * @return string
205
         */
206
        public function getSubDir(string $fileName): string
1✔
207
        {
208
                return (string) (ord(substr($fileName, 0, 1)) % 42);
1✔
209
        }
210

211

212
        /**
213
         * @internal
214
         * @param array $args
215
         * @return array
216
         */
217
        public function getOptions(array $args = []): array
1✔
218
        {
219
                $type = [];
1✔
220

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

225
                return (array) (($args ?: []) + $type + $this->config);
1✔
226
        }
227

228

229
        private function createImage(string $srcPath, string $destPath, array $options = []): array
1✔
230
        {
231
                if (!$options) {
1✔
232
                        $options = $this->config;
1✔
233
                }
234

235
                try {
236
                        $type = null;
1✔
237
                        $image = Nette\Utils\Image::fromFile($srcPath);
1✔
238

239
                        Nette\Utils\FileSystem::createDir(dirname($destPath));
1✔
240

241
                        $this->transformImage($image, $options, $srcPath, $type);
1✔
242

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

245
                        return [$image->getWidth(), $image->getHeight()];
1✔
246

247
                } catch (\Throwable $e) {
×
248
                        throw new Nette\Utils\ImageException($e->getMessage(), $e->getCode(), $e);
×
249
                }
250
        }
251

252

253
        private function transformImage(Nette\Utils\Image &$image, array $options, string $srcPath, ?int &$type)
1✔
254
        {
255
                $resizeFlags = Nette\Utils\Image::FIT;
1✔
256

257
                if (!empty($options['transform'])) {
1✔
258
                        if (strpos($options['transform'], '|') !== false) {
1✔
259
                                $resizeFlags = 0;
×
260

261
                                foreach (explode('|', $options['transform']) as $flag) {
×
262
                                        if (isset(self::RESIZE_FLAGS[$flag])) {
×
263
                                                $resizeFlags |= self::RESIZE_FLAGS[$flag];
×
264

265
                                        } elseif ($flag === self::RESIZE_FILL_EXACT) {
×
266
                                                $this->transformFillExact($image, $options, $srcPath, $type);
×
267

268
                                                return;
×
269
                                        }
270
                                }
271

272
                        } elseif (isset(self::RESIZE_FLAGS[$options['transform']])) {
1✔
273
                                $resizeFlags = self::RESIZE_FLAGS[$options['transform']];
1✔
274

275
                        } elseif ($options['transform'] === self::RESIZE_FILL_EXACT) {
×
276
                                $this->transformFillExact($image, $options, $srcPath, $type);
×
277

278
                                return;
×
279
                        }
280
                }
281

282
                $image->resize($options['width'], $options['height'], $resizeFlags);
1✔
283
        }
1✔
284

285

286
        private function transformFillExact(Nette\Utils\Image &$image, array $options, string $srcPath, ?int &$type)
×
287
        {
288
                if ($this->isTransparentPng($srcPath)) {
×
289
                        $color = Nette\Utils\Image::rgb(255, 255, 255, 127);
×
290
                } else {
291
                        $color = Nette\Utils\Image::rgb(255, 255, 255);
×
292
                }
293

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

306

307
        private function isTransparentPng(string $path): bool
×
308
        {
309
                $type = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path);
×
310
                if ($type !== image_type_to_mime_type(IMAGETYPE_PNG)) {
×
311
                        return false;
×
312
                }
313

314
                $image = imagecreatefrompng($path);
×
315
                $width = imagesx($image);
×
316
                $height = imagesy($image);
×
317

318
                for ($i = 0; $i < $width; $i++) {
×
319
                        for ($j = 0; $j < $height; $j++) {
×
320
                                $rgba = imagecolorat($image, $i, $j);
×
321
                                if (($rgba & 0x7F000000) >> 24) {
×
322
                                        return true;
×
323
                                }
324
                        }
325
                }
326

327
                return false;
×
328
        }
329

330

331
        private function getPlaceholderImage(array $options): ?Image
1✔
332
        {
333
                if (is_readable($this->placeholder)) {
1✔
334
                        return new Image(
1✔
335
                                $this->createRelativeWWWPath($this->placeholder),
1✔
336
                                (int) $options['width'],
1✔
337
                                (int) $options['height']
1✔
338
                        );
339
                }
340

341
                return null;
×
342
        }
343

344

345
        private function createRelativeWWWPath(string $path): string
1✔
346
        {
347
                return substr($path, strlen($this->config['wwwDir']));
1✔
348
        }
349

350

351
        private function getCompressionPath(string $fileName): string
1✔
352
        {
353
                return $this->compressionDir . DIRECTORY_SEPARATOR . $this->getSubDir($fileName) . DIRECTORY_SEPARATOR
1✔
354
                        . $fileName;
1✔
355
        }
356

357

358
        private function getOrigPath(string $fileName): string
1✔
359
        {
360
                return $this->origDir . DIRECTORY_SEPARATOR . $this->getSubDir($fileName) . DIRECTORY_SEPARATOR . $fileName;
1✔
361
        }
362

363

364
        private function getDestPath(string $fileName, array $options = []): string
1✔
365
        {
366
                if (!$options) {
1✔
367
                        $options = $this->config;
×
368
                }
369

370
                if (!empty($options['destDir'])) {
1✔
371
                        $destDir = $options['destDir'];
×
372

373
                } elseif (!empty($options['type']) && array_key_exists($options['type'], $this->types)) {
1✔
374
                        $destDir = $options['type'];
1✔
375

376
                } else {
377
                        $destDir = "w{$options['width']}h{$options['height']}";
1✔
378
                }
379

380
                return $this->baseDir . DIRECTORY_SEPARATOR . $destDir . DIRECTORY_SEPARATOR . $this->getSubDir($fileName)
1✔
381
                        . DIRECTORY_SEPARATOR . $fileName;
1✔
382
        }
383

384

385
        private function getUniqueFileName(string $fileName): string
1✔
386
        {
387
                do {
388
                        $fileName = $this->getRandomFileName($fileName);
1✔
389
                } while (file_exists($this->getOrigPath($fileName)));
1✔
390

391
                return $fileName;
1✔
392
        }
393

394

395
        private function getRandomFileName(string $fileName): string
1✔
396
        {
397
                $name = Nette\Utils\Random::generate(10);
1✔
398
                $name = $name . substr(md5($name), -5) . substr(str_shuffle(md5($fileName)), -5) . '.'
1✔
399
                        . pathinfo($fileName, PATHINFO_EXTENSION);
1✔
400

401
                return $name;
1✔
402
        }
403

404

405
        /**
406
         * @param string|IImage|mixed $fileName
407
         * @return string
408
         */
409
        private function resolveFileName($fileName): string
1✔
410
        {
411
                if ($fileName instanceof IImage) {
1✔
412
                        return $fileName->getFileName();
×
413
                }
414

415
                return (string) $fileName;
1✔
416
        }
417
}
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

© 2025 Coveralls, Inc