Skip to content

Commit 5f0cd2f

Browse files
feat: ability to upload and work with HEIF images by convert them to JPEG (#3946)
Co-authored-by: nbalkandzhiyski <nikolay.balkandzhiyski@gmail.com>
1 parent 236d657 commit 5f0cd2f

File tree

15 files changed

+467
-59
lines changed

15 files changed

+467
-59
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Photo\Convert;
10+
11+
use App\Contracts\PhotoCreate\PhotoConverter;
12+
use App\Exceptions\CannotConvertMediaFileException;
13+
use App\Image\Files\NativeLocalFile;
14+
use App\Image\Files\TemporaryJobFile;
15+
use App\Repositories\ConfigManager;
16+
17+
class HeifToJpeg implements PhotoConverter
18+
{
19+
public function __construct(
20+
private ConfigManager $config_manager,
21+
) {
22+
}
23+
24+
/**
25+
* @throws \Exception
26+
*/
27+
public function handle(NativeLocalFile $tmp_file): TemporaryJobFile
28+
{
29+
if ($this->config_manager->hasImagick() === false) {
30+
throw new CannotConvertMediaFileException('Imagick is not available.');
31+
}
32+
33+
$path = $tmp_file->getRealPath();
34+
$pathinfo = pathinfo($path);
35+
$file_name = $pathinfo['filename'];
36+
$new_path = $pathinfo['dirname'] . '/' . $file_name . '.jpg';
37+
38+
// Convert to Jpeg
39+
try {
40+
$imagick_converted = new \Imagick($path);
41+
42+
if ($imagick_converted->getNumberImages() > 1) {
43+
$imagick_converted->setIteratorIndex(0);
44+
}
45+
46+
$imagick_converted->setImageFormat('jpeg');
47+
$imagick_converted->setImageCompression(\Imagick::COMPRESSION_JPEG);
48+
$imagick_converted->setImageCompressionQuality(92);
49+
50+
$imagick_converted->autoOrient();
51+
$imagick_converted->writeImage($new_path);
52+
} catch (\ImagickException $e) {
53+
throw new CannotConvertMediaFileException('Failed to convert HEIC/HEIF to JPEG.', $e);
54+
}
55+
56+
// Delete old file
57+
$tmp_file->delete();
58+
59+
return new TemporaryJobFile($new_path);
60+
}
61+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Photo\Convert;
10+
11+
use App\Contracts\PhotoCreate\PhotoConverter;
12+
use App\Enum\ConvertableImageType;
13+
14+
class PhotoConverterFactory
15+
{
16+
public function make(string $extension): ?PhotoConverter
17+
{
18+
return match (true) {
19+
ConvertableImageType::isHeifImageType($extension) => resolve(HeifToJpeg::class),
20+
default => null,
21+
};
22+
}
23+
}

app/Actions/Photo/Create.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public function add(NativeLocalFile $source_file, ?AbstractAlbum $album, ?int $f
8080
);
8181

8282
$pre_pipes = [
83+
Init\ConvertUnsupportedMedia::class,
8384
Init\AssertSupportedMedia::class,
8485
Init\FetchLastModifiedTime::class,
8586
Init\MayLoadFileMetadata::class,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Photo\Pipes\Init;
10+
11+
use App\Actions\Photo\Convert\PhotoConverterFactory;
12+
use App\Contracts\PhotoCreate\InitPipe;
13+
use App\DTO\PhotoCreate\InitDTO;
14+
use App\Exceptions\CannotConvertMediaFileException;
15+
16+
class ConvertUnsupportedMedia implements InitPipe
17+
{
18+
/**
19+
* Tries to convert the file to a supported format.
20+
*
21+
* @throws CannotConvertMediaFileException
22+
*/
23+
public function handle(InitDTO $state, \Closure $next): InitDTO
24+
{
25+
$ext = ltrim($state->source_file->getOriginalExtension(), '.');
26+
27+
$factory = new PhotoConverterFactory();
28+
$converter = $factory->make($ext);
29+
if ($converter === null) {
30+
return $next($state);
31+
}
32+
33+
$state->source_file = $converter->handle($state->source_file);
34+
35+
return $next($state);
36+
}
37+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Contracts\PhotoCreate;
10+
11+
use App\Image\Files\NativeLocalFile;
12+
use App\Image\Files\TemporaryJobFile;
13+
14+
interface PhotoConverter
15+
{
16+
public function handle(NativeLocalFile $tmp_file): TemporaryJobFile;
17+
}

app/Enum/ConvertableImageType.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Enum;
10+
11+
enum ConvertableImageType: string
12+
{
13+
case HEIC = 'heic';
14+
case HEIF = 'heif';
15+
16+
public static function isHeifImageType(string $extension): bool
17+
{
18+
$extension = str($extension)->lower()->toString();
19+
20+
return in_array($extension, [
21+
self::HEIC->value,
22+
self::HEIF->value,
23+
], true);
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2026 LycheeOrg.
7+
*/
8+
9+
namespace App\Exceptions;
10+
11+
use Symfony\Component\HttpFoundation\Response;
12+
13+
/**
14+
* CannotConvertMediaFileException.
15+
*
16+
* Indicates that a media file cannot be converted to another format.
17+
*/
18+
class CannotConvertMediaFileException extends BaseLycheeException
19+
{
20+
public const string DEFAULT_MESSAGE = 'Cannot convert media file to another format';
21+
22+
public function __construct(string $msg = self::DEFAULT_MESSAGE, ?\Throwable $previous = null)
23+
{
24+
parent::__construct(Response::HTTP_UNPROCESSABLE_ENTITY, $msg, $previous);
25+
}
26+
}

app/Services/Image/FileExtensionService.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class FileExtensionService
2020
IMAGETYPE_PNG,
2121
IMAGETYPE_WEBP,
2222
IMAGETYPE_AVIF,
23+
20, // IMAGETYPE_HEIF;
2324
];
2425

2526
public const SUPPORTED_IMAGE_FILE_EXTENSIONS = [
@@ -29,6 +30,8 @@ class FileExtensionService
2930
'.gif',
3031
'.webp',
3132
'.avif',
33+
'.heic',
34+
'.heif',
3235
];
3336

3437
public const SUPPORTED_VIDEO_FILE_EXTENSIONS = [
@@ -48,6 +51,8 @@ class FileExtensionService
4851
'image/png',
4952
'image/webp',
5053
'image/avif',
54+
'image/heif',
55+
'image/heic',
5156
];
5257

5358
public const SUPPORTED_VIDEO_MIME_TYPES = [
@@ -70,6 +75,8 @@ class FileExtensionService
7075
'image/png' => '.png',
7176
'image/webp' => '.webp',
7277
'image/avif' => '.avif',
78+
'image/heif' => '.heif',
79+
'image/heic' => '.heic',
7380
'video/mp4' => '.mp4',
7481
'video/mpeg' => '.mpg',
7582
'image/x-tga' => '.mpg',

0 commit comments

Comments
 (0)