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