ImageProcessor::isApplicable()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 4
ccs 0
cts 0
cp 0
crap 6
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 http\Exception\InvalidArgumentException;
21
use Intervention\Image\Image;
22
use Intervention\Image\ImageManager;
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\PathBuilder\PathBuilderInterface;
27
use Phauthentic\Infrastructure\Storage\FileStorageInterface;
28
use Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface;
29
use Phauthentic\Infrastructure\Storage\UrlBuilder\UrlBuilderInterface;
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<int, string>
43
     */
44
    protected array $mimeTypes = [
45
        'image/gif',
46
        'image/jpg',
47
        'image/jpeg',
48
        'image/png'
49
    ];
50
51
    /**
52
     * @var array<int, string>
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 \Phauthentic\Infrastructure\Storage\UrlBuilder\UrlBuilderInterface
68
     */
69
    protected ?UrlBuilderInterface $urlBuilder;
70
71
    /**
72
     * @var \Intervention\Image\ImageManager
73
     */
74
    protected ImageManager $imageManager;
75
76
    /**
77
     * @var \Intervention\Image\Image
78
     */
79
    protected Image $image;
80
81
    /**
82
     * Quality setting for writing images
83
     *
84
     * @var int
85
     */
86
    protected int $quality = 90;
87
88
    /**
89
     * @param \Phauthentic\Infrastructure\Storage\FileStorageInterface $storageHandler File Storage Handler
90
     * @param \Phauthentic\Infrastructure\Storage\PathBuilder\PathBuilderInterface $pathBuilder Path Builder
91
     * @param \Intervention\Image\ImageManager $imageManager Image Manager
92
     */
93
    public function __construct(
94
        FileStorageInterface $storageHandler,
95
        PathBuilderInterface $pathBuilder,
96
        ImageManager $imageManager,
97
        ?UrlBuilderInterface $urlBuilder = null
98
    ) {
99
        $this->storageHandler = $storageHandler;
100
        $this->pathBuilder = $pathBuilder;
101
        $this->imageManager = $imageManager;
102
        $this->urlBuilder = $urlBuilder;
103
    }
104
105
    /**
106
     * @param array<int, string> $mimeTypes Mime Type List
107
     * @return $this
108
     */
109
    protected function setMimeTypes(array $mimeTypes): self
110
    {
111
        $this->mimeTypes = $mimeTypes;
112
113
        return $this;
114
    }
115
116
    /**
117
     * @param int $quality Quality
118
     * @return $this
119
     */
120
    public function setQuality(int $quality): self
121
    {
122
        if ($quality > 100 || $quality <= 0) {
123
            throw new InvalidArgumentException(sprintf(
124
                'Quality has to be a positive integer between 1 and 100. %s was provided',
125
                (string)$quality
126
            ));
127
        }
128
129
        $this->quality = $quality;
130
131
        return $this;
132
    }
133
134
    /**
135
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File
136
     * @return bool
137
     */
138
    protected function isApplicable(FileInterface $file): bool
139
    {
140
        return $file->hasVariants()
141
            && in_array($file->mimeType(), $this->mimeTypes, true);
142
    }
143
144
    /**
145
     * @param array<int, string> $variants Variants by name
146
     * @return $this
147
     */
148
    public function processOnlyTheseVariants(array $variants): self
149
    {
150
        $this->processOnlyTheseVariants = $variants;
151
152
        return $this;
153
    }
154
155
    /**
156
     * @return $this
157
     */
158
    public function processAll(): self
159
    {
160
        $this->processOnlyTheseVariants = [];
161
162
        return $this;
163
    }
164
165
    /**
166
     * Read the data from the files resource if (still) present,
167
     * if not fetch it from the storage backend and write the data
168
     * to the stream of the temp file
169
     *
170
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File
171
     * @param resource $tempFileStream Temp File Stream Resource
172
     * @return int|bool False on error
173
     */
174
    protected function copyOriginalFileData(FileInterface $file, $tempFileStream)
175
    {
176
        $stream = $file->resource();
177
        $storage = $this->storageHandler->getStorage($file->storage());
178
179
        if ($stream === null) {
180
            $stream = $storage->readStream($file->path());
181
            $stream = $stream['stream'];
182
        } else {
183
            rewind($stream);
184
        }
185
        $result = stream_copy_to_stream(
186
            $stream,
187
            $tempFileStream
188
        );
189
        fclose($tempFileStream);
190
191
        return $result;
192
    }
193
194
    /**
195
     * @param string $variant Variant name
196
     * @param array<string, mixed> $variantData Variant data
197
     * @return bool
198
     */
199
    protected function shouldProcessVariant(string $variant, array $variantData): bool
200
    {
201
        return !(
202
            // Empty operations
203
            empty($variantData['operations'])
204
            || (
205
                // Check if the operation should be processed
206
                !empty($this->processOnlyTheseVariants)
207
                && !in_array($variant, $this->processOnlyTheseVariants, true)
208
            )
209
        );
210
    }
211
212
    /**
213
     * @inheritDoc
214
     */
215
    public function process(FileInterface $file): FileInterface
216
    {
217
        if (!$this->isApplicable($file)) {
218
            return $file;
219
        }
220
221
        $storage = $this->storageHandler->getStorage($file->storage());
222
223
        // Create a local tmp file on the processing system / machine
224
        $tempFile = TemporaryFile::create();
225
        $tempFileStream = fopen($tempFile, 'wb+');
226
227
        // Read the data from the files resource if (still) present,
228
        // if not fetch it from the storage backend and write the data
229
        // to the stream of the temp file
230
        $result = $this->copyOriginalFileData($file, $tempFileStream);
231
232
        // Stop if the temp file could not be generated
233
        if ($result === false) {
234
            throw TempFileCreationFailedException::withFilename($tempFile);
235
        }
236
237
        // Iterate over the variants described as an array
238
        foreach ($file->variants() as $variant => $data) {
239
            if (!$this->shouldProcessVariant($variant, $data)) {
240
                continue;
241
            }
242
243
            $this->image = $this->imageManager->make($tempFile);
244
            $operations = new Operations($this->image);
245
246
            // Apply the operations
247
            foreach ($data['operations'] as $operation => $arguments) {
248
                $operations->{$operation}($arguments);
249
            }
250
251
            $path = $this->pathBuilder->pathForVariant($file, $variant);
252
253
            if (isset($data['optimize']) && $data['optimize'] === true) {
254
                $this->optimizeAndStore($file, $path);
255
            } else {
256
                $storage->writeStream(
257
                    $path,
258
                    StreamWrapper::getResource($this->image->stream($file->extension(), $this->quality)),
259
                    new Config()
260
                );
261
            }
262
263
            $data['path'] = $path;
264
            $file = $file->withVariant($variant, $data);
265
266
            if ($this->urlBuilder !== null) {
267
                $data['url'] = $this->urlBuilder->urlForVariant($file, $variant);
268
            }
269
270
            $file = $file->withVariant($variant, $data);
271
        }
272
273
        unlink($tempFile);
274
275
        return $file;
276
    }
277
278
    /**
279
     * @param \Phauthentic\Infrastructure\Storage\FileInterface $file File
280
     * @param string $path Path
281
     * @return void
282
     */
283
    protected function optimizeAndStore(FileInterface $file, string $path): void
284
    {
285
        $storage = $this->storageHandler->getStorage($file->storage());
286
287
        // We need more tmp files because the optimizer likes to write
288
        // and read the files from disk, not from a stream. :(
289
        $optimizerTempFile = TemporaryFile::create();
290
        $optimizerOutput = TemporaryFile::create();
291
292
        // Save the image to the tmp file
293
        $this->image->save($optimizerTempFile, 90, $file->extension());
294
        // Optimize it and write it to another file
295
        $this->optimizer()->optimize($optimizerTempFile, $optimizerOutput);
296
        // Open a new stream for the storage system
297
        $optimizerOutputHandler = fopen($optimizerOutput, 'rb+');
298
299
        // And store it...
300
        $storage->writeStream(
301
            $path,
302
            $optimizerOutputHandler,
303
            new Config()
304
        );
305
306
        // Cleanup
307
        fclose($optimizerOutputHandler);
308
        unlink($optimizerTempFile);
309
        unlink($optimizerOutput);
310
311
        // Cleanup
312
        unset(
313
            $optimizerOutputHandler,
314
            $optimizerTempFile,
315
            $optimizerOutput
316
        );
317
    }
318
}
319