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

plank / laravel-mediable / 12171312057

05 Dec 2024 01:13AM UTC coverage: 94.091% (-0.6%) from 94.663%
12171312057

Pull #369

github

frasmage
test without lowest
Pull Request #369: add php 8.4 to test suite

1481 of 1574 relevant lines covered (94.09%)

77.02 hits per line

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

90.29
/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\Exceptions\MediaUpload\ConfigurationException;
13
use Plank\Mediable\SourceAdapters\SourceAdapterInterface;
14
use Plank\Mediable\SourceAdapters\StreamAdapter;
15
use Psr\Http\Message\StreamInterface;
16
use Spatie\ImageOptimizer\OptimizerChain;
17

18
class ImageManipulator
19
{
20
    private ?ImageManager $imageManager;
21

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

27
    private array $variantDefinitionGroups = [];
28

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

34
    private ImageOptimizer $imageOptimizer;
35

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

46
    public function defineVariant(
47
        string $variantName,
48
        ImageManipulation $manipulation,
49
        ?array $tags = []
50
    ) {
51
        if (!$this->imageManager) {
87✔
52
            throw ConfigurationException::interventionImageNotConfigured();
3✔
53
        }
54
        $this->variantDefinitions[$variantName] = $manipulation;
84✔
55
        foreach ($tags as $tag) {
84✔
56
            $this->variantDefinitionGroups[$tag][] = $variantName;
3✔
57
        }
58
    }
59

60
    public function hasVariantDefinition(string $variantName): bool
61
    {
62
        return isset($this->variantDefinitions[$variantName]);
3✔
63
    }
64

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

76
        throw ImageManipulationException::unknownVariant($variantName);
3✔
77
    }
78

79
    public function getAllVariantDefinitions(): Collection
80
    {
81
        return collect($this->variantDefinitions);
6✔
82
    }
83

84
    public function getAllVariantNames(): array
85
    {
86
        return array_keys($this->variantDefinitions);
3✔
87
    }
88

89
    public function getVariantDefinitionsByTag(string $tag): Collection
90
    {
91
        return $this->getAllVariantDefinitions()
3✔
92
            ->intersectByKeys(array_flip($this->getVariantNamesByTag($tag)));
3✔
93
    }
94

95
    public function getVariantNamesByTag(string $tag): array
96
    {
97
        return $this->variantDefinitionGroups[$tag] ?? [];
3✔
98
    }
99

100
    /**
101
     * @param Media $media
102
     * @param string $variantName
103
     * @param bool $forceRecreate
104
     * @return Media
105
     * @throws ImageManipulationException
106
     */
107
    public function createImageVariant(
108
        Media $media,
109
        string $variantName,
110
        bool $forceRecreate = false
111
    ): Media {
112
        if (!$this->imageManager) {
81✔
113
            throw ConfigurationException::interventionImageNotConfigured();
3✔
114
        }
115

116
        $this->validateMedia($media);
78✔
117

118
        $modelClass = config('mediable.model');
75✔
119
        /** @var Media $variant */
120
        $variant = new $modelClass();
75✔
121
        $recreating = false;
75✔
122
        $originalVariant = null;
75✔
123

124
        // don't recreate if that variant already exists for the model
125
        if ($media->hasVariant($variantName)) {
75✔
126
            $variant = $media->findVariant($variantName);
9✔
127
            if ($forceRecreate) {
9✔
128
                // replace the existing variant
129
                $recreating = true;
6✔
130
                $originalVariant = clone $variant;
6✔
131
            } else {
132
                // variant already exists, nothing more to do
133
                return $variant;
3✔
134
            }
135
        }
136

137
        $manipulation = $this->getVariantDefinition($variantName);
72✔
138

139
        $outputFormat = $this->determineOutputFormat($manipulation, $media);
69✔
140
        if (method_exists($this->imageManager, 'read')) {
66✔
141
            // Intervention Image  >=3.0
142
            $image = $this->imageManager->read($media->contents());
66✔
143
        } else {
144
            // Intervention Image <3.0
145
            $image = $this->imageManager->make($media->contents());
×
146
        }
147

148
        $callback = $manipulation->getCallback();
66✔
149
        $callback($image, $media);
66✔
150

151
        $outputStream = $this->imageToStream(
66✔
152
            $image,
66✔
153
            $outputFormat,
66✔
154
            $manipulation->getOutputQuality()
66✔
155
        );
66✔
156

157
        if ($manipulation->shouldOptimize()) {
66✔
158
            $outputStream = $this->imageOptimizer->optimizeImage(
3✔
159
                $outputStream,
3✔
160
                $manipulation->getOptimizerChain()
3✔
161
            );
3✔
162
        }
163

164
        $variant->variant_name = $variantName;
66✔
165
        $variant->original_media_id = $media->isOriginal()
66✔
166
            ? $media->getKey()
63✔
167
            : $media->original_media_id; // attach variants of variants to the same original
6✔
168

169
        $variant->disk = $manipulation->getDisk() ?? $media->disk;
66✔
170
        $variant->directory = $manipulation->getDirectory() ?? $media->directory;
66✔
171
        $variant->filename = $this->determineFilename(
66✔
172
            $media->findOriginal(),
66✔
173
            $manipulation,
66✔
174
            $variant,
66✔
175
            $outputStream
66✔
176
        );
66✔
177
        $variant->extension = $outputFormat;
66✔
178
        $variant->mime_type = $this->getMimeTypeForOutputFormat($outputFormat);
66✔
179
        $variant->aggregate_type = Media::TYPE_IMAGE;
66✔
180
        $variant->size = $outputStream->getSize();
66✔
181

182
        $this->checkForDuplicates($variant, $manipulation, $originalVariant);
66✔
183
        if ($beforeSave = $manipulation->getBeforeSave()) {
63✔
184
            $beforeSave($variant, $originalVariant);
9✔
185
            // destination may have been changed, check for duplicates again
186
            $this->checkForDuplicates($variant, $manipulation, $originalVariant);
9✔
187
        }
188

189
        if ($recreating) {
60✔
190
            // delete the original file for that variant
191
            $this->filesystem->disk($originalVariant->disk)
6✔
192
                ->delete($originalVariant->getDiskPath());
6✔
193
        }
194

195
        $visibility = $manipulation->getVisibility();
60✔
196
        if ($visibility == 'match') {
60✔
197
            $visibility = ($media->isVisible() ? 'public' : 'private');
6✔
198
        }
199
        $options = [];
60✔
200
        if ($visibility) {
60✔
201
            $options = ['visibility' => $visibility];
12✔
202
        }
203

204
        $this->filesystem->disk($variant->disk)
60✔
205
            ->writeStream(
60✔
206
                $variant->getDiskPath(),
60✔
207
                $outputStream->detach(),
60✔
208
                $options
60✔
209
            );
60✔
210

211
        $variant->save();
60✔
212

213
        return $variant;
60✔
214
    }
215

216
    /**
217
     * @param Media $media
218
     * @param SourceAdapterInterface $source
219
     * @param ImageManipulation $manipulation
220
     * @return StreamAdapter
221
     * @throws ImageManipulationException
222
     */
223
    public function manipulateUpload(
224
        Media $media,
225
        SourceAdapterInterface $source,
226
        ImageManipulation $manipulation
227
    ): StreamAdapter {
228
        if (!$this->imageManager) {
6✔
229
            throw ConfigurationException::interventionImageNotConfigured();
3✔
230
        }
231

232
        $outputFormat = $this->determineOutputFormat($manipulation, $media);
3✔
233
        if (method_exists($this->imageManager, 'read')) {
3✔
234
            // Intervention Image  >=3.0
235
            $image = $this->imageManager->read($source->getStream()->getContents());
3✔
236
        } else {
237
            // Intervention Image <3.0
238
            $image = $this->imageManager->make($source->getStream()->getContents());
×
239
        }
240

241
        $callback = $manipulation->getCallback();
3✔
242
        $callback($image, $media);
3✔
243

244
        $outputStream = $this->imageToStream(
3✔
245
            $image,
3✔
246
            $outputFormat,
3✔
247
            $manipulation->getOutputQuality()
3✔
248
        );
3✔
249

250
        if ($manipulation->shouldOptimize()) {
3✔
251
            $outputStream = $this->imageOptimizer->optimizeImage(
×
252
                $outputStream,
×
253
                $manipulation->getOptimizerChain()
×
254
            );
×
255
        }
256

257
        $media->extension = $outputFormat;
3✔
258
        $media->mime_type = $this->getMimeTypeForOutputFormat($outputFormat);
3✔
259
        $media->aggregate_type = Media::TYPE_IMAGE;
3✔
260
        $media->size = $outputStream->getSize();
3✔
261

262
        return new StreamAdapter($outputStream);
3✔
263
    }
264

265
    private function getMimeTypeForOutputFormat(string $outputFormat): string
266
    {
267
        return ImageManipulation::MIME_TYPE_MAP[$outputFormat];
69✔
268
    }
269

270
    /**
271
     * @param ImageManipulation $manipulation
272
     * @param Media $media
273
     * @return string
274
     * @throws ImageManipulationException If output format cannot be determined
275
     */
276
    private function determineOutputFormat(
277
        ImageManipulation $manipulation,
278
        Media $media
279
    ): string {
280
        if ($format = $manipulation->getOutputFormat()) {
72✔
281
            return $format;
15✔
282
        }
283

284
        // attempt to infer the format from the mime type
285
        $mime = strtolower($media->mime_type);
57✔
286
        $format = array_search($mime, ImageManipulation::MIME_TYPE_MAP);
57✔
287
        if ($format !== false) {
57✔
288
            return $format;
54✔
289
        }
290

291
        // attempt to infer the format from the file extension
292
        $extension = strtolower($media->extension);
3✔
293
        if (in_array($extension, ImageManipulation::VALID_IMAGE_FORMATS)) {
3✔
294
            return $extension;
×
295
        }
296
        if ($extension === 'jpeg') {
3✔
297
            return ImageManipulation::FORMAT_JPG;
×
298
        }
299
        if ($extension === 'tiff') {
3✔
300
            return ImageManipulation::FORMAT_TIFF;
×
301
        }
302

303
        throw ImageManipulationException::unknownOutputFormat();
3✔
304
    }
