Feat/image crop hotspot - #5452
Closed
SvenAlHamad wants to merge 9 commits into
Closed
Conversation
…ebsite Builder Non-destructive image editing (crop + hotspot + alt/caption) with aspect-ratio previews, across the stack: - website-builder-sdk: framework-agnostic geometry core + value types (WebinyImageEdit/WebinyImageValue; crop and hotspot as normalized 0-1 coordinates, resolution-independent). - admin-ui: reusable <ImageEditor> dialog on a dark editing stage - a single crop rectangle with a focal point constrained to the crop, rule-of-thirds guides, live square/4:3/16:9 previews (selectable), and alt/caption. - app-file-manager: asset-level editor in the File Details drawer, persisted on file.metadata.imageEdit. - app-website-builder: per-usage override via the file input's edit action; new placements inherit the asset-level default at pick time. - website-builder-react / -nextjs: <WebinyImage> + getWebinyImageProps render the crop + hotspot with pure CSS (SSR- and static-export-safe, no image backend required); the shipped Image component now honors the stored edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the sharp-based asset delivery so images can be served in modern formats and at a controllable quality, approaching next/image's built-in optimization - out of the box, with no external image CDN. - New query params on /files and /private asset URLs: `format` (auto | webp | avif | jpeg | png) and `quality` (1-100). Backward compatible - `width` and existing URLs are unchanged. - `format=auto` negotiates AVIF > WebP > original from the request Accept header, resolved server-side to a concrete format so cache keys stay unambiguous. The API CloudFront distribution already varies on Accept and all query strings, so no CDN change is required. - A single sharp pipeline (resize + format + quality, correct output content-type) shared by the S3 and local transform strategies. Variants are cached as before; the object-hash cache key differentiates each combination. - Per-format default quality, configurable via the asset-delivery feature params (imageQuality). - The shipped Website Builder Image component now requests `format=auto`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The File Manager crop/hotspot editor stored the edit under
file.metadata.imageEdit, but metadata is a typed CMS object field, so
GraphQL rejected the unknown key ("Field 'imageEdit' is not defined by type
'FmFile_MetadataInput'"). Add imageEdit as a free-form json() sub-field of
metadata (like exif/iptc) so both the generated input and output types
include it, and select metadata.imageEdit in the admin FILE_FIELDS so it is
read back.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grid, table, and details-drawer image previews now render the saved crop (and hotspot, for the cover thumbnails) instead of the original image. A shared CroppedFileImage measures its slot and reuses the editor's geometry to place the cropped region; files with no crop render exactly as before via a plain Image. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Saving the crop/hotspot in the drawer updated the shared cache but not the FileDetailsPresenter's in-memory file, so the preview only refreshed after closing and reopening the drawer. Add FileDetailsPresenter.setFile and call it with the updated file after a successful save so the observing drawer re-renders in place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CMS single and multi file field renderers now expose an 'Edit image' (pencil) action for image values, opening the shared editor. Since the CMS file field stores only a URL, the edit is applied at the asset level (the File's metadata.imageEdit): the file is resolved from the URL via GetFile and saved via UpdateFile. Non-image values are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The asset-delivery pipeline now bakes a file's asset-level crop into the served image, so a URL-only consumer (e.g. a Headless CMS file field) gets the cropped result with no frontend work. - The crop travels in the delivery KV metadata (MetadataWriter), not via a CMS query. A new WriteMetadataAfterUpdate handler re-writes it on file update so edits propagate. - Both asset resolvers (S3, local) read imageEdit and attach it to the Asset. - Both sharp strategies extract the crop rect (cropImageBuffer) as the base step, before resize/format/quality. - The crop is folded into the derivative cache keys (crop signature) so changing a file's crop invalidates its cached images; un-cropped keys are unchanged. Server-side applies the crop rectangle only; the hotspot remains a render-time concern (WebinyImage). Only verifiable against a deployed API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a unified WebinyAsset value plus a first-class Headless CMS Asset field (a typed GraphQL object that deprecates the File field), and non-destructive crop + focal point editing/rendering across File Manager, Headless CMS, and Website Builder. Backwards compatible: existing pages and models render unchanged via a value normalizer; nothing is migrated on disk (values upgrade on re-save). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move image framing (crop + focal + aspect ratio) from client-side CSS to the server delivery pipeline, so a framed image is delivered as pixels via a URL and works with any renderer (Next.js, React, Vue, Nuxt, and future Angular / mobile / vanilla adapters) instead of only React DOM. Delivery pipeline (@webiny/api-file-manager[-s3|-server]) - AssetRequest accepts `crop`, `aspectRatio`, and `focal`; normalizeImageOptions parses/validates them off the URL (`?crop=t,l,b,r`, `?aspectRatio=16:9`, `?focal=x,y`). - transformImage gains getVisibleRect / extractFramedRegion / hasFraming (largest aspect-ratio rect inside the crop, centered on the focal point). Sharp stays confined to the transform strategies. - getFramingSignature folds crop+focal+aspectRatio into the cache key while preserving the legacy crop-only key (no cache invalidation of existing variants). - SharpTransform + LocalSharpTransform frame via extractFramedRegion using `crop ?? asset.getImageEdit()?.crop` (per-usage crop replaces asset-level). Headless CMS (@webiny/api-headless-cms) - Asset field read type exposes a computed, read-only `url`: the stored `src` with the per-usage crop baked in as a delivery param, so headless consumers get a turnkey URL without knowing the param contract. `src` stays the pristine original. - Injected via `extend type` + a resolver in ObjectToGraphQL's ReadApi, gated on isAssetField; new dependency-free resolveAssetUrl helper (no SDK import). Website Builder (@webiny/website-builder-sdk / -react / -nextjs) - New framework-agnostic core: IMAGE_RESIZE_WIDTHS (aligned to the server ladder), getWebinyImageDimensions, and getWebinyImageSrcSet (crop/focal/format baked into every width; trims the ladder to a fixed CSS width). - getWebinyAssetUrl extended with aspectRatio + focal. - Next.js Image renderer uses next/image + a delivery-URL loader, with the shared dimensions helper and a responsive plain-<img> fallback. - Removed the client CSS rendering layer (WebinyImage, WebinyBackgroundImage, getWebinyImageProps) and the SDK geometry modules. React / Vue / Nuxt renderers still use the legacy flat value and are migrated to this contract in a fast-follow; until then crop/focal render on Next and via the CMS `url` field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Non-destructive image crop + focal point + a first-class Asset field
Base:
release/6.5.0· Head:feat/image-crop-hotspot· 103 files, +5,270 / −302Summary
Adds non-destructive image editing (crop, focal point, alt/caption) across File
Manager, Headless CMS, and Website Builder, plus a new first-class Headless
CMS Asset field to replace the bare-URL
filefield. Framing is appliedserver-side in the delivery pipeline and delivered as a URL, so it works with any
frontend (Next.js, React, Vue, Nuxt, and future Angular / mobile / vanilla) — not
just React DOM.
Nothing mutates the original upload. Edits are stored as resolution-independent
(0..1) values and applied at delivery time, so they survive re-encoding, resizing,
and CDN caching.
Why
filefield stores only a URL — no crop, focal point, or accessibility text.in React DOM; a delivery-URL contract works everywhere.
What's included
1. Unified
WebinyAssetvalueA single MIME-discriminated shape shared by File Manager, CMS, and Website Builder:
normalizeToAssetupgrades every legacy shape (flat WB file value,WebinyImageValuewith
edit,mimeType→type,hotspot→focalPoint) so existing content keepsworking with no migration.
2. First-class Headless CMS Asset field
fields.asset()(with.imagesOnly()/.accept([...])), implemented as anobjectfield with a fixed nested schema so it reuses all object-field machinery(types, storage, indexing, validation, AST). Tagged
wby:asset.the generic object form; no nested-field modelling.
urlfield (read API): the storedsrcwith the per-usage crop bakedin as a delivery param, so headless consumers get a turnkey URL without knowing the
param contract.
srcstays the pristine original.3. Image editor (crop + focal point + alt/caption)
cropis a hard rectangle;focalPointstays in framewhen a target aspect ratio forces further cutting.
input. Aspect-ratio presets + custom ratios; live preview.
4. Server-side delivery framing (
@webiny/api-file-manager[-s3|-server])?crop=t,l,b,r,?aspectRatio=16:9,?focal=x,y, plus theexisting
?width/?format(incl.autovia Accept) /?quality.getVisibleRect/extractFramedRegioncompute the largest aspect-ratio rectinside the crop, centered on the focal point; Sharp is confined to the transform
strategies (never imported into the GraphQL handler path).
metadata.imageEdit) is applied automatically on anyGET; a per-usage crop replaces it (
crop ?? asset.getImageEdit()?.crop).key — no invalidation of existing cached variants.
5. Website Builder framework-agnostic core (
@webiny/website-builder-sdk)getWebinyAssetUrl(asset, { width, format, quality, crop, aspectRatio, focal })—the one URL builder every framework uses.
IMAGE_RESIZE_WIDTHS(aligned to the server ladder → cache hits),getWebinyImageDimensions, andgetWebinyImageSrcSet(crop/focal/format baked intoevery width; trims the ladder to a fixed CSS width).
next/image+ a delivery-URL loader and the shareddimensions helper (responsive plain-
<img>fallback when dims are unknown).WebinyImage,WebinyBackgroundImage,getWebinyImageProps) and the SDK geometry modules.Backwards compatibility
value;
normalizeToAssetupgrades it at render time. No page migration.filefields are untouched;assetis additive.crop-only cache key is preserved.
Testing
crop/frame extraction).
resolveAssetUrl.normalizeToAsset,getWebinyAssetUrl/getAssetCropParam,getWebinyImageDimensions/getWebinyImageSrcSet.adio,oxfmt,oxlint,sync-dependencies, tsconfig sync.CMS Asset crop re-adjust → GraphQL, WB runtime render, deploy.
Reviewer guide
website-builder-sdk/src/asset/types.ts,.../normalize.tswebsite-builder-sdk/src/asset/deliveryUrl.ts,.../imageSrcSet.tsapi-file-manager/src/features/assetDelivery/transformation/,api-file-manager-s3/src/assetDelivery/,api-file-manager-server/src/assetDelivery/api-file-manager/src/features/assetDelivery/normalizeImageOptions.ts,.../delivery/AssetDelivery/AssetRequest.tsurl—api-headless-cms/src/features/modelBuilder/fields/,.../features/graphql/fields/base/ObjectToGraphQL.tsadmin-ui/,app-file-manager/,app-headless-cms/website-builder-nextjs/src/editorComponents/Image.tsxSuggested order: value shape → delivery params/framing → cache key → CMS field +
url→ SDK URL/srcSet → Next renderer → admin editor/renderers.Out of scope / follow-ups
the legacy flat value and ignore crop today). Until then, crop/focal render on
Next and via the CMS
urlfield.@webiny/imagepackage; Angular / React Native adapters.urlfield arguments (url(width:, format:)).Notes for reviewers testing locally
release/6.5.0(notnext).(e.g.
app-headless-cms-common) must be rebuilt too — rebuildingadmin-uialoneleaves it stale.