diff --git a/changes.xml b/changes.xml index a13f5ef3..5f3c8c64 100644 --- a/changes.xml +++ b/changes.xml @@ -23,6 +23,12 @@ xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/plugins/maven-changes-plugin/xsd/changes-1.0.0.xsd"> + + + Dynamic Media Support: Ensure smart-cropped renditions fulfill minimum size requirements. + + + Dynamic Media Support: Do not rely on Dynamic Media feature flag to detect DM capability on publish instances. In "AUTO" mode only the availability of DM metadata on a given asset is checked. diff --git a/pom.xml b/pom.xml index 41609de4..165871ac 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ io.wcm io.wcm.handler.media - 1.14.12 + 1.14.14 jar Media Handler @@ -49,7 +49,7 @@ handler/media - 2022-10-10T08:22:12Z + 2022-10-20T15:25:23Z @@ -131,7 +131,7 @@ io.wcm io.wcm.testing.aem-mock.junit5 - 5.0.0 + 5.1.0 test @@ -161,7 +161,7 @@ io.wcm io.wcm.testing.wcm-io-mock.caconfig - 1.1.0 + 1.2.0 test diff --git a/src/main/java/io/wcm/handler/media/format/MediaFormat.java b/src/main/java/io/wcm/handler/media/format/MediaFormat.java index 30200375..f3c8ebf1 100644 --- a/src/main/java/io/wcm/handler/media/format/MediaFormat.java +++ b/src/main/java/io/wcm/handler/media/format/MediaFormat.java @@ -213,7 +213,7 @@ public double getRatioHeightAsDouble() { /** * Returns the ratio defined in the media format definition. - * If no ratio is defined an the media format has a fixed with/height it is calculated automatically. + * If no ratio is defined an the media format has a fixed width/height it is calculated automatically. * Otherwise 0 is returned. * @return Ratio */ @@ -505,7 +505,7 @@ String getCombinedTitle() { List extParts = new ArrayList<>(); - // with/height restrictions + // width/height restrictions if (minWidthHeight != 0) { extParts.add("min. " + minWidthHeight + "px width/height"); } diff --git a/src/main/java/io/wcm/handler/media/impl/DummyImageServlet.java b/src/main/java/io/wcm/handler/media/impl/DummyImageServlet.java index 11cd9238..b9bc22f5 100644 --- a/src/main/java/io/wcm/handler/media/impl/DummyImageServlet.java +++ b/src/main/java/io/wcm/handler/media/impl/DummyImageServlet.java @@ -80,7 +80,7 @@ protected Layer createLayer(ImageContext ctx) throws RepositoryException, IOExce int height = parser.get(SUFFIX_HEIGHT, 0); String name = parser.get(SUFFIX_MEDIA_FORMAT_NAME, String.class); - // validate with/height + // validate width/height if (width < 1 || height < 1) { return new Layer(1, 1, null); } diff --git a/src/main/java/io/wcm/handler/mediasource/dam/AssetRendition.java b/src/main/java/io/wcm/handler/mediasource/dam/AssetRendition.java index 17fadec6..ddbeb91d 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/AssetRendition.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/AssetRendition.java @@ -94,7 +94,7 @@ private AssetRendition() { *

* @param rendition Rendition * @param suppressLogWarningNoRenditionsMetadata If set to true, no log warnings is generated when - * renditions metadata containing the with/height of the rendition does not exist (yet). + * renditions metadata containing the width/height of the rendition does not exist (yet). * @return Dimension or null if dimension could not be detected, not even in fallback mode */ public static @Nullable Dimension getDimension(@NotNull Rendition rendition, @@ -172,7 +172,7 @@ private static long getAssetMetadataValueAsLong(Asset asset, String... propertyN * Fallback: Read dimension by loading image binary into memory. * @param rendition Rendition * @param suppressLogWarningNoRenditionsMetadata If set to true, no log warnings is generated when - * renditions metadata containing the with/height of the rendition does not exist (yet). + * renditions metadata containing the width/height of the rendition does not exist (yet). * @return Dimension or null */ @SuppressWarnings("PMD.GuardLogStatement") @@ -202,7 +202,7 @@ private static long getAssetMetadataValueAsLong(Asset asset, String... propertyN } /** - * Convert with/height to dimension + * Convert width/height to dimension * @param width Width * @param height Height * @return Dimension or null if width or height are not valid diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamAsset.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamAsset.java index 1b56a6b3..6dc10965 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamAsset.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamAsset.java @@ -70,7 +70,7 @@ public DamAsset(Media media, com.day.cq.dam.api.Asset damAsset, MediaHandlerConf this.cropDimension = media.getCropDimension(); this.rotation = media.getRotation(); this.defaultMediaArgs = media.getMediaRequest().getMediaArgs(); - this.damContext = new DamContext(damAsset, defaultMediaArgs.getUrlMode(), mediaHandlerConfig, + this.damContext = new DamContext(damAsset, defaultMediaArgs, mediaHandlerConfig, dynamicMediaSupportService, adaptable); } diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamContext.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamContext.java index f15fa0ab..5b35f551 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamContext.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamContext.java @@ -23,7 +23,10 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.adapter.Adaptable; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,11 +34,11 @@ import com.day.cq.dam.scene7.api.constants.Scene7Constants; import io.wcm.handler.media.Dimension; +import io.wcm.handler.media.MediaArgs; import io.wcm.handler.media.spi.MediaHandlerConfig; import io.wcm.handler.mediasource.dam.impl.dynamicmedia.DynamicMediaSupportService; import io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfile; import io.wcm.handler.mediasource.dam.impl.dynamicmedia.NamedDimension; -import io.wcm.handler.url.UrlMode; /** * Context objects require in DAM support implementation. @@ -43,7 +46,7 @@ public final class DamContext implements Adaptable { private final Asset asset; - private final UrlMode urlMode; + private final MediaArgs mediaArgs; private final MediaHandlerConfig mediaHandlerConfig; private final DynamicMediaSupportService dynamicMediaSupportService; private final Adaptable adaptable; @@ -62,15 +65,15 @@ public final class DamContext implements Adaptable { /** * @param asset DAM asset - * @param urlMode urlMode + * @param mediaArgs Media Args from media request * @param mediaHandlerConfig Media handler config * @param dynamicMediaSupportService Dynamic media support service * @param adaptable Adaptable from current context */ - public DamContext(@NotNull Asset asset, @Nullable UrlMode urlMode, @NotNull MediaHandlerConfig mediaHandlerConfig, + public DamContext(@NotNull Asset asset, @NotNull MediaArgs mediaArgs, @NotNull MediaHandlerConfig mediaHandlerConfig, @NotNull DynamicMediaSupportService dynamicMediaSupportService, @NotNull Adaptable adaptable) { this.asset = asset; - this.urlMode = urlMode; + this.mediaArgs = mediaArgs; this.mediaHandlerConfig = mediaHandlerConfig; this.dynamicMediaSupportService = dynamicMediaSupportService; this.adaptable = adaptable; @@ -94,8 +97,12 @@ public MediaHandlerConfig getMediaHandlerConfig() { * @return Whether dynamic media is enabled on this AEM instance */ public boolean isDynamicMediaEnabled() { + // check that DM is not disabled globally return dynamicMediaSupportService.isDynamicMediaEnabled() - && dynamicMediaSupportService.isDynamicMediaCapabilityEnabled(isDynamicMediaAsset()); + // check that DM capability is enabled for the given asset + && dynamicMediaSupportService.isDynamicMediaCapabilityEnabled(isDynamicMediaAsset()) + // ensure DM is not disabled within MediaArgs for this media request + && !mediaArgs.isDynamicMediaDisabled(); } /** @@ -128,7 +135,7 @@ public boolean isDynamicMediaAsset() { */ public @Nullable String getDynamicMediaServerUrl() { if (dynamicMediaServerUrl == null) { - dynamicMediaServerUrl = dynamicMediaSupportService.getDynamicMediaServerUrl(asset, urlMode, adaptable); + dynamicMediaServerUrl = dynamicMediaSupportService.getDynamicMediaServerUrl(asset, mediaArgs.getUrlMode(), adaptable); } return dynamicMediaServerUrl; } @@ -162,6 +169,21 @@ public boolean isDynamicMediaAsset() { } } + /** + * @return Resource resolver from current context + */ + public @NotNull ResourceResolver getResourceResolver() { + if (adaptable instanceof Resource) { + return ((Resource)adaptable).getResourceResolver(); + } + else if (adaptable instanceof SlingHttpServletRequest) { + return ((SlingHttpServletRequest)adaptable).getResourceResolver(); + } + else { + throw new IllegalStateException("Adaptable is neither Resoucre nor SlingHttpServletRequest"); + } + } + @Override public @Nullable AdapterType adaptTo(@NotNull Class type) { return adaptable.adaptTo(type); diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamRendition.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamRendition.java index a28c049b..b6cb72e9 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamRendition.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamRendition.java @@ -27,6 +27,7 @@ import org.apache.sling.api.resource.Resource; 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; @@ -113,42 +114,55 @@ class DamRendition extends SlingAdaptable implements Rendition { @Override public String getUrl() { - if (this.rendition == null) { + if (rendition == null) { return null; } String url = null; - boolean dynamicMediaEnabled = damContext.isDynamicMediaEnabled() && !mediaArgs.isDynamicMediaDisabled(); - if (dynamicMediaEnabled && damContext.isDynamicMediaAsset()) { - // if DM is enabled: try to get rendition URL from dynamic media - String dynamicMediaPath = this.rendition.getDynamicMediaPath(this.mediaArgs.isContentDispositionAttachment(), damContext); - String productionAssetUrl = damContext.getDynamicMediaServerUrl(); - if (productionAssetUrl != null) { - url = productionAssetUrl + dynamicMediaPath; + if (damContext.isDynamicMediaEnabled()) { + if (damContext.isDynamicMediaAsset()) { + url = buildDynamicMediaUrl(); + if (url == null) { + // asset is valid DM asset, but no valid rendition could be generated + // reason might be that the smart-cropped rendition was too small for the requested size + return null; + } } - } - if (url == null) { - if (dynamicMediaEnabled) { + else { + // DM is enabled, but given asset is not a DM asset if (damContext.isDynamicMediaAemFallbackDisabled()) { - if (log.isWarnEnabled()) { - log.warn("Asset is not a valid DM asset, fallback disabled, rendition invalid: {}", this.rendition.getRendition().getPath()); - } + log.warn("Asset is not a valid DM asset, fallback disabled, rendition invalid: {}", rendition.getRendition().getPath()); return null; } else { - if (log.isTraceEnabled()) { - log.trace("Asset is not a valid DM asset, fallback to AEM-rendered rendition: {}", this.rendition.getRendition().getPath()); - } + log.trace("Asset is not a valid DM asset, fallback to AEM-rendered rendition: {}", rendition.getRendition().getPath()); } } + } + if (url == null) { // Render renditions in AEM: build externalized URL UrlHandler urlHandler = AdaptTo.notNull(damContext, UrlHandler.class); - String mediaPath = this.rendition.getMediaPath(this.mediaArgs.isContentDispositionAttachment()); - url = urlHandler.get(mediaPath).urlMode(this.mediaArgs.getUrlMode()) - .buildExternalResourceUrl(this.rendition.adaptTo(Resource.class)); + String mediaPath = rendition.getMediaPath(mediaArgs.isContentDispositionAttachment()); + url = urlHandler.get(mediaPath).urlMode(mediaArgs.getUrlMode()) + .buildExternalResourceUrl(rendition.adaptTo(Resource.class)); } return url; } + /** + * Build DM URL for this rendition based on the calculated DM path and the configured DM hostname. + * @return DM URL or null if either DM path or configured DM hostname is null + */ + private @Nullable String buildDynamicMediaUrl() { + String dynamicMediaPath = rendition.getDynamicMediaPath(mediaArgs.isContentDispositionAttachment(), damContext); + String productionAssetUrl = damContext.getDynamicMediaServerUrl(); + if (dynamicMediaPath != null && productionAssetUrl != null) { + return productionAssetUrl + dynamicMediaPath; + } + else { + return null; + } + } + @Override public String getPath() { if (this.rendition != null) { diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamUriTemplate.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamUriTemplate.java index d73d876f..355c07cb 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/DamUriTemplate.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/DamUriTemplate.java @@ -52,7 +52,7 @@ class DamUriTemplate implements UriTemplate { private static String buildUriTemplate(@NotNull UriTemplateType type, @NotNull DamContext damContext, @NotNull MediaArgs mediaArgs) { String url = null; - if (!mediaArgs.isDynamicMediaDisabled() && damContext.isDynamicMediaEnabled() && damContext.isDynamicMediaAsset()) { + if (damContext.isDynamicMediaEnabled() && damContext.isDynamicMediaAsset()) { // if DM is enabled: try to get rendition URL from dynamic media String productionAssetUrl = damContext.getDynamicMediaServerUrl(); if (productionAssetUrl != null) { diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/DefaultRenditionHandler.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/DefaultRenditionHandler.java index 4c004d0e..a7d7d4c9 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/DefaultRenditionHandler.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/DefaultRenditionHandler.java @@ -333,7 +333,7 @@ else if (!candidates.isEmpty()) { */ private RenditionMetadata getVirtualRendition(final Set candidates, MediaArgs mediaArgs) { - // get from fixed with/height + // get from fixed width/height if (mediaArgs.getFixedWidth() > 0 || mediaArgs.getFixedHeight() > 0) { long destWidth = mediaArgs.getFixedWidth(); long destHeight = mediaArgs.getFixedHeight(); diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/RenditionMetadata.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/RenditionMetadata.java index 3d747070..c2d36470 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/RenditionMetadata.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/RenditionMetadata.java @@ -28,6 +28,7 @@ import org.apache.sling.api.adapter.SlingAdaptable; import org.apache.sling.api.resource.Resource; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import com.day.cq.dam.api.Rendition; import com.day.image.Layer; @@ -196,7 +197,7 @@ else if (MediaFileType.isBrowserImage(getFileExtension()) || !MediaFileType.isIm * @param damContext DAM context * @return Dynamic media path part or null if dynamic media not supported for this rendition */ - public @NotNull String getDynamicMediaPath(boolean contentDispositionAttachment, DamContext damContext) { + public @Nullable String getDynamicMediaPath(boolean contentDispositionAttachment, DamContext damContext) { if (contentDispositionAttachment) { // serve static content from dynamic media for content disposition attachment return DynamicMediaPath.buildContent(damContext, true); @@ -316,7 +317,7 @@ else if (otherIsOriginalRendition && !thisIsOriginalRendition) { String thisPath = getRendition().getPath(); String otherPath = obj.getRendition().getPath(); if (!StringUtils.equals(thisPath, otherPath)) { - // same with/height - compare paths as last resort + // same width/height - compare paths as last resort return thisPath.compareTo(otherPath); } else { diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualRenditionMetadata.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualRenditionMetadata.java index 01786282..4b0a924f 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualRenditionMetadata.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualRenditionMetadata.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import com.day.cq.dam.api.Rendition; import com.day.image.Layer; @@ -88,7 +89,7 @@ public long getHeight() { } @Override - public @NotNull String getDynamicMediaPath(boolean contentDispositionAttachment, DamContext damContext) { + public @Nullable String getDynamicMediaPath(boolean contentDispositionAttachment, DamContext damContext) { if (contentDispositionAttachment) { // serve static content from dynamic media for content disposition attachment return DynamicMediaPath.buildContent(damContext, true); diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualTransformedRenditionMetadata.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualTransformedRenditionMetadata.java index 693b9867..fb31e2c5 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualTransformedRenditionMetadata.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/VirtualTransformedRenditionMetadata.java @@ -24,6 +24,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import com.day.cq.dam.api.Rendition; import com.day.image.Layer; @@ -93,7 +94,7 @@ public Integer getRotation() { } @Override - public @NotNull String getDynamicMediaPath(boolean contentDispositionAttachment, DamContext damContext) { + public @Nullable String getDynamicMediaPath(boolean contentDispositionAttachment, DamContext damContext) { // render virtual rendition with dynamic media (we ignore contentDispositionAttachment here) return DynamicMediaPath.buildImage(damContext, getWidth(), getHeight(), this.cropDimension, this.rotation); } diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPath.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPath.java index 756f8532..09e8eff0 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPath.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPath.java @@ -22,7 +22,6 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; @@ -94,7 +93,7 @@ private DynamicMediaPath() { * @param height Height * @return Media path */ - public static @NotNull String buildImage(@NotNull DamContext damContext, long width, long height) { + public static @Nullable String buildImage(@NotNull DamContext damContext, long width, long height) { return buildImage(damContext, width, height, null, null); } @@ -107,7 +106,7 @@ private DynamicMediaPath() { * @param rotation Rotation * @return Media path */ - public static @NotNull String buildImage(@NotNull DamContext damContext, long width, long height, + public static @Nullable String buildImage(@NotNull DamContext damContext, long width, long height, @Nullable CropDimension cropDimension, @Nullable Integer rotation) { Dimension dimension = calcWidthHeight(damContext, width, height); @@ -115,11 +114,16 @@ private DynamicMediaPath() { result.append(IMAGE_SERVER_PATH).append(encodeDynamicMediaObject(damContext)); // check for smart cropping when no cropping was applied by default, or auto-crop is enabled - if ((cropDimension == null || cropDimension.isAutoCrop()) && rotation == null) { + if (SmartCrop.canApply(cropDimension, rotation)) { // check for matching image profile and use predefined cropping preset if match found - Optional smartCroppingDef = getSmartCropDimension(damContext, width, height); - if (smartCroppingDef.isPresent()) { - result.append("%3A").append(smartCroppingDef.get().getName()).append("?") + NamedDimension smartCropDef = SmartCrop.getDimension(damContext.getImageProfile(), width, height); + if (smartCropDef != null) { + if (!SmartCrop.isMatchingSize(damContext.getAsset(), damContext.getResourceResolver(), smartCropDef, width, height)) { + // smart crop should be applied, but selected area is too small, treat as invalid + logResult(damContext, ""); + return null; + } + result.append("%3A").append(smartCropDef.getName()).append("?") .append("wid=").append(dimension.getWidth()).append("&") .append("hei=").append(dimension.getHeight()).append("&") // cropping/width/height is pre-calculated to fit with original ratio, make sure there are no 1px background lines visible @@ -144,7 +148,7 @@ private DynamicMediaPath() { return result.toString(); } - private static void logResult(@NotNull DamContext damContext, @NotNull StringBuilder result) { + private static void logResult(@NotNull DamContext damContext, @NotNull CharSequence result) { if (log.isTraceEnabled()) { log.trace("Build dynamic media path for {}: {}", damContext.getAsset().getPath(), result); } @@ -174,18 +178,6 @@ private static Dimension calcWidthHeight(@NotNull DamContext damContext, long wi return new Dimension(width, height); } - private static Optional<@NotNull NamedDimension> getSmartCropDimension(@NotNull DamContext damContext, long width, long height) { - Double requestedRatio = Ratio.get(width, height); - ImageProfile imageProfile = damContext.getImageProfile(); - if (imageProfile == null) { - return Optional.empty(); - } - Optional matchingDimension = imageProfile.getSmartCropDefinitions().stream() - .filter(def -> Ratio.matches(Ratio.get(def), requestedRatio)) - .findFirst(); - return matchingDimension.map(def -> new NamedDimension(def.getName(), width, height)); - } - /** * Splits dynamic media folder and file name and URL-encodes them separately (may contain spaces or special chars). * @param damContext DAM context diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/ImageProfileImpl.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/ImageProfileImpl.java index af042967..df42fa1e 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/ImageProfileImpl.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/ImageProfileImpl.java @@ -30,11 +30,22 @@ /** * Wraps access to dynamic media image profile. */ -final class ImageProfileImpl implements ImageProfile { +public final class ImageProfileImpl implements ImageProfile { - static final String PN_CROP_TYPE = "crop_type"; - static final String CROP_TYPE_SMART = "crop_smart"; - static final String PN_BANNER = "banner"; + /** + * Crop type + */ + public static final String PN_CROP_TYPE = "crop_type"; + + /** + * Smart cropping crop type. + */ + public static final String CROP_TYPE_SMART = "crop_smart"; + + /** + * Banner property with string like: Crop-1,100,60|Crop-2,50,30 + */ + public static final String PN_BANNER = "banner"; private final List smartCropDefinitions; diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/SmartCrop.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/SmartCrop.java new file mode 100644 index 00000000..5d3f0900 --- /dev/null +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/SmartCrop.java @@ -0,0 +1,144 @@ +/* + * #%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 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"; + + 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 + */ + 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 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 + */ + static @Nullable NamedDimension getDimension(@Nullable ImageProfile imageProfile, long width, long height) { + if (imageProfile == null) { + return null; + } + Double requestedRatio = Ratio.get(width, height); + NamedDimension matchingDimension = imageProfile.getSmartCropDefinitions().stream() + .filter(def -> Ratio.matches(Ratio.get(def), requestedRatio)) + .findFirst().orElse(null); + if (matchingDimension != null) { + // create new named dimension with actual requested width/height + return new NamedDimension(matchingDimension.getName(), width, height); + } + else { + return null; + } + } + + /** + * 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 + */ + @SuppressWarnings("java:S1075") // no filesystem paths + static boolean isMatchingSize(@NotNull Asset asset, @NotNull ResourceResolver resourceResolver, + @NotNull NamedDimension smartCropDef, long width, long height) { + // 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) { + // if this rendition is not found in repository, we assume the size should be fine + // 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 true; + } + ValueMap props = smartCropRendition.getValueMap(); + double normalizedWidth = props.get(PN_NORMALIZED_WIDTH, 0d); + double normalizedHeight = props.get(PN_NORMALIZED_HEIGHT, 0d); + Dimension originalDimension = AssetRendition.getDimension(asset.getOriginal()); + if (normalizedWidth < MIN_NORMALIZED_WIDTH_HEIGHT || normalizedHeight < MIN_NORMALIZED_WIDTH_HEIGHT + || originalDimension == null) { + // skip further validation if dimensions are not found + return true; + } + + // check if smart cropping area is large enough + long croppedWidth = Math.round(originalDimension.getWidth() * normalizedWidth); + long croppedHeight = Math.round(originalDimension.getHeight() * normalizedHeight); + boolean isMatchingSize = (croppedWidth >= 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; + } + +} diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataGenerator.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataGenerator.java index 39260f4a..53b6b9c7 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataGenerator.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataGenerator.java @@ -278,7 +278,7 @@ public boolean renditionRemoved(String renditionPath) throws PersistenceExceptio } /** - * Get dimension (with/height) of rendition. + * Get dimension (width/height) of rendition. * @param renditionResource Rendition * @return Dimension or null if it could not be detected */ diff --git a/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataNameConstants.java b/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataNameConstants.java index b18845d8..8ed31e30 100644 --- a/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataNameConstants.java +++ b/src/main/java/io/wcm/handler/mediasource/dam/impl/metadata/RenditionMetadataNameConstants.java @@ -30,7 +30,7 @@ public final class RenditionMetadataNameConstants { public static final String NN_RENDITIONS_METADATA = "renditionsMetadata"; /** - * Property for image with in pixels + * Property for image width in pixels */ public static final String PN_IMAGE_WIDTH = "imageWidth"; diff --git a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplEnd2EndDynamicMediaSmartCropTest.java b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplEnd2EndDynamicMediaSmartCropTest.java new file mode 100644 index 00000000..ecb94b77 --- /dev/null +++ b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplEnd2EndDynamicMediaSmartCropTest.java @@ -0,0 +1,124 @@ +/* + * #%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.media.impl; + +import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT; +import static com.day.cq.dam.api.DamConstants.RENDITIONS_FOLDER; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.CROP_TYPE_SMART; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.PN_BANNER; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.PN_CROP_TYPE; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_HEIGHT; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_WIDTH; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.apache.sling.api.resource.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.day.cq.dam.api.Asset; +import com.day.cq.dam.api.DamConstants; +import com.day.cq.dam.scene7.api.constants.Scene7Constants; +import com.google.common.collect.ImmutableList; + +import io.wcm.handler.media.Media; +import io.wcm.handler.media.MediaArgs.PictureSource; +import io.wcm.handler.media.MediaHandler; +import io.wcm.handler.media.MediaInvalidReason; +import io.wcm.handler.media.Rendition; +import io.wcm.handler.media.testcontext.AppAemContext; +import io.wcm.handler.media.testcontext.DummyMediaFormats; +import io.wcm.sling.commons.adapter.AdaptTo; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; +import io.wcm.wcm.commons.contenttype.ContentType; + +/** + * Test media handling with Dynamic Media and Smart Cropping end-2-end. + */ +@ExtendWith(AemContextExtension.class) +class MediaHandlerImplEnd2EndDynamicMediaSmartCropTest { + + private final AemContext context = AppAemContext.newAemContext(); + + private Asset asset; + private MediaHandler mediaHandler; + + @BeforeEach + void setUp() { + Resource profile1 = context.create().resource("/conf/global/settings/dam/adminui-extension/imageprofile/profile1", + PN_CROP_TYPE, CROP_TYPE_SMART, + PN_BANNER, "16-10,16,10|4-3,40,30"); + + Resource assetFolder = context.create().resource("/content/dam/folder1"); + context.create().resource(assetFolder, JCR_CONTENT, DamConstants.IMAGE_PROFILE, profile1.getPath()); + + asset = context.create().asset(assetFolder.getPath() + "/test.jpg", 160, 100, ContentType.JPEG, + Scene7Constants.PN_S7_FILE, "DummyFolder/test"); + context.create().assetRenditionWebEnabled(asset, 128, 80); // simulate web rendition that is a bit smaller + + // original asset size is 160x100px + // the 4-3 smart crop rendition defines a cropping area of 80x60px + String smartCropRenditionPath = asset.getPath() + "/" + JCR_CONTENT + "/" + RENDITIONS_FOLDER + + "/4-3/" + JCR_CONTENT; + context.create().resource(smartCropRenditionPath, + PN_NORMALIZED_WIDTH, 0.5d, + PN_NORMALIZED_HEIGHT, 0.6d); + + mediaHandler = AdaptTo.notNull(context.request(), MediaHandler.class); + } + + @Test + void testValidSmartCroppedRendition() { + Media media = getMediaWithWidths(80, 40); + assertTrue(media.isValid()); + + List renditions = ImmutableList.copyOf(media.getRenditions()); + assertEquals(2, renditions.size()); + assertEquals("https://dummy.scene7.com/is/image/DummyFolder/test%3A4-3?wid=80&hei=60&fit=stretch", renditions.get(0).getUrl()); + assertEquals("https://dummy.scene7.com/is/image/DummyFolder/test%3A4-3?wid=40&hei=30&fit=stretch", renditions.get(1).getUrl()); + } + + @Test + void testInvalidSmartCroppedRendition() { + Media media = getMediaWithWidths(100); + assertFalse(media.isValid()); + assertEquals(MediaInvalidReason.NO_MATCHING_RENDITION, media.getMediaInvalidReason()); + } + + @Test + void testSomeInvalidSmartCroppedRendition() { + Media media = getMediaWithWidths(100, 80, 40); + assertFalse(media.isValid()); + assertEquals(MediaInvalidReason.NOT_ENOUGH_MATCHING_RENDITIONS, media.getMediaInvalidReason()); + } + + private Media getMediaWithWidths(long... widths) { + return mediaHandler.get(asset.getPath()) + .pictureSource(new PictureSource(DummyMediaFormats.RATIO2).widths(widths)) + .autoCrop(true) + .build(); + } + +} diff --git a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndDynamicMediaNoFallbackTest.java b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndDynamicMediaNoFallbackTest.java index b5751f58..8bd87e61 100644 --- a/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndDynamicMediaNoFallbackTest.java +++ b/src/test/java/io/wcm/handler/media/impl/MediaHandlerImplImageFileTypesEnd2EndDynamicMediaNoFallbackTest.java @@ -36,9 +36,7 @@ /** * Executes the same "end-to-end" as {@link MediaHandlerImplImageFileTypesEnd2EndTest}, but - * with rendering via dynamic media. As for some cases that are not suited for dynamic media - * standard media handling delivery is used, this method overrides only the test cases where - * scene7 is actually used. + * with rendering via dynamic media. The fallback to AEM-rendered renditions is disabled. */ @ExtendWith(AemContextExtension.class) class MediaHandlerImplImageFileTypesEnd2EndDynamicMediaNoFallbackTest extends MediaHandlerImplImageFileTypesEnd2EndTest { diff --git a/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPathTest.java b/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPathTest.java index 0d0bdd64..c64562ed 100644 --- a/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPathTest.java +++ b/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/DynamicMediaPathTest.java @@ -20,10 +20,14 @@ 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 static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.CROP_TYPE_SMART; import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.PN_BANNER; import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.PN_CROP_TYPE; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_HEIGHT; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_WIDTH; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import org.apache.sling.api.resource.Resource; import org.junit.jupiter.api.BeforeEach; @@ -35,6 +39,7 @@ import com.day.cq.dam.scene7.api.constants.Scene7Constants; import io.wcm.handler.media.CropDimension; +import io.wcm.handler.media.MediaArgs; import io.wcm.handler.media.spi.MediaHandlerConfig; import io.wcm.handler.media.testcontext.AppAemContext; import io.wcm.handler.mediasource.dam.impl.DamContext; @@ -52,6 +57,7 @@ class DynamicMediaPathTest { private MediaHandlerConfig mediaHandlerConfig; private DynamicMediaSupportService dynamicMediaSupportService; private Resource assetFolder; + private Asset asset; @BeforeEach void setUp() { @@ -65,9 +71,9 @@ void setUp() { assetFolder = context.create().resource("/content/dam/folder1"); context.create().resource(assetFolder, JCR_CONTENT, DamConstants.IMAGE_PROFILE, profile1.getPath()); - Asset asset = context.create().asset(assetFolder.getPath() + "/test.jpg", 50, 30, ContentType.JPEG, + asset = context.create().asset(assetFolder.getPath() + "/test.jpg", 50, 30, ContentType.JPEG, Scene7Constants.PN_S7_FILE, "DummyFolder/test"); - damContext = new DamContext(asset, null, mediaHandlerConfig, + damContext = new DamContext(asset, new MediaArgs(), mediaHandlerConfig, dynamicMediaSupportService, context.request()); } @@ -83,6 +89,17 @@ void testWidthHeight_ImplicitSmartCrop() { assertEquals("/is/image/DummyFolder/test%3ACrop-1?wid=30&hei=20&fit=stretch", result); } + @Test + void testWidthHeight_ImplicitSmartCrop_CroppingAreaTooSmall() { + String smartCropRenditionPath = asset.getPath() + "/" + JCR_CONTENT + "/" + RENDITIONS_FOLDER + + "/Crop-1/" + JCR_CONTENT; + context.create().resource(smartCropRenditionPath, + PN_NORMALIZED_WIDTH, 0.5d, + PN_NORMALIZED_HEIGHT, 0.5666d); + + assertNull(DynamicMediaPath.buildImage(damContext, 30, 20)); + } + @Test void testCrop() { String result = DynamicMediaPath.buildImage(damContext, 30, 20, new CropDimension(5, 2, 10, 8), null); @@ -147,7 +164,7 @@ void testBuildContent_Download() { void testBuildImage_SpecialChars() { Asset assetSpecialChars = context.create().asset(assetFolder.getPath() + "/test with spaces äöü߀.jpg", 50, 30, ContentType.JPEG, Scene7Constants.PN_S7_FILE, "DummyFolder/test with spaces äöü߀"); - damContext = new DamContext(assetSpecialChars, null, mediaHandlerConfig, + damContext = new DamContext(assetSpecialChars, new MediaArgs(), mediaHandlerConfig, dynamicMediaSupportService, context.request()); String result = DynamicMediaPath.buildContent(damContext, false); diff --git a/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/SmartCropTest.java b/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/SmartCropTest.java new file mode 100644 index 00000000..a581e0fc --- /dev/null +++ b/src/test/java/io/wcm/handler/mediasource/dam/impl/dynamicmedia/SmartCropTest.java @@ -0,0 +1,139 @@ +/* + * #%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 static io.wcm.handler.media.testcontext.AppAemContext.DAM_PATH; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.CROP_TYPE_SMART; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.PN_BANNER; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.ImageProfileImpl.PN_CROP_TYPE; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_HEIGHT; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.PN_NORMALIZED_WIDTH; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.canApply; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.getDimension; +import static io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop.isMatchingSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.day.cq.dam.api.Asset; + +import io.wcm.handler.media.CropDimension; +import io.wcm.handler.media.testcontext.AppAemContext; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; +import io.wcm.wcm.commons.contenttype.ContentType; + +@ExtendWith(AemContextExtension.class) +class SmartCropTest { + + private final AemContext context = AppAemContext.newAemContext(); + + private Asset asset; + private NamedDimension dimension16_10; + + @BeforeEach + void setUp() { + asset = context.create().asset(DAM_PATH + "/image1.jpg", 160, 100, ContentType.JPEG); + dimension16_10 = new NamedDimension("16-10", 16, 10); + } + + @Test + void testCanApply() { + assertTrue(canApply(null, null)); + assertTrue(canApply(new CropDimension(0, 0, 10, 10, true), null)); + + assertFalse(canApply(null, 90)); + assertFalse(canApply(new CropDimension(0, 0, 10, 10, true), 90)); + assertFalse(canApply(new CropDimension(0, 0, 10, 10, false), null)); + } + + @Test + void testGetDimension() { + ImageProfile profile1 = new ImageProfileImpl( + context.create().resource("/conf/global/settings/dam/adminui-extension/imageprofile/profile1", + PN_CROP_TYPE, CROP_TYPE_SMART, + PN_BANNER, "16-10,160,100|4-3,40,30")); + + assertNull(getDimension(profile1, 500, 500)); + assertNamedDimension(getDimension(profile1, 320, 200), "16-10", 320, 200); + assertNamedDimension(getDimension(profile1, 400, 300), "4-3", 400, 300); + } + + @Test + void testGetDimension_NoProfile() { + assertNull(getDimension(null, 100, 100)); + } + + @Test + void testIsMatchingSize_NoRenditionResource() { + // assume everything is ok if no "16-10" rendition exists (we have no other chance) + assertTrue(isMatchingSize(asset, context.resourceResolver(), dimension16_10, 80, 50)); + } + + @Test + void testIsMatchingSize_MatchesExact() { + setSmartCropRenditionNormalizedSize(0.5, 0.5); // results in 80x50 cropping area + assertTrue(isMatchingSize(asset, context.resourceResolver(), dimension16_10, 80, 50)); + } + + @Test + void testIsMatchingSize_MatchesSmaller() { + setSmartCropRenditionNormalizedSize(0.5, 0.5); // results in 80x50 cropping area + assertTrue(isMatchingSize(asset, context.resourceResolver(), dimension16_10, 40, 25)); + } + + @Test + void testIsMatchingSize_TooSmall() { + setSmartCropRenditionNormalizedSize(0.5, 0.5); // results in 80x50 cropping area + assertFalse(isMatchingSize(asset, context.resourceResolver(), dimension16_10, 120, 75)); + } + + @Test + void testIsMatchingSize_InvalidNormalizedWidthHeight() { + setSmartCropRenditionNormalizedSize(0, 0); + // assume true because no valid normalized width/height provided - we do not know the cropping area + assertTrue(isMatchingSize(asset, context.resourceResolver(), dimension16_10, 80, 50)); + } + + private static void assertNamedDimension(NamedDimension namedDimension, + String expectedName, long expectedWith, long expectedHeight) { + assertNotNull(namedDimension); + assertEquals(expectedName, namedDimension.getName()); + assertEquals(expectedWith, namedDimension.getWidth()); + assertEquals(expectedHeight, namedDimension.getHeight()); + } + + private void setSmartCropRenditionNormalizedSize(double normalizedWith, double normalizedHeight) { + String smartCropRenditionPath = asset.getPath() + "/" + JCR_CONTENT + "/" + RENDITIONS_FOLDER + + "/" + dimension16_10.getName() + "/" + JCR_CONTENT; + context.create().resource(smartCropRenditionPath, + PN_NORMALIZED_WIDTH, normalizedWith, + PN_NORMALIZED_HEIGHT, normalizedHeight); + } + +}