305

306
    public function determineFilename(
307
        Media $originalMedia,
308
        ImageManipulation $manipulation,
309
        Media $variant,
310
        StreamInterface $stream
311
    ): string {
312
        if ($filename = $manipulation->getFilename()) {
66✔
313
            return $filename;
3✔
314
        }
315

316
        if ($manipulation->isUsingHashForFilename()) {
63✔
317
            return $this->getHashFromStream(
3✔
318
                $stream,
3✔
319
                $manipulation->getHashFilenameAlgo() ?? 'md5'
3✔
320
            );
3✔
321
        }
322
        return sprintf('%s-%s', $originalMedia->filename, $variant->variant_name);
60✔
323
    }
324

325
    public function validateMedia(Media $media): void
326
    {
327
        if ($media->aggregate_type != Media::TYPE_IMAGE) {
78✔
328
            throw ImageManipulationException::invalidMediaType($media->aggregate_type);
3✔
329
        }
330
    }
331

332
    private function getHashFromStream(StreamInterface $stream, string $algo): string
333
    {
334
        $stream->rewind();
3✔
335
        $hash = hash_init($algo);
3✔
336
        while ($chunk = $stream->read(2048)) {
3✔
337
            hash_update($hash, $chunk);
3✔
338
        }
339
        $filename = hash_final($hash);
3✔
340
        $stream->rewind();
3✔
341

342
        return $filename;
3✔
343
    }
344

345
    private function checkForDuplicates(
346
        Media $variant,
347
        ImageManipulation $manipulation,
348
        ?Media $originalVariant = null
349
    ) {
350
        if ($originalVariant
66✔
351
            && $variant->disk === $originalVariant->disk
66✔
352
            && $variant->getDiskPath() === $originalVariant->getDiskPath()
66✔
353
        ) {
354
            // same as the original, no conflict as we are going to replace the file anyways
355
            return;
3✔
356
        }
357

358
        if (!$this->filesystem->disk($variant->disk)->exists($variant->getDiskPath())) {
66✔
359
            // no conflict, carry on
360
            return;
60✔
361
        }
362

363
        switch ($manipulation->getOnDuplicateBehaviour()) {
12✔
364
            case ImageManipulation::ON_DUPLICATE_ERROR:
12✔
365
                throw ImageManipulationException::fileExists($variant->getDiskPath());
6✔
366

367
            case ImageManipulation::ON_DUPLICATE_INCREMENT:
6✔
368
            default:
369
                $variant->filename = $this->generateUniqueFilename($variant);
6✔
370
                break;
6✔
371
        }
372
    }
373

374
    /**
375
     * Increment model's filename until one is found that doesn't already exist.
376
     * @param Media $model
377
     * @return string
378
     */
379
    private function generateUniqueFilename(Media $model): string
380
    {
381
        $storage = $this->filesystem->disk($model->disk);
6✔
382
        $counter = 0;
6✔
383
        do {
384
            $filename = "{$model->filename}";
6✔
385
            if ($counter > 0) {
6✔
386
                $filename .= '-' . $counter;
6✔
387
            }
388
            $path = "{$model->directory}/{$filename}.{$model->extension}";
6✔
389
            ++$counter;
6✔
390
        } while ($storage->exists($path));
6✔
391

392
        return $filename;
6✔
393
    }
394

395
    private function imageToStream(
396
        Image $image,
397
        string $outputFormat,
398
        int $outputQuality
399
    ) {
400
        if (class_exists(StreamCommand::class)) {
69✔
401
            // Intervention Image  <3.0
402
            return $image->stream(
×
403
                $outputFormat,
×
404
                $outputQuality
×
405
            );
×
406
        }
407

408
        $formatted = match ($outputFormat) {
69✔
409
            ImageManipulation::FORMAT_JPG => $image->toJpeg($outputQuality),
69✔
410
            ImageManipulation::FORMAT_PNG => $image->toPng(),
60✔
411
            ImageManipulation::FORMAT_GIF => $image->toGif(),
3✔
412
            ImageManipulation::FORMAT_WEBP => $image->toBitmap(),
×
413
            ImageManipulation::FORMAT_TIFF => $image->toTiff($outputQuality),
×
414
            ImageManipulation::FORMAT_HEIC => $image->toHeic($outputQuality),
×
415
            default => throw ImageManipulationException::unknownOutputFormat(),
×
416
        };
69✔
417
        return Utils::streamFor($formatted->toFilePointer());
69✔
418
    }
419
}
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

© 2025 Coveralls, Inc