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.MediaFileServletConstants;
40  import io.wcm.handler.mediasource.dam.impl.dynamicmedia.DynamicMediaPath;
41  import io.wcm.handler.mediasource.dam.impl.dynamicmedia.NamedDimension;
42  import io.wcm.handler.mediasource.dam.impl.dynamicmedia.SmartCrop;
43  import io.wcm.handler.mediasource.dam.impl.weboptimized.WebOptimizedImageDeliveryParams;
44  import io.wcm.handler.url.UrlHandler;
45  import io.wcm.sling.commons.adapter.AdaptTo;
46  
47  /**
48   * Generates URI templates for asset renditions - with or without Dynamic Media.
49   */
50  final class DamUriTemplate implements UriTemplate {
51  
52    private static final long DUMMY_WIDTH = 999991;
53    private static final long DUMMY_HEIGHT = 999992;
54  
55    private final UriTemplateType type;
56    private final String uriTemplate;
57    private final Dimension dimension;
58  
59    DamUriTemplate(@NotNull UriTemplateType type, @NotNull Dimension dimension,
60        @NotNull Rendition rendition, @Nullable CropDimension cropDimension, @Nullable Integer rotation,
61        @Nullable Double ratio, @NotNull DamContext damContext) {
62      this.type = type;
63  
64      String url = null;
65      Dimension validatedDimension = null;
66      if (damContext.isDynamicMediaEnabled() && damContext.isDynamicMediaAsset()) {
67        // if DM is enabled: try to get rendition URL from dynamic media
68        NamedDimension smartCropDef = getDynamicMediaSmartCropDef(cropDimension, rotation, ratio, damContext);
69        url = buildUriTemplateDynamicMedia(type, cropDimension, rotation, smartCropDef, damContext);
70        // get actual max. dimension from smart crop rendition
71        if (url != null && smartCropDef != null) {
72          validatedDimension = SmartCrop.getCropDimensionForAsset(damContext.getAsset(), damContext.getResourceResolver(), smartCropDef);
73        }
74      }
75      if (url == null && (!damContext.isDynamicMediaEnabled() || !damContext.isDynamicMediaAemFallbackDisabled())) {
76        if (damContext.isWebOptimizedImageDeliveryEnabled()) {
77          // Render renditions via web-optimized image delivery: build externalized URL
78          url = buildUriTemplateWebOptimizedImageDelivery(type, cropDimension, rotation, damContext);
79        }
80        if (url == null) {
81          // Render renditions in AEM: build externalized URL
82          url = buildUriTemplateDam(type, rendition, cropDimension, rotation,
83              damContext.getMediaArgs().getImageQualityPercentage(), damContext);
84        }
85      }
86      this.uriTemplate = url;
87  
88      if (validatedDimension == null) {
89        validatedDimension = dimension;
90      }
91      this.dimension = validatedDimension;
92    }
93  
94    private static String buildUriTemplateDam(@NotNull UriTemplateType type, @NotNull Rendition rendition,
95        @Nullable CropDimension cropDimension, @Nullable Integer rotation, @Nullable Double imageQualityPercentage,
96        @NotNull DamContext damContext) {
97  
98      // build rendition URL with dummy width/height parameters (otherwise externalization will fail)
99      MediaArgs mediaArgs = damContext.getMediaArgs();
100     String mediaPath = RenditionMetadata.buildMediaPath(rendition.getPath()
101         + "." + ImageFileServletSelector.build(DUMMY_WIDTH, DUMMY_HEIGHT, cropDimension, rotation, imageQualityPercentage, false)
102         + "." + MediaFileServletConstants.EXTENSION,
103         ImageFileServlet.getImageFileName(damContext.getAsset().getName(), mediaArgs.getEnforceOutputFileExtension()));
104     UrlHandler urlHandler = AdaptTo.notNull(damContext, UrlHandler.class);
105     String url = urlHandler.get(mediaPath).urlMode(mediaArgs.getUrlMode())
106         .buildExternalResourceUrl(damContext.getAsset().adaptTo(Resource.class));
107 
108     // replace dummy width/height parameters with actual placeholders
109     switch (type) {
110       case CROP_CENTER:
111         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
112         url = StringUtils.replace(url, Long.toString(DUMMY_HEIGHT), URI_TEMPLATE_PLACEHOLDER_HEIGHT);
113         break;
114       case SCALE_WIDTH:
115         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
116         url = StringUtils.replace(url, Long.toString(DUMMY_HEIGHT), "0");
117         break;
118       case SCALE_HEIGHT:
119         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), "0");
120         url = StringUtils.replace(url, Long.toString(DUMMY_HEIGHT), URI_TEMPLATE_PLACEHOLDER_HEIGHT);
121         break;
122       default:
123         throw new IllegalArgumentException("Unsupported type: " + type);
124     }
125     return url;
126   }
127 
128   private static String buildUriTemplateWebOptimizedImageDelivery(@NotNull UriTemplateType type,
129       @Nullable CropDimension cropDimension, @Nullable Integer rotation, @NotNull DamContext damContext) {
130     // scale by height is not supported by Web-Optimized Image Delivery
131     if (type == UriTemplateType.SCALE_HEIGHT) {
132       return null;
133     }
134 
135     // build rendition URL with dummy width/height parameters (otherwise API call will fail)
136     String url = damContext.getWebOptimizedImageDeliveryUrl(new WebOptimizedImageDeliveryParams()
137         .width(DUMMY_WIDTH).cropDimension(cropDimension).rotation(rotation));
138     if (url == null) {
139       return null;
140     }
141 
142     // replace dummy width/height parameters with actual placeholders
143     switch (type) {
144       case CROP_CENTER:
145         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
146         break;
147       case SCALE_WIDTH:
148         url = StringUtils.replace(url, Long.toString(DUMMY_WIDTH), URI_TEMPLATE_PLACEHOLDER_WIDTH);
149         break;
150       default:
151         throw new IllegalArgumentException("Unsupported type for Web-optimized image delivery: " + type);
152     }
153     return url;
154   }
155 
156   private static @Nullable String buildUriTemplateDynamicMedia(@NotNull UriTemplateType type,
157       @Nullable CropDimension cropDimension, @Nullable Integer rotation, @Nullable NamedDimension smartCropDef,
158       @NotNull DamContext damContext) {
159     String productionAssetUrl = damContext.getDynamicMediaServerUrl();
160     if (productionAssetUrl == null) {
161       return null;
162     }
163     StringBuilder result = new StringBuilder();
164     result.append(productionAssetUrl).append(DynamicMediaPath.buildImage(damContext));
165 
166     // build DM URL with smart cropping
167     if (smartCropDef != null) {
168       result.append("%3A").append(smartCropDef.getName()).append("?")
169           .append(getDynamicMediaWidthHeightParameters(type))
170           .append("&fit=constrain");
171       return result.toString();
172     }
173 
174     // build DM URL without smart cropping
175     result.append("?");
176     if (cropDimension != null) {
177       result.append("crop=").append(cropDimension.getCropStringWidthHeight()).append("&");
178     }
179     if (rotation != null) {
180       result.append("rotate=").append(rotation).append("&");
181     }
182     result.append(getDynamicMediaWidthHeightParameters(type));
183     return result.toString();
184   }
185 
186   private static String getDynamicMediaWidthHeightParameters(UriTemplateType type) {
187     switch (type) {
188       case CROP_CENTER:
189         return "wid=" + URI_TEMPLATE_PLACEHOLDER_WIDTH + "&hei=" + URI_TEMPLATE_PLACEHOLDER_HEIGHT + "&fit=crop";
190       case SCALE_WIDTH:
191         return "wid=" + URI_TEMPLATE_PLACEHOLDER_WIDTH;
192       case SCALE_HEIGHT:
193         return "hei=" + URI_TEMPLATE_PLACEHOLDER_HEIGHT;
194       default:
195         throw new IllegalArgumentException("Unsupported type for Dynamic Media: " + type);
196     }
197   }
198 
199   private static NamedDimension getDynamicMediaSmartCropDef(@Nullable CropDimension cropDimension, @Nullable Integer rotation,
200       @Nullable Double ratio, @NotNull DamContext damContext) {
201     if (SmartCrop.canApply(cropDimension, rotation) && ratio != null) {
202       // check for matching image profile and use predefined cropping preset if match found
203       return SmartCrop.getDimensionForRatio(damContext.getImageProfile(), ratio);
204     }
205     return null;
206   }
207 
208   @Override
209   public @NotNull UriTemplateType getType() {
210     return type;
211   }
212 
213   @Override
214   public @NotNull String getUriTemplate() {
215     return uriTemplate;
216   }
217 
218   @Override
219   public long getMaxWidth() {
220     return dimension.getWidth();
221   }
222 
223   @Override
224   public long getMaxHeight() {
225     return dimension.getHeight();
226   }
227 
228   @Override
229   public String toString() {
230     return uriTemplate;
231   }
232 
233 }