Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion specs/focal-point.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down Expand Up @@ -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 |
Expand All @@ -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` |
17 changes: 16 additions & 1 deletion src/Admin/FocalPointMediaField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);

Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/Images/WpImageResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand Down
22 changes: 22 additions & 0 deletions tests/Integration/FocalPointMediaFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
12 changes: 12 additions & 0 deletions tests/Integration/WpImageResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading