AssetRendition.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2019 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;
import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.day.cq.dam.api.DamConstants.EXIF_PIXELXDIMENSION;
import static com.day.cq.dam.api.DamConstants.EXIF_PIXELYDIMENSION;
import static com.day.cq.dam.api.DamConstants.METADATA_FOLDER;
import static com.day.cq.dam.api.DamConstants.ORIGINAL_FILE;
import static com.day.cq.dam.api.DamConstants.TIFF_IMAGELENGTH;
import static com.day.cq.dam.api.DamConstants.TIFF_IMAGEWIDTH;
import static io.wcm.handler.mediasource.dam.impl.metadata.RenditionMetadataNameConstants.NN_RENDITIONS_METADATA;
import static io.wcm.handler.mediasource.dam.impl.metadata.RenditionMetadataNameConstants.PN_IMAGE_HEIGHT;
import static io.wcm.handler.mediasource.dam.impl.metadata.RenditionMetadataNameConstants.PN_IMAGE_WIDTH;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
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.osgi.annotation.versioning.ProviderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.image.Layer;
import io.wcm.handler.media.Dimension;
import io.wcm.handler.media.MediaFileType;
import io.wcm.sling.commons.adapter.AdaptTo;
/**
* Helper methods for getting metadata for DAM renditions.
*/
@ProviderType
public final class AssetRendition {
private static final Logger log = LoggerFactory.getLogger(AssetRendition.class);
private AssetRendition() {
// static methods only
}
/**
* Get dimension (width, height) of given DAM rendition.
* <p>
* It reads the dimension information from the
* asset metadata for the original rendition, or from the rendition metadata generated by the
* "DamRenditionMetadataService". If both is not available it gets the dimension from the renditions
* binary file, but this is inefficient and should not happen under sound conditions.
* </p>
* @param rendition Rendition
* @return Dimension or null if dimension could not be detected, not even in fallback mode
*/
public static @Nullable Dimension getDimension(@NotNull Rendition rendition) {
return getDimension(rendition, false);
}
/**
* Get dimension (width, height) of given DAM rendition.
* <p>
* It reads the dimension information from the
* asset metadata for the original rendition, or from the rendition metadata generated by the
* "DamRenditionMetadataService". If both is not available it gets the dimension from the renditions
* binary file, but this is inefficient and should not happen under sound conditions.
* </p>
* @param rendition Rendition
* @param suppressLogWarningNoRenditionsMetadata If set to true, no log warnings is generated when
* 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,
boolean suppressLogWarningNoRenditionsMetadata) {
boolean isOriginal = isOriginal(rendition);
String fileExtension = FilenameUtils.getExtension(getFilename(rendition));
// get image width/height
Dimension dimension = null;
if (isOriginal) {
// get width/height from metadata for original renditions
dimension = getDimensionFromOriginal(rendition);
}
// dimensions for non-original renditions only supported for image binaries
if (MediaFileType.isImage(fileExtension)) {
if (dimension == null) {
// check if rendition metadata is present in <rendition>/jcr:content/metadata provided by AEMaaCS asset compute
dimension = getDimensionFromAemRenditionMetadata(rendition);
}
if (dimension == null) {
// otherwise get from rendition metadata written by {@link DamRenditionMetadataService}
dimension = getDimensionFromMediaHandlerRenditionMetadata(rendition);
}
// fallback: if width/height could not be read from either asset or rendition metadata load the image
// into memory and get width/height from there - but log an warning because this is inefficient
if (dimension == null) {
dimension = getDimensionFromImageBinary(rendition, suppressLogWarningNoRenditionsMetadata);
}
}
return dimension;
}
/**
* Read dimension for original rendition from asset metadata.
* @param rendition Rendition
* @return Dimension or null
*/
private static @Nullable Dimension getDimensionFromOriginal(@NotNull Rendition rendition) {
Asset asset = rendition.getAsset();
// asset may have stored dimension in different property names
long width = getAssetMetadataValueAsLong(asset, TIFF_IMAGEWIDTH, EXIF_PIXELXDIMENSION);
long height = getAssetMetadataValueAsLong(asset, TIFF_IMAGELENGTH, EXIF_PIXELYDIMENSION);
return toValidDimension(width, height);
}
private static long getAssetMetadataValueAsLong(Asset asset, String... propertyNames) {
for (String propertyName : propertyNames) {
long value = NumberUtils.toLong(StringUtils.defaultString(asset.getMetadataValueFromJcr(propertyName), "0"));
if (value > 0L) {
return value;
}
}
return 0L;
}
/**
* Read dimension for non-original rendition from renditions metadata generated by "DamRenditionMetadataService".
* @param rendition Rendition
* @return Dimension or null
*/
@SuppressWarnings("java:S1075") // not a file path
private static @Nullable Dimension getDimensionFromMediaHandlerRenditionMetadata(@NotNull Rendition rendition) {
Asset asset = rendition.getAsset();
String metadataPath = JCR_CONTENT + "/" + NN_RENDITIONS_METADATA + "/" + rendition.getName();
Resource metadataResource = AdaptTo.notNull(asset, Resource.class).getChild(metadataPath);
if (metadataResource != null) {
ValueMap props = metadataResource.getValueMap();
long width = props.get(PN_IMAGE_WIDTH, 0L);
long height = props.get(PN_IMAGE_HEIGHT, 0L);
return toValidDimension(width, height);
}
return null;
}
/**
* Asset Compute from AEMaaCS writes rendition metadata including width/height to jcr:content/metadata of the
* rendition resource - try to read it from there (it may be missing for not fully processed assets, or in local
* AEMaaCS SDK or AEM 6.5 instances).
* @param rendition Rendition
* @return Dimension or null
*/
private static @Nullable Dimension getDimensionFromAemRenditionMetadata(@NotNull Rendition rendition) {
Resource metadataResource = rendition.getChild(JCR_CONTENT + "/" + METADATA_FOLDER);
if (metadataResource != null) {
ValueMap props = metadataResource.getValueMap();
long width = props.get(TIFF_IMAGEWIDTH, 0L);
long height = props.get(TIFF_IMAGELENGTH, 0L);
return toValidDimension(width, height);
}
return null;
}
/**
* 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 width/height of the rendition does not exist (yet).
* @return Dimension or null
*/
@SuppressWarnings("PMD.GuardLogStatement")
private static @Nullable Dimension getDimensionFromImageBinary(@NotNull Rendition rendition,
boolean suppressLogWarningNoRenditionsMetadata) {
try (InputStream is = rendition.getStream()) {
if (is != null) {
Layer layer = new Layer(is);
long width = layer.getWidth();
long height = layer.getHeight();
Dimension dimension = toValidDimension(width, height);
if (!suppressLogWarningNoRenditionsMetadata) {
log.warn("Unable to detect rendition metadata for {}, "
+ "fallback to inefficient detection by loading image into in memory (detected dimension={}). "
+ "Please check if the service user for the bundle 'io.wcm.handler.media' is configured properly.",
rendition.getPath(), dimension);
}
return dimension;
}
else {
log.warn("Unable to get binary stream for rendition {}", rendition.getPath());
}
}
catch (IOException ex) {
log.warn("Unable to read binary stream to layer for rendition {}", rendition.getPath(), ex);
}
return null;
}
/**
* Convert width/height to dimension.
* @param width Width
* @param height Height
* @return Dimension or null if width or height are not valid
*/
private static @Nullable Dimension toValidDimension(long width, long height) {
if (width > 0L && height > 0L) {
return new Dimension(width, height);
}
return null;
}
/**
* Checks if the given rendition is the original file of the asset
* @param rendition DAM rendition
* @return true if rendition is the original
*/
public static boolean isOriginal(@NotNull Rendition rendition) {
return StringUtils.equals(rendition.getName(), ORIGINAL_FILE);
}
/**
* Checks if the given rendition is a thumbnail rendition generated automatically by AEM
* (with <code>cq5dam.thumbnail.</code> prefix).
* @param rendition DAM rendition
* @return true if rendition is a thumbnail rendition
*/
public static boolean isThumbnailRendition(@NotNull Rendition rendition) {
return AemRenditionType.THUMBNAIL_RENDITION.matches(rendition);
}
/**
* Checks if the given rendition is a web rendition generated automatically by AEM for the image editor/cropping
* (with <code>cq5dam.web.</code> prefix).
* @param rendition DAM rendition
* @return true if rendition is a web rendition
*/
public static boolean isWebRendition(@NotNull Rendition rendition) {
return AemRenditionType.WEB_RENDITION.matches(rendition);
}
/**
* Get file name of given rendition. If it is the original rendition get asset name as file name.
* @param rendition Rendition
* @return File extension or null if it could not be detected
*/
public static String getFilename(@NotNull Rendition rendition) {
boolean isOriginal = isOriginal(rendition);
if (isOriginal) {
return rendition.getAsset().getName();
}
else {
return rendition.getName();
}
}
}