From 8610cf44c87369c8f3db6aa4cb41668717c2133f Mon Sep 17 00:00:00 2001 From: Bernard Hamlin <948122+blueo@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:03:00 +1200 Subject: [PATCH] DOC Align image editor docs with house tone Tone-only follow-up to #895: removes emphasiser words, editorialising headings and em-dash asides, replaces "the platform" with "the CMS" and "no-op" with plain language, and expands the resize limits explanation. No content or behaviour changes. --- .../09_Image_Editor.md | 76 +++++++++---------- en/08_Changelogs/6.3.0.md | 4 +- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/09_Image_Editor.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/09_Image_Editor.md index 50db55d8..8fba8c29 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/09_Image_Editor.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/09_Image_Editor.md @@ -10,55 +10,58 @@ The CMS includes an image editor for making composition changes to images: crop, ![Image editor screenshot](../../_images/image-editor.png) -Applying an edit **replaces the original file in place**. The record keeps its **ID**, its folder, and its filename, so every page, block, and field already pointing at that image resolves to the edited pixels with nothing to repoint. +Applying an edit replaces the original file in place. The record keeps its ID, its folder, and its filename, so anything already pointing at the image (pages, blocks, and fields) resolves to the edited version without any change. -"Apply" stays disabled until a crop, rotate, flip or resize has actually been chosen, so a no-op edit cannot re-encode the image or leave a backup copy behind. Ticking the backup checkbox is not itself an edit. +"Apply" stays disabled until a crop, rotate, flip, or resize has been chosen, so an edit that changes nothing cannot re-encode the image or leave a backup copy behind. Ticking the backup checkbox is not itself an edit. -## Draft only - publishing is the author's job +## Draft edits -The replacement is written as a **new draft version** of the file. The editor never publishes anything, so the published version is left exactly as it was: immediately after an edit, the live site still serves the pre-edit pixels while the draft shows the new ones. Someone has to publish the image for the change to reach visitors. This is the same behaviour as the CMS's existing "replace file" action. +The replacement is written as a new draft version of the file, the same as the CMS's existing "replace file" action. The editor does not publish anything, so the published version is unchanged and the live site continues to serve the pre-edit image until the file is published. ## Backing up the original -Because the original's bytes are overwritten, the editor offers a "Back up the original image" checkbox. When it is ticked, the pre-edit bytes are copied into a **new draft file in the same folder** before the replacement is written. +Because the original's bytes are overwritten, the editor offers a "Back up the original image" checkbox. When it is ticked, the pre-edit bytes are copied into a new draft file in the same folder before the replacement is written. -The backup is a genuine copy: the record is duplicated *and* the pre-edit bytes are written into it as its own stored asset, so replacing the original cannot corrupt it. Its name comes from the platform's own de-duplicating name generator - the same one that renames any colliding upload - so `beach.jpg` is backed up as `beach-v2.jpg`, then `beach-v3.jpg`, and so on. +The record is duplicated and the pre-edit bytes are written into it as its own stored asset, so replacing the original cannot corrupt the backup. The CMS names the backup using the same de-duplication it applies to colliding uploads, so `beach.jpg` is backed up as `beach-v2.jpg`, then `beach-v3.jpg`. -When the checkbox is unticked, **no backup is made and the original file is gone**, recoverable only if [`keep_archived_assets`](/developer_guides/files/file_storage/#archived) is enabled - it is off by default. +When the checkbox is unticked, no backup is made and the original bytes are overwritten. They can only be recovered if [`keep_archived_assets`](/developer_guides/files/file_storage/#archived) is enabled, which it is not by default. ### Default state of the backup checkbox -The checkbox starts ticked. Set `backup_original_by_default` to change that - a configured `false` genuinely produces an unticked box: +The checkbox starts ticked. Set `backup_original_by_default` to `false` to start it unticked: ```yml # app/_config/image-editor.yml SilverStripe\AssetAdmin\Model\ImageEditor: - # Start the "Back up the original image" checkbox unticked backup_original_by_default: false ``` ## Resize -The editor can also resize the output. The aspect ratio is **locked** and there is no option to unlock it: the author types either a width or a height, and the other dimension is derived from the crop output's ratio. +The editor can also resize the output. The aspect ratio is locked and cannot be unlocked: enter either a width or a height, and the other dimension is derived from the aspect ratio of the cropped image. -**Upsizing is refused.** The ceiling is the crop output's dimensions - not the source's, since a 90 or 270 degree rotate swaps the axes - and the minimum is 1 pixel. The modal shows an inline message naming the actual ceiling and disables "Apply" while the value is invalid; the endpoint rejects an over-large dimension with a 400 regardless of what the UI did, so a request made outside the editor is refused too. +Resizing can only make the image smaller. Enlarging a raster image cannot add detail that is not already in it; it only stretches the existing pixels, so the editor caps the resize at the size the image already has. The smallest allowed size is 1 pixel, because a dimension of zero would leave no image to render. + +The maximum is the size of the *cropped* image, not the original source file. A crop lowers it, and a 90 or 270 degree rotate swaps width and height, so after a quarter turn each axis is capped by the opposite side of the source. For example, a 4000 x 3000 source rotated a quarter turn presents as 3000 x 4000, so its width can be resized up to 3000 rather than 4000. + +The modal shows an inline message with the current maximum and disables "Apply" while the value is invalid. The endpoint independently rejects an over-large dimension with a `400` response, so a request made outside the editor is validated too. ## Customising what gets saved -An edit makes at most two writes - the backup copy, then the replacement - and each one is bracketed by a pair of extension hooks. [Apply an extension](/developer_guides/extending/extensions/) to [`ImageEditor`](api:SilverStripe\AssetAdmin\Model\ImageEditor) to adjust what is about to be saved, or to react once it has been. +An edit makes up to two writes: the backup copy, then the replacement. Each write is bracketed by a pair of extension hooks. [Apply an extension](/developer_guides/extending/extensions/) to [`ImageEditor`](api:SilverStripe\AssetAdmin\Model\ImageEditor) to change what is saved, or to react once it has been saved. | Hook | Called | Arguments | | ---- | ------ | --------- | -| `onBeforeCreateBackup` | Once the backup holds the pre-edit bytes, immediately before it is written | The unwritten backup [`Image`](api:SilverStripe\Assets\Image), and the source it was copied from | -| `onAfterCreateBackup` | Once the backup has been written | The written backup [`Image`](api:SilverStripe\Assets\Image), and the source it was copied from | -| `onBeforeReplaceOriginal` | Once the rendered bytes are on the source, immediately before it is written | The unwritten source [`Image`](api:SilverStripe\Assets\Image), and the transforms | -| `onAfterReplaceOriginal` | Once the replacement has been written | The replaced source [`Image`](api:SilverStripe\Assets\Image), and the transforms | +| `onBeforeCreateBackup` | After the backup holds the pre-edit bytes, before it is written | The unwritten backup [`Image`](api:SilverStripe\Assets\Image), and the source it was copied from | +| `onAfterCreateBackup` | After the backup has been written | The written backup [`Image`](api:SilverStripe\Assets\Image), and the source it was copied from | +| `onBeforeReplaceOriginal` | After the rendered bytes are on the source, before it is written | The unwritten source [`Image`](api:SilverStripe\Assets\Image), and the transforms | +| `onAfterReplaceOriginal` | After the replacement has been written | The replaced source [`Image`](api:SilverStripe\Assets\Image), and the transforms | -All four are called after the editor's permission and validation checks have passed, so your extension is never handed a request that was going to be rejected. A pair is only called when the write it brackets actually happens - if the author declines the backup, neither backup hook is called. +The hooks are called only after the editor's permission and validation checks have passed. A pair is called only when the write it brackets happens, so if the author declines the backup, neither backup hook is called. -The `$transforms` argument is the validated edit the render already used: the flip flags, the rotation, the crop ratios, and the resize. It is passed for context, and changing it does not render anything again. +The `$transforms` argument is the validated edit used to render the image: the flip flags, the rotation, the crop ratios, and the resize. It is provided for context; changing it does not re-render the image. -Each `onBefore` hook is handed the record with the bytes it is about to save already on it, so `$source->getString()` returns the render. Put your own bytes back with `setFromString()` and they are what gets written - to add a watermark, for example. +Each `onBefore` hook receives the record with the bytes it is about to save already set on it, so `$source->getString()` returns the rendered image. Call `setFromString()` to replace those bytes, for example to add a watermark: ```php // app/src/Extensions/ImageEditorExtension.php @@ -85,7 +88,7 @@ SilverStripe\AssetAdmin\Model\ImageEditor: - App\Extensions\ImageEditorExtension ``` -Use the `onAfter` hooks for anything that should only happen once the write has succeeded, such as logging, notifications, or clearing a cache. They are also the only place the saved record is fully settled: the backup's name is de-duplicated *during* its write, so `onBeforeCreateBackup` still sees `bird.jpg` with an ID of `0`, while `onAfterCreateBackup` sees the `bird-v2.jpg` the author will see, with a real ID. +Use the `onAfter` hooks for anything that should happen only once the write has succeeded, such as logging, notifications, or clearing a cache. The saved record is fully resolved only at this point: the backup's name is de-duplicated during its write, so `onBeforeCreateBackup` sees `bird.jpg` with an ID of `0`, while `onAfterCreateBackup` sees the final `bird-v2.jpg` with a database ID. ```php // app/src/Extensions/ImageEditorExtension.php @@ -106,68 +109,65 @@ class ImageEditorExtension extends Extension ``` > [!WARNING] -> Avoid using these hooks to check permissions or to abort an edit. Permissions are checked before any rendering starts (see [permissions](#permissions)), and no hook can cancel a write that is already under way - if the backup has been written, it stays written regardless of what happens next. +> Do not use these hooks to check permissions or to abort an edit. Permissions are checked before rendering starts (see [permissions](#permissions)), and a hook cannot cancel a write that has already happened: once the backup is written, it stays written. ## Permissions -The "Edit image" action is only shown, and the API endpoint backing it only succeeds, when all of the following are true: +The "Edit image" action is shown, and the API endpoint behind it succeeds, only when all of the following are true: -- The record is a raster [`Image`](api:SilverStripe\Assets\Image) - SVG uploads are plain [`File`](api:SilverStripe\Assets\File) records and are excluded. -- The member can **edit** the original image, because the edit modifies its bytes. -- The member can create files in the folder the original lives in, for the backup copy. +- The record is a raster [`Image`](api:SilverStripe\Assets\Image). SVG uploads are plain [`File`](api:SilverStripe\Assets\File) records and are excluded. +- The member can edit the original image, because the edit modifies its bytes. +- The member can create files in the folder the original is in, when a backup will be written (the default). -The action on the detail view is a convenience gate only. The endpoint re-checks both permissions itself and is the real security boundary. +The action on the detail view is a convenience only. The endpoint re-checks these permissions and is the security boundary. -The endpoint applies the create check only to an edit that actually writes a backup - an edit that declines the backup asks nothing of the folder. The action on the detail view still demands the create permission unconditionally, so a member who cannot create files in the folder does not get the editor at all: they could otherwise edit an image with no backup to restore from, which the default behaviour would have written for them. +The endpoint applies the create check only to an edit that actually writes a backup; an edit that declines the backup asks nothing of the folder. The detail-view action still requires create permission in every case, so a member who cannot create files in the folder does not see the editor at all. Otherwise they could edit an image with no backup to fall back on, which the default behaviour would have written for them. ## Configuration ### Maximum source size -Sources above a configurable megapixel bound are refused before they are decoded, and the user is shown an error naming the limit - for example `Exceeds 50 megapixel (width x height) limit`. The default bound is 50 megapixels. +Sources above a configurable megapixel limit are rejected before they are decoded, and the user is shown an error naming the limit, for example `Exceeds 50 megapixel (width x height) limit`. The default is 50 megapixels. -The bound is measured in *pixels*, as `width x height`, not in file size. That is deliberate: rendering an edit decodes the source into an uncompressed bitmap of the full resolution and holds it in memory, and the size of that bitmap is determined by the pixel count alone. File size is not a usable proxy for it, because compression ratios vary enormously - a small file can decode to a bitmap large enough to exhaust the memory available to PHP, so a megabyte-based cap would not protect anything. +The limit is measured in pixels (`width x height`), not file size, because rendering an edit decodes the source into an uncompressed bitmap held in memory, and the size of that bitmap depends on the pixel count rather than the file size. Compression ratios vary widely, so a small file can decode to a bitmap large enough to exhaust PHP's memory limit. ```yml # app/_config/image-editor.yml SilverStripe\AssetAdmin\Model\ImageEditor: - # Allow sources up to 100 megapixels - make sure your memory limit can handle it max_source_megapixels: 100 ``` -Set `max_source_megapixels` to `0` to remove the bound entirely. A sufficiently large image can then exhaust the memory available to PHP. +Set `max_source_megapixels` to `0` to remove the limit. A sufficiently large image can then exhaust PHP's memory limit. ### Output quality -Edited images are encoded in the same format as the source, at the quality configured for the image backend. This is the same setting used for resampled images - see [resampled image quality](/developer_guides/files/images/#resampled-image-quality). +Edited images are encoded in the same format as the source. By default they use the quality configured for the image backend, the same setting used for resampled images (see [resampled image quality](/developer_guides/files/images/#resampled-image-quality)). Set `output_quality` to a value between 1 and 100 to give the editor its own quality, leaving resampled images on the backend's: ```yml # app/_config/image-editor.yml SilverStripe\AssetAdmin\Model\ImageEditor: - # Encode edited images at 90, whatever quality the image backend is configured with output_quality: 90 ``` -Leave it unset (or `null`) to follow the backend. The setting only applies to formats where quality is meaningful, such as JPEG and WebP. +Leave it unset (or `null`) to follow the backend. The setting applies only to formats where quality is meaningful, such as JPEG and WebP. ### Metadata and animation The edit re-encodes the image, and what survives that depends on the image backend's driver: -- **Metadata and colour profiles.** The default GD driver strips EXIF, IPTC and XMP metadata and the ICC colour profile; Imagick preserves them. +- **Metadata and colour profiles.** The default GD driver strips EXIF, IPTC, and XMP metadata and the ICC colour profile; Imagick preserves them. - **Animation.** The GD driver flattens an animated GIF to its first frame, so the edited file is a still image. ### Duplicate submission lock -While an edit is being rendered, a short-lived lock keyed on the member and the source file rejects a second edit request for the same image, so a double submission cannot apply the edit twice or leave two backup copies behind. The lock is released as soon as the edit finishes, so its time-to-live only bounds how long a crashed or abandoned request keeps the lock held. +While an edit is being rendered, a short-lived lock keyed on the member and the source file rejects a second edit request for the same image, so a double submission cannot apply the edit twice or create two backup copies. The lock is released when the edit finishes, so its time-to-live only bounds how long an abandoned request holds it. ```yml # app/_config/image-editor.yml SilverStripe\AssetAdmin\Controller\AssetAdmin: - # Give slow renders more headroom before an abandoned lock expires edit_image_lock_ttl: 60 ``` -The lock is stored in a dedicated cache service, `Psr\SimpleCache\CacheInterface.assetAdminImageEditor`. If you customise that service, it must remain a shared (not in-memory) cache so the lock is visible across requests. +The lock is stored in a dedicated cache service, `Psr\SimpleCache\CacheInterface.assetAdminImageEditor`. If you customise that service it must remain a shared (not in-memory) cache, so the lock is visible across requests. diff --git a/en/08_Changelogs/6.3.0.md b/en/08_Changelogs/6.3.0.md index 247bba89..a37c43a8 100644 --- a/en/08_Changelogs/6.3.0.md +++ b/en/08_Changelogs/6.3.0.md @@ -31,10 +31,10 @@ In the "Files" section, click an image to open its detail view, then click the " The editor is focused on image composition (where the subject sits in the frame) rather than retouching i.e. there are no brightness, colour, filter, or background removal controls. -A few things are worth knowing about how it behaves: +Note the following behaviours: - **The original file is replaced in place.** Applying an edit writes the rendered result over the origin file. The record keeps its ID, folder, and filename, so everything already using that image picks up the edited pixels with nothing to repoint. -- **The original can be backed up first.** A checkbox in the editor - ticked by default - copies the pre-edit bytes into a new draft file in the same folder before the replacement is written, named by the platform's usual de-duplication, so `beach.jpg` is backed up as `beach-v2.jpg`. Untick it and the original bytes are gone for good. +- **The original can be backed up first.** A checkbox in the editor - ticked by default - copies the pre-edit bytes into a new draft file in the same folder before the replacement is written, named by the CMS's usual de-duplication, so `beach.jpg` is backed up as `beach-v2.jpg`. Untick it and the original bytes are gone for good. - **The edit is saved as a draft.** The published version of the file is untouched, so the live site keeps serving the pre-edit image until you publish it yourself. - **Resizing keeps the aspect ratio.** Type a width or a height and the other is derived. The output can only be made smaller than the cropped image, never larger. - **Raster images only.** SVG files are not editable through the image editor.