View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2019 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;
21  
22  import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
23  import static com.day.cq.dam.api.DamConstants.EXIF_PIXELXDIMENSION;
24  import static com.day.cq.dam.api.DamConstants.EXIF_PIXELYDIMENSION;
25  import static com.day.cq.dam.api.DamConstants.METADATA_FOLDER;
26  import static com.day.cq.dam.api.DamConstants.ORIGINAL_FILE;
27  import static com.day.cq.dam.api.DamConstants.TIFF_IMAGELENGTH;
28  import static com.day.cq.dam.api.DamConstants.TIFF_IMAGEWIDTH;
29  import static io.wcm.handler.mediasource.dam.impl.metadata.RenditionMetadataNameConstants.NN_RENDITIONS_METADATA;
30  import static io.wcm.handler.mediasource.dam.impl.metadata.RenditionMetadataNameConstants.PN_IMAGE_HEIGHT;
31  import static io.wcm.handler.mediasource.dam.impl.metadata.RenditionMetadataNameConstants.PN_IMAGE_WIDTH;
32  
33  import java.io.IOException;
34  import java.io.InputStream;
35  
36  import org.apache.commons.io.FilenameUtils;
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.commons.lang3.math.NumberUtils;
39  import org.apache.sling.api.resource.Resource;
40  import org.apache.sling.api.resource.ValueMap;
41  import org.jetbrains.annotations.NotNull;
42  import org.jetbrains.annotations.Nullable;
43  import org.osgi.annotation.versioning.ProviderType;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import com.day.cq.dam.api.Asset;
48  import com.day.cq.dam.api.Rendition;
49  import com.day.image.Layer;
50  
51  import io.wcm.handler.media.Dimension;
52  import io.wcm.handler.media.MediaFileType;
53  import io.wcm.sling.commons.adapter.AdaptTo;
54  
55  /**
56   * Helper methods for getting metadata for DAM renditions.
57   */
58  @ProviderType
59  public final class AssetRendition {
60  
61    private static final Logger log = LoggerFactory.getLogger(AssetRendition.class);
62  
63    private AssetRendition() {
64      // static methods only
65    }
66  
67    /**
68     * Get dimension (width, height) of given DAM rendition.
69     *
70     * <p>
71     * It reads the dimension information from the
72     * asset metadata for the original rendition, or from the rendition metadata generated by the
73     * "DamRenditionMetadataService". If both is not available it gets the dimension from the renditions
74     * binary file, but this is inefficient and should not happen under sound conditions.
75     * </p>
76     * @param rendition Rendition
77     * @return Dimension or null if dimension could not be detected, not even in fallback mode
78     */
79    public static @Nullable Dimension getDimension(@NotNull Rendition rendition) {
80      return getDimension(rendition, false);
81    }
82  
83    /**
84     * Get dimension (width, height) of given DAM rendition.
85     *
86     * <p>
87     * It reads the dimension information from the
88     * asset metadata for the original rendition, or from the rendition metadata generated by the
89     * "DamRenditionMetadataService". If both is not available it gets the dimension from the renditions
90     * binary file, but this is inefficient and should not happen under sound conditions.
91     * </p>
92     * @param rendition Rendition
93     * @param suppressLogWarningNoRenditionsMetadata If set to true, no log warnings is generated when
94     *          renditions metadata containing the width/height of the rendition does not exist (yet).
95     * @return Dimension or null if dimension could not be detected, not even in fallback mode
96     */
97    public static @Nullable Dimension getDimension(@NotNull Rendition rendition,
98        boolean suppressLogWarningNoRenditionsMetadata) {
99  
100     boolean isOriginal = isOriginal(rendition);
101     String fileExtension = FilenameUtils.getExtension(getFilename(rendition));
102 
103     // get image width/height
104     Dimension dimension = null;
105     if (isOriginal) {
106       // get width/height from metadata for original renditions
107       dimension = getDimensionFromOriginal(rendition);
108     }
109 
110     // dimensions for non-original renditions only supported for image binaries
111     if (MediaFileType.isImage(fileExtension)) {
112       if (dimension == null) {
113         // check if rendition metadata is present in <rendition>/jcr:content/metadata provided by AEMaaCS asset compute
114         dimension = getDimensionFromAemRenditionMetadata(rendition);
115       }
116 
117       if (dimension == null) {
118         // otherwise get from rendition metadata written by {@link DamRenditionMetadataService}
119         dimension = getDimensionFromMediaHandlerRenditionMetadata(rendition);
120       }
121 
122       // fallback: if width/height could not be read from either asset or rendition metadata load the image
123       // into memory and get width/height from there - but log an warning because this is inefficient
124       if (dimension == null) {
125         dimension = getDimensionFromImageBinary(rendition, suppressLogWarningNoRenditionsMetadata);
126       }
127     }
128 
129     return dimension;
130   }
131 
132   /**
133    * Read dimension for original rendition from asset metadata.
134    * @param rendition Rendition
135    * @return Dimension or null
136    */
137   private static @Nullable Dimension getDimensionFromOriginal(@NotNull Rendition rendition) {
138     Asset asset = rendition.getAsset();
139     // asset may have stored dimension in different property names
140     long width = getAssetMetadataValueAsLong(asset, TIFF_IMAGEWIDTH, EXIF_PIXELXDIMENSION);
141     long height = getAssetMetadataValueAsLong(asset, TIFF_IMAGELENGTH, EXIF_PIXELYDIMENSION);
142     return toValidDimension(width, height);
143   }
144 
145   private static long getAssetMetadataValueAsLong(Asset asset, String... propertyNames) {
146     for (String propertyName : propertyNames) {
147       long value = NumberUtils.toLong(StringUtils.defaultString(asset.getMetadataValueFromJcr(propertyName), "0"));
148       if (value > 0L) {
149         return value;
150       }
151     }
152     return 0L;
153   }
154 
155   /**
156    * Read dimension for non-original rendition from renditions metadata generated by "DamRenditionMetadataService".
157    * @param rendition Rendition
158    * @return Dimension or null
159    */
160   @SuppressWarnings("java:S1075") // not a file path
161   private static @Nullable Dimension getDimensionFromMediaHandlerRenditionMetadata(@NotNull Rendition rendition) {
162     Asset asset = rendition.getAsset();
163     String metadataPath = JCR_CONTENT + "/" + NN_RENDITIONS_METADATA + "/" + rendition.getName();
164     Resource metadataResource = AdaptTo.notNull(asset, Resource.class).getChild(metadataPath);
165     if (metadataResource != null) {
166       ValueMap props = metadataResource.getValueMap();
167       long width = props.get(PN_IMAGE_WIDTH, 0L);
168       long height = props.get(PN_IMAGE_HEIGHT, 0L);
169       return toValidDimension(width, height);
170     }
171     return null;
172   }
173 
174   /**
175    * Asset Compute from AEMaaCS writes rendition metadata including width/height to jcr:content/metadata of the
176    * rendition resource - try to read it from there (it may be missing for not fully processed assets, or in local
177    * AEMaaCS SDK or AEM 6.5 instances).
178    * @param rendition Rendition
179    * @return Dimension or null
180    */
181   private static @Nullable Dimension getDimensionFromAemRenditionMetadata(@NotNull Rendition rendition) {
182     Resource metadataResource = rendition.getChild(JCR_CONTENT + "/" + METADATA_FOLDER);
183     if (metadataResource != null) {
184       ValueMap props = metadataResource.getValueMap();
185       long width = props.get(TIFF_IMAGEWIDTH, 0L);
186       long height = props.get(TIFF_IMAGELENGTH, 0L);
187       return toValidDimension(width, height);
188     }
189     return null;
190   }
191 
192   /**
193    * Fallback: Read dimension by loading image binary into memory.
194    * @param rendition Rendition
195    * @param suppressLogWarningNoRenditionsMetadata If set to true, no log warnings is generated when
196    *          renditions metadata containing the width/height of the rendition does not exist (yet).
197    * @return Dimension or null
198    */
199   @SuppressWarnings("PMD.GuardLogStatement")
200   private static @Nullable Dimension getDimensionFromImageBinary(@NotNull Rendition rendition,
201       boolean suppressLogWarningNoRenditionsMetadata) {
202     try (InputStream is = rendition.getStream()) {
203       if (is != null) {
204         Layer layer = new Layer(is);
205         long width = layer.getWidth();
206         long height = layer.getHeight();
207         Dimension dimension = toValidDimension(width, height);
208         if (!suppressLogWarningNoRenditionsMetadata) {
209           log.warn("Unable to detect rendition metadata for {}, "
210               + "fallback to inefficient detection by loading image into in memory (detected dimension={}). "
211               + "Please check if the service user for the bundle 'io.wcm.handler.media' is configured properly.",
212               rendition.getPath(), dimension);
213         }
214         return dimension;
215       }
216       else {
217         log.warn("Unable to get binary stream for rendition {}", rendition.getPath());
218       }
219     }
220     catch (IOException ex) {
221       log.warn("Unable to read binary stream to layer for rendition {}", rendition.getPath(), ex);
222     }
223     return null;
224   }
225 
226   /**
227    * Convert width/height to dimension.
228    * @param width Width
229    * @param height Height
230    * @return Dimension or null if width or height are not valid
231    */
232   private static @Nullable Dimension toValidDimension(long width, long height) {
233     if (width > 0L && height > 0L) {
234       return new Dimension(width, height);
235     }
236     return null;
237   }
238 
239   /**
240    * Checks if the given rendition is the original file of the asset
241    * @param rendition DAM rendition
242    * @return true if rendition is the original
243    */
244   public static boolean isOriginal(@NotNull Rendition rendition) {
245     return StringUtils.equals(rendition.getName(), ORIGINAL_FILE);
246   }
247 
248   /**
249    * Checks if the given rendition is a thumbnail rendition generated automatically by AEM
250    * (with <code>cq5dam.thumbnail.</code> prefix).
251    * @param rendition DAM rendition
252    * @return true if rendition is a thumbnail rendition
253    */
254   public static boolean isThumbnailRendition(@NotNull Rendition rendition) {
255     return AemRenditionType.THUMBNAIL_RENDITION.matches(rendition);
256   }
257 
258   /**
259    * Checks if the given rendition is a web rendition generated automatically by AEM for the image editor/cropping
260    * (with <code>cq5dam.web.</code> prefix).
261    * @param rendition DAM rendition
262    * @return true if rendition is a web rendition
263    */
264   public static boolean isWebRendition(@NotNull Rendition rendition) {
265     return AemRenditionType.WEB_RENDITION.matches(rendition);
266   }
267 
268   /**
269    * Get file name of given rendition. If it is the original rendition get asset name as file name.
270    * @param rendition Rendition
271    * @return File extension or null if it could not be detected
272    */
273   public static String getFilename(@NotNull Rendition rendition) {
274     boolean isOriginal = isOriginal(rendition);
275     if (isOriginal) {
276       return rendition.getAsset().getName();
277     }
278     else {
279       return rendition.getName();
280     }
281   }
282 
283 }