DynamicMediaPath.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2020 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.impl.dynamicmedia;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.wcm.handler.media.CropDimension;
import io.wcm.handler.media.Dimension;
import io.wcm.handler.media.format.Ratio;
import io.wcm.handler.media.impl.ImageQualityPercentage;
import io.wcm.handler.mediasource.dam.impl.DamContext;
import io.wcm.wcm.commons.contenttype.ContentType;
/**
* Build part of dynamic media/scene7 URL to render renditions.
*/
public final class DynamicMediaPath {
/**
* Fixed path part for dynamic media image serving API for serving images.
*/
@SuppressWarnings("java:S1075") // not a file path
private static final String IMAGE_SERVER_PATH = "/is/image/";
/**
* Fixed path part for dynamic media image serving API for serving static content.
*/
@SuppressWarnings("java:S1075") // not a file path
private static final String CONTENT_SERVER_PATH = "/is/content/";
/**
* Suffix is appended to static content dynamic media URLs that should be served with
* Content-Disposition: attachment header.
* This is configured via a custom ruleset, see https://wcm.io/handler/media/dynamic-media.html
*/
public static final String DOWNLOAD_SUFFIX = "?cdh=attachment";
private static final Logger log = LoggerFactory.getLogger(DynamicMediaPath.class);
private DynamicMediaPath() {
// static methods only
}
/**
* Build media path for serving static content via dynamic media/scene7.
* @param damContext DAM context objects
* @param contentDispositionAttachment Whether to send content disposition: attachment header for downloads
* @return Media path
*/
public static @NotNull String buildContent(@NotNull DamContext damContext, boolean contentDispositionAttachment) {
StringBuilder result = new StringBuilder();
result.append(CONTENT_SERVER_PATH).append(encodeDynamicMediaObject(damContext));
if (contentDispositionAttachment) {
result.append(DOWNLOAD_SUFFIX);
}
return result.toString();
}
/**
* Build media path for rendering image via dynamic media/scene7.
* @param damContext DAM context objects
* @return Media path
*/
public static @NotNull String buildImage(@NotNull DamContext damContext) {
return IMAGE_SERVER_PATH + encodeDynamicMediaObject(damContext);
}
/**
* Build media path for rendering image with dynamic media/scene7.
* @param damContext DAM context objects
* @param width Width
* @param height Height
* @return Media path
*/
public static @Nullable String buildImage(@NotNull DamContext damContext, long width, long height) {
return buildImage(damContext, width, height, null, null);
}
/**
* Build media path for rendering image with dynamic media/scene7.
* @param damContext DAM context objects
* @param width Width
* @param height Height
* @param cropDimension Crop dimension
* @param rotation Rotation
* @return Media path
*/
public static @Nullable String buildImage(@NotNull DamContext damContext, long width, long height,
@Nullable CropDimension cropDimension, @Nullable Integer rotation) {
Dimension dimension = calcWidthHeight(damContext, width, height);
StringBuilder result = new StringBuilder();
result.append(IMAGE_SERVER_PATH).append(encodeDynamicMediaObject(damContext));
// check for smart cropping when no cropping was applied by default, or auto-crop is enabled
if (SmartCrop.canApply(cropDimension, rotation)) {
// check for matching image profile and use predefined cropping preset if match found
NamedDimension smartCropDef = SmartCrop.getDimensionForWidthHeight(damContext.getImageProfile(), width, height);
if (smartCropDef != null) {
if (damContext.isDynamicMediaValidateSmartCropRenditionSizes()
&& !SmartCrop.isMatchingSize(damContext.getAsset(), damContext.getResourceResolver(), smartCropDef, width, height)) {
// smart crop should be applied, but selected area is too small, treat as invalid
logResult(damContext, "<too small for " + width + "x" + height + ">");
return null;
}
result.append("%3A").append(smartCropDef.getName()).append("?");
appendWidthHeigtFormatQuality(result, dimension, damContext);
logResult(damContext, result);
return result.toString();
}
}
result.append("?");
if (cropDimension != null) {
result.append("crop=").append(cropDimension.getCropStringWidthHeight()).append("&");
}
if (rotation != null) {
result.append("rotate=").append(rotation).append("&");
}
appendWidthHeigtFormatQuality(result, dimension, damContext);
logResult(damContext, result);
return result.toString();
}
private static void appendWidthHeigtFormatQuality(@NotNull StringBuilder result, @NotNull Dimension dimension, @NotNull DamContext damContext) {
result.append("wid=").append(dimension.getWidth()).append("&")
.append("hei=").append(dimension.getHeight()).append("&")
// cropping/width/height is pre-calculated to fit with original ratio, make sure there are no 1px background lines visible
.append("fit=stretch");
if (isPNG(damContext)) {
// if original image is PNG image, make sure alpha channel is preserved
result.append("&fmt=png-alpha");
}
else if (damContext.isDynamicMediaSetImageQuality()) {
// it not PNG lossy format is used, apply image quality setting
result.append("&qlt=").append(ImageQualityPercentage.getAsInteger(damContext.getMediaArgs(), damContext.getMediaHandlerConfig()));
}
}
private static void logResult(@NotNull DamContext damContext, @NotNull CharSequence result) {
if (log.isTraceEnabled()) {
log.trace("Build dynamic media path for {}: {}", damContext.getAsset().getPath(), result);
}
}
/**
* Checks if width or height is bigger than the allowed max. width/height.
* Reduces both to the max limit keeping aspect ration is required.
* @param width With
* @param height Height
* @return Dimension with capped width/height
*/
private static Dimension calcWidthHeight(@NotNull DamContext damContext, long width, long height) {
Dimension sizeLimit = damContext.getDynamicMediaImageSizeLimit();
if (width > sizeLimit.getWidth()) {
double ratio = Ratio.get(width, height);
long newWidth = sizeLimit.getWidth();
long newHeight = Math.round(newWidth / ratio);
return calcWidthHeight(damContext, newWidth, newHeight);
}
if (height > sizeLimit.getHeight()) {
double ratio = Ratio.get(width, height);
long newHeight = sizeLimit.getHeight();
long newWidth = Math.round(newHeight * ratio);
return new Dimension(newWidth, newHeight);
}
return new Dimension(width, height);
}
/**
* Splits dynamic media folder and file name and URL-encodes them separately (may contain spaces or special chars).
* @param damContext DAM context
* @return Encoded path
*/
private static String encodeDynamicMediaObject(@NotNull DamContext damContext) {
String[] pathParts = StringUtils.split(damContext.getDynamicMediaObject(), "/");
for (int i = 0; i < pathParts.length; i++) {
pathParts[i] = URLEncoder.encode(pathParts[i], StandardCharsets.UTF_8);
// replace "+" with %20 in URL paths
pathParts[i] = StringUtils.replace(pathParts[i], "+", "%20");
}
return StringUtils.join(pathParts, "/");
}
private static boolean isPNG(@NotNull DamContext damContext) {
return StringUtils.equals(damContext.getAsset().getMimeType(), ContentType.PNG);
}
}