How to implement crops in a block grid backoffice preview?

I am trying to create a preview for a hero image in the block grid in the backoffice (in lit/ts). I’d like to leverage a crop from the media library for this preview.

I tried to look in the source of Umbraco-CMS, specifically the umb-image-cropper-preview component. However I can’t seem to figure out how I can retrieve a crop’s URL with the corresponding query params by its alias.

Any help would be greatly appreciated.

Hi @tanuki

Welcome to the forum!

There isn’t a backoffice API that gives you a crop URL by alias. The backoffice imaging endpoint works on width, height and mode, not crop aliases, so the alias lookup you get from GetCropUrl server side doesn’t exist client side.

According to AI, the easiest route is to take the width and height of your crop preset and hand them to the built in thumbnail element, which requests a resized and cropped URL for you. In a block custom view (AI code):

import { html, customElement, property, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbBlockEditorCustomViewElement } from '@umbraco-cms/backoffice/block-custom-view';
import type { UmbBlockDataType } from '@umbraco-cms/backoffice/block';
import type { UmbMediaPickerPropertyValueEntry } from '@umbraco-cms/backoffice/media';

// Match these to your "hero" crop preset
const HERO_CROP = { width: 1600, height: 600 };

@customElement('hero-block-view')
export class HeroBlockView extends UmbLitElement implements UmbBlockEditorCustomViewElement {
  @property({ attribute: false })
  content?: UmbBlockDataType;

  override render() {
    const image = (this.content?.image as Array<UmbMediaPickerPropertyValueEntry> | undefined)?.[0];

    return html`
      ${image
        ? html`<umb-media-thumbnail
            .unique=${image.mediaKey}
            .width=${HERO_CROP.width}
            .height=${HERO_CROP.height}
            alt=""></umb-media-thumbnail>`
        : nothing}
      <p>${this.content?.heading}</p>
    `;
  }

  static override styles = css`
    umb-media-thumbnail {
      --umb-media-thumbnail-background: none;
    }
  `;
}

export default HeroBlockView;

A few notes:

  • umb-media-thumbnail is available from 17.6. On earlier 17.x use umb-imaging-thumbnail with the same properties.
  • Swap image for whatever your media picker alias is on the block.
  • The checkerboard background is on by default. The custom property above turns it off, which you’ll want for a hero preview. That’s also 17.6+.
  • There’s an optional mode property if you need something other than the default crop mode.

Justin

Hey Justin,

Thanks for the help. I think I didn’t describe well what the exact problem was because I didn’t include my need for the focal point as well. Although a hardcoded crop would could solve the problem for a general crop, I do lack the coordinates of the position of the crop’s focal point.

What is also a bit confusing is that when I look into the property that is being returned by the backoffice for the media item, it does return an empty crops array and a focalPoint property with value null, even when multiple crops exist and a focal point has been configured for some of those crops.

Would you know how I could get the focal point in this case?

So below is a thought experiment, no testing… :slight_smile:

So when you say crop from the media library.. are you talking global crops that you have stored on the image mediaType?

Or maybe you are meaning you have a mediaPicker in the block element, which can have localcrops?

block data should carry local crops, with the mediaKey.

{
  "image": [
    {
      "mediaKey": "3d5f...",
      "crops": [
        {
          "alias": "thumbnail",
          "width": 300,
          "height": 300,
          "coordinates": {
            "x1": 0.1,
            "y1": 0.1,
            "x2": 0.9,
            "y2": 0.9
          }
        }
      ],
      "focalPoint": {
        "left": 0.4,
        "top": 0.3
      }
    }
  ]
}

so then you’d need to fetch the media entity from the mediadetailepository to get the actual src url and then use the crop/focal point to use.

const repo = new UmbMediaDetailRepository(this);
const { data } = await repo.requestByUnique(mediaKey);
console.log(JSON.stringify(data, null, 2));

{
  "values": [
    {
      "alias": "umbracoFile",
      "value": {
        "src": "/media/....jpg",
        "focalPoint": {
          "left": 0.5,
          "top": 0.5
        },
        "crops": [
          {
            "alias": "thumbnail",
            "width": 300,
            "height": 300
          }
        ]
      }
    }
  ]
}

so then you have local or global crops and you can pass the one you want to the cropper preview.

<umb-image-cropper-preview
    .src=${src}
    .crop=${crop}
    .focalPoint=${focalPoint}>
</umb-image-cropper-preview>

However, that might not be the ui you want.. but you now have the params you need to generate..

const url =
  `${src}?crop=${crop.coordinates.x1},${crop.coordinates.y1},${crop.coordinates.x2},${crop.coordinates.y2}` +
  `&width=300&height=300`;

or

const url =
  `${src}?width=${width}&height=${height}&mode=crop` +
  `&center=${focalPoint.top},${focalPoint.left}`;

<img src=${cropUrl} />

