Storage::getImageWidthHeight()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 18

Duplication

Lines 6
Ratio 33.33 %

Importance

Changes 0
Metric Value
dl 6
loc 18
rs 9.6666
c 0
b 0
f 0
cc 3
nc 3
nop 0
1
<?php
2
3
namespace Image;
4
5
use Phalcon\Mvc\User\Component;
6
7
define('IMG_ROOT_REL_PATH', 'img');
8
define('DIR_SEP', '/');
9
define('IMG_ROOT_PATH', ROOT . DIR_SEP);
10
define('IMG_STORAGE_SERVER', '');
11
define('IMG_EXTENSION', 'jpg');
12
define('NOIMAGE', '/static/images/noimage.jpg');
13
14
define('IMG_DEBUG_MODE', true);
15
16
class Storage extends Component
17
{
18
19
    private static $STRATEGIES = [
20
        'w', // Масштабируем по ширине
21
        'wh', // Масштабируем по заданной ширине и высоте. Изображение подганяется в этот прямоугольник
22
        'a', // Центрируем и обрезаем изображение по заданной высоте и ширине таким образом, чтоб оно полностью заполнило пространство
23
    ];
24
    private $id = null;
25
    private $image_hash = null;
26
    private $type = 'publication';
27
    private $strategy = 'w';
28
    private $width = 100;
29
    private $height = null;
30
    private $hash = false;
31
    private $attributes = [];
32
    private $exists = true;
33
    private $widthHeight = true;
34
    private $stretch = true;
35
36
    public function __construct(array $params = [], array $attributes = [])
37
    {
38
        $this->setIdFromParams($params);
39
        $this->attributes = $attributes;
40
41
        $this->type = (isset($params['type'])) ? $params['type'] : 'publication';
42
        $this->strategy = (isset($params['strategy'])) ? $params['strategy'] : 'w';
43
        $this->image_hash = (isset($params['image_hash'])) ? $params['image_hash'] : null;
44
        $this->hash = (isset($params['hash'])) ? $params['hash'] : false;
45
46
        $this->setDimensionsAttributes($params);
47
    }
48
49
    private function setDimensionsAttributes(array $params = [])
50
    {
51
        $this->width = (isset($params['width'])) ? $params['width'] : 100;
52
        $this->height = (isset($params['height'])) ? $params['height'] : null;
53
54
        $this->widthHeight = (isset($params['widthHeight'])) ? $params['widthHeight'] : true;
55
        $this->widthHeight = (isset($params['widthHeight']) && MOBILE_DEVICE) ? false : true;
56
57
        $this->stretch = (isset($params['stretch'])) ? $params['stretch'] : null;
58
    }
59
60
    private function setIdFromParams($params)
61
    {
62
        if (isset($params['id'])) {
63
            if (preg_match('/^\d+$/', $params['id'])) {
64
                $this->id = (int) $params['id'];
65
            } else {
66
                $this->id = $params['id'];
67
            }
68
        } else {
69
            if (IMG_DEBUG_MODE) {
70
                throw new \Exception("ID не определен");
71
            }
72
        }
73
    }
74
75
    /**
76
     * HTML-тег изображения, готовый к использованию
77
     * <img src="" alt="" />
78
     */
79
    public function imageHtml()
80
    {
81
        //Из заданных параметров и атрибутов составляем html-тэг
82
        $this->attributesForImageHtml();
83
84
        // Получаем относительный адрес файла кешированного изображения
85
        $src = $this->cachedRelPath();
86
87
        if ($this->exists) {
88
            if ($this->hash) {
89
                $src .= '?' . microtime();
90
            }
91
        } else {
92
            $src = NOIMAGE;
93
            $this->attributes['width'] = $this->width;
94
            $this->attributes['height'] = $this->height;
95
        }
96
97
        $attr_src = 'src="' . $this->getDi()->get('config')->base_path . $src . '"';
0 ignored issues
show
Bug introduced by
The method get cannot be called on $this->getDi() (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
98
        $result = '<img ' . $attr_src . $this->attributesResultForImageHtml($this->attributes) . '/>';
99
100
        return $result;
101
    }
102
103
    private function attributesForImageHtml()
104
    {
105
        if ($this->widthHeight) {
106
            if ($this->stretch && in_array($this->strategy, ['wh', 'a'])) {
107
                $this->stretch = false;
108
            }
109
            $this->changeAttributesInAccordanceWithStretch();
110
        }
111
        $this->attributes['alt'] = (isset($this->attributes['alt'])) ?
112
            htmlspecialchars($this->attributes['alt'], ENT_QUOTES) :
113
            '';
114
    }
115
116
    private function changeAttributesInAccordanceWithStretch()
117
    {
118
        if ($this->stretch) {
119
            $this->changeAttributesForStretch();
120
        } else {
121
            $this->changeAttributesWithoutStretch();
122
        }
123
    }
124
125
    private function changeAttributesForStretch()
126
    {
127
        if ($this->width) {
128
            $this->attributes['width'] = $this->width;
129
        }
130
        if ($this->height) {
131
            $this->attributes['height'] = $this->height;
132
        }
133
    }
134
135
    private function changeAttributesWithoutStretch()
136
    {
137
        $widthHeight = $this->getImageWidthHeight();
138
        if ($widthHeight['width']) {
139
            $this->attributes['width'] = $widthHeight['width'];
140
        }
141
        if ($widthHeight['height']) {
142
            $this->attributes['height'] = $widthHeight['height'];
143
        }
144
    }
145
146
    private function attributesResultForImageHtml($attributes)
147
    {
148
        $attributesHtmlArray = [];
149
        foreach ($attributes as $el => $val) {
150
            $attributesHtmlArray[] = $el . '="' . $val . '"';
151
        }
152
        $attributesHtml = implode(' ', $attributesHtmlArray);
153
        $attributesHtmlResult = ($attributesHtml) ? ' ' . $attributesHtml : '';
154
155
        return $attributesHtmlResult;
156
    }
157
158
    /**
159
     * Относительный адрес файла кешированного изображения
160
     * /img/preview/405102/405102_1_w_100.jpg
161
     */
162
    public function cachedRelPath()
163
    {
164
        // Рассчитываем по входящим параметрам относительный путь к кешированному файлу
165
        $cachedRelPath = $this->calculateCachedRelPath();
166
        // Совмещаем относительный путь с корневым, получаем абсолютный путь
167
        $cachedAbsPath = IMG_ROOT_PATH . $cachedRelPath;
168
        // Проверяем существование такого файла. если файл не существует:
169
        if (!file_exists($cachedAbsPath)) {
170
            // Генерируем кеш-файл по заданным параметрам
171
            $this->generateCachedImage();
172
        }
173
174
        return IMG_STORAGE_SERVER . $cachedRelPath;
175
    }
176
177
    /**
178
     * @return string
179
     */
180
    public function cachedAbsPath()
181
    {
182
        return IMG_ROOT_PATH . $this->cachedRelPath();
183
    }
184
185
    /**
186
     * Относительный адрес файла оригинального изображения
187
     */
188
    public function originalRelPath()
189
    {
190
        return IMG_STORAGE_SERVER . $this->calculateOriginalRelPath();
191
    }
192
193
    /**
194
     * Абсолютный адрес файла оригинального изображения
195
     */
196
    public function originalAbsPath()
197
    {
198
        return $this->getAbsPath(true);
199
    }
200
201
    /**
202
     * @param $file
203
     * @return bool
204
     */
205
    public function save($file)
206
    {
207
        if (file_exists($file)) {
208
            return copy($file, $this->originalAbsPath());
209
        }
210
    }
211
212
    /**
213
     * @return array
214
     */
215
    public function originalWidthHeight()
216
    {
217
        $imageSize = getimagesize($this->originalAbsPath());
218 View Code Duplication
        if (!empty($imageSize)) {
219
            return [
220
                'width'  => $imageSize[0],
221
                'height' => $imageSize[1]
222
            ];
223
        }
224
    }
225
226
    /**
227
     * @return int
228
     */
229
    public function cachedFileSize()
230
    {
231
        $path = $this->cachedAbsPath();
232
        if (file_exists($path)) {
233
            return filesize($path);
234
        }
235
    }
236
237
    /**
238
     * @return bool
239
     */
240
    public function isExists()
241
    {
242
        return (file_exists($this->getAbsPath(true))) ? true : false ;
243
    }
244
245
    /**
246
     * Рассчитываем по входящим параметрам относительный путь к кешированному файлу
247
     * /img/preview/405/405102_1_w_100.jpg
248
     */
249
    private function calculateCachedRelPath()
250
    {
251
        $pathParts = [];
252
        $pathParts[] = IMG_ROOT_REL_PATH;
253
        $pathParts[] = 'cache';
254
        $pathParts[] = $this->type;
255
256 View Code Duplication
        if (is_int($this->id)) {
257
            $idPart = floor($this->id / 1000);
258
        } else {
259
            $idPart = $this->id;
260
        }
261
        $pathParts[] = $idPart;
262
263
        $fileParts = [];
264
        $fileParts[] = $this->id;
265
        if ($this->image_hash) {
266
            $fileParts[] = $this->image_hash;
267
        }
268
        if (in_array($this->strategy, self::$STRATEGIES)) {
269
            $fileParts[] = $this->strategy;
270
271
            $fileParts[] = $this->width;
272
            if ($this->height) {
273
                $fileParts[] = $this->height;
274
            }
275
276
            // "img/preview/405"
277
            $path = implode(DIR_SEP, $pathParts);
278
            // "405102_1_w_100"
279
            $file = implode('_', $fileParts);
280
281
            return $path . DIR_SEP . $file . '.jpg';
282
283
        } else {
284
            if (IMG_DEBUG_MODE) {
285
                throw new \Exception("Параметр 'strategy' указан неверно");
286
            }
287
        }
288
    }
289
290
    /**
291
     * Рассчитываем по входящим параметрам относительный путь к оригинальному файлу
292
     * /img/original/preview/405/405102_1.jpg
293
     * @return string
294
     */
295
    private function calculateOriginalRelPath()
296
    {
297
        $pathParts = [];
298
        $pathParts[] = IMG_ROOT_REL_PATH;
299
        $pathParts[] = 'original';
300
        $pathParts[] = $this->type;
301
302 View Code Duplication
        if (is_int($this->id)) {
303
            $idPart = floor($this->id / 1000);
304
        } else {
305
            $idPart = $this->id;
306
        }
307
        $pathParts[] = $idPart;
308
309
        $fileParts = [];
310
        $fileParts[] = $this->id;
311
        if ($this->image_hash) {
312
            $fileParts[] = $this->image_hash;
313
        }
314
315
        // "img/original/preview/405"
316
        $path = implode(DIR_SEP, $pathParts);
317
        // "405102_1"
318
        $file = implode('_', $fileParts);
319
320
        return $path . DIR_SEP . $file . '.jpg';
321
    }
322
323
    /**
324
     * генерируем кеш-файл по заданным параметрам
325
     * @throws \Exception
326
     */
327
    private function generateCachedImage()
328
    {
329
        // Абсолютный путь оригинального изображения
330
        $originalAbsPath = IMG_ROOT_PATH . $this->calculateOriginalRelPath();
331
        $this->checkOriginalExists($originalAbsPath);
332
333
        require_once __DIR__ . '/PHPThumb/ThumbLib.inc.php';
334
        $image = \PhpThumbFactory::create($originalAbsPath);
335
        // Для мобильных устройств отдаем изображение с качеством на уровне 60%
336
        if (MOBILE_DEVICE) {
337
            $options = ['jpegQuality' => 60];
338
            $image->setOptions($options);
339
        }
340
        switch ($this->strategy) {
341
            case 'w':
342
                // Масштабируем по ширине
343
                $image->resize($this->width);
344
                break;
345
            case 'wh':
346
                // Масштабируем по заданной ширине и высоте. Изображение подганяется в этот прямоугольник
347
                $image->resize($this->width, $this->height);
348
                break;
349
            case 'a':
350
                // Центрируем и обрезаем изображение по заданной высоте и ширине таким образом, чтоб оно полностью заполнило пространство
351
                $image->adaptiveResize($this->width, $this->height);
352
                break;
353
        }
354
355
        $this->saveImage($image, $originalAbsPath);
356
    }
357
358
    /**
359
     * @param $left
360
     * @param $top
361
     * @param $width
362
     * @param $height
363
     * @throws \Exception
364
     */
365
    public function cropOriginal($left, $top, $width, $height)
366
    {
367
        $originalAbsPath = IMG_ROOT_PATH . $this->calculateOriginalRelPath(); // Абсолютный путь оригинального изображения
368
        $this->checkOriginalExists($originalAbsPath);
369
370
        require_once __DIR__ . '/PHPThumb/ThumbLib.inc.php';
371
        $image = \PhpThumbFactory::create($originalAbsPath);
372
        $image->crop($left, $top, $width, $height);
373
374
        $this->saveImage($image, $originalAbsPath);
375
    }
376
377
    /**
378
     * @param $originalAbsPath
379
     * @throws \Exception
380
     */
381
    private function checkOriginalExists($originalAbsPath)
382
    {
383
        if (!file_exists($originalAbsPath)) {
384
            if (IMG_DEBUG_MODE) {
385
                throw new \Exception("Файл {$originalAbsPath} не существует");
386
            } else {
387
                $this->exists = false;
388
            }
389
        }
390
    }
391
392
    /**
393
     * @param $image
394
     * @param $originalAbsPath
395
     * @throws \Exception
396
     */
397
    private function saveImage($image, $originalAbsPath)
398
    {
399
        // Если оригинал не заблокирован, блокируем. Это необходимо для предотвращения множественной генерации кеш-файла параллельными запросами
400
        if ($this->lockOriginal($originalAbsPath)) {
401
            // Сохраняем кешированное изображение
402
            $image->save($this->getAbsPath(false));
403
            // Снимаем блокировку
404
            $this->unlockOriginal($originalAbsPath);
405
        } else {
406
            if (IMG_DEBUG_MODE) {
407
                throw new \Exception("Файл {$originalAbsPath} заблокирован механизмом проверки _LOCK или не существует");
408
            } else {
409
                $this->exists = false;
410
            }
411
        }
412
    }
413
414
    /**
415
     * Удаляет оригинальные и кешированные изображения
416
     * @param bool $removeAll
417
     */
418
    public function remove($removeAll = true)
419
    {
420
        $this->removeCached();
421
        $this->removeOriginal($removeAll);
422
    }
423
424
    /**
425
     * Удаляет оригинальные изображения
426
     * @param bool $removeAll
427
     */
428
    public function removeOriginal($removeAll = true)
429
    {
430
        if (!$removeAll) {
431
            if (file_exists($this->originalAbsPath())) {
432
                unlink($this->originalAbsPath());
433
            }
434
        } else {
435
            $originalAbsPath = IMG_ROOT_PATH . $this->calculateOriginalRelPath();
436
            $originalAbsPathDir = implode(DIR_SEP, array_slice(explode(DIR_SEP, $originalAbsPath), 0, -1)); // Абсолютный путь директории
437
438
            if ($this->image_hash) {
439
                $search = $originalAbsPathDir . "/" . $this->id . "_*.jpg";
440
            } else {
441
                $search = $originalAbsPathDir . "/" . $this->id . ".jpg";
442
            }
443
            $files = glob($search);
444
            if (!empty($files)) {
445
                foreach ($files as $file) {
446
                    if (is_file($file)) {
447
                        unlink($file);
448
                    }
449
                }
450
            }
451
        }
452
    }
453
454
    /**
455
     * Удаляет кешированные изображения
456
     */
457
    public function removeCached()
458
    {
459
        $cachedAbsPath = IMG_ROOT_PATH . $this->calculateCachedRelPath();
460
        $cachedAbsPathDir = implode(DIR_SEP, array_slice(explode(DIR_SEP, $cachedAbsPath), 0, -1)); // Абсолютный путь директории
461
462
        $search = $cachedAbsPathDir . "/" . $this->id . "_*.jpg";
463
        $files = glob($search);
464
        if (!empty($files)) {
465
            foreach ($files as $file) {
466
                if (is_file($file)) {
467
                    unlink($file);
468
                }
469
            }
470
        }
471
    }
472
473
    /**
474
     * Размеры кешированного изображения
475
     */
476
    public function getImageWidthHeight()
477
    {
478
        $cachedAbsPath = IMG_ROOT_PATH . $this->calculateCachedRelPath();
479
        if (file_exists($cachedAbsPath)) {
480
            $imageSize = getimagesize($cachedAbsPath);
481 View Code Duplication
            if (!empty($imageSize)) {
482
                return [
483
                    'width'  => $imageSize[0],
484
                    'height' => $imageSize[1]
485
                ];
486
            }
487
        } else {
488
            return [
489
                'width'  => null,
490
                'height' => null
491
            ];
492
        }
493
    }
494
495
    /**
496
     * Проверяем блокировку оригинала изображения. Если нет, то блокируем
497
     * @param string $originalAbsPath
498
     * @return boolean true|false
499
     */
500
    private function lockOriginal($originalAbsPath)
501
    {
502
        $lockFileName = $this->getLockFileName($originalAbsPath);
503
        if (file_exists($lockFileName)) {
504
            return false;
505
        } else {
506
            $handle = fopen($lockFileName, 'w+');
507
            if (flock($handle, LOCK_EX)) {
508
                fwrite($handle, '1');
509
                flock($handle, LOCK_UN);
510
                fclose($handle);
511
                return true;
512
            } else {
513
                if ($handle) {
514
                    fclose($handle);
515
                }
516
                return false;
517
            }
518
        }
519
    }
520
521
    /**
522
     * Снимаем блокировку оригинала изображения
523
     * @param string $originalAbsPath
524
     */
525
    private function unlockOriginal($originalAbsPath)
526
    {
527
        unlink($this->getLockFileName($originalAbsPath));
528
    }
529
530
    /**
531
     * Возвращает имя файла для блокировки оригинала изображения
532
     * @param string $originalAbsPath
533
     * @return string
534
     */
535
    private function getLockFileName($originalAbsPath)
536
    {
537
        return preg_replace('/\.' . IMG_EXTENSION . '/i', '_lock.' . IMG_EXTENSION, $originalAbsPath);
538
    }
539
540
    /**
541
     * @param bool $original
542
     * @return string
543
     * @throws \Exception
544
     */
545
    private function getAbsPath($original = true)
546
    {
547
        $absPath = IMG_ROOT_PATH . (($original) ? $this->calculateOriginalRelPath() : $this->calculateCachedRelPath());
548
549
        // Абсолютный путь директории
550
        $absPathDir = implode(DIR_SEP, array_slice(explode(DIR_SEP, $absPath), 0, -1));
551
552
        // Если директория отсутствует
553
        if (!is_dir($absPathDir)) {
554
            // Создаем дерево директорий
555
            mkdir($absPathDir, 0777, true);
556
        }
557
558
        return $absPath;
559
    }
560
561
}