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

plank / laravel-mediable / 12171157427

05 Dec 2024 12:58AM UTC coverage: 94.663% (-0.05%) from 94.717%
12171157427

push

github

web-flow
Merge pull request #368 from plank/heic

add support for HEIC

2 of 3 new or added lines in 2 files covered. (66.67%)

1 existing line in 1 file now uncovered.

1490 of 1574 relevant lines covered (94.66%)

154.56 hits per line

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

93.71
/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;
954✔
42
        $this->filesystem = $filesystem;
954✔
43
        $this->imageOptimizer = $imageOptimizer;
954✔
44
    }
45

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

60
    public function hasVariantDefinition(string $variantName): bool
61
    {
62
        return isset($this->variantDefinitions[$variantName]);
6✔
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])) {
150✔
73
            return $this->variantDefinitions[$variantName];
144✔
74
        }
75

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

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

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

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

95
    public function getVariantNamesByTag(string $tag): array
96
    {
97
        return $this->variantDefinitionGroups[$tag] ?? [];
6✔
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) {
162✔
113
            throw ConfigurationException::interventionImageNotConfigured();
6✔
114
        }
115

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

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

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

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

139
        $outputFormat = $this->determineOutputFormat($manipulation, $media);
138✔
140
        if (method_exists($this->imageManager, 'read')) {
132✔
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());
66✔
146
        }
147

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

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

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

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

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

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

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

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

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

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

213
        return $variant;
120✔
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) {
12✔
229
            throw ConfigurationException::interventionImageNotConfigured();
6✔
230
        }
231

232
        $outputFormat = $this->determineOutputFormat($manipulation, $media);
6✔
233
        if (method_exists($this->imageManager, 'read')) {
6✔
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());
3✔
239
        }
240

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

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

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

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

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

265
    private function getMimeTypeForOutputFormat(string $outputFormat): string
266
    {
267
        return ImageManipulation::MIME_TYPE_MAP[$outputFormat];
138✔
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()) {
144✔
281
            return $format;
30✔
282
        }
283

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

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

303
        throw ImageManipulationException::unknownOutputFormat();
6✔
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()) {
132✔
313
            return $filename;
6✔
314
        }
315

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

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

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

342
        return $filename;
6✔
343
    }
344

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

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

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

367
            case ImageManipulation::ON_DUPLICATE_INCREMENT:
6✔
368
            default:
369
                $variant->filename = $this->generateUniqueFilename($variant);
12✔
370
                break;
12✔
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);
12✔
382
        $counter = 0;
12✔
383
        do {
384
            $filename = "{$model->filename}";
12✔
385
            if ($counter > 0) {
12✔
386
                $filename .= '-' . $counter;
12✔
387
            }
388
            $path = "{$model->directory}/{$filename}.{$model->extension}";
12✔
389
            ++$counter;
12✔
390
        } while ($storage->exists($path));
12✔
391

392
        return $filename;
12✔
393
    }
394

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

408
        $formatted = match ($outputFormat) {
69✔
409
            ImageManipulation::FORMAT_JPG => $image->toJpeg($outputQuality),
49✔
410
            ImageManipulation::FORMAT_PNG => $image->toPng(),
59✔
411
            ImageManipulation::FORMAT_GIF => $image->toGif(),
3✔
412
            ImageManipulation::FORMAT_WEBP => $image->toBitmap(),
×
413
            ImageManipulation::FORMAT_TIFF => $image->toTiff($outputQuality),
×
NEW
414
            ImageManipulation::FORMAT_HEIC => $image->toHeic($outputQuality),
×
UNCOV
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