From 3a9d18a6cddea100d923370dd1ad1c9b1c9aeecd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 23 May 2025 16:12:03 +0100 Subject: [PATCH 1/2] Images: Added base avif support Includes handling for animated avif images like apng. --- app/Http/Controller.php | 2 +- app/Uploads/ImageResizer.php | 52 ++++++++++++++++++++++++++++++------ app/Uploads/ImageService.php | 2 +- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/app/Http/Controller.php b/app/Http/Controller.php index 090cf523a..652e2ccf3 100644 --- a/app/Http/Controller.php +++ b/app/Http/Controller.php @@ -163,7 +163,7 @@ abstract class Controller extends BaseController */ protected function getImageValidationRules(): array { - return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)]; + return ['image_extension', 'mimes:jpeg,png,gif,webp,avif', 'max:' . (config('app.upload_limit') * 1000)]; } /** diff --git a/app/Uploads/ImageResizer.php b/app/Uploads/ImageResizer.php index 5f095658f..8d7571c82 100644 --- a/app/Uploads/ImageResizer.php +++ b/app/Uploads/ImageResizer.php @@ -6,6 +6,7 @@ use BookStack\Exceptions\ImageUploadException; use Exception; use GuzzleHttp\Psr7\Utils; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; use Intervention\Image\Decoders\BinaryImageDecoder; use Intervention\Image\Drivers\Gd\Decoders\NativeObjectDecoder; use Intervention\Image\Drivers\Gd\Driver; @@ -93,8 +94,8 @@ class ImageResizer $imageData = $disk->get($imagePath); - // Do not resize apng images where we're not cropping - if ($keepRatio && $this->isApngData($image, $imageData)) { + // Do not resize animated images where we're not cropping + if ($keepRatio && $this->isAnimated($image, $imageData)) { Cache::put($thumbCacheKey, $image->path, static::THUMBNAIL_CACHE_TIME); return $this->storage->getPublicUrl($image->path); @@ -240,15 +241,50 @@ class ImageResizer /** * Check if the given image and image data is apng. */ - protected function isApngData(Image $image, string &$imageData): bool + protected function isApngData(string &$imageData): bool { - $isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png'; - if (!$isPng) { - return false; - } - $initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT')); return str_contains($initialHeader, 'acTL'); } + + /** + * Check if the given avif image data represents an animated image. + * This is based up the answer here: https://stackoverflow.com/a/79457313 + */ + protected function isAnimatedAvifData(string &$imageData): bool + { + $stszPos = strpos($imageData, 'stsz'); + if ($stszPos === false) { + return false; + } + + // Look 12 bytes after the start of 'stsz' + $start = $stszPos + 12; + $end = $start + 4; + if ($end > strlen($imageData) - 1) { + return false; + } + + $data = substr($imageData, $start, 4); + $count = unpack('Nvalue', $data)['value']; + return $count > 1; + } + + /** + * Check if the given image is animated. + */ + protected function isAnimated(Image $image, string &$imageData): bool + { + $extension = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)); + if ($extension === 'png') { + return $this->isApngData($imageData); + } + + if ($extension === 'avif') { + return $this->isAnimatedAvifData($imageData); + } + + return false; + } } diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 4d6d49197..a8f144517 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class ImageService { - protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif']; public function __construct( protected ImageStorage $storage, From 131ac29df456263fa471fed5875228e0402db9d9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 23 May 2025 17:19:34 +0100 Subject: [PATCH 2/2] Images: Added testing to cover animated avif handling --- tests/Uploads/ImageTest.php | 15 ++++++++++++++- tests/test-data/animated.avif | Bin 0 -> 1168 bytes 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/test-data/animated.avif diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 2c36f5f35..a2f03df34 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -68,7 +68,20 @@ class ImageTest extends TestCase $this->files->deleteAtRelativePath($imgDetails['path']); $this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery); - $this->assertStringNotContainsString('thumbs-', $imgDetails['response']->thumbs->display); + $this->assertStringNotContainsString('scaled-', $imgDetails['response']->thumbs->display); + } + + public function test_image_display_thumbnail_generation_for_animated_avif_images_uses_original_file() + { + $page = $this->entities->page(); + $admin = $this->users->admin(); + $this->actingAs($admin); + + $imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'animated.avif'); + $this->files->deleteAtRelativePath($imgDetails['path']); + + $this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery); + $this->assertStringNotContainsString('scaled-', $imgDetails['response']->thumbs->display); } public function test_image_edit() diff --git a/tests/test-data/animated.avif b/tests/test-data/animated.avif new file mode 100644 index 0000000000000000000000000000000000000000..92f71459bb8182f0f6d2854dca8630a315676010 GIT binary patch literal 1168 zcmb7DUq}>T5TD(>>^v+El{!k~9C!P3BYee5CV9ukBKMB$>p*UavnUFJ)(%+Abj=9_Q6`Q`(F7Q?$} zD?W1pi6pYYT|;KBb&xYd=1eiV4#|hn{gvyULUBm@a+>LcDuB&YudI_ijrO3UcPay# zsRvP;ZRYVHM8JO|GcA>@BLD(^^am-AQbulKY`{Pnps)=MT`XCOpO!l;(+YC-G25}x zae~>Zg|eQxwoZXakrKykHpB>!qR}Xd8pW48#-N6vI@L0r0o5D=G58SL%4}XfAr=-9 z#4DIdoZyz_qubaBddG z{|<-H&BF_?_j!=Evhb7^`WiGx4~HTwt!B_MleiVe3iYnnYodP3_1yCHP>vBA7nDSm zIYT6ZL(+*VF_Dlssrc#vxm9j!mD6(4WW9>T_u{~Wet<-=h-&2s#SEsKLF%}w25p0p zFgoFSK|9H!0Bm%ebgb14-dYJKUjt&nbEv@RV#t?5-!RHD5l#iQAn!(QM#h}M13;D5 zNO_bmxu$rrYJ%=OET*HGR2-&9LF{@jUO#&CUExyS*~wV#__LqM9qdc(qvY|0{Dt$` zOmkYA83h>r0Z_ehJ_dJ}USB)aex~O}+or+O#)3QG&)_r6P8o_|}J8mwz{pFUXL{_DlgY^Et)B}O8pzyb!Y>?ssyw6~8ZU-6dx Q_m?`Z4o@#8y61X-181VpD*ylh literal 0 HcmV?d00001