SmartCrop.java
- /*
- * #%L
- * wcm.io
- * %%
- * Copyright (C) 2022 wcm.io
- * %%
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * #L%
- */
- package io.wcm.handler.mediasource.dam.impl.dynamicmedia;
- import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
- import static com.day.cq.dam.api.DamConstants.RENDITIONS_FOLDER;
- import java.util.Arrays;
- import org.apache.sling.api.resource.Resource;
- import org.apache.sling.api.resource.ResourceResolver;
- import org.apache.sling.api.resource.ValueMap;
- import org.jetbrains.annotations.NotNull;
- import org.jetbrains.annotations.Nullable;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import com.day.cq.dam.api.Asset;
- import io.wcm.handler.media.CropDimension;
- import io.wcm.handler.media.Dimension;
- import io.wcm.handler.media.format.Ratio;
- import io.wcm.handler.mediasource.dam.AssetRendition;
- /**
- * Apply Dynamic Media Smart Cropping.
- */
- public final class SmartCrop {
- /**
- * Normalized width (double value 0..1 as percentage of original image).
- */
- public static final String PN_NORMALIZED_WIDTH = "normalizedWidth";
- /**
- * Normalized height (double value 0..1 as percentage of original image).
- */
- public static final String PN_NORMALIZED_HEIGHT = "normalizedHeight";
- /**
- * Left margin (double value 0..1 as percentage of original image).
- */
- public static final String PN_LEFT = "left";
- /**
- * Top margin (double value 0..1 as percentage of original image).
- */
- public static final String PN_TOP = "top";
- private static final double MIN_NORMALIZED_WIDTH_HEIGHT = 0.0001;
- private static final Logger log = LoggerFactory.getLogger(SmartCrop.class);
- private SmartCrop() {
- // static methods only
- }
- /**
- * Smart cropping can be applied when no manual cropping was applied, or auto cropping is enabled.
- * Additionally, combination with rotation is not allowed.
- * @param cropDimension Manual crop definition
- * @param rotation Rotation
- * @return true if Smart Cropping can be applied
- */
- public static boolean canApply(@Nullable CropDimension cropDimension, @Nullable Integer rotation) {
- return (cropDimension == null || cropDimension.isAutoCrop()) && rotation == null;
- }
- /**
- * Checks DM image profile for a smart cropping definition matching the ratio of the requested ratio.
- * @param imageProfile Image profile from DAM context (null if no is defined)
- * @param requestedRatio Requested ratio
- * @return Named dimension or null. The provided width/height can usually be ignored, because they
- * are the width/height from the image profile which only describe the aspect ratio, but not
- * any width/height values used in reality.
- */
- public static @Nullable NamedDimension getDimensionForRatio(@Nullable ImageProfile imageProfile, double requestedRatio) {
- if (imageProfile == null) {
- return null;
- }
- return imageProfile.getSmartCropDefinitions().stream()
- .filter(def -> Ratio.matches(Ratio.get(def), requestedRatio))
- .findFirst().orElse(null);
- }
- /**
- * Checks DM image profile for a smart cropping definition matching the ratio of the requested width/height.
- * @param imageProfile Image profile from DAM context (null if no is defined)
- * @param width Width
- * @param height Height
- * @return Smart cropping definition with requested width/height - or null if no match
- */
- public static @Nullable NamedDimension getDimensionForWidthHeight(@Nullable ImageProfile imageProfile, long width, long height) {
- Double requestedRatio = Ratio.get(width, height);
- NamedDimension matchingDimension = getDimensionForRatio(imageProfile, requestedRatio);
- if (matchingDimension != null) {
- // create new named dimension with actual requested width/height
- return new NamedDimension(matchingDimension.getName(), width, height);
- }
- else {
- return null;
- }
- }
- /**
- * Gets the actual smart-cropped dimension for the given asset and smart cropping definition (aspect ratio).
- * @param asset Asset
- * @param resourceResolver Resource resolver
- * @param smartCropDef Smart cropping definition from image profile
- * @return Actual dimension of the smart cropping area or null if not found
- */
- @SuppressWarnings("java:S1075") // no filesystem paths
- public static @Nullable CropDimension getCropDimensionForAsset(@NotNull Asset asset,
- @NotNull ResourceResolver resourceResolver, @NotNull NamedDimension smartCropDef) {
- // at this path smart cropping parameters may be stored for each ratio (esp. if manual cropping was applied)
- String smartCropRenditionPath = asset.getPath()
- + "/" + JCR_CONTENT
- + "/" + RENDITIONS_FOLDER
- + "/" + smartCropDef.getName()
- + "/" + JCR_CONTENT;
- Resource smartCropRendition = resourceResolver.getResource(smartCropRenditionPath);
- if (smartCropRendition == null) {
- // on AEMaaCS this path should always exist, in AEMaaCS SDK it seems to be created only when manual cropping
- // is applied in the Assets UI
- return null;
- }
- ValueMap props = smartCropRendition.getValueMap();
- double leftPercentage = props.get(PN_LEFT, 0d);
- double topPercentage = props.get(PN_TOP, 0d);
- double widthPercentage = props.get(PN_NORMALIZED_WIDTH, 0d);
- double heightPercentage = props.get(PN_NORMALIZED_HEIGHT, 0d);
- Dimension originalDimension = AssetRendition.getDimension(asset.getOriginal());
- if (originalDimension == null
- || !isValidTopLeft(leftPercentage, topPercentage)
- || !isValidWidthHeight(widthPercentage, heightPercentage)) {
- // ignore smart cropping rendition with invalid dimension
- return null;
- }
- // calculate actual cropping dimension
- long originalWidth = originalDimension.getWidth();
- long originalHeight = originalDimension.getHeight();
- long left = Math.round(originalWidth * leftPercentage);
- long top = Math.round(originalHeight * topPercentage);
- long width = Math.round(originalWidth * widthPercentage);
- long height = Math.round(originalHeight * heightPercentage);
- // it may happen that DM provides inconsistent normalizedWidth/normalizedHeight values which results
- // in renditions not matching the ratio of the cropping definition. In that case use only the one from the
- // the two which results in the smaller rendition and calculate the missing value from the other
- double expectedRatio = Ratio.get(smartCropDef.getWidth(), smartCropDef.getHeight());
- double actualRatio = Ratio.get(width, height);
- if (!Ratio.matches(expectedRatio, actualRatio)) {
- if (actualRatio > expectedRatio) {
- width = Math.round(height * expectedRatio);
- }
- else {
- height = Math.round(width / expectedRatio);
- }
- }
- return new CropDimension(left, top, width, height, true);
- }
- /**
- * Verifies that the actual image area picked in smart cropping (either automatic or manual) results in
- * a rendition size that fulfills at least the requested width/height.
- * @param asset DAM asset
- * @param resourceResolver Resource resolve
- * @param smartCropDef Smart cropping dimension
- * @param width Requested width
- * @param height Requested height
- * @return true if size is matching, or no width/height information for the cropped area is available
- */
- public static boolean isMatchingSize(@NotNull Asset asset, @NotNull ResourceResolver resourceResolver,
- @NotNull NamedDimension smartCropDef, long width, long height) {
- CropDimension cropDimension = getCropDimensionForAsset(asset, resourceResolver, smartCropDef);
- if (cropDimension == null) {
- // smart cropping rendition is not found in repository or it contains invalid values,
- // we assume the size should be fine and skip further checking
- return true;
- }
- // check if smart cropping area is large enough
- long croppedWidth = cropDimension.getWidth();
- long croppedHeight = cropDimension.getHeight();
- boolean isMatchingSize = (cropDimension.getWidth() >= width && croppedHeight >= height);
- if (!isMatchingSize) {
- log.debug("Smart cropping area '{}' for asset {} is too small ({} x {}) for requested size {} x {}.",
- smartCropDef.getName(), asset.getPath(), croppedWidth, croppedHeight, width, height);
- }
- return isMatchingSize;
- }
- private static boolean isValidTopLeft(double... numbers) {
- return Arrays.stream(numbers).allMatch(value -> value >= 0 && value <= 1);
- }
- private static boolean isValidWidthHeight(double... numbers) {
- return Arrays.stream(numbers).allMatch(value -> value >= MIN_NORMALIZED_WIDTH_HEIGHT && value <= 1);
- }
- }