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);
+ }
+
+}