View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2021 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.handler.mediasource.dam.impl;
21  
22  import static io.wcm.handler.media.MediaNameConstants.URI_TEMPLATE_PLACEHOLDER_HEIGHT;
23  import static io.wcm.handler.media.MediaNameConstants.URI_TEMPLATE_PLACEHOLDER_WIDTH;
24  
25  import org.apache.commons.lang3.StringUtils;
26  import org.apache.sling.api.resource.Resource;
27  import org.jetbrains.annotations.NotNull;
28  import org.jetbrains.annotations.Nullable;
29  
30  import com.day.cq.dam.api.Rendition;
31  
32  import io.wcm.handler.media.CropDimension;
33  import io.wcm.handler.media.Dimension;
34  import io.wcm.handler.media.MediaArgs;
35  import io.wcm.handler.media.UriTemplate;
36  import io.wcm.handler.media.UriTemplateType;
37  import io.wcm.handler.media.impl.ImageFileServlet;
38  import io.wcm.handler.media.impl.ImageFileServletSelector;
39  import io.wcm.handler.media.impl.ImageQualityPercentage;
40  import io.wcm.handler.media.impl.MediaFileServletConstants;
41  import io.wcm.handler.mediasource.dam.impl.dynamicmedia.DynamicMediaPath;
42  import io.wcm.handler.mediasource.dam.impl.dynamicmedia.NamedDimension;
43  import io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop;
44  import io.wcm.handler.mediasource.dam.impl.weboptimized.WebOptimizedImageDeliveryParams;
45  import io.wcm.handler.url.UrlHandler;
46  import io.wcm.sling.commons.adapter.AdaptTo;
47  
48  /**
49   * Generates URI templates for asset renditions - with or without Dynamic Media.
50   */
51  final class DamUriTemplate implements UriTemplate {
52  
53    private static final long DUMMY_WIDTH = 999991;
54    private static final long DUMMY_HEIGHT = 999992;
55  
56    private final UriTemplateType type;
57    private final String uriTemplate;
58    private final Dimension dimension;
59  
60    DamUriTemplate(@NotNull UriTemplateType type, @NotNull Dimension dimension,
61        @NotNull Rendition rendition, @Nullable CropDimension cropDimension, @Nullable Integer rotation,
62        @Nullable Double ratio, @NotNull DamContext damContext) {
63      this.type = type;
64  
65      String url = null;
66      Dimension validatedDimension = null;
67      if (damContext.isDynamicMediaEnabled() && damContext.isDynamicMediaAsset()) {
68        // if DM is enabled: try to get rendition URL from dynamic media
69        NamedDimension smartCropDef = getDynamicMediaSmartCropDef(cropDimension, rotation, ratio, damContext);
70        url = buildUriTemplateDynamicMedia(type, cropDimension, rotation, smartCropDef, damContext);
71        // get actual max. dimension from smart crop rendition
72        if (url != null && smartCropDef != null) {
73          validatedDimension = SmartCrop.getCropDimensionForAsset(damContext.getAsset(), damContext.getResourceResolver(), smartCropDef);
74        }
75      }
76      if (url == null && (!damContext.isDynamicMediaEnabled() || !damContext.isDynamicMediaAemFallbackDisabled())) {
77        if (damContext.isWebOptimizedImageDeliveryEnabled()) {
78          // Render renditions via web-optimized image delivery: build externalized URL
79          url = buildUriTemplateWebOptimizedImageDelivery(type, cropDimension, rotation, damContext);
80        }
81        if (url == null) {
82          // Render renditions in AEM: build externalized URL
83          url = buildUriTemplateDam(type, rendition, cropDimension, rotation,
84              damContext.getMediaArgs().getImageQualityPercentage(), damContext);
85        }
86      }
87      this.uriTemplate = url;
88  
89      if (validatedDimension == null) {
90        validatedDimension = dimension;
91      }
92      this.dimension = validatedDimension;
93    }
94  
95    private static String buildUriTemplateDam(@NotNull UriTemplateType type, @NotNull Rendition rendition,
96        @Nullable CropDimension cropDimension, @Nullable Integer rotation, @Nullable Double imageQualityPercentage,
97        @NotNull DamContext damContext) {
98  
99      // build rendition URL with dummy width/height parameters (otherwise externalization will fail)
100     MediaArgs mediaArgs = damContext.getMediaArgs();
101     String mediaPath = RenditionMetadata.buildMediaPath(rendition.getPath()
102         + "." + ImageFileServletSelector.build(DUMMY_WIDTH, DUMMY_HEIGHT, cropDimension, rotation, imageQualityPercentage, false)
103         + "." + MediaFileServletConstants.EXTENSION,
104         ImageFileServlet.getImageFileName(damContext.getAsset().getName(), mediaArgs.getEnforceOutputFileExtension()));
105     UrlHandler urlHandler = AdaptTo.notNull(damContext, UrlHandler.class);
106     String url = urlHandler.get(mediaPath).urlMode(mediaArgs.getUrlMode())
107         .buildExternalResourceUrl(damContext.getAsset().adaptTo(Resource.class));
108 
109     // replace dummy width/height parameters with actual placeholders
110     switch (type) {
111       case CROP_CENTER:
112         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
113         url = StringUtils.replace(url, Long.toString(DUMMY_HEIGHT), URI_TEMPLATE_PLACEHOLDER_HEIGHT);
114         break;
115       case SCALE_WIDTH:
116         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
117         url = StringUtils.replace(url, Long.toString(DUMMY_HEIGHT), "0");
118         break;
119       case SCALE_HEIGHT:
120         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), "0");
121         url = StringUtils.replace(url, Long.toString(DUMMY_HEIGHT), URI_TEMPLATE_PLACEHOLDER_HEIGHT);
122         break;
123       default:
124         throw new IllegalArgumentException("Unsupported type: " + type);
125     }
126     return url;
127   }
128 
129   private static String buildUriTemplateWebOptimizedImageDelivery(@NotNull UriTemplateType type,
130       @Nullable CropDimension cropDimension, @Nullable Integer rotation, @NotNull DamContext damContext) {
131     // scale by height is not supported by Web-Optimized Image Delivery
132     if (type == UriTemplateType.SCALE_HEIGHT) {
133       return null;
134     }
135 
136     // build rendition URL with dummy width/height parameters (otherwise API call will fail)
137     String url = damContext.getWebOptimizedImageDeliveryUrl(new WebOptimizedImageDeliveryParams()
138         .width(DUMMY_WIDTH).cropDimension(cropDimension).rotation(rotation));
139     if (url == null) {
140       return null;
141     }
142 
143     // replace dummy width/height parameters with actual placeholders
144     switch (type) {
145       case CROP_CENTER:
146         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
147         break;
148       case SCALE_WIDTH:
149         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
150         break;
151       default:
152         throw new IllegalArgumentException("Unsupported type for Web-optimized image delivery: " + type);
153     }
154     return url;
155   }
156 
157   private static @Nullable String buildUriTemplateDynamicMedia(@NotNull UriTemplateType type,
158       @Nullable CropDimension cropDimension, @Nullable Integer rotation, @Nullable NamedDimension smartCropDef,
159       @NotNull DamContext damContext) {
160     String productionAssetUrl = damContext.getDynamicMediaServerUrl();
161     if (productionAssetUrl == null) {
162       return null;
163     }
164     StringBuilder result = new StringBuilder();
165     result.append(productionAssetUrl).append(DynamicMediaPath.buildImage(damContext));
166 
167     // build DM URL with smart cropping
168     if (smartCropDef != null) {
169       result.append("%3A").append(smartCropDef.getName()).append("?")
170           .append(getDynamicMediaWidthHeightParameters(type))
171           .append("&fit=constrain");
172       appendDynamicMediaQuality(result, damContext);
173       return result.toString();
174     }
175 
176     // build DM URL without smart cropping
177     result.append("?");
178     if (cropDimension != null) {
179       result.append("crop=").append(cropDimension.getCropStringWidthHeight()).append("&");
180     }
181     if (rotation != null) {
182       result.append("rotate=").append(rotation).append("&");
183     }
184     result.append(getDynamicMediaWidthHeightParameters(type));
185     appendDynamicMediaQuality(result, damContext);
186     return result.toString();
187   }
188 
189   private static String getDynamicMediaWidthHeightParameters(UriTemplateType type) {
190     switch (type) {
191       case CROP_CENTER:
192         return "wid=" + URI_TEMPLATE_PLACEHOLDER_WIDTH + "&hei=" + URI_TEMPLATE_PLACEHOLDER_HEIGHT + "&fit=crop";
193       case SCALE_WIDTH:
194         return "wid=" + URI_TEMPLATE_PLACEHOLDER_WIDTH;
195       case SCALE_HEIGHT:
196         return "hei=" + URI_TEMPLATE_PLACEHOLDER_HEIGHT;
197       default:
198         throw new IllegalArgumentException("Unsupported type for Dynamic Media: " + type);
199     }
200   }
201 
202   private static NamedDimension getDynamicMediaSmartCropDef(@Nullable CropDimension cropDimension, @Nullable Integer rotation,
203       @Nullable Double ratio, @NotNull DamContext damContext) {
204     if (SmartCrop.canApply(cropDimension, rotation) && ratio != null) {
205       // check for matching image profile and use predefined cropping preset if match found
206       return SmartCrop.getDimensionForRatio(damContext.getImageProfile(), ratio);
207     }
208     return null;
209   }
210 
211   private static void appendDynamicMediaQuality(@NotNull StringBuilder result, @NotNull DamContext damContext) {
212     if (damContext.isDynamicMediaSetImageQuality() && !DynamicMediaPath.isLosslessImageFormat(damContext)) {
213       // it not PNG lossy format is used, apply image quality setting
214       result.append("&qlt=").append(ImageQualityPercentage.getAsInteger(damContext.getMediaArgs(), damContext.getMediaHandlerConfig()));
215     }
216   }
217 
218   @Override
219   public @NotNull UriTemplateType getType() {
220     return type;
221   }
222 
223   @Override
224   public @NotNull String getUriTemplate() {
225     return uriTemplate;
226   }
227 
228   @Override
229   public long getMaxWidth() {
230     return dimension.getWidth();
231   }
232 
233   @Override
234   public long getMaxHeight() {
235     return dimension.getHeight();
236   }
237 
238   @Override
239   public String toString() {
240     return uriTemplate;
241   }
242 
243 }