ps there is also umb-media-thumbnail which is the suggested way to rendered images in the backoffice.. though doesn’t seem to have any crop or focalpoint support.. prob why there are no utility helpers etc …

Alternatively you could go server side..

public class CropRequest
{
    public Guid MediaKey { get; set; }
    public string CropAlias { get; set; } = string.Empty;

    public LocalCropDto? LocalCrop { get; set; }
    public FocalPointDto? FocalPoint { get; set; }
}

public class LocalCropDto
{
    public string Alias { get; set; } = string.Empty;
    public CropCoordinatesDto? Coordinates { get; set; }
}

public class CropCoordinatesDto
{
    public decimal X1 { get; set; }
    public decimal Y1 { get; set; }
    public decimal X2 { get; set; }
    public decimal Y2 { get; set; }
}

public class FocalPointDto
{
    public decimal Left { get; set; }
    public decimal Top { get; set; }
}
[HttpPost("crop-url")]
public IActionResult GetCropUrl([FromBody] CropRequest request)
{
    var media = _umbracoHelper.Media(request.MediaKey);

    if (media == null)
        return NotFound();

    // Local crop takes precedence
    if (request.LocalCrop?.Coordinates != null)
    {
        var mediaUrl = media.Url();

        var c = request.LocalCrop.Coordinates;

        var cropUrl = ImageCropperTemplateCoreExtensions.GetCropUrl(
            mediaUrl,
            width: 600,
            height: 400,
            imageCropCoordinates: new ImageCropperValue.ImageCropperCropCoordinates
            {
                X1 = c.X1,
                Y1 = c.Y1,
                X2 = c.X2,
                Y2 = c.Y2
            });

        return Ok(cropUrl);
    }

    // Fallback to normal Umbraco crop alias
    return Ok(media.GetCropUrl(request.CropAlias));
}

then lit side.. (made easier if you are using the umbraco package template and npm run generate-client which does the CRUD tasks for you)

const url = await api.getCropUrl({
    mediaKey: picker.mediaKey,
    cropAlias: "card",
    localCrop: picker.crops?.find(x => x.alias === "card"),
    focalPoint: picker.focalPoint
});

This is a utility class I’m using for this, if its useful:

import type { UmbFocalPointModel, UmbCropModel } from '@umbraco-cms/backoffice/media';

export const getCropUrl = (
    mediaUrl: string | null | undefined,
    cropName: string | null | undefined,
    cropOptions: UmbCropModel[],
    focalPoint: UmbFocalPointModel | null | undefined
): string | null => {


    if (!mediaUrl) {
        return null;
    }

    if (!cropName) {
        return mediaUrl;
    }

    const cropCoordinates = cropOptions.find((x) => x.alias === cropName);
   
    const baseQuery = `width=${cropCoordinates?.width ?? "100%"}&height=${cropCoordinates?.height ?? "100%"}`;

    if (!cropCoordinates?.coordinates && !focalPoint) {
        return `${mediaUrl}?${baseQuery}`
    }

    var imagePositionParameters = "";

    if (cropCoordinates?.coordinates) {
        imagePositionParameters = `cc=${cropCoordinates?.coordinates?.x1},${cropCoordinates?.coordinates?.y1},${cropCoordinates?.coordinates?.x2},${cropCoordinates?.coordinates?.y2}`;
    } else if (focalPoint) {
        imagePositionParameters = `rxy=${focalPoint?.left},${focalPoint?.top}`;
    }

    return `${mediaUrl}?${imagePositionParameters}&${baseQuery}`;
};

Then using it in a custom view:

import { UmbMediaUrlRepository, type UmbMediaPickerPropertyValueEntry } from '@umbraco-cms/backoffice/media';
import { getCropUrl } from '../../utilities/image-crop.utils.js';

 #mediaUrlRepository = new UmbMediaUrlRepository(this);
@property() _imageUrl?: string | null;

const image = (this.content?.image as Array<UmbMediaPickerPropertyValueEntry> | undefined)?.[0];
const imageUnique = image?.mediaKey as string | undefined;

if (imageUnique) {

    this.#mediaUrlRepository
        .requestItems([imageUnique])
        .then((response) => {
            if (response?.data) {
                const imageUrl = response?.data[0]?.url;

                if (imageUrl && image) {

                    const defaultCardCrop = 'portrait';
                    const crop = ((this.content?.imageCrop as Array<string | undefined>)?.[0]?.toLowerCase()) ?? defaultCardCrop;
                    const cropUrl = getCropUrl(imageUrl, crop, image?.crops ??  [], image?.focalPoint ?? null);

                    this._imageUrl = cropUrl;

                }
            }
        });
}

Thanks everyone, your answers have helped me a ton. I wasn’t familiar with the repositories yet but thanks to your suggestions and clear examples I can accomplish what I wanted in the backoffice preview!