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

plank / laravel-mediable / 8585433686

07 Apr 2024 02:04AM UTC coverage: 95.282% (-0.5%) from 95.789%
8585433686

push

github

web-flow
Merge pull request #343 from plank/v6

V6

338 of 375 new or added lines in 30 files covered. (90.13%)

1 existing line in 1 file now uncovered.

1434 of 1505 relevant lines covered (95.28%)

117.76 hits per line

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

95.83
/src/ImageManipulator.php
1
<?php
2

3
namespace Plank\Mediable;
4

5
use GuzzleHttp\Psr7\Utils;
6
use Illuminate\Filesystem\FilesystemManager;
7
use Illuminate\Support\Collection;
8
use Intervention\Image\Commands\StreamCommand;
9
use Intervention\Image\Image;
10
use Intervention\Image\ImageManager;
11
use Plank\Mediable\Exceptions\ImageManipulationException;
12
use Plank\Mediable\SourceAdapters\SourceAdapterInterface;
13
use Plank\Mediable\SourceAdapters\StreamAdapter;
14
use Psr\Http\Message\StreamInterface;
15
use Spatie\ImageOptimizer\OptimizerChain;
16

17
class ImageManipulator
18
{
19
    private ImageManager $imageManager;
20

21
    /**
22
     * @var ImageManipulation[]
23
     */
24
    private array $variantDefinitions = [];
25

26
    private array $variantDefinitionGroups = [];
27

28
    /**
29
     * @var FilesystemManager
30
     */
31
    private $filesystem;
32

33
    private ImageOptimizer $imageOptimizer;
34

35
    public function __construct(
36
        ImageManager $imageManager,
37
        FilesystemManager $filesystem,
38
        ImageOptimizer $imageOptimizer
39
    ) {
40
        $this->imageManager = $imageManager;
936✔
41
        $this->filesystem = $filesystem;
936✔
42
        $this->imageOptimizer = $imageOptimizer;
936✔
43
    }
44

45
    public function defineVariant(
46
        string $variantName,
47
        ImageManipulation $manipulation,
48
        ?array $tags = []
49
    ) {
50
        $this->variantDefinitions[$variantName] = $manipulation;
168✔
51
        foreach ($tags as $tag) {
168✔
52
            $this->variantDefinitionGroups[$tag][] = $variantName;
6✔
53
        }
54
    }
55

56
    public function hasVariantDefinition(string $variantName): bool
57
    {
58
        return isset($this->variantDefinitions[$variantName]);
6✔
59
    }
60

61
    /**
62
     * @param string $variantName
63
     * @return ImageManipulation
64
     * @throws ImageManipulationException if Variant is not defined
65
     */
66
    public function getVariantDefinition(string $variantName): ImageManipulation
67
    {
68
        if (isset($this->variantDefinitions[$variantName])) {
150✔
69
            return $this->variantDefinitions[$variantName];
144✔
70
        }
71

72
        throw ImageManipulationException::unknownVariant($variantName);
6✔
73
    }
74

75
    public function getAllVariantDefinitions(): Collection
76
    {
77
        return collect($this->variantDefinitions);
12✔
78
    }
79

80
    public function getAllVariantNames(): array
81
    {
82
        return array_keys($this->variantDefinitions);
6✔
83
    }
84

85
    public function getVariantDefinitionsByTag(string $tag): Collection
86
    {
87
        return $this->getAllVariantDefinitions()
6✔
88
            ->intersectByKeys(array_flip($this->getVariantNamesByTag($tag)));
6✔
89
    }
90

91
    public function getVariantNamesByTag(string $tag): array
92
    {
93
        return $this->variantDefinitionGroups[$tag] ?? [];
6✔
94
    }
95

96
    /**
97
     * @param Media $media
98
     * @param string $variantName
99
     * @param bool $forceRecreate
100
     * @return Media
101
     * @throws ImageManipulationException
102
     */
103
    public function createImageVariant(
104
        Media $media,
105
        string $variantName,
106
        bool $forceRecreate = false
107
    ): Media {
108
        $this->validateMedia($media);
156✔
109

110
        $modelClass = config('mediable.model');
150✔
111
        /** @var Media $variant */
112
        $variant = new $modelClass();
150✔
113
        $recreating = false;
150✔
114
        $originalVariant = null;
150✔
115

116
        // don't recreate if that variant already exists for the model
117
        if ($media->hasVariant($variantName)) {
150✔
118
            $variant = $media->findVariant($variantName);
18✔
119
            if ($forceRecreate) {
18✔
120
                // replace the existing variant
121
                $recreating = true;
12✔
122
                $originalVariant = clone $variant;
12✔
123
            } else {
124
                // variant already exists, nothing more to do
125
                return $variant;
6✔
126
            }
127
        }
128

129
        $manipulation = $this->getVariantDefinition($variantName);
144✔
130

131
        $outputFormat = $this->determineOutputFormat($manipulation, $media);
138✔
132
        if (method_exists($this->imageManager, 'read')) {
132✔
133
            // Intervention Image  >=3.0
134
            $image = $this->imageManager->read($media->contents());
66✔
135
        } else {
136
            // Intervention Image <3.0
137
            $image = $this->imageManager->make($media->contents());
66✔
138
        }
139

140
        $callback = $manipulation->getCallback();
132✔
141
        $callback($image, $media);
132✔
142

143
        $outputStream = $this->imageToStream(
132✔
144
            $image,
132✔
145
            $outputFormat,
132✔
146
            $manipulation->getOutputQuality()
132✔
147
        );
132✔
148

149
        if ($manipulation->shouldOptimize()) {
132✔
150
            $outputStream = $this->imageOptimizer->optimizeImage(
6✔
151
                $outputStream,
6✔
152
                $manipulation->getOptimizerChain()
6✔
153
            );
6✔
154
        }
155

156
        $variant->variant_name = $variantName;
132✔
157
        $variant->original_media_id = $media->isOriginal()
132✔
158
            ? $media->getKey()
126✔
159
            : $media->original_media_id; // attach variants of variants to the same original
12✔
160

161
        $variant->disk = $manipulation->getDisk() ?? $media->disk;
132✔
162
        $variant->directory = $manipulation->getDirectory() ?? $media->directory;
132✔
163
        $variant->filename = $this->determineFilename(
132✔
164
            $media->findOriginal(),
132✔
165
            $manipulation,
132✔
166
            $variant,
132✔
167
            $outputStream
132✔
168
        );
132✔
169
        $variant->extension = $outputFormat;
132✔
170
        $variant->mime_type = $this->getMimeTypeForOutputFormat($outputFormat);
132✔
171
        $variant->aggregate_type = Media::TYPE_IMAGE;
132✔
172
        $variant->size = $outputStream->getSize();
132✔
173

174
        $this->checkForDuplicates($variant, $manipulation, $originalVariant);
132✔
175
        if ($beforeSave = $manipulation->getBeforeSave()) {
126✔
176
            $beforeSave($variant, $originalVariant);
18✔
177
            // destination may have been changed, check for duplicates again
178
            $this->checkForDuplicates($variant, $manipulation, $originalVariant);
18✔
179
        }
180

181
        if ($recreating) {
120✔
182
            // delete the original file for that variant
183
            $this->filesystem->disk($originalVariant->disk)
12✔
184
                ->delete($originalVariant->getDiskPath());
12✔
185
        }
186

187
        $visibility = $manipulation->getVisibility();
120✔
188
        if ($visibility == 'match') {
120✔
189
            $visibility = ($media->isVisible() ? 'public' : 'private');
12✔
190
        }
191
        $options = [];
120✔
192
        if ($visibility) {
120✔
193
            $options = ['visibility' => $visibility];
24✔
194
        }
195

196
        $this->filesystem->disk($variant->disk)
120✔
197
            ->writeStream(
120✔
198
                $variant->getDiskPath(),
120✔
199
                $outputStream->detach(),
120✔
200
                $options
120✔
201
            );
120✔
202

203
        $variant->save();
120✔
204

205
        return $variant;
120✔
206
    }
207

208
    /**
209
     * @param Media $media
210
     * @param SourceAdapterInterface $source
211
     * @param ImageManipulation $manipulation
212
     * @return StreamAdapter
213
     * @throws ImageManipulationException
214
     */
215
    public function manipulateUpload(
216
        Media $media,
217
        SourceAdapterInterface $source,
218
        ImageManipulation $manipulation
219
    ): StreamAdapter {
220
        $outputFormat = $this->determineOutputFormat($manipulation, $media);
6✔
221
        if (method_exists($this->imageManager, 'read')) {
6✔
222
            // Intervention Image  >=3.0
223
            $image = $this->imageManager->read($source->getStream()->getContents());
3✔
224
        } else {
225
            // Intervention Image <3.0
226
            $image = $this->imageManager->make($source->getStream()->getContents());
3✔
227
        }
228

229
        $callback = $manipulation->getCallback();
6✔
230
        $callback($image, $media);
6✔
231

232
        $outputStream = $this->imageToStream(
6✔
233
            $image,
6✔
234
            $outputFormat,
6✔
235
            $manipulation->getOutputQuality()
6✔
236
        );
6✔
237

238
        if ($manipulation->shouldOptimize()) {
6✔
NEW
239
            $outputStream = $this->imageOptimizer->optimizeImage(
×
NEW
240
                $outputStream,
×
NEW
241
                $manipulation->getOptimizerChain()
×
NEW
242
            );
×
243
        }
244

245
        $media->extension = $outputFormat;
6✔
246
        $media->mime_type = $this->getMimeTypeForOutputFormat($outputFormat);
6✔
247
        $media->aggregate_type = Media::TYPE_IMAGE;
6✔
248
        $media->size = $outputStream->getSize();
6✔
249

250
        return new StreamAdapter($outputStream);
6✔
251
    }
252

253
    private function getMimeTypeForOutputFormat(string $outputFormat): string
254
    {
255
        return ImageManipulation::MIME_TYPE_MAP[$outputFormat];
138✔
256
    }
257

258
    /**
259
     * @param ImageManipulation $manipulation
260
     * @param Media $media
261
     * @return string
262
     * @throws ImageManipulationException If output format cannot be determined
263
     */
264
    private function determineOutputFormat(
265
        ImageManipulation $manipulation,
266
        Media $media
267
    ): string {
268
        if ($format = $manipulation->getOutputFormat()) {
144✔
269
            return $format;
30✔
270
        }
271

272
        // attempt to infer the format from the mime type
273
        $mime = strtolower($media->mime_type);
114✔
274
        $format = array_search($mime, ImageManipulation::MIME_TYPE_MAP);
114✔
275
        if ($format !== false) {
114✔
276
            return $format;
108✔
277
        }
278

279
        // attempt to infer the format from the file extension
280
        $extension = strtolower($media->extension);
6✔
281
        if (in_array($extension, ImageManipulation::VALID_IMAGE_FORMATS)) {
6✔
282
            return $extension;
×
283
        }
284
        if ($extension === 'jpeg') {
6✔
285
            return ImageManipulation::FORMAT_JPG;
×
286
        }
287
        if ($extension === 'tiff') {
6✔
288
            return ImageManipulation::FORMAT_TIFF;
×
289
        }
290

291
        throw ImageManipulationException::unknownOutputFormat();
6✔
292
    }
293

294
    public function determineFilename(
295
        Media $originalMedia,
296
        ImageManipulation $manipulation,
297
        Media $variant,
298
        StreamInterface $stream
299
    ): string {
300
        if ($filename = $manipulation->getFilename()) {
132✔
301
            return $filename;
6✔
302
        }
303

304
        if ($manipulation->isUsingHashForFilename()) {
126✔
305
            return $this->getHashFromStream(
6✔
306
                $stream,
6✔
307
                $manipulation->getHashFilenameAlgo() ?? 'md5'
6✔
308
            );
6✔
309
        }
310
        return sprintf('%s-%s', $originalMedia->filename, $variant->variant_name);
120✔
311
    }
312

313
    public function validateMedia(Media $media): void
314
    {
315
        if ($media->aggregate_type != Media::TYPE_IMAGE) {
156✔
316
            throw ImageManipulationException::invalidMediaType($media->aggregate_type);
6✔
317
        }
318
    }
319

320
    private function getHashFromStream(StreamInterface $stream, string $algo): string
321
    {
322
        $stream->rewind();
6✔
323
        $hash = hash_init($algo);
6✔
324
        while ($chunk = $stream->read(2048)) {
6✔
325
            hash_update($hash, $chunk);
6✔
326
        }
327
        $filename = hash_final($hash);
6✔
328
        $stream->rewind();
6✔
329

330
        return $filename;
6✔
331
    }
332

333
    private function checkForDuplicates(
334
        Media $variant,
335
        ImageManipulation $manipulation,
336
        Media $originalVariant = null
337
    ) {
338
        if ($originalVariant
132✔
339
            && $variant->disk === $originalVariant->disk
132✔
340
            && $variant->getDiskPath() === $originalVariant->getDiskPath()
132✔
341
        ) {
342
            // same as the original, no conflict as we are going to replace the file anyways
343
            return;
6✔
344
        }
345

346
        if (!$this->filesystem->disk($variant->disk)->exists($variant->getDiskPath())) {
132✔
347
            // no conflict, carry on
348
            return;
120✔
349
        }
350

351
        switch ($manipulation->getOnDuplicateBehaviour()) {
24✔
352
            case ImageManipulation::ON_DUPLICATE_ERROR:
24✔
353
                throw ImageManipulationException::fileExists($variant->getDiskPath());
12✔
354

355
            case ImageManipulation::ON_DUPLICATE_INCREMENT:
12✔
356
            default:
357
                $variant->filename = $this->generateUniqueFilename($variant);
12✔
358
                break;
12✔
359
        }
360
    }
361

362
    /**
363
     * Increment model's filename until one is found that doesn't already exist.
364
     * @param Media $model
365
     * @return string
366
     */
367
    private function generateUniqueFilename(Media $model): string
368
    {
369
        $storage = $this->filesystem->disk($model->disk);
12✔
370
        $counter = 0;
12✔
371
        do {
372
            $filename = "{$model->filename}";
12✔
373
            if ($counter > 0) {
12✔
374
                $filename .= '-' . $counter;
12✔
375
            }
376
            $path = "{$model->directory}/{$filename}.{$model->extension}";
12✔
377
            ++$counter;
12✔
378
        } while ($storage->exists($path));
12✔
379

380
        return $filename;
12✔
381
    }
382

383
    private function imageToStream(
384
        Image $image,
385
        string $outputFormat,
386
        int $outputQuality
387
    ) {
388
        if (class_exists(StreamCommand::class)) {
138✔
389
            // Intervention Image  <3.0
390
            return $image->stream(
69✔
391
                $outputFormat,
69✔
392
                $outputQuality
69✔
393
            );
69✔
394
        }
395

396
        $formatted = match ($outputFormat) {
69✔
397
            ImageManipulation::FORMAT_JPG => $image->toJpeg($outputQuality),
69✔
398
            ImageManipulation::FORMAT_PNG => $image->toPng(),
69✔
399
            ImageManipulation::FORMAT_GIF => $image->toGif(),
69✔
400
            ImageManipulation::FORMAT_WEBP => $image->toBitmap(),
69✔
401
            ImageManipulation::FORMAT_TIFF => $image->toTiff($outputQuality),
69✔
402
            default => throw ImageManipulationException::unknownOutputFormat(),
69✔
403
        };
69✔
404
        return Utils::streamFor($formatted->toFilePointer());
69✔
405
    }
406
}
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

© 2026 Coveralls, Inc