Skip to content

Feat/image crop hotspot - #5452

Closed
SvenAlHamad wants to merge 9 commits into
release/6.5.0from
feat/image-crop-hotspot
Closed

Feat/image crop hotspot#5452
SvenAlHamad wants to merge 9 commits into
release/6.5.0from
feat/image-crop-hotspot

Conversation

@SvenAlHamad

@SvenAlHamad SvenAlHamad commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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 / −302

Summary

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 file field. Framing is applied
server-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

  • The file field stores only a URL — no crop, focal point, or accessibility text.
  • Cropping needs to be non-destructive (originals preserved, edits reversible).
  • Framing has to be framework-agnostic — an early CSS-based approach only worked
    in React DOM; a delivery-URL contract works everywhere.

What's included

1. Unified WebinyAsset value

A single MIME-discriminated shape shared by File Manager, CMS, and Website Builder:

{ id, src, name, type, size, image?, document?, video? }
image = { width, height, crop{top,left,bottom,right}, focalPoint{x,y}, alt, caption }

normalizeToAsset upgrades every legacy shape (flat WB file value, WebinyImageValue
with edit, mimeTypetype, hotspotfocalPoint) so existing content keeps
working with no migration.

2. First-class Headless CMS Asset field

  • fields.asset() (with .imagesOnly() / .accept([...])), implemented as an
    object field with a fixed nested schema so it reuses all object-field machinery
    (types, storage, indexing, validation, AST). Tagged wby:asset.
  • Admin: dedicated asset picker + image editor renderer (single and multi), replacing
    the generic object form; no nested-field modelling.
  • Resolved url field (read API): 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.

3. Image editor (crop + focal point + alt/caption)

  • Sanity/imgix-style model: crop is a hard rectangle; focalPoint stays in frame
    when a target aspect ratio forces further cutting.
  • Available on the File Manager file details, the CMS file field, and the CMS Asset
    input. Aspect-ratio presets + custom ratios; live preview.

4. Server-side delivery framing (@webiny/api-file-manager[-s3|-server])

  • Delivery URL params: ?crop=t,l,b,r, ?aspectRatio=16:9, ?focal=x,y, plus the
    existing ?width / ?format (incl. auto via Accept) / ?quality.
  • getVisibleRect / extractFramedRegion compute the largest aspect-ratio rect
    inside the crop, centered on the focal point; Sharp is confined to the transform
    strategies (never imported into the GraphQL handler path).
  • Asset-level crop (File Manager metadata.imageEdit) is applied automatically on any
    GET; a per-usage crop replaces it (crop ?? asset.getImageEdit()?.crop).
  • Cache key folds crop+focal+aspectRatio in while preserving the legacy crop-only
    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, and getWebinyImageSrcSet (crop/focal/format baked into
    every width; trims the ladder to a fixed CSS width).
  • Next.js renderer uses next/image + a delivery-URL loader and the shared
    dimensions helper (responsive plain-<img> fallback when dims are unknown).
  • Removed the client CSS rendering layer (WebinyImage, WebinyBackgroundImage,
    getWebinyImageProps) and the SDK geometry modules.

Backwards compatibility

  • Existing Website Builder pages: the Image element still accepts the legacy flat
    value; normalizeToAsset upgrades it at render time. No page migration.
  • Existing file fields are untouched; asset is additive.
  • Delivery: URLs without the new params behave exactly as before; the legacy
    crop-only cache key is preserved.

Testing

  • api-file-manager: 129 passing (delivery option parsing, framing signature,
    crop/frame extraction).
  • api-headless-cms: modelBuilder suite incl. Asset field + resolveAssetUrl.
  • website-builder-sdk: normalizeToAsset, getWebinyAssetUrl/getAssetCropParam,
    getWebinyImageDimensions/getWebinyImageSrcSet.
  • Full gate green: adio, oxfmt, oxlint, sync-dependencies, tsconfig sync.
  • Manually verified: admin modelling + Asset input, File Manager crop → WB insert,
    CMS Asset crop re-adjust → GraphQL, WB runtime render, deploy.

Reviewer guide

  • Value shape + normalizerwebsite-builder-sdk/src/asset/types.ts, .../normalize.ts
  • Delivery URL + srcSet corewebsite-builder-sdk/src/asset/deliveryUrl.ts, .../imageSrcSet.ts
  • Server framingapi-file-manager/src/features/assetDelivery/transformation/, api-file-manager-s3/src/assetDelivery/, api-file-manager-server/src/assetDelivery/
  • Delivery paramsapi-file-manager/src/features/assetDelivery/normalizeImageOptions.ts, .../delivery/AssetDelivery/AssetRequest.ts
  • CMS Asset field + urlapi-headless-cms/src/features/modelBuilder/fields/, .../features/graphql/fields/base/ObjectToGraphQL.ts
  • Admin editor + renderersadmin-ui/, app-file-manager/, app-headless-cms/
  • Next rendererwebsite-builder-nextjs/src/editorComponents/Image.tsx

Suggested order: value shape → delivery params/framing → cache key → CMS field +
url → SDK URL/srcSet → Next renderer → admin editor/renderers.

Out of scope / follow-ups

  • Migrate the React / Vue / Nuxt WB renderers onto the SDK core (they still use
    the legacy flat value and ignore crop today). Until then, crop/focal render on
    Next and via the CMS url field.
  • Extract a lean @webiny/image package; Angular / React Native adapters.
  • Optionally give the CMS url field arguments (url(width:, format:)).

Notes for reviewers testing locally

  • Set the PR base to release/6.5.0 (not next).
  • After pulling: rebuild the changed API + admin packages; feature-package code
    (e.g. app-headless-cms-common) must be rebuilt too — rebuilding admin-ui alone
    leaves it stale.

SvenAlHamad and others added 8 commits July 20, 2026 10:40
…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>
@SvenAlHamad
SvenAlHamad changed the base branch from next to release/6.5.0 July 21, 2026 10:04
@SvenAlHamad
SvenAlHamad requested a review from Pavel910 July 21, 2026 10:21
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>
@brunozoric brunozoric added this to the 6.5.0 milestone Jul 23, 2026
@Pavel910 Pavel910 closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants