semsen1 / image-safe-area
Nova image field with safe area overlay
Requires
- php: ^8.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel Nova image field that shows safe areas on top of the image preview. Use it to show editors which parts of a picture will be covered by texts, menus, captions, slider controls or other UI elements, so they don't put important content there.
The field extends Laravel\Nova\Fields\Image, so it works like a normal Nova image field. It also adds aspect ratio switching, an adjustable overlay opacity, optional client-side resizing and fully customizable texts.
Requirements
- PHP 8.1+
- Laravel Nova 4+
Installation
composer require semsen1/nova-image-safe-area
Quick start
use semsen1\ImageSafeArea\ImageSafeArea; ImageSafeArea::make('Slide image', 'image') ->disk('public') ->path('slides');
Without any configuration the field shows the preview in 16:9 and 4:3 ratios, with an overlay opacity of 60%, and without any zones. Zones are not set by default. Define your own with safeZones() if you need them.
Methods
ratios(array $ratios, ?string $default = null)
Sets the aspect ratios the editor can switch between in the preview. Ratios are strings in the W:H format. $default is the ratio selected when the form opens. If it is omitted, the first item of the array is used.
Defaults: ['16:9', '4:3'], default ratio 16:9.
ImageSafeArea::make('Banner', 'banner') ->ratios(['21:9', '16:9', '1:1'], '16:9');
safeZones(array $zones, bool $merge = true)
Defines the areas that will be covered by texts or controls. Zones are grouped by aspect ratio. Each ratio key holds a list of rectangles, so the same image can have different zones in 16:9 and 4:3.
Every rectangle has the following keys. All numeric values are percentages of the image size.
| Key | Type | Description |
|---|---|---|
x |
int / float | Left offset from the left edge of the image, 0–100 |
y |
int / float | Top offset from the top edge of the image, 0–100 |
width |
int / float | Zone width, 0–100 |
height |
int / float | Zone height, 0–100 |
label |
string | Text displayed inside the zone |
$merge controls how the passed array is combined with the zones returned by defaultZones(). With true (default), ratios from $zones are added to the defaults, and a ratio that exists in both is replaced by the new list. With false, the defaults are discarded and only $zones is used. Because defaultZones() is empty by default, both modes give the same result unless you override it.
ImageSafeArea::make('Slide image', 'image') ->safeZones([ '16:9' = [ ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 15, 'label' = 'Title and description'], ['x' = 0, 'y' = 85, 'width' = 100, 'height' = 15, 'label' = 'News slider'], ], '4:3' = [ ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 20, 'label' = 'Title and description'], ], ]);
To use the same zone in several ratios, build the array in a loop:
$header = ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 15, 'label' = 'Header']; ImageSafeArea::make('Cover', 'cover') ->safeZones([ '16:9' = [$header], '4:3' = [$header], ]);
defaultOpacity(int $percent)
Sets the initial opacity of the overlay in percent. The value is clamped to the 0–100 range. The editor can still change it with the opacity slider in the field.
Default: 60.
ImageSafeArea::make('Slide image', 'image') ->defaultOpacity(40);
resize(?int $maxWidth = 1920, ?int $maxHeight = 1080, int $quality = 90,bool $cropToRatio = false)
Enables the resize action for the selected file. The editor gets a button to downscale the image before upload. Resizing is disabled until you call this method.
$maxWidthand$maxHeightare the maximum dimensions in pixels. The image is scaled down to fit and keeps its proportions. Passnullto leave a side unrestricted.$qualityis the compression quality from 1 to 100. It is converted to a 0.1–1 value and clamped to that range.$cropToRatiocrops the image to the currently selected aspect ratio in addition to scaling. The editor then sees a separate "Resize and crop" button.
// Scale down to fit 1920×1080, JPEG quality 85 ImageSafeArea::make('Slide image', 'image') ->resize(1920, 1080, 85); // Limit only the width, crop to the selected ratio ImageSafeArea::make('Slide image', 'image') ->resize(maxWidth: 1600, maxHeight: null, cropToRatio: true);
texts(array $texts)
Overrides several interface texts at once. Only the passed keys are changed, the rest keep their current values. An unknown key throws an InvalidArgumentException that lists the allowed keys, so typos are caught immediately.
ImageSafeArea::make('Slide image', 'image') ->texts([ 'overlay' = 'Covered area', 'hint' = 'Do not place important content in the shaded areas.', ]);
Available keys:
| Key | Default | Where it is used |
|---|---|---|
overlay |
Safe area |
Label of the overlay toggle |
opacity |
Opacity |
Label of the opacity slider |
hint |
empty | Help text shown under the field |
chooseFile |
Choose file |
File selection button |
cancelSelection |
Cancel selection |
Button that discards a newly selected file |
delete |
Delete |
Delete button |
resize |
Resize to :size |
Resize button. :size is replaced with the target size |
resizeCrop |
Resize to :size + crop |
Resize and crop button |
processing |
Processing |
Text shown while the image is being processed |
confirmDelete |
Delete the image? |
Delete confirmation |
deleted |
Image deleted |
Message after deletion |
zoneHeader |
Header / UI |
Predefined zone caption |
zoneFooter |
Captions / controls |
Predefined zone caption |
unitB |
B |
Unit for file size in bytes |
unitKB |
KB |
Unit for file size in kilobytes |
unitMB |
MB |
Unit for file size in megabytes |
text(string $key, string $value)
Overrides one text. It is a shortcut for texts([$key => $value]) and has the same key validation.
ImageSafeArea::make('Slide image', 'image') ->text('hint', 'Keep faces and logos out of the shaded areas.');
defaultTexts() (static)
Returns the array of default texts. Strings are passed through Laravel's __() helper, so they can be translated with your language files. You can call it to read the defaults, or override it in a subclass to change texts for every instance of the field.
$keys = array_keys(ImageSafeArea::defaultTexts());
Translations
Default texts are wrapped in Laravel's __() helper and use the English string as the translation key. They are translated on the server using the locale of the current request, so you only need JSON language files.
Add the translations to the JSON file of your application, for example lang/ru.json (resources/lang/ru.json in Laravel 9 and older):
{
"overlay": "__(Safe area)",
"opacity": "__(Opacity)",
"hint": "",
"chooseFile": "__(Choose file)",
"cancelSelection": "__(Cancel selection)",
"delete": "__(Delete)",
"resize": "__(Resize to :size)",
"resizeCrop": "__(Resize to :size + crop)",
"processing": "__(Processing)",
"confirmDelete": "__(Delete the image?)",
"deleted": "__(Image deleted)",
"zoneHeader": "__(Header / UI)",
"zoneFooter": "__(Captions / controls)",
"unitB": "__(B)",
"unitKB": "__(KB)",
"unitMB": "__(MB)"
}
Keep the :size placeholder in translated strings. The field replaces it with the target size in the browser.
The hint has no text by default
Custom texts
Texts passed through texts() and text() are used as is. Wrap them in __() if they should be translated:
ImageSafeArea::make('Slide image', 'image') ->texts([ 'deleted' = __('Image deleted'), ]);
Zone labels set in safeZones() are not translated automatically either:
->safeZones([ '16:9' = [ ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 15, 'label' = __('Title and description')], ], ])
Shipping translations with a package
If you build on top of this field in your own package, load the JSON files in the service provider:
public function boot(): void { $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); }
Translations from the application's lang directory override the ones loaded from a package.
defaultZones() (static)
Returns the zones used when safeZones() is not called. It returns an empty array, so no zones are displayed unless you configure them. Override it in a subclass if you want project-wide zones.
class SlideImage extends ImageSafeArea { public static function defaultZones(): array { return [ '16:9' = [ ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 15, 'label' = 'Title and description'], ['x' = 0, 'y' = 85, 'width' = 100, 'height' = 15, 'label' = 'News slider'], ], ]; } }
Then SlideImage::make('Slide image', 'image') has the zones already, and safeZones() adds to them or replaces them (see $merge).
Full example
ImageSafeArea::make('Slide image', 'image') ->disk('s3') ->path('slides') ->ratios(['16:9', '4:3'], '16:9') ->defaultOpacity(50) ->safeZones([ '16:9' = [ ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 15, 'label' = 'Title and description'], ['x' = 0, 'y' = 85, 'width' = 100, 'height' = 15, 'label' = 'News slider'], ], '4:3' = [ ['x' = 0, 'y' = 0, 'width' = 100, 'height' = 20, 'label' = 'Title and description'], ['x' = 0, 'y' = 80, 'width' = 100, 'height' = 20, 'label' = 'News slider'], ], ]) ->resize(1920, 1080, 90, cropToRatio: true) ->texts([ 'hint' = 'Do not place important content in the shaded areas.', ]) ->rules('required', 'image', 'max:4096');
Inherited methods
Because the field extends Laravel\Nova\Fields\Image, you can use disk(), path(), storeAs(), store(), preview(), thumbnail(), maxWidth(), deletable(), prunable(), rules() and the other methods of the Nova File and Image fields. The field accepts only image/* files.
License
MIT