Passed
Push — develop ( 47d001...0a7564 )
by Henry
13:07
created

Media::getBlank()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 28
ccs 0
cts 15
cp 0
crap 12
rs 9.7998
1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\Models\Media;
12
13
use Exception;
14
use Divergence\App;
15
use Divergence\Models\Model;
16
use Divergence\Models\Mapping\Column;
17
18
/**
19
 * Media Model
20
 *
21
 * @author Henry Paradiz <[email protected]>
22
 * @author  Chris Alfano <[email protected]>
23
 *
24
 * {@inheritDoc}
25
 */
26
class Media extends Model
27
{
28
    public static $useCache = true;
29
    public static $singularNoun = 'media item';
30
    public static $pluralNoun = 'media items';
31
32
    // support subclassing
33
    public static $rootClass = __CLASS__;
34
    public static $defaultClass = __CLASS__;
35
    public static $subClasses = [__CLASS__, Image::class, PDF::class, Video::class, Audio::class];
36
    public static $collectionRoute = '/media';
37
38
    public static $tableName = 'media';
39
40
    #[Column(notnull: false, default:null)]
41
    protected $ContextClass;
42
43
    #[Column(type:'int', notnull: false, default:null)]
44
    protected $ContextID;
45
46
    protected $MIMEType;
47
48
    #[Column(type:'int', unsigned: true, notnull:false)]
49
    protected $Width;
50
51
    #[Column(type:'int', unsigned: true, notnull:false)]
52
    protected $Height;
53
54
    #[Column(type:'float', unsigned: true, notnull:false, default: 0)]
55
    protected $Duration;
56
57
    #[Column(notnull:false)]
58
    protected $Caption;
59
60
61
    public static $relationships = [
62
        'Creator' => [
63
            'type' => 'one-one',
64
            'class' => 'Person',
65
            'local' => 'CreatorID',
66
        ],
67
        'Context' => [
68
            'type' => 'context-parent',
69
        ],
70
    ];
71
72
    public static $searchConditions = [
73
        'Caption' => [
74
            'qualifiers' => ['any','caption'],
75
            'points' => 2,
76
            'sql' => 'Caption LIKE "%%%s%%"',
77
        ],
78
        'CaptionLike' => [
79
            'qualifiers' => ['caption-like'],
80
            'points' => 2,
81
            'sql' => 'Caption LIKE "%s"',
82
        ],
83
        'CaptionNot' => [
84
            'qualifiers' => ['caption-not'],
85
            'points' => 2,
86
            'sql' => 'Caption NOT LIKE "%%%s%%"',
87
        ],
88
        'CaptionNotLike' => [
89
            'qualifiers' => ['caption-not-like'],
90
            'points' => 2,
91
            'sql' => 'Caption NOT LIKE "%s"',
92
        ],
93
    ];
94
95
    public static $webPathFormat = '/media/open/%u'; // 1=mediaID
96
    public static $thumbnailRequestFormat = '/thumbnail/%1$u/%2$ux%3$u%4$s'; // 1=media_id 2=width 3=height 4=fill_color
97
    #public static $blankThumbnailRequestFormat = '/thumbnail/%1$s/%2$ux%3$u%4$s'; // 1=class 2=width 3=height 4=fill_color
98
    public static $thumbnailJPEGCompression = 90;
99
    public static $thumbnailPNGCompression = 9;
100
    public static $defaultFilenameFormat = 'default.%s.jpg';
101
    public static $newDirectoryPermissions = 0775;
102
    public static $newFilePermissions = 0664;
103
    public static $magicPath = null;//'/usr/share/misc/magic.mgc';
104
    public static $useFaceDetection = true;
105
    public static $faceDetectionTimeLimit = 10;
106
107
    public static $mimeHandlers = [
108
        'image/gif' => Image::class,
109
        'image/jpeg' => Image::class,
110
        'image/png' => Image::class,
111
        'image/tiff' => Image::class,
112
        'application/psd' => Image::class,
113
        'audio/mpeg' => Audio::class,
114
        'application/pdf' => PDF::class,
115
        'application/postscript' => PDF::class,
116
        'image/svg+xml' => PDF::class,
117
        'video/x-flv' => Video::class,
118
        'video/mp4' => Video::class,
119
        'video/quicktime' => Video::class,
120
    ];
121
122
    public static $mimeRewrites = [
123
        'image/photoshop'              => 'application/psd',
124
        'image/x-photoshop'            => 'application/psd',
125
        'image/psd'                    => 'application/psd',
126
        'application/photoshop'        => 'application/psd',
127
        'image/vnd.adobe.photoshop'    => 'application/psd',
128
    ];
129
130 4
    public function getValue($name)
131
    {
132
        switch ($name) {
133 4
            case 'Data':
134 4
            case 'SummaryData':
135 4
            case 'JsonTranslation':
136
                return [
137
                    'ID' => $this->getValue('ID'),
138
                    'Class' => $this->getValue('Class'),
139
                    'ContextClass' => $this->getValue('ContextClass'),
140
                    'ContextID' => $this->getValue('ContextID'),
141
                    'MIMEType' => $this->getValue('MIMEType'),
142
                    'Width' => $this->getValue('Width'),
143
                    'Height' => $this->getValue('Height'),
144
                    'Duration' => $this->getValue('Duration'),
145
                ];
146
147 4
            case 'Filename':
148
                return $this->getFilename();
149
150 4
            case 'ThumbnailMIMEType':
151
                return $this->getValue('MIMEType');
152
153 4
            case 'Extension':
154
                throw new Exception('Unable to find extension for mime-type: '.$this->getValue('MIMEType'));
155
156 4
            case 'WebPath':
157
                return sprintf(
158
                        static::$webPathFormat,
159
                        $this->getValue('ID')
160
                    );
161
162 4
            case 'FilesystemPath':
163 2
                return $this->getFilesystemPath();
164
165
166
            default:
167 4
                return parent::getValue($name);
168
        }
169
    }
170
171
    public function getThumbnailRequest($width, $height = null, $fillColor = null, $cropped = false)
172
    {
173
        return sprintf(
174
            static::$thumbnailRequestFormat,
175
            $this->getValue('ID'),
176
            $width,
177
            $height ?: $width,
178
            (is_string($fillColor) ? 'x'.$fillColor : '')
179
        ).($cropped ? '/cropped' : '');
180
    }
181
182
    public function getImage($sourceFile = null)
183
    {
184
        if (!isset($sourceFile)) {
185
            $sourceFile = $this->getValue('FilesystemPath') ? $this->getValue('FilesystemPath') : $this->getValue('BlankPath');
186
        }
187
188
        switch ($this->getValue('MIMEType')) {
189
            case 'application/psd':
190
            case 'image/tiff':
191
192
                //Converts PSD to PNG temporarily on the real file system.
193
                $tempFile = tempnam('/tmp', 'media_convert');
194
                exec("convert -density 100 ".$this->getValue('FilesystemPath')."[0] -flatten $tempFile.png");
195
196
                return imagecreatefrompng("$tempFile.png");
197
198
            case 'application/pdf':
199
200
                return PDF::getImage($sourceFile);
201
202
            case 'application/postscript':
203
204
                return imagecreatefromstring(shell_exec("gs -r150 -dEPSCrop -dNOPAUSE -dBATCH -sDEVICE=png48 -sOutputFile=- -q $this->getValue('FilesystemPath')"));
205
206
            default:
207
208
                if (!$fileData = file_get_contents($sourceFile)) {
209
                    throw new Exception('Could not load media source: '.$sourceFile);
210
                }
211
212
                $image = imagecreatefromstring($fileData);
213
214
                if ($this->getValue('MIMEType') == 'image/jpeg' && ($exifData = exif_read_data($sourceFile)) && !empty($exifData['Orientation'])) {
215
                    switch ($exifData['Orientation']) {
216
                        case 1: // nothing
217
                            break;
218
                        case 2: // horizontal flip
219
                            imageflip($image, IMG_FLIP_HORIZONTAL); // TODO: need PHP 5.3 compat method
220
                            break;
221
                        case 3: // 180 rotate left
222
                            $image = imagerotate($image, 180, 0);
223
                            break;
224
                        case 4: // vertical flip
225
                            imageflip($image, IMG_FLIP_VERTICAL); // TODO: need PHP 5.3 compat method
226
                            break;
227
                        case 5: // vertical flip + 90 rotate right
228
                            imageflip($image, IMG_FLIP_VERTICAL); // TODO: need PHP 5.3 compat method
229
                            $image = imagerotate($image, -90, 0);
230
                            break;
231
                        case 6: // 90 rotate right
232
                            $image = imagerotate($image, -90, 0);
233
                            break;
234
                        case 7: // horizontal flip + 90 rotate right
235
                            imageflip($image, IMG_FLIP_HORIZONTAL); // TODO: need PHP 5.3 compat method
236
                            $image = imagerotate($image, -90, 0);
237
                            break;
238
                        case 8: // 90 rotate left
239
                            $image = imagerotate($image, 90, 0);
240
                            break;
241
                    }
242
                }
243
244
                return $image;
245
        }
246
    }
247
248
    /**
249
     * Gives us the path to a thumbnail and if it doesn't exist creates it
250
     *
251
     * @param int $maxWidth
252
     * @param int $maxHeight
253
     * @param string|bool $fillColor
254
     * @param boolean $cropped
255
     * @return void
256
     */
257
    public function getThumbnail($maxWidth, $maxHeight, $fillColor = false, $cropped = false)
258
    {
259
        $thumbFormat = sprintf('%ux%u', $maxWidth, $maxHeight);
260
261
        if ($fillColor) {
262
            $thumbFormat .= 'x'.strtoupper($fillColor);
263
        }
264
265
        if ($cropped) {
266
            $thumbFormat .= '.cropped';
267
        }
268
269
        $thumbPath = App::$App->ApplicationPath.'/media/'.$thumbFormat.'/'.$this->getValue('Filename');
270
271
        // look for cached thumbnail
272
        if (!file_exists($thumbPath)) {
273
            // ensure directory exists
274
            $thumbDir = dirname($thumbPath);
275
            if (!is_dir($thumbDir)) {
276
                mkdir($thumbDir, static::$newDirectoryPermissions, true);
277
            }
278
279
            // create new thumbnail
280
            $this->createThumbnailImage($thumbPath, $maxWidth, $maxHeight, $fillColor, $cropped);
281
        }
282
283
        return $thumbPath;
284
    }
285
286
    public function createThumbnailImage($thumbPath, $maxWidth, $maxHeight, $fillColor = false, $cropped = false)
287
    {
288
        $thumbWidth = $maxWidth;
289
        $thumbHeight = $maxHeight;
290
291
        // load source image
292
        $srcImage = $this->getImage();
293
        $srcWidth = imagesx($srcImage);
294
        $srcHeight = imagesy($srcImage);
295
296
        // calculate
297
        if ($srcWidth && $srcHeight) {
298
            $widthRatio = ($srcWidth > $maxWidth) ? ($maxWidth / $srcWidth) : 1;
299
            $heightRatio = ($srcHeight > $maxHeight) ? ($maxHeight / $srcHeight) : 1;
300
301
            // crop width/height to scale size if fill disabled
302
            if ($cropped) {
303
                $ratio = max($widthRatio, $heightRatio);
304
            } else {
305
                $ratio = min($widthRatio, $heightRatio);
306
            }
307
308
            $scaledWidth = round($srcWidth * $ratio);
309
            $scaledHeight = round($srcHeight * $ratio);
310
        } else {
311
            $scaledWidth = $maxWidth;
312
            $scaledHeight = $maxHeight;
313
        }
314
315
        if (!$fillColor && !$cropped) {
316
            $thumbWidth = $scaledWidth;
317
            $thumbHeight = $scaledHeight;
318
        }
319
320
        // create thumbnail images
321
        $image = imagecreatetruecolor($thumbWidth, $thumbHeight);
322
323
        // paint fill color
324
        if ($fillColor) {
325
            // extract decimal values from hex triplet
326
            $fillColor = sscanf($fillColor, '%2x%2x%2x');
327
328
            // convert to color index
329
            $fillColor = imagecolorallocate($image, $fillColor[0], $fillColor[1], $fillColor[2]);
330
331
            // fill background
332
            imagefill($image, 0, 0, $fillColor);
333
        } elseif (($this->getValue('MIMEType') == 'image/gif') || ($this->getValue('MIMEType') == 'image/png')) {
334
            $trans_index = imagecolortransparent($srcImage);
335
336
            // check if there is a specific transparent color
337
            if ($trans_index >= 0 && $trans_index < imagecolorstotal($srcImage)) {
338
                $trans_color = imagecolorsforindex($srcImage, $trans_index);
339
340
                // allocate in thumbnail
341
                $trans_index = imagecolorallocate($image, $trans_color['red'], $trans_color['green'], $trans_color['blue']);
342
343
                // fill background
344
                imagefill($image, 0, 0, $trans_index);
345
                imagecolortransparent($image, $trans_index);
346
            } elseif ($this->getValue('MIMEType') == 'image/png') {
347
                imagealphablending($image, false);
348
                $trans_color = imagecolorallocatealpha($image, 0, 0, 0, 127);
349
                imagefill($image, 0, 0, $trans_color);
350
                imagesavealpha($image, true);
351
            }
352
        }
353
354
        // resize photo to thumbnail
355
        if ($cropped) {
356
            imagecopyresampled(
357
                $image,
358
                $srcImage,
359
                ($thumbWidth - $scaledWidth) / 2,
360
                ($thumbHeight - $scaledHeight) / 2,
361
                0,
362
                0,
363
                $scaledWidth,
364
                $scaledHeight,
365
                $srcWidth,
366
                $srcHeight
367
            );
368
        } else {
369
            imagecopyresampled(
370
                $image,
371
                $srcImage,
372
                round(($thumbWidth - $scaledWidth) / 2),
373
                round(($thumbHeight - $scaledHeight) / 2),
374
                0,
375
                0,
376
                $scaledWidth,
377
                $scaledHeight,
378
                $srcWidth,
379
                $srcHeight
380
            );
381
        }
382
383
        // save thumbnail to disk
384
        switch ($this->getValue('ThumbnailMIMEType')) {
385
            case 'image/gif':
386
                imagegif($image, $thumbPath);
387
                break;
388
389
            case 'image/jpeg':
390
                imagejpeg($image, $thumbPath, static::$thumbnailJPEGCompression);
391
                break;
392
393
            case 'image/png':
394
                imagepng($image, $thumbPath, static::$thumbnailPNGCompression);
395
                break;
396
397
            default:
398
                throw new Exception('Unhandled thumbnail format');
399
        }
400
401
        chmod($thumbPath, static::$newFilePermissions);
402
        return true;
403
    }
404
405
    // static methods
406
    public static function createFromUpload($uploadedFile, $fieldValues = []): static | false
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '|', expecting '{' or ';' on line 406 at column 86
Loading history...
407
    {
408
        // handle recieving a field array from $_FILES
409
        if (is_array($uploadedFile)) {
410
            if (isset($uploadedFile['error'])) {
411
                return false;
412
            }
413
414
            if (!empty($uploadedFile['name']) && empty($fieldValues['Caption'])) {
415
                $fieldValues['Caption'] = preg_replace('/\.[^.]+$/', '', $uploadedFile['name']);
416
            }
417
418
            $uploadedFile = $uploadedFile['tmp_name'];
419
        }
420
421
        // sanity check
422
        if (!is_uploaded_file($uploadedFile)) {
423
            throw new Exception('Supplied file is not a valid upload');
424
        }
425
426
        return static::createFromFile($uploadedFile, $fieldValues);
427
    }
428
429 2
    public static function createFromFile($file, $fieldValues = []): static | false
430
    {
431
        try {
432
            // handle url input
433 2
            if (filter_var($file, FILTER_VALIDATE_URL)) {
434
                $tempName = tempnam('/tmp', 'remote_media');
435
                copy($file, $tempName);
436
                $file = $tempName;
437
            }
438
439
            // analyze file
440 2
            $mediaInfo = static::analyzeFile($file);
441
442
            // create media object
443
            /**
444
             * @var static
445
             */
446 2
            $Media = $mediaInfo['className']::create($fieldValues);
447
448
            // init media
449 2
            $Media->initializeFromAnalysis($mediaInfo);
450
451
            // save media
452 2
            $Media->save();
453
454
            // write file
455 2
            $Media->writeFile($file);
456
457 2
            return $Media;
458
        } catch (Exception $e) {
459
            throw $e;
460
        }
461
462
        // remove photo record
463
        if ($Media) {
464
            $Media->destroy();
465
        }
466
467
        return false;
468
    }
469
470 2
    public function initializeFromAnalysis($mediaInfo)
471
    {
472 2
        $this->setValue('MIMEType',$mediaInfo['mimeType']);
473 2
        $this->setValue('Width',$mediaInfo['width']);
474 2
        $this->setValue('Height',$mediaInfo['height']);
475 2
        $this->setValue('Duration',$mediaInfo['duration']);
476
    }
477
478
479 2
    public static function analyzeFile($filename)
480
    {
481
        // DO NOT CALL FROM decendent's override, parent calls child
482
483
        // check file
484 2
        if (!is_readable($filename)) {
485
            throw new Exception('Unable to read media file for analysis: "'.$filename.'"');
486
        }
487
488
        // get mime type
489 2
        $finfo = finfo_open(FILEINFO_MIME_TYPE, static::$magicPath);
490
491 2
        if (!$finfo || !($mimeType = finfo_file($finfo, $filename))) {
492
            throw new Exception('Unable to load media file info');
493
        }
494
495 2
        finfo_close($finfo);
496
497
        // dig deeper if only generic mimetype returned
498 2
        if ($mimeType == 'application/octet-stream') {
499
            $finfo = finfo_open(FILEINFO_NONE, static::$magicPath);
500
501
            if (!$finfo || !($fileInfo = finfo_file($finfo, $filename))) {
502
                throw new Exception('Unable to load media file info');
503
            }
504
505
            finfo_close($finfo);
506
507
            // detect EPS
508
            if (preg_match('/^DOS EPS/i', $fileInfo)) {
509
                $mimeType = 'application/postscript';
510
            }
511 2
        } elseif (array_key_exists($mimeType, static::$mimeRewrites)) {
512
            $mimeType = static::$mimeRewrites[$mimeType];
513
        }
514
515
        // compile mime data
516 2
        $mediaInfo = [
517 2
            'mimeType' => $mimeType,
518 2
        ];
519
520
        // determine handler
521 2
        $staticClass = get_called_class();
522 2
        if (!isset(static::$mimeHandlers[$mediaInfo['mimeType']]) || $staticClass != __CLASS__) {
523
            throw new Exception('No class registered for mime type "' . $mediaInfo['mimeType'] . '"');
524
        } else {
525 2
            $className = $mediaInfo['className'] = static::$mimeHandlers[$mediaInfo['mimeType']];
526
527
            // call registered type's analyzer
528 2
            $mediaInfo = $className::analyzeFile($filename, $mediaInfo);
529
        }
530
531 2
        return $mediaInfo;
532
    }
533
534
    public static function getSupportedTypes(): array
535
    {
536
        return array_unique(array_merge(array_keys(static::$mimeHandlers), array_keys(static::$mimeRewrites)));
537
    }
538
539 2
    public function getFilesystemPath($variant = 'original', $filename = null): ?string
540
    {
541 2
        if ($this->isPhantom) {
542
            return null;
543
        }
544
545 2
        return App::$App->ApplicationPath.'/media/'.$variant.'/'.($filename ?: $this->getFilename($variant));
546
    }
547
548 2
    public function getFilename(): string
549
    {
550 2
        if ($this->isPhantom) {
551
            return 'default.'.$this->getValue('Extension');
552
        }
553
554 2
        return $this->getValue('ID').'.'.$this->getValue('Extension');
555
    }
556
557
    public function getMIMEType(): string
558
    {
559
        return $this->getValue('MIMEType');
560
    }
561
562 2
    public function writeFile($sourceFile): bool
563
    {
564 2
        $targetDirectory = dirname($this->getValue('FilesystemPath'));
565
566
        // create target directory if needed
567 2
        if (!is_dir($targetDirectory)) {
568 1
            mkdir($targetDirectory, static::$newDirectoryPermissions, true);
569
        }
570
571
        // move source file to target path
572 2
        if (!rename($sourceFile, $this->getValue('FilesystemPath'))) {
573
            throw new \Exception('Failed to move source file to destination');
574
        }
575
576
        // set file permissions
577 2
        return chmod($this->getValue('FilesystemPath'), static::$newFilePermissions);
578
    }
579
}
580