/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2013 Adobe * %% * 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 com.adobe.acs.commons.images.transformers.impl; import com.adobe.acs.commons.images.ImageTransformer; import com.day.image.Layer; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.resource.ValueMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Rectangle; /** * ACS AEM Commons - Image Transformer - Crop with Smart-Bounding */ @Component @Property( name = ImageTransformer.PROP_TYPE, value = CropImageTransformerImpl.TYPE) @Service public class CropImageTransformerImpl implements ImageTransformer { private static final Logger log = LoggerFactory.getLogger(CropImageTransformerImpl.class); static final String TYPE = "crop"; private static final String KEY_BOUNDS = "bounds"; private static final String KEY_SMART_BOUNDING = "smart"; private static final int NUM_BOUNDS_PARAMS = 4; private static final int PARAM_INDEX_X = 0; private static final int PARAM_INDEX_Y = 1; private static final int PARAM_INDEX_WIDTH = 2; private static final int PARAM_INDEX_HEIGHT = 3; @Override public final Layer transform(final Layer layer, final ValueMap properties) { if (properties == null || properties.isEmpty()) { log.warn("Transform [ {} ] requires parameters.", TYPE); return layer; } log.debug("Transforming with [ {} ]", TYPE); final boolean smartBounding = properties.get(KEY_SMART_BOUNDING, true); final String[] bounds = StringUtils.split(properties.get(KEY_BOUNDS, ""), ","); if (bounds.length == NUM_BOUNDS_PARAMS) { int x = parseLength(bounds[PARAM_INDEX_X], layer.getWidth()); int y = parseLength(bounds[PARAM_INDEX_Y], layer.getHeight()); int width = parseLength(bounds[PARAM_INDEX_WIDTH], layer.getWidth()); int height = parseLength(bounds[PARAM_INDEX_HEIGHT], layer.getHeight()); Rectangle rectangle = new Rectangle(); if (smartBounding) { rectangle = this.getSmartBounds(x, y, width, height, layer.getWidth(), layer.getHeight()); } else { rectangle.setBounds(x, y, width, height); } layer.crop(rectangle); if (smartBounding && layer.getWidth() != width || layer.getHeight() != height) { log.debug("SmartBounding resulted in an image of an incorrect size (based on crop params). " + "resizing to: [ width: {}, height: {} ]", width, height); layer.resize(width, height); } } return layer; } private Rectangle getSmartBounds(int x, int y, int width, int height, int layerWidth, int layerHeight) { final Rectangle rectangle = new Rectangle(); final int x2 = x + width; final int y2 = y + height; if (x2 >= layerWidth && y2 >= layerHeight) { // Both exceed, pick the dimension to use as the "master" clip final float clipPercentX = ((float) (x2 - layerWidth) / width); final float clipPercentY = ((float) (y2 - layerHeight) / height); if (clipPercentX >= clipPercentY) { // The proportional amount to clip is greatest on the x-axis, so trim both dimensions by the same % return constrainByWidth(x, y, width, height, layerWidth, x2); } else { // The proportional amount to clip is greatest on the y-axis, so trim both dimensions by the same % return constrainByHeight(x, y, width, height, layerHeight, y2); } } else if (x2 >= layerWidth && y2 < layerHeight) { return constrainByWidth(x, y, width, height, layerWidth, x2); } else if (x2 < layerWidth && y2 >= layerHeight) { return constrainByHeight(x, y, width, height, layerHeight, y2); } // Crop within layer size rectangle.setBounds(x, y, width, height); return rectangle; } private Rectangle constrainByHeight(final int x, final int y, final int width, final int height, final int layerHeight, final int y2) { final Rectangle rectangle = new Rectangle(); // Compute amount of overflow for y (that requires clipping) final int deltaY = y2 - layerHeight; // Compute amount to clip width (X) constrained by the % of total width that was removed for deltaY // (Amount clipped from Y should be proportionately equals to amount clipped from Y) int deltaX = Math.round(((float) deltaY / height) * width); // Set the bounds to be the new clipped width/height rectangle.setBounds(x, y, width - deltaX, height - deltaY); return rectangle; } private Rectangle constrainByWidth(final int x, final int y, final int width, final int height, final int layerWidth, final int x2) { final Rectangle rectangle = new Rectangle(); // Compute amount of overflow for x (that requires clipping) int deltaX = x2 - layerWidth; // Compute amount to clip height (Y) constrained by the % of total width that was removed for deltaX // (Amount clipped from Y should be proportionately equals to amount clipped from X) int deltaY = Math.round(((float) deltaX / width) * height); // Set the bounds to be the new clipped width/height rectangle.setBounds(x, y, width - deltaX, height - deltaY); return rectangle; } private static int parseLength(String lengthStr, int totalLength) { int lengthPx; if (lengthStr.endsWith("%")) { String percentageStr = lengthStr.substring(0, lengthStr.length() - 1); double percentage = Double.parseDouble(percentageStr); lengthPx = (int) Math.round((percentage / 100) * totalLength); } else { lengthPx = Integer.parseInt(lengthStr); } return lengthPx; } }