From 439c6982ea13932eb5338b5af208b1c5af16aa1b Mon Sep 17 00:00:00 2001 From: Marco Luzi Date: Fri, 31 Jul 2026 07:52:22 +0200 Subject: [PATCH 1/5] fix: only offer the focal point picker for croppable attachments --- src/Admin/FocalPointMediaField.php | 13 ++++++++++++- tests/Integration/FocalPointMediaFieldTest.php | 10 ++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Admin/FocalPointMediaField.php b/src/Admin/FocalPointMediaField.php index ff2d87e..b4a7b51 100644 --- a/src/Admin/FocalPointMediaField.php +++ b/src/Admin/FocalPointMediaField.php @@ -23,7 +23,7 @@ public function register(): void */ public function addField(array $formFields, WP_Post $attachment): array { - if (! wp_attachment_is_image($attachment->ID)) { + if (! $this->isCroppable($attachment->ID)) { return $formFields; } @@ -76,6 +76,17 @@ public function printAssets(): void echo $this->script(); } + private function isCroppable(int $attachmentId): bool + { + if (! wp_attachment_is_image($attachmentId)) { + return false; + } + + $mime = get_post_mime_type($attachmentId); + + return $mime !== false && wp_image_editor_supports(['mime_type' => $mime]); + } + private function markup(int $attachmentId, string $preview, float $x, float $y): string { return sprintf( diff --git a/tests/Integration/FocalPointMediaFieldTest.php b/tests/Integration/FocalPointMediaFieldTest.php index b4a6aba..005adf2 100644 --- a/tests/Integration/FocalPointMediaFieldTest.php +++ b/tests/Integration/FocalPointMediaFieldTest.php @@ -64,4 +64,14 @@ public function test_uses_the_real_attachment_id_in_the_input_names(): void $this->assertStringNotContainsString('{{ID}}', $html); $this->assertStringContainsString('draggable="false"', $html); } + + public function test_does_not_offer_the_picker_for_an_svg_attachment(): void + { + $id = $this->seedAttachment('example.svg'); + $attachment = get_post($id); + + $fields = (new FocalPointMediaField)->addField([], $attachment); + + $this->assertArrayNotHasKey('sproutset_focal_point', $fields); + } } From db3b3b77ebb904dac8e3b695e11d1ee9b2dbd3ed Mon Sep 17 00:00:00 2001 From: Marco Luzi Date: Fri, 31 Jul 2026 07:57:28 +0200 Subject: [PATCH 2/5] fix: reject focal point saves for non-croppable attachments --- src/Admin/FocalPointMediaField.php | 4 ++++ tests/Integration/FocalPointMediaFieldTest.php | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Admin/FocalPointMediaField.php b/src/Admin/FocalPointMediaField.php index b4a7b51..45f9e17 100644 --- a/src/Admin/FocalPointMediaField.php +++ b/src/Admin/FocalPointMediaField.php @@ -58,6 +58,10 @@ public function saveField(array $post, array $attachment): array return $post; } + if (! $this->isCroppable($id)) { + return $post; + } + FocalPointMeta::write($id, (float) $attachment['sproutset_focal_x'], (float) $attachment['sproutset_focal_y']); FocalPointMeta::clearApplied($id); diff --git a/tests/Integration/FocalPointMediaFieldTest.php b/tests/Integration/FocalPointMediaFieldTest.php index 005adf2..5c3aea9 100644 --- a/tests/Integration/FocalPointMediaFieldTest.php +++ b/tests/Integration/FocalPointMediaFieldTest.php @@ -74,4 +74,16 @@ public function test_does_not_offer_the_picker_for_an_svg_attachment(): void $this->assertArrayNotHasKey('sproutset_focal_point', $fields); } + + public function test_ignores_a_focal_point_save_for_an_svg_attachment(): void + { + $id = $this->seedAttachment('example.svg'); + + (new FocalPointMediaField)->saveField( + ['ID' => $id], + ['sproutset_focal_x' => '25', 'sproutset_focal_y' => '75'], + ); + + $this->assertNull(FocalPointMeta::read($id)); + } } From 830eb15e2c4ae6829c9f21ea369be52033de7c6a Mon Sep 17 00:00:00 2001 From: Marco Luzi Date: Fri, 31 Jul 2026 08:01:20 +0200 Subject: [PATCH 3/5] fix: stop emitting focal point styles for SVG attachments --- src/Images/WpImageResolver.php | 2 +- tests/Integration/WpImageResolverTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Images/WpImageResolver.php b/src/Images/WpImageResolver.php index bd4b491..55b20ab 100644 --- a/src/Images/WpImageResolver.php +++ b/src/Images/WpImageResolver.php @@ -55,7 +55,7 @@ private function resolveSvg(ImageRequest $request): ?ResolvedImage width: null, height: null, alt: $this->alt($request), - style: FocalPointPosition::forCover($this->cssFocal($request), $request->focalPoint), + style: null, isSvg: true, ); } diff --git a/tests/Integration/WpImageResolverTest.php b/tests/Integration/WpImageResolverTest.php index be02ca5..98efe8c 100644 --- a/tests/Integration/WpImageResolverTest.php +++ b/tests/Integration/WpImageResolverTest.php @@ -244,4 +244,16 @@ public function test_explicit_override_without_stored_metadata_never_triggers_ph $this->assertSame([], FocalPointMeta::appliedAt($id)); } + + public function test_emits_no_focal_style_for_an_svg(): void + { + $id = $this->seedAttachment('example.svg'); + FocalPointMeta::write($id, 25.0, 75.0); + + $resolved = $this->focalResolver(true)->resolve($this->focalRequest($id, 'large', true, 10.0, 20.0)); + + $this->assertNotNull($resolved); + $this->assertTrue($resolved->isSvg); + $this->assertNull($resolved->style); + } } From 9f27bc3e741e88d415c067d3bfd42ce74d5746da Mon Sep 17 00:00:00 2001 From: Marco Luzi Date: Fri, 31 Jul 2026 08:04:49 +0200 Subject: [PATCH 4/5] docs: spec picker eligibility and SVG focal exclusion --- specs/focal-point.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/specs/focal-point.md b/specs/focal-point.md index 0a37e14..a01b157 100644 --- a/specs/focal-point.md +++ b/specs/focal-point.md @@ -14,7 +14,8 @@ the image into a box, so the two mechanisms split by size type: - **Hard-crop registered sizes** — the generated subsize file is physically re-cropped around the focal point, sourced from the original. Focal-correct across the whole `srcset`, no CSS. - **CSS `object-fit: cover; object-position: x% y%`** — emitted only where cover is in play: an - explicit per-call `focal-point` request, the crop-size upscale-too-small fallback, and SVG. + explicit per-call `focal-point` request and the crop-size upscale-too-small fallback. SVG is + excluded from the feature and never receives focal styling. - **Non-crop sizes without explicit cover** — no focal effect. **Precedence.** CSS position uses `explicit per-call coords → attachment metadata → center`. @@ -34,6 +35,11 @@ render re-crops. A crop failure trips a per-attachment fuse and the image serves **Configuration.** `config('sproutset.focal_point')` (default `true`) gates the picker, metadata honoring, and cropping. The explicit per-call attribute is independent of the flag. +**Picker eligibility.** The picker is offered — and focal saves accepted — only for attachments +the registered WordPress image editor can process, probed with `wp_image_editor_supports()`. +This excludes SVG. Because the probe reflects the server's GD/Imagick build, eligibility is +capability-dependent: a host without AVIF support shows no picker on AVIF uploads. + Scenario: computes a crop window centred on the focal point Given an original larger than a square crop target and a focal point of 25/75 When the crop window is computed @@ -99,6 +105,16 @@ Scenario: saving the picker stores and clamps the focal point and invalidates cr When the attachment form is saved Then the coordinates are clamped and stored and the applied-marker is cleared +Scenario: the picker is only offered for attachments the image editor can crop + Given an SVG attachment + When the attachment edit fields are built, and when a focal point save is posted for it + Then no focal point field is added and no coordinates are stored + +Scenario: SVG receives no focal styling + Given an SVG attachment with a stored focal point and an explicit per-call focal request + When the image is resolved + Then no style is produced + ## Acceptance criteria | Scenario | Test | @@ -116,3 +132,5 @@ Scenario: saving the picker stores and clamps the focal point and invalidates cr | an explicit per-call focal point overrides the stored one for CSS | `tests/Integration/WpImageResolverTest.php` → `test_explicit_coordinates_override_the_attachment_focal_point` | | the feature is inert when disabled | `tests/Integration/WpImageResolverTest.php` → `test_is_inert_when_the_feature_is_disabled`; `tests/Feature/FocalPointConfigTest.php` → `it('reflects a disabled focal point config flag')` | | saving the picker stores and clamps the focal point and invalidates crops | `tests/Integration/FocalPointMediaFieldTest.php` → `test_saves_and_clamps_the_focal_point_from_the_form`, `test_clears_the_applied_marker_when_the_focal_point_is_saved` | +| the picker is only offered for attachments the image editor can crop | `tests/Integration/FocalPointMediaFieldTest.php` → `test_does_not_offer_the_picker_for_an_svg_attachment`, `test_ignores_a_focal_point_save_for_an_svg_attachment` | +| SVG receives no focal styling | `tests/Integration/WpImageResolverTest.php` → `test_emits_no_focal_style_for_an_svg` | From 2cb534c63bd8d40cb38826cdbadae87f8692486a Mon Sep 17 00:00:00 2001 From: Marco Luzi Date: Fri, 31 Jul 2026 08:11:30 +0200 Subject: [PATCH 5/5] docs: document image/* gate in picker eligibility spec --- specs/focal-point.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specs/focal-point.md b/specs/focal-point.md index a01b157..38d7419 100644 --- a/specs/focal-point.md +++ b/specs/focal-point.md @@ -38,7 +38,10 @@ honoring, and cropping. The explicit per-call attribute is independent of the fl **Picker eligibility.** The picker is offered — and focal saves accepted — only for attachments the registered WordPress image editor can process, probed with `wp_image_editor_supports()`. This excludes SVG. Because the probe reflects the server's GD/Imagick build, eligibility is -capability-dependent: a host without AVIF support shows no picker on AVIF uploads. +capability-dependent: a host without AVIF support shows no picker on AVIF uploads. A first gate, +`wp_attachment_is_image()`, constrains eligibility to `image/*` mime types; it is deliberately +retained rather than replaced by the editor probe, because `wp_image_editor_supports()` alone +returns `true` for `application/pdf` on an Imagick build with a Ghostscript delegate. Scenario: computes a crop window centred on the focal point Given an original larger than a square crop target and a focal point of 25/75