From 3af047c230f95a7b6608bf081caab4f83a63a11b Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 31 Aug 2026 10:27:47 +0100 Subject: [PATCH 1/4] use asset thumbnail in the focal point editor instead of the full preview image improves performance when working with large source images (#15270) --- resources/js/components/assets/Editor/Editor.vue | 1 + resources/js/components/assets/Editor/FocalPointEditor.vue | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/js/components/assets/Editor/Editor.vue b/resources/js/components/assets/Editor/Editor.vue index a2b805b5120..7aa161ca2f5 100644 --- a/resources/js/components/assets/Editor/Editor.vue +++ b/resources/js/components/assets/Editor/Editor.vue @@ -168,6 +168,7 @@ v-if="showFocalPointEditor && isFocalPointEditorEnabled" :data="values.focus" :image="asset.preview" + :thumbnail="asset.thumbnail" @selected="selectFocalPoint" @closed="closeFocalPointEditor" /> diff --git a/resources/js/components/assets/Editor/FocalPointEditor.vue b/resources/js/components/assets/Editor/FocalPointEditor.vue index f91f801f5a0..5c3410d9d0e 100644 --- a/resources/js/components/assets/Editor/FocalPointEditor.vue +++ b/resources/js/components/assets/Editor/FocalPointEditor.vue @@ -55,7 +55,7 @@ :x="x" :y="y" :z="z" - :image-url="image" + :image-url="thumbnail || image" :image-dimensions="imageDimensions" /> @@ -84,6 +84,7 @@ export default { props: [ 'data', // The initial focus point data stored in the asset, if applicable. 'image', // The url of the image. + 'thumbnail', ], data() { From 2f5a19f6add6b8d5b8d2ea210d6bbdb30c2cd2a2 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 1 Sep 2026 09:09:22 +0100 Subject: [PATCH 2/4] revert "use asset thumbnail in the focal point editor instead of the full preview image" This reverts commit 3af047c230f95a7b6608bf081caab4f83a63a11b. the small listing preset is too low quality for the preview frames, and the main picker image was still decoding the full-size original --- resources/js/components/assets/Editor/Editor.vue | 1 - resources/js/components/assets/Editor/FocalPointEditor.vue | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/js/components/assets/Editor/Editor.vue b/resources/js/components/assets/Editor/Editor.vue index 7aa161ca2f5..a2b805b5120 100644 --- a/resources/js/components/assets/Editor/Editor.vue +++ b/resources/js/components/assets/Editor/Editor.vue @@ -168,7 +168,6 @@ v-if="showFocalPointEditor && isFocalPointEditorEnabled" :data="values.focus" :image="asset.preview" - :thumbnail="asset.thumbnail" @selected="selectFocalPoint" @closed="closeFocalPointEditor" /> diff --git a/resources/js/components/assets/Editor/FocalPointEditor.vue b/resources/js/components/assets/Editor/FocalPointEditor.vue index 5c3410d9d0e..f91f801f5a0 100644 --- a/resources/js/components/assets/Editor/FocalPointEditor.vue +++ b/resources/js/components/assets/Editor/FocalPointEditor.vue @@ -55,7 +55,7 @@ :x="x" :y="y" :z="z" - :image-url="thumbnail || image" + :image-url="image" :image-dimensions="imageDimensions" /> @@ -84,7 +84,6 @@ export default { props: [ 'data', // The initial focus point data stored in the asset, if applicable. 'image', // The url of the image. - 'thumbnail', ], data() { From 26291a7944c678fe91266bafa33fc63d82bec427 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 1 Sep 2026 09:11:36 +0100 Subject: [PATCH 3/4] add a built-in `large` cp thumbnail preset and expose it in asset payloads a 1600px preset generated through the existing cp thumbnail route, so it works for both public and private containers --- .../Resources/CP/Assets/HasThumbnails.php | 1 + src/Imaging/Manager.php | 2 +- tests/Feature/Assets/ImageThumbnailTest.php | 30 +++++++++++++++++++ tests/Feature/Assets/ShowAssetTest.php | 23 ++++++++++++++ tests/Imaging/ManagerTest.php | 6 ++++ 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Http/Resources/CP/Assets/HasThumbnails.php b/src/Http/Resources/CP/Assets/HasThumbnails.php index a0ad4cfe4b2..87c40b76376 100644 --- a/src/Http/Resources/CP/Assets/HasThumbnails.php +++ b/src/Http/Resources/CP/Assets/HasThumbnails.php @@ -27,6 +27,7 @@ private function getImageThumbnail(): array 'is_image' => true, 'preview' => $this->previewUrl(), 'thumbnail' => $this->thumbnailUrl('small'), + 'large_thumbnail' => $this->thumbnailUrl('large'), 'can_be_transparent' => $this->isSvg() || $this->extensionIsOneOf(['svg', 'png', 'webp', 'avif']), 'alt' => $this->alt, 'orientation' => $this->orientation(), diff --git a/src/Imaging/Manager.php b/src/Imaging/Manager.php index 0339633b53a..cb82c4dd3d5 100644 --- a/src/Imaging/Manager.php +++ b/src/Imaging/Manager.php @@ -86,7 +86,7 @@ public function userManipulationPresets() public function cpManipulationPresets() { $presets = array_merge( - ['small' => 400], + ['small' => 400, 'large' => 1600], config('statamic.cp.thumbnail_presets', []) ); diff --git a/tests/Feature/Assets/ImageThumbnailTest.php b/tests/Feature/Assets/ImageThumbnailTest.php index ba1c65a5575..c3fdbab319d 100644 --- a/tests/Feature/Assets/ImageThumbnailTest.php +++ b/tests/Feature/Assets/ImageThumbnailTest.php @@ -3,8 +3,10 @@ namespace Tests\Feature\Assets; use Illuminate\Http\UploadedFile; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\AssetContainer; +use Statamic\Facades\Glide; use Statamic\Facades\User; use Tests\FakesRoles; use Tests\PreventSavingStacheItemsToDisk; @@ -21,6 +23,8 @@ public function setUp(): void { parent::setUp(); + Glide::cacheStore()->flush(); + config(['filesystems.disks.test' => [ 'driver' => 'local', 'root' => $this->tempDir = __DIR__.'/tmp', @@ -51,6 +55,32 @@ public function it_returns_thumbnail() ->assertSuccessful(); } + #[Test] + #[DataProvider('presetProvider')] + public function it_returns_thumbnail_using_a_preset($preset) + { + $container = AssetContainer::make('test')->disk('test')->save(); + $container + ->makeAsset('one.png') + ->upload(UploadedFile::fake()->image('one.png')); + + $this->setTestRoles(['test' => ['access cp', 'view test assets']]); + $user = User::make()->assignRole('test')->save(); + + $this + ->actingAs($user) + ->getJson('/cp/thumbnails/'.base64_encode('test::one.png').'/'.$preset) + ->assertSuccessful(); + } + + public static function presetProvider() + { + return [ + 'small' => ['small'], + 'large' => ['large'], + ]; + } + #[Test] public function it_404s_when_the_asset_doesnt_exist() { diff --git a/tests/Feature/Assets/ShowAssetTest.php b/tests/Feature/Assets/ShowAssetTest.php index 1bcd2fa5c19..01428ac91d7 100644 --- a/tests/Feature/Assets/ShowAssetTest.php +++ b/tests/Feature/Assets/ShowAssetTest.php @@ -52,6 +52,29 @@ public function it_returns_json() ->assertJson(['data' => ['id' => 'test::one.txt']]); } + #[Test] + public function it_returns_thumbnail_urls_for_images() + { + $container = AssetContainer::make('test')->disk('test')->save(); + $container + ->makeAsset('one.png') + ->upload(UploadedFile::fake()->image('one.png')); + + $this->setTestRoles(['test' => ['access cp', 'view test assets']]); + $user = User::make()->assignRole('test')->save(); + + $encodedAsset = base64_encode('test::one.png'); + + $this + ->actingAs($user) + ->getJson('/cp/assets/'.$encodedAsset) + ->assertSuccessful() + ->assertJson(['data' => [ + 'thumbnail' => "http://localhost/cp/thumbnails/{$encodedAsset}/small", + 'large_thumbnail' => "http://localhost/cp/thumbnails/{$encodedAsset}/large", + ]]); + } + #[Test] public function it_404s_when_the_asset_doesnt_exist() { diff --git a/tests/Imaging/ManagerTest.php b/tests/Imaging/ManagerTest.php index 4e2434605d3..e4acb19a8f8 100644 --- a/tests/Imaging/ManagerTest.php +++ b/tests/Imaging/ManagerTest.php @@ -69,6 +69,9 @@ public function it_gets_manipulation_presets() 'cp_thumbnail_small_landscape' => ['w' => '400', 'h' => '400', 'fit' => 'contain'], 'cp_thumbnail_small_portrait' => ['h' => '400', 'fit' => 'contain'], 'cp_thumbnail_small_square' => ['w' => '400', 'h' => '400'], + 'cp_thumbnail_large_landscape' => ['w' => '1600', 'h' => '1600', 'fit' => 'contain'], + 'cp_thumbnail_large_portrait' => ['h' => '1600', 'fit' => 'contain'], + 'cp_thumbnail_large_square' => ['w' => '1600', 'h' => '1600'], ], $this->manager->cpManipulationPresets()); $this->manager->registerCustomManipulationPresets([ @@ -87,6 +90,9 @@ public function it_gets_manipulation_presets() 'cp_thumbnail_small_landscape' => ['w' => '400', 'h' => '400', 'fit' => 'contain'], 'cp_thumbnail_small_portrait' => ['h' => '400', 'fit' => 'contain'], 'cp_thumbnail_small_square' => ['w' => '400', 'h' => '400'], + 'cp_thumbnail_large_landscape' => ['w' => '1600', 'h' => '1600', 'fit' => 'contain'], + 'cp_thumbnail_large_portrait' => ['h' => '1600', 'fit' => 'contain'], + 'cp_thumbnail_large_square' => ['w' => '1600', 'h' => '1600'], 'og_image' => ['w' => 1146, 'h' => 600], 'twitter_image' => ['w' => 1200, 'h' => 600], ], $this->manager->manipulationPresets()); From 1692906bd391d96814ab529c3b17370f812ecc28 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 1 Sep 2026 09:11:53 +0100 Subject: [PATCH 4/4] use the `large` thumbnail preset in the focal point editor both the picker image and the nine preview frames render the same 1600px thumbnail, so the full-size original is never decoded (#15270) --- resources/js/components/assets/Editor/Editor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/components/assets/Editor/Editor.vue b/resources/js/components/assets/Editor/Editor.vue index a2b805b5120..e6933f47dec 100644 --- a/resources/js/components/assets/Editor/Editor.vue +++ b/resources/js/components/assets/Editor/Editor.vue @@ -167,7 +167,7 @@