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);
- }
- }