diff --git a/specs/focal-point.md b/specs/focal-point.md index 0a37e14..38d7419 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,14 @@ 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. 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 When the crop window is computed @@ -99,6 +108,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 +135,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` | diff --git a/src/Admin/FocalPointMediaField.php b/src/Admin/FocalPointMediaField.php index ff2d87e..45f9e17 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; } @@ -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); @@ -76,6 +80,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/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/FocalPointMediaFieldTest.php b/tests/Integration/FocalPointMediaFieldTest.php index b4a6aba..5c3aea9 100644 --- a/tests/Integration/FocalPointMediaFieldTest.php +++ b/tests/Integration/FocalPointMediaFieldTest.php @@ -64,4 +64,26 @@ 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); + } + + 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)); + } } 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); + } }