Failed Conditions
Push — master ( bc7390...c9ddc4 )
by Florian
08:03
created

ImageProcessor::callback()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 15
ccs 0
cts 8
cp 0
crap 12
rs 10
1
<?php
2
3
/**
4
 * Copyright (c) Florian Krämer (https://florian-kraemer.net)
5
 * Licensed under The MIT License
6
 * For full copyright and license information, please see the LICENSE.txt
7
 * Redistributions of files must retain the above copyright notice.
8
 *
9
 * @copyright Copyright (c) Florian Krämer (https://florian-kraemer.net)
10
 * @author    Florian Krämer
11
 * @link      https://github.com/Phauthentic
12
 * @license   https://opensource.org/licenses/MIT MIT License
13
 */
14
15
declare(strict_types=1);
16
17
namespace Phauthentic\Infrastructure\Storage\Processor\Image;
18
19
use GuzzleHttp\Psr7\StreamWrapper;
20
use Intervention\Image\Image;
21
use Intervention\Image\ImageManager;
22
use InvalidArgumentException;
23
use League\Flysystem\Config;
24
use Phauthentic\Infrastructure\Storage\FileInterface;
25
use Phauthentic\Infrastructure\Storage\Processor\Image\Exception\TempFileCreationFailedException;
26
use Phauthentic\Infrastructure\Storage\Processor\Image\Exception\UnsupportedOperationException;
27
use Phauthentic\Infrastructure\Storage\PathBuilder\PathBuilderInterface;
28
use Phauthentic\Infrastructure\Storage\FileStorageInterface;
29
use Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface;
30
use Phauthentic\Infrastructure\Storage\Utility\TemporaryFile;
31
32
use function Phauthentic\Infrastructure\Storage\fopen;
33
34
/**
35
 * Image Operator
36
 */
37
class ImageProcessor implements ProcessorInterface
38
{
39
    use OptimizerTrait;
40
41
    /**
42
     * @var array
43
     */
44
    protected array $mimeTypes = [
45
        'image/gif',
46
        'image/jpg',
47
        'image/jpeg',
48
        'image/png'
49
    ];
50
51
    /**
52
     * @var array
53
     */
54
    protected array $processOnlyTheseVariants = [];
55
56
    /**
57
     * @var \Phauthentic\Infrastructure\Storage\FileStorageInterface
58
     */
59
    protected FileStorageInterface $storageHandler;
60
61
    /**
62
     * @var \Phauthentic\Infrastructure\Storage\PathBuilder\PathBuilderInterface
63
     */
64
    protected PathBuilderInterface $pathBuilder;
65
66
    /**
67
     * @var \Intervention\Image\ImageManager
68
     */
69
    protected ImageManager $imageManager;
70
71
    /**
72
     * @var \Intervention\Image\Image
73
     */
74
    protected Image $image;
75
76
    /**
77
     * @param \Phauthentic\Infrastructure\Storage\FileStorageInterface $storageHandler File Storage Handler
78
     * @param \Phauthentic\Infrastructure\Storage\PathBuilder\PathBuilderInterface $pathBuilder Path Builder
79
     * @param \Intervention\Image\ImageManager $imageManager Image Manager
80
     */
81
    public function __construct(
82
        FileStorageInterface $storageHandler,
83
        PathBuilderInterface $pathBuilder,
84
        ImageManager $imageManager
85
    ) {
86
        $this->storageHandler = $storageHandler;
87
        $this->pathBuilder = $pathBuilder;
88
        $this->imageManager = $imageManager;
89
    }
90
91
    /**
92
     * @param array $mimeTypes Mime Type List
93
     * @return $this
94
     */
95
    protected function setMimeTypes(array $mimeTypes): self
96
    {
97
        $this->mimeTypes = $mimeTypes;
98
99
        return $this;
100
    }
101
102
    /**
103
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File
104
     * @return bool
105
     */
106
    protected function isApplicable(FileInterface $file): bool
107
    {
108
        return $file->hasVariants()
109
            && in_array($file->mimeType(), $this->mimeTypes, true);
110
    }
111
112
    /**
113
     * @param array $variants Variants by name
114
     * @return $this
115
     */
116
    public function processOnlyTheseVariants(array $variants): self
117
    {
118
        $this->processOnlyTheseVariants = $variants;
119
120
        return $this;
121
    }
122
123
    /**
124
     * @return $this
125
     */
126
    public function processAll(): self
127
    {
128
        $this->processOnlyTheseVariants = [];
129
130
        return $this;
131
    }
132
133
    /**
134
     * Read the data from the files resource if (still) present,
135
     * if not fetch it from the storage backend and write the data
136
     * to the stream of the temp file
137
     *
138
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File
139
     * @param resource $tempFileStream Temp File Stream Resource
140
     * @return int|bool False on error
141
     */
142
    protected function copyOriginalFileData(FileInterface $file, $tempFileStream)
143
    {
144
        $stream = $file->resource();
145
        $storage = $this->storageHandler->getStorage($file->storage());
146
147
        if ($stream === null) {
148
            $stream = $storage->readStream($file->path());
149
            $stream = $stream['stream'];
150
        } else {
151
            rewind($stream);
152
        }
153
        $result = stream_copy_to_stream(
154
            $stream,
155
            $tempFileStream
156
        );
157
        fclose($tempFileStream);
158
159
        return $result;
160
    }
161
162
    /**
163
     * @inheritDoc
164
     */
165
    public function process(FileInterface $file): FileInterface
166
    {
167
        if (!$this->isApplicable($file)) {
168
            return $file;
169
        }
170
171
        $storage = $this->storageHandler->getStorage($file->storage());
172
173
        // Create a local tmp file on the processing system / machine
174
        $tempFile = TemporaryFile::create();
175
        $tempFileStream = fopen($tempFile, 'wb+');
176
177
        // Read the data from the files resource if (still) present,
178
        // if not fetch it from the storage backend and write the data
179
        // to the stream of the temp file
180
        $result = $this->copyOriginalFileData($file, $tempFileStream);
181
182
        // Stop if the temp file could not be generated
183
        if ($result === false) {
184
            throw TempFileCreationFailedException::withFilename($tempFile);
185
        }
186
187
        // Iterate over the variants described as an array
188
        foreach ($file->variants() as $variant => $data) {
189
            if (
190
                empty($data['operations'])
191
                || (
192
                    !empty($this->processOnlyTheseVariants)
193
                    && !in_array($variant, $this->processOnlyTheseVariants, true)
194
                )
195
            ) {
196
                continue;
197
            }
198
199
            $this->image = $this->imageManager->make($tempFile);
200
201
            // Apply the operations
202
            foreach ($data['operations'] as $operation => $arguments) {
203
                if (!method_exists($this, $operation)) {
204
                    throw UnsupportedOperationException::withName($operation);
205
                }
206
207
                $this->$operation($arguments);
208
            }
209
210
            $path = $this->pathBuilder->pathForVariant($file, $variant);
211
212
            if (isset($data['optimize']) && $data['optimize'] === true) {
213
                $this->optimizeAndStore($file, $path);
214
            } else {
215
                $storage->writeStream(
216
                    $path,
217
                    StreamWrapper::getResource($this->image->stream($file->extension(), 90)),
218
                    new Config()
219
                );
220
            }
221
222
            $data['path'] = $path;
223
            $file = $file->withVariant($variant, $data);
224
        }
225
226
        unlink($tempFile);
227
228
        return $file;
229
    }
230
231
    /**
232
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File
233
     * @param string $path Path
234
     * @return void
235
     */
236
    protected function optimizeAndStore(FileInterface $file, string $path): void
237
    {
238
        $storage = $this->storageHandler->getStorage($file->storage());
239
240
        // We need more tmp files because the optimizer likes to write
241
        // and read the files from disk, not from a stream. :(
242
        $optimizerTempFile = TemporaryFile::create();
243
        $optimizerOutput = TemporaryFile::create();
244
245
        // Save the image to the tmp file
246
        $this->image->save($optimizerTempFile, 90, $file->extension());
247
        // Optimize it and write it to another file
248
        $this->optimizer()->optimize($optimizerTempFile, $optimizerOutput);
249
        // Open a new stream for the storage system
250
        $optimizerOutputHandler = fopen($optimizerOutput, 'rb+');
251
252
        // And store it...
253
        $storage->writeStream(
254
            $path,
255
            $optimizerOutputHandler,
256
            new Config()
257
        );
258
259
        // Cleanup
260
        fclose($optimizerOutputHandler);
261
        unlink($optimizerTempFile);
262
        unlink($optimizerOutput);
263
264
        // Cleanup
265
        unset(
266
            $optimizerOutputHandler,
267
            $optimizerTempFile,
268
            $optimizerOutput
269
        );
270
    }
271
272
    /**
273
     * Crops the image
274
     *
275
     * @link http://image.intervention.io/api/fit
276
     * @param array $arguments Arguments
277
     * @return void
278
     */
279
    protected function fit(array $arguments): void
280
    {
281
        if (!isset($arguments['width'])) {
282
            throw new InvalidArgumentException('Missing width');
283
        }
284
285
        $preventUpscale = $arguments['preventUpscale'] ?? false;
286
        $height = $arguments['height'] ?? null;
287
288
        $this->image->fit(
289
            (int)$arguments['width'],
290
            (int)$height,
291
            static function ($constraint) use ($preventUpscale) {
292
                if ($preventUpscale) {
293
                    $constraint->upsize();
294
                }
295
            }
296
        );
297
    }
298
299
    /**
300
     * Crops the image
301
     *
302
     * @link http://image.intervention.io/api/crop
303
     * @param array $arguments Arguments
304
     * @return void
305
     */
306
    protected function crop(array $arguments): void
307
    {
308
        if (!isset($arguments['height'], $arguments['width'])) {
309
            throw new InvalidArgumentException('Missing height or width');
310
        }
311
312
        $height = $arguments['height'] ? (int)$arguments['height'] : null;
313
        $width = $arguments['width'] ? (int)$arguments['width'] : null;
314
        $x = $arguments['x'] ? (int)$arguments['x'] : null;
315
        $y = $arguments['y'] ? (int)$arguments['y'] : null;
316
317
        $this->image->crop($width, $height, $x, $y);
318
    }
319
320
    /**
321
     * Flips the image horizontal
322
     *
323
     * @link http://image.intervention.io/api/flip
324
     * @param array $arguments Arguments
325
     * @return void
326
     */
327
    protected function flipHorizontal(array $arguments): void
328
    {
329
        $this->flip(['direction' => 'h']);
330
    }
331
332
    /**
333
     * Flips the image vertical
334
     *
335
     * @link http://image.intervention.io/api/flip
336
     * @param array $arguments Arguments
337
     * @return void
338
     */
339
    protected function flipVertical(array $arguments): void
340
    {
341
        $this->flip(['direction' => 'v']);
342
    }
343
344
    /**
345
     * Flips the image
346
     *
347
     * @link http://image.intervention.io/api/flip
348
     * @param array $arguments Arguments
349
     * @return void
350
     */
351
    protected function flip(array $arguments): void
352
    {
353
        if (!isset($arguments['direction'])) {
354
            throw new InvalidArgumentException('Direction missing');
355
        }
356
357
        if ($arguments['direction'] !== 'v' && $arguments['direction'] !== 'h') {
358
            throw new InvalidArgumentException(
359
                'Invalid argument, you must provide h or v'
360
            );
361
        }
362
363
        $this->image->flip($arguments['direction']);
364
    }
365
366
    /**
367
     * Resizes the image
368
     *
369
     * @link http://image.intervention.io/api/resize
370
     * @param array $arguments Arguments
371
     * @return void
372
     */
373
    protected function resize(array $arguments): void
374
    {
375
        if (!isset($arguments['height'], $arguments['width'])) {
376
            throw new InvalidArgumentException(
377
                'Missing height or width'
378
            );
379
        }
380
381
        $aspectRatio = $arguments['aspectRatio'] ?? true;
382
        $preventUpscale = $arguments['preventUpscale'] ?? false;
383
384
        $this->image->resize(
385
            $arguments['width'],
386
            $arguments['height'],
387
            static function ($constraint) use ($aspectRatio, $preventUpscale) {
388
                if ($aspectRatio) {
389
                    $constraint->aspectRatio();
390
                }
391
                if ($preventUpscale) {
392
                    $constraint->upsize();
393
                }
394
            }
395
        );
396
    }
397
398
    /**
399
     * @link http://image.intervention.io/api/widen
400
     * @param array $arguments Arguments
401
     * @return void
402
     */
403
    public function widen(array $arguments): void
404
    {
405
        if (!isset($arguments['width'])) {
406
            throw new InvalidArgumentException(
407
                'Missing width'
408
            );
409
        }
410
411
        $preventUpscale = $arguments['preventUpscale'] ?? false;
0 ignored issues
show
Unused Code introduced by
The assignment to $preventUpscale is dead and can be removed.
Loading history...
412
413
        $this->image->widen((int)$arguments['width'], function () {
414
            if ($preventUpscale) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $preventUpscale seems to be never defined.
Loading history...
415
                $constraint->upsize();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $constraint seems to be never defined.
Loading history...
416
            }
417
        });
418
    }
419
420
    /**
421
     * @link http://image.intervention.io/api/heighten
422
     * @param array $arguments Arguments
423
     * @return void
424
     */
425
    public function highten(array $arguments): void
426
    {
427
        if (!isset($arguments['height'])) {
428
            throw new InvalidArgumentException(
429
                'Missing height'
430
            );
431
        }
432
433
        $preventUpscale = $arguments['preventUpscale'] ?? false;
0 ignored issues
show
Unused Code introduced by
The assignment to $preventUpscale is dead and can be removed.
Loading history...
434
435
        $this->image->highten((int)$arguments['height'], function () {
436
            if ($preventUpscale) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $preventUpscale seems to be never defined.
Loading history...
437
                $constraint->upsize();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $constraint seems to be never defined.
Loading history...
438
            }
439
        });
440
    }
441
442
    /**
443
     * @link http://image.intervention.io/api/rotate
444
     * @param array $arguments Arguments
445
     * @return void
446
     */
447
    public function rotate(array $arguments): void
448
    {
449
        if (!isset($arguments['angle'])) {
450
            throw new InvalidArgumentException(
451
                'Missing angle'
452
            );
453
        }
454
455
        $bgcolor = $arguments['bgcolor'] ?? null;
456
457
        $this->image->highten((float)$arguments['angle'], $bgcolor, function () {
458
            if ($preventUpscale) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $preventUpscale seems to be never defined.
Loading history...
459
                $constraint->upsize();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $constraint seems to be never defined.
Loading history...
460
            }
461
        });
462
    }
463
464
    /**
465
     * @link http://image.intervention.io/api/rotate
466
     * @param array $arguments Arguments
467
     * @return void
468
     */
469
    public function sharpen(array $arguments): void
470
    {
471
        if (!isset($arguments['amount'])) {
472
            throw new InvalidArgumentException(
473
                'Missing amount'
474
            );
475
        }
476
477
        $this->image->sharpen((int)$arguments['amount']);
478
    }
479
480
    /**
481
     * Allows the declaration of a callable that gets the image manager instance
482
     * and the arguments passed to it.
483
     *
484
     * @param array $arguments Arguments
485
     * @return void
486
     */
487
    public function callback(array $arguments): void
488
    {
489
        if (!isset($arguments['callback'])) {
490
            throw new InvalidArgumentException(
491
                'Missing angle'
492
            );
493
        }
494
495
        if (!is_callable($arguments['callback'])) {
496
            throw new InvalidArgumentException(
497
                'Provided value for callback is not a callable'
498
            );
499
        }
500
501
        $arguments['callable']($this->image, $arguments);
502
    }
503
}
504