package com.psddev.cms.db; import java.io.IOException; import java.io.StringWriter; import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.PageContext; import javax.servlet.jsp.tagext.DynamicAttributes; import javax.servlet.jsp.tagext.TagSupport; import com.psddev.dari.util.CompactMap; import com.psddev.dari.util.HtmlWriter; import com.psddev.dari.util.ImageEditorPrivateUrl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.psddev.cms.tool.CmsTool; import com.psddev.dari.db.Application; import com.psddev.dari.db.ObjectField; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Recordable; import com.psddev.dari.db.State; import com.psddev.dari.util.CollectionUtils; import com.psddev.dari.util.ImageEditor; import com.psddev.dari.util.ImageResizeStorageItemListener; import com.psddev.dari.util.JspUtils; import com.psddev.dari.util.ObjectMap; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.Settings; import com.psddev.dari.util.StorageItem; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.TypeReference; import com.psddev.dari.util.WebPageContext; /** * Equivalent to the HTML {@code img} tag where its {@code src} attribute * may be set to a URL or a StorageItem object. */ @SuppressWarnings("serial") public class ImageTag extends TagSupport implements DynamicAttributes { public static final String HOTSPOT_CLASS = "com.psddev.image.HotSpots"; protected static final Logger LOGGER = LoggerFactory.getLogger(ImageTag.class); protected static final String ORIGINAL_WIDTH_METADATA_PATH = "image/originalWidth"; protected static final String ORIGINAL_HEIGHT_METADATA_PATH = "image/originalHeight"; protected Builder tagBuilder = new Builder(); private static Boolean useHotSpotCrop; private static boolean useHotSpotCrop() { if (useHotSpotCrop == null) { if (ObjectUtils.getClassByName(HOTSPOT_CLASS) != null) { useHotSpotCrop = Settings.getOrDefault(Boolean.class, "cms/image/useHotSpotCrop", Boolean.TRUE); } else { useHotSpotCrop = false; } } return useHotSpotCrop; } /** * Sets the source object, which may be either a URL or a Dari * object. */ public void setSrc(Object src) { WebPageContext wp = new WebPageContext(pageContext); if (src instanceof String || src instanceof URI || src instanceof URL) { String path = JspUtils.resolvePath(wp.getServletContext(), wp.getRequest(), src.toString()); StorageItem pathItem; if (path.startsWith("/")) { pathItem = StorageItem.Static.createUrl(JspUtils.getAbsoluteUrl(wp.getRequest(), path)); } else { pathItem = StorageItem.Static.createUrl(path); } tagBuilder.setItem(pathItem); } else if (src instanceof StorageItem) { StorageItem item = (StorageItem) src; //Resets StorageItem's MetaData size on subsequent calls to the same storage Item if (CollectionUtils.getByPath(item.getMetadata(), ImageTag.ORIGINAL_HEIGHT_METADATA_PATH) != null) { item.getMetadata().put("height", CollectionUtils.getByPath(item.getMetadata(), ImageTag.ORIGINAL_HEIGHT_METADATA_PATH)); } if (CollectionUtils.getByPath(item.getMetadata(), ImageTag.ORIGINAL_WIDTH_METADATA_PATH) != null) { item.getMetadata().put("width", CollectionUtils.getByPath(item.getMetadata(), ImageTag.ORIGINAL_WIDTH_METADATA_PATH)); } tagBuilder.setItem(item); } else if (src instanceof State || src instanceof Recordable) { // -- Hack to ensure backwards compatibility String field = (String) super.getValue("field"); tagBuilder.setField(field); if (src instanceof State) { tagBuilder.setState((State) src); } else if (src instanceof Recordable) { tagBuilder.setRecordable((Recordable) src); } // -- End hack } } /** * Sets the field that contains the image. If not set, the first * field with {@value ObjectField#FILE_TYPE} type is used. * @deprecated No replacement */ @Deprecated public void setField(String field) { tagBuilder.setField(field); } /** * Sets the name of the {@linkplain ImageEditor image editor} * to use. */ public void setEditor(Object object) { ImageEditor editor = null; if (object instanceof ImageEditor) { editor = (ImageEditor) object; } else if (object instanceof String) { editor = ImageEditor.Static.getInstance((String) object); } else { editor = ImageEditor.Static.getDefault(); } tagBuilder.setEditor(editor); } /** * Sets the internal name of the {@linkplain StandardImageSize * image size} to use. */ public void setSize(Object size) { if (size instanceof StandardImageSize) { tagBuilder.setStandardImageSize((StandardImageSize) size); } else if (size instanceof String) { tagBuilder.setStandardImageSize(getStandardImageSizeByName((String) size)); } } /** * Sets the width. Note that this will override the width provided * by the image size set with {@link #setSize(Object)}. */ public void setWidth(String width) { if (width != null && width.endsWith("px")) { width = width.substring(0, width.length() - 2); } tagBuilder.setWidth(ObjectUtils.to(Integer.class, width)); } /** * Sets the height. Note that this will override the height provided * by the image size set with {@link #setSize(Object)}. */ public void setHeight(String height) { if (height != null && height.endsWith("px")) { height = height.substring(0, height.length() - 2); } tagBuilder.setHeight(ObjectUtils.to(Integer.class, height)); } /** * Sets the crop option. Note that this will override the crop option * provided by the image size set with {@link #setSize(Object)}. */ public void setCropOption(Object cropOptionObject) { CropOption cropOption = null; if (cropOptionObject instanceof CropOption) { cropOption = (CropOption) cropOptionObject; } else if (cropOptionObject instanceof String) { cropOption = CropOption.Static.fromImageEditorOption((String) cropOptionObject); } tagBuilder.setCropOption(cropOption); } /** * Sets the resize option. Note that this will override the resize option * provided by the image size set with {@link #setSize(Object)}. */ public void setResizeOption(Object resizeOptionObject) { ResizeOption resizeOption = null; if (resizeOptionObject instanceof ResizeOption) { resizeOption = (ResizeOption) resizeOptionObject; } else if (resizeOptionObject instanceof String) { resizeOption = ResizeOption.Static.fromImageEditorOption((String) resizeOptionObject); } tagBuilder.setResizeOption(resizeOption); } public void setTagName(String tagName) { tagBuilder.setTagName(tagName); } /** * Overrides the default attribute (src) used to place the image URL. This * is usually used in the conjunction with lazy loading scripts that copy * the image URL from this attribute to the "src" attribute at some point * after the page has loaded. */ public void setSrcAttr(String srcAttr) { tagBuilder.setSrcAttribute(srcAttr); } /** * When set to {@code true}, suppresses the "width" and "height" attributes * from the final HTML output. */ public void setHideDimensions(Object hideDimensions) { if (ObjectUtils.to(boolean.class, hideDimensions)) { tagBuilder.hideDimensions(); } } public void setOverlay(Object overlay) { tagBuilder.setOverlay(ObjectUtils.to(boolean.class, overlay)); } /** * When set to {@code true}, suppresses automatic cropping using hot spots * @param disableHotSpotCrop */ public void setDisableHotSpotCrop(Object disableHotSpotCrop) { tagBuilder.setDisableHotSpotCrop(ObjectUtils.to(boolean.class, disableHotSpotCrop)); } // --- DynamicAttribute support --- @Override public void setDynamicAttribute(String uri, String localName, Object value) { tagBuilder.addAttribute(localName, value); } // --- TagSupport support --- @Override public int doStartTag() throws JspException { JspWriter writer = pageContext.getOut(); try { writer.print(tagBuilder.toHtml()); } catch (IOException e) { throw new JspException(e); } finally { tagBuilder.reset(); } return SKIP_BODY; } private static final Set<String> VOID_ELEMENTS = new HashSet<String>(Arrays.asList( "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr")); private static String convertAttributesToHtml(String tagName, Map<String, String> attributes) { StringBuilder builder = new StringBuilder(); if (!attributes.isEmpty()) { if (tagName == null) { tagName = "img"; } builder.append("<"); builder.append(tagName); for (Map.Entry<String, String> e : attributes.entrySet()) { String key = e.getKey(); String value = e.getValue(); if (!(ObjectUtils.isBlank(key) || ObjectUtils.isBlank(value))) { builder.append(" "); builder.append(StringUtils.escapeHtml(key)); builder.append("=\""); builder.append(StringUtils.escapeHtml(value)); builder.append("\""); } } if (VOID_ELEMENTS.contains(tagName.toLowerCase(Locale.ENGLISH))) { if (Settings.get(boolean.class, "dari/selfClosingElements")) { builder.append('/'); } builder.append('>'); } else { builder.append("></"); builder.append(tagName); builder.append(">"); } } return builder.toString(); } /** * Finds the dimension {@code name} ("width", or "height") for the given * StorageItem {@code item}. */ protected static Integer findDimension(StorageItem item, String name) { if (item == null) { return null; } Integer dimension = null; Map<String, Object> metadata = item.getMetadata(); if (metadata != null) { dimension = ObjectUtils.to(Integer.class, metadata.get(name)); if (dimension == null || dimension == 0) { dimension = null; } } return dimension; } /** * Returns {@code true} if the specified ImageCrop has any boundaries outside the region (0.0,0.0) to (1.0,1.0). * @param imageCrop * @return {@code true} if the specified ImageCrop will be processed as a padded crop. */ protected static boolean isPaddedCrop(ImageCrop imageCrop) { return imageCrop.getX() < 0.0 || imageCrop.getY() < 0.0 || imageCrop.getX() + imageCrop.getWidth() > 1.0 || imageCrop.getY() + imageCrop.getHeight() > 1.0; } /** * Returns a new ImageCrop instance, constrained to the region (0.0,0.0) to (1.0,1.0). * @param imageCrop an unconstrained ImageCrop * @return a constrained ImageCrop */ protected static ImageCrop getPaddedCrop(ImageCrop imageCrop) { ImageCrop paddedCrop = new ImageCrop(); paddedCrop.getState().putAll(imageCrop.getState().getValues()); paddedCrop.setX(Math.min(Math.max(imageCrop.getX(), 0.0), 1.0)); paddedCrop.setY(Math.min(Math.max(imageCrop.getY(), 0.0), 1.0)); paddedCrop.setWidth(Math.min(Math.max(imageCrop.getX() + imageCrop.getWidth(), 0.0), 1.0) - paddedCrop.getX()); paddedCrop.setHeight(Math.min(Math.max(imageCrop.getY() + imageCrop.getHeight(), 0.0), 1.0) - paddedCrop.getY()); return paddedCrop; } protected static ImageCrop getFocusCrop(StorageItem item, StandardImageSize standardImageSize) { if (item == null || ObjectUtils.isBlank(item.getMetadata()) || standardImageSize == null) { return null; } CompactMap<String, Double> focusPoint = ObjectUtils.to( new TypeReference<CompactMap<String, Double>>() { }, item.getMetadata().get("cms.focus")); if (ObjectUtils.isBlank(focusPoint)) { return null; } Double focusX = ObjectUtils.to(Double.class, focusPoint.get("x")); Double focusY = ObjectUtils.to(Double.class, focusPoint.get("y")); if (focusX == null || focusY == null) { return null; } // Handle legacy focus point values set as // percentage instead of value from 0 - 1 if (focusX > 1 && focusX < 100) { focusX /= 100; } if (focusY > 1 && focusY < 100) { focusY /= 100; } Integer imageWidth = findDimension(item, "width"); Integer imageHeight = findDimension(item, "height"); Double imageAspectRatio = null; if (imageWidth != null && imageHeight != null) { imageAspectRatio = (double) imageWidth / (double) imageHeight; } if (imageAspectRatio == null) { return null; } Integer sizeWidth = standardImageSize.getWidth(); Integer sizeHeight = standardImageSize.getHeight(); if (sizeWidth <= 0 || sizeHeight <= 0) { return null; } Double sizeAspectRatio = (double) sizeWidth / (double) sizeHeight; ImageCrop crop = new ImageCrop(); Double adjustedValue; Double adjustedDifference; Double adjustedPercentage; Double focusDifference; if (imageAspectRatio > sizeAspectRatio) { adjustedValue = imageHeight * sizeAspectRatio; adjustedDifference = imageWidth - adjustedValue; adjustedPercentage = adjustedDifference / imageWidth; crop.setWidth(1 - adjustedPercentage); crop.setHeight(1); Double tempX = adjustedPercentage / 2; focusDifference = Math.max(tempX - (0.5 - focusX), 0); if (focusDifference + crop.getWidth() > 1) { focusDifference = 1 - crop.getWidth(); } crop.setX(focusDifference); } else if (imageAspectRatio < sizeAspectRatio) { adjustedValue = imageWidth / sizeAspectRatio; adjustedDifference = imageHeight - adjustedValue; adjustedPercentage = adjustedDifference / imageHeight; crop.setHeight(1 - adjustedPercentage); crop.setWidth(1); Double tempY = adjustedPercentage / 2; focusDifference = Math.max(tempY - (0.5 - focusY), 0); if (focusDifference + crop.getHeight() > 1) { focusDifference = 1 - crop.getHeight(); } crop.setY(focusDifference); } return crop; } /** * Finds the crop information for the StorageItem {@code item}. */ protected static Map<String, ImageCrop> findImageCrops(StorageItem item) { if (item == null) { return null; } Map<String, ImageCrop> crops = null; Map<String, Object> metadata = item.getMetadata(); if (metadata != null) { crops = ObjectUtils.to(new TypeReference<Map<String, ImageCrop>>() { }, metadata.get("cms.crops")); } if (crops == null) { crops = new HashMap<String, ImageCrop>(); } return crops; } /** * Finds the StorageItem that best matches the provided size. This works * in conjunction with the ImageResizeStorageItemListener class to use a * presized image that is smaller than the original image in an effort to * improve resize performance. */ private static StorageItem findStorageItemForSize(StorageItem item, Integer width, Integer height) { if (width == null || height == null) { return item; } StorageItem override = StorageItem.Static.createIn(item.getStorage()); new ObjectMap(override).putAll(new ObjectMap(item)); CollectionUtils.putByPath(override.getMetadata(), ORIGINAL_WIDTH_METADATA_PATH, ImageTag.findDimension(item, "width")); CollectionUtils.putByPath(override.getMetadata(), ORIGINAL_HEIGHT_METADATA_PATH, ImageTag.findDimension(item, "height")); boolean overridden = ImageResizeStorageItemListener.overridePathWithNearestSize(override, width, height); if (overridden) { return override; } return item; } /** * @deprecated No replacement */ @Deprecated private static Map<String, String> getAttributes(WebPageContext wp, Object src, String field, ImageEditor editor, StandardImageSize standardSize, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption, String srcAttr, Map<String, String> dynamicAttributes) { Builder tagBuilder = new Builder(); if (src instanceof String || src instanceof URI || src instanceof URL) { tagBuilder.setItem(StorageItem.Static.createUrl( JspUtils.getEmbeddedAbsolutePath( wp.getServletContext(), wp.getRequest(), src.toString()))); } else if (src instanceof StorageItem) { tagBuilder.setItem((StorageItem) src); } else if (src instanceof State || src instanceof Recordable) { // -- Hack to ensure backwards compatibility tagBuilder.setField(field); if (src instanceof State) { tagBuilder.setState((State) src); } else if (src instanceof Recordable) { tagBuilder.setRecordable((Recordable) src); } // -- End hack } return tagBuilder.setEditor(editor) .setStandardImageSize(standardSize) .setWidth(width) .setHeight(height) .setCropOption(cropOption) .setResizeOption(resizeOption) .setSrcAttribute(srcAttr) .addAllAttributes(dynamicAttributes) .toAttributes(); } /** * @deprecated No replacement */ @Deprecated private static String findStorageItemField(State state) { String field = null; ObjectType objectType = state.getType(); if (objectType != null) { for (ObjectField objectField : objectType.getFields()) { if (ObjectField.FILE_TYPE.equals(objectField.getInternalType())) { field = objectField.getInternalName(); break; } } } return field; } /** * @deprecated No replacement */ @Deprecated private static StorageItem findStorageItem(State state, String field) { StorageItem item = null; if (field != null) { Object fieldValue = state.get(field); if (fieldValue instanceof StorageItem) { item = (StorageItem) fieldValue; } } return item; } /** * @deprecated Use {@link #findImageCrops(StorageItem)} instead */ @Deprecated private static Map<String, ImageCrop> findImageCrops(State state, String field) { Map<String, ImageCrop> crops = null; Object fieldValue = state.get(field); if (fieldValue instanceof StorageItem) { crops = findImageCrops((StorageItem) fieldValue); } if (crops == null || crops.isEmpty()) { crops = ObjectUtils.to(new TypeReference<Map<String, ImageCrop>>() { }, state.getValue(field + ".crops")); } if (crops == null) { crops = new HashMap<String, ImageCrop>(); } return crops; } /** * Finds the dimension value with the given {@code field} and * {@code name} from the given {@code state}. * @deprecated Use {@link #findDimension(StorageItem, String)} instead. */ @Deprecated private static Integer findDimension(State state, String field, String name) { Integer dimension = null; Object fieldValue = state.get(field); if (fieldValue instanceof StorageItem) { dimension = findDimension((StorageItem) fieldValue, name); } if (dimension == null || dimension == 0) { dimension = ObjectUtils.to(Integer.class, state.getValue(field + ".metadata/" + name)); if (dimension == null || dimension == 0) { dimension = ObjectUtils.to(Integer.class, state.getValue(field + "." + name)); if (dimension == null || dimension == 0) { dimension = null; } } } return dimension; } protected static StandardImageSize getStandardImageSizeByName(String size) { StandardImageSize standardImageSize = null; for (StandardImageSize standardSize : StandardImageSize.findAll()) { if (standardSize.getInternalName().equals(size)) { standardImageSize = standardSize; break; } } return standardImageSize; } /** * <p>Static utility class for building HTML 'img' tags and URLs edited by * an {@link ImageEditor}. This class is functionally equivalent to calling * the JSTL <cms:img> tag in your JSP code. Example usage:</p> * <pre> * StorageItem myStorageItem; * * String imageTagHtml = new ImageTag.Builder(myStorageItem) * .setWidth(300) * .setHeight(200) * .addAttribute("class", "thumbnail") * .addAttribute("alt", "My image") * .toHtml(); * </pre> * You can also grab just the image URL instead of the entire HTML output * by calling: * <pre> * String imageUrl = new ImageTag.Builder(myStorageItem) * .setWidth(300) * .setHeight(200) * .toUrl() * </pre> */ public static final class Builder { private StorageItem item; @Deprecated private String field; private ImageEditor editor; private StandardImageSize standardImageSize; private Integer width; private Integer height; private CropOption cropOption; private ResizeOption resizeOption; private String tagName; private String srcAttribute; private boolean hideDimensions; private boolean overlay; private boolean disableHotSpotCrop; private boolean edits = true; private boolean privateUrl = false; private final Map<String, String> attributes = new LinkedHashMap<String, String>(); // for backwards compatibility private State state; public Builder(StorageItem item) { this.item = item; } private Builder() { } public StorageItem getItem() { return this.item; } protected Builder setItem(StorageItem item) { this.item = item; return this; } /** Resets all fields back to null */ private void reset() { item = null; field = null; editor = null; standardImageSize = null; width = null; height = null; cropOption = null; resizeOption = null; tagName = null; srcAttribute = null; hideDimensions = false; disableHotSpotCrop = false; privateUrl = false; attributes.clear(); state = null; } /** * Sets the field that contains the image. If not set, the first * field with {@value ObjectField#FILE_TYPE} type is used. * @deprecated No replacement */ @Deprecated private Builder setField(String field) { this.field = field; return this; } /** * Sets the name of the {@linkplain ImageEditor image editor} * to use. */ public Builder setEditor(ImageEditor editor) { this.editor = editor; return this; } /** * Sets the internal name of the {@linkplain StandardImageSize * image size} to use. */ public Builder setStandardImageSize(StandardImageSize standardImageSize) { this.standardImageSize = standardImageSize; return this; } /** * Sets the width. Note that this will override the width provided * by the image size set with {@link #setSize(Object)}. */ public Builder setWidth(Integer width) { this.width = width; return this; } /** * Sets the height. Note that this will override the height provided * by the image size set with {@link #setSize(Object)}. */ public Builder setHeight(Integer height) { this.height = height; return this; } /** * Sets the crop option. Note that this will override the crop option * provided by the image size set with {@link #setSize(Object)}. */ public Builder setCropOption(CropOption cropOption) { this.cropOption = cropOption; return this; } /** * Sets the resize option. Note that this will override the resize option * provided by the image size set with {@link #setSize(Object)}. */ public Builder setResizeOption(ResizeOption resizeOption) { this.resizeOption = resizeOption; return this; } public Builder setTagName(String tagName) { this.tagName = tagName; return this; } /** * Overrides the default attribute (src) used to place the image URL. This * is usually used in the conjunction with lazy loading scripts that copy * the image URL from this attribute to the "src" attribute at some point * after the page has loaded. */ public Builder setSrcAttribute(String srcAttribute) { this.srcAttribute = srcAttribute; return this; } /** * Set to true if the resulting image dimensions should be removed * from the final tag output. */ public Builder hideDimensions() { this.hideDimensions = true; return this; } public boolean isOverlay() { return overlay; } public void setOverlay(boolean overlay) { this.overlay = overlay; } public boolean isDisableHotSpotCrop() { return disableHotSpotCrop; } public void setDisableHotSpotCrop(boolean disableHotSpotCrop) { this.disableHotSpotCrop = disableHotSpotCrop; } public boolean isEdits() { return edits; } public Builder setEdits(boolean edits) { this.edits = edits; return this; } public boolean isPrivateUrl() { return privateUrl; } public Builder setPrivateUrl(boolean privateUrl) { this.privateUrl = privateUrl; return this; } /** * Adds an attribute to be placed on the tag. */ public Builder addAttribute(String name, Object value) { this.attributes.put(name, value != null ? value.toString() : null); return this; } /** * Adds all the attributes to be placed on the tag. */ public Builder addAllAttributes(Map<String, ?> attributes) { if (attributes != null) { for (Map.Entry<String, ?> entry : attributes.entrySet()) { addAttribute(entry.getKey(), entry.getValue()); } } return this; } /** * For backwards compatibility * * @deprecated */ @Deprecated private Builder setState(State state) { this.state = state; return this; } /** * For backwards compatibility * * @deprecated */ @Deprecated private Builder setRecordable(Recordable recordable) { setState(State.getInstance(recordable)); return this; } /** * * @return the HTML for an img tag constructed by this Builder. */ public String toHtml() { String html = convertAttributesToHtml(tagName, toAttributes()); StorageItem item = null; Map<String, ImageCrop> crops = null; if (this.state != null) { State objectState = this.state; String field = this.field; if (ObjectUtils.isBlank(field)) { field = findStorageItemField(objectState); } item = findStorageItem(objectState, field); if (item != null) { crops = findImageCrops(objectState, field); } } else { item = this.item; if (item != null) { crops = findImageCrops(item); } } ImageCrop crop = null; if (item != null && crops != null && standardImageSize != null) { crop = crops.get(standardImageSize.getId().toString()); } if (crop != null) { ImageCrop originalCrop = crop; boolean isPaddedCrop = isPaddedCrop(crop); if (isPaddedCrop || isOverlay()) { String id = "i" + UUID.randomUUID().toString().replace("-", ""); // START LOGIC TO DETERMINE DISPLAY SIZE // set fields from this standard size if they haven't already been set Integer width = this.width; Integer height = this.height; width = width != null && width <= 0 ? null : width; height = height != null && height <= 0 ? null : height; Integer originalWidth = null; Integer originalHeight = null; if (standardImageSize != null) { Integer standardWidth = standardImageSize.getWidth(); Integer standardHeight = standardImageSize.getHeight(); if (standardWidth <= 0) { standardWidth = null; } if (standardHeight <= 0) { standardHeight = null; } Double standardAspectRatio = null; if (standardWidth != null && standardHeight != null) { standardAspectRatio = (double) standardWidth / (double) standardHeight; } // if only one of either width or height is set then calculate // the other dimension based on the standardImageSize aspect // ratio rather than blindly taking the other standardImageSize // dimension. if (standardAspectRatio != null && (width != null || height != null)) { if (width != null && height == null) { height = (int) (width / standardAspectRatio); } else if (width == null && height != null) { width = (int) (height * standardAspectRatio); } } else { // get the standard image dimensions if (width == null) { width = standardWidth; } if (height == null) { height = standardHeight; } } // get the crop and resize options if (cropOption == null) { cropOption = standardImageSize.getCropOption(); } if (resizeOption == null) { resizeOption = standardImageSize.getResizeOption(); } // get a potentially smaller image from the StorageItem. This improves // resize performance on large images. StorageItem alternateItem = findStorageItemForSize(item, width, height); if (alternateItem != item) { item = alternateItem; originalHeight = findDimension(item, "height"); originalWidth = findDimension(item, "width"); } // get the crop coordinates if (originalWidth != null && originalHeight != null) { // Handles standard image size with height or width of 0 if (height == null && width != null) { height = (int) (width / (originalCrop.getWidth() / originalCrop.getHeight())); } else if (width == null && height != null) { width = (int) (height * (originalCrop.getWidth() / originalCrop.getHeight())); } } } // END LOGIC TO DETERMINE DISPLAY SIZE String overlayCss = "#" + id + "{display:inline-block;overflow:hidden;position:relative;width:" + width + "px;height:" + height + "px;}"; if (isPaddedCrop) { crop = getPaddedCrop(crop); // Calculate the CSS padding-top for use with in rendering padded crop HTML. double paddingTop = originalCrop.getY() < 0 ? (-originalCrop.getY() / originalCrop.getHeight()) : 0; // Calculate the CSS padding-left for use with in rendering padded crop HTML. double paddingLeft = originalCrop.getX() < 0 ? (-originalCrop.getX() / originalCrop.getWidth()) : 0; double paddingRight = (originalCrop.getX() + originalCrop.getWidth()) > 1 ? 1 - paddingLeft - crop.getWidth() / originalCrop.getWidth() : 0; double paddingBottom = (originalCrop.getY() + originalCrop.getHeight()) > 1 ? 1 - paddingTop - crop.getHeight() / originalCrop.getHeight() : 0; StringWriter paddedCropWriter = new StringWriter(); HtmlWriter paddedCropHtml = new HtmlWriter(paddedCropWriter); try { paddedCropHtml.writeStart("span", "style", "display: inline-block;" + "overflow: hidden;" + "position: absolute;" + "top: " + paddingTop * 100 + "%;" + "right: " + paddingRight * 100 + "%;" + "bottom: " + paddingBottom * 100 + "%;" + "left: " + paddingLeft * 100 + "%;"); paddedCropHtml.writeRaw(html); paddedCropHtml.writeEnd(); } catch (IOException e) { // Ignore. } html = paddedCropWriter.toString(); } if (isOverlay()) { List<ImageTextOverlay> textOverlays = crop.getTextOverlays(); boolean hasOverlays = false; for (ImageTextOverlay textOverlay : textOverlays) { if (!ObjectUtils.isBlank(textOverlay.getText())) { hasOverlays = true; break; } } if (hasOverlays) { String overlayHtml = ""; CmsTool cms = Application.Static.getInstance(CmsTool.class); String defaultCss = cms.getDefaultTextOverlayCss(); if (!ObjectUtils.isBlank(defaultCss)) { overlayCss += "#" + id + "{" + defaultCss + "}"; } for (CmsTool.CssClassGroup group : cms.getTextCssClassGroups()) { String groupName = group.getInternalName(); for (CmsTool.CssClass cssClass : group.getCssClasses()) { overlayCss += "#" + id + " .cms-" + groupName + "-" + cssClass.getInternalName() + "{" + cssClass.getCss() + "}"; } } for (ImageTextOverlay textOverlay : textOverlays) { String text = textOverlay.getText(); overlayHtml += "<span style=\""; overlayHtml += "left: " + textOverlay.getX() * 100 + "%;"; overlayHtml += "position: absolute;"; overlayHtml += "top: " + textOverlay.getY() * 100 + "%;"; overlayHtml += "font-size: " + textOverlay.getSize() * standardImageSize.getHeight() + "px;"; overlayHtml += "width: " + (textOverlay.getWidth() != 0.0 ? textOverlay.getWidth() * 100 : 100.0) + "%;\">"; overlayHtml += text + "</span>"; } html += overlayHtml; } } html = "<style type=\"text/css\">" + overlayCss + "</style><span id=\"" + id + "\">" + html + "</span>"; } } return html; } /** * * @return the URL to the image as a String. */ public String toUrl() { return toAttributes().get(srcAttribute != null ? srcAttribute : "src"); } /** Returns all the attributes that will get placed on the img tag. */ public Map<String, String> toAttributes() { // set all the attributes Map<String, String> attributes = new LinkedHashMap<String, String>(); ImageEditor editor = this.editor; StandardImageSize standardImageSize = this.standardImageSize; Integer width = this.width; Integer height = this.height; CropOption cropOption = this.cropOption; ResizeOption resizeOption = this.resizeOption; String srcAttr = this.srcAttribute; boolean hideDimensions = this.hideDimensions; StorageItem item = null; Integer originalWidth = null; Integer originalHeight = null; Map<String, ImageCrop> crops = null; if (this.state != null) { // backwards compatibility path State objectState = this.state; String field = this.field; if (ObjectUtils.isBlank(field)) { field = findStorageItemField(objectState); } item = findStorageItem(objectState, field); if (item != null) { originalWidth = findDimension(objectState, field, "width"); originalHeight = findDimension(objectState, field, "height"); crops = findImageCrops(objectState, field); } } else { // new code path item = this.item; if (item != null) { originalWidth = findDimension(item, "width"); originalHeight = findDimension(item, "height"); crops = findImageCrops(item); } } // null out all dimensions that are less than or equal to zero originalWidth = originalWidth != null && originalWidth <= 0 ? null : originalWidth; originalHeight = originalHeight != null && originalHeight <= 0 ? null : originalHeight; width = width != null && width <= 0 ? null : width; height = height != null && height <= 0 ? null : height; if (item != null) { Map<String, Object> options = new LinkedHashMap<String, Object>(); Integer cropX = null, cropY = null, cropWidth = null, cropHeight = null; // set fields from this standard size if they haven't already been set if (standardImageSize != null) { Integer standardWidth = standardImageSize.getWidth(); Integer standardHeight = standardImageSize.getHeight(); if (standardWidth <= 0) { standardWidth = null; } if (standardHeight <= 0) { standardHeight = null; } Double standardAspectRatio = null; if (standardWidth != null && standardHeight != null) { standardAspectRatio = (double) standardWidth / (double) standardHeight; } // if only one of either width or height is set then calculate // the other dimension based on the standardImageSize aspect // ratio rather than blindly taking the other standardImageSize // dimension. if (standardAspectRatio != null && (width != null || height != null)) { if (width != null && height == null) { height = (int) (width / standardAspectRatio); } else if (width == null && height != null) { width = (int) (height * standardAspectRatio); } } else { // get the standard image dimensions if (width == null) { width = standardWidth; } if (height == null) { height = standardHeight; } } // get the crop and resize options if (cropOption == null) { cropOption = standardImageSize.getCropOption(); } if (resizeOption == null) { resizeOption = standardImageSize.getResizeOption(); } // get a potentially smaller image from the StorageItem. This improves // resize performance on large images. StorageItem alternateItem = findStorageItemForSize(item, width, height); if (alternateItem != item) { item = alternateItem; originalWidth = findDimension(item, "width"); originalHeight = findDimension(item, "height"); } // get the crop coordinates if (crops != null) { if (originalWidth != null && originalHeight != null) { ImageCrop crop = crops.get(standardImageSize.getId().toString()); if (crop != null) { boolean isPaddedCrop = isPaddedCrop(crop); ImageCrop originalCrop = crop; if (isPaddedCrop) { crop = getPaddedCrop(crop); } cropX = (int) (crop.getX() * originalWidth); cropY = (int) (crop.getY() * originalHeight); cropWidth = (int) (crop.getWidth() * originalWidth); cropHeight = (int) (crop.getHeight() * originalHeight); // Handles standard image size with height or width of 0 if (height == null && width != null) { height = (int) (width / (originalCrop.getWidth() / originalCrop.getHeight())); } else if (width == null && height != null) { width = (int) (height * (originalCrop.getWidth() / originalCrop.getHeight())); } if (isPaddedCrop && height != null && width != null) { height = (int) ((double) height * crop.getHeight() / originalCrop.getHeight()); width = (int) ((double) width * crop.getWidth() / originalCrop.getWidth()); } else { // This accounts for rounding error in crop width or height if (standardAspectRatio != null) { cropHeight = (int) Math.round(cropWidth / standardAspectRatio); } } } else { crop = getFocusCrop(item, standardImageSize); if (crop != null) { cropX = (int) (crop.getX() * originalWidth); cropY = (int) (crop.getY() * originalHeight); cropWidth = (int) (crop.getWidth() * originalWidth); if (standardAspectRatio != null) { cropHeight = (int) Math.round(cropWidth / standardAspectRatio); } else { cropHeight = (int) (crop.getHeight() * originalHeight); } } } } } } // if the crop info is unavailable, assume that the image // dimensions are the crop dimensions in case the image editor // knows how to crop without the x & y coordinates if (cropWidth == null) { cropWidth = width; } if (cropHeight == null) { cropHeight = height; } // set the options if (cropOption != null) { options.put(ImageEditor.CROP_OPTION, cropOption.getImageEditorOption()); } if (resizeOption != null) { options.put(ImageEditor.RESIZE_OPTION, resizeOption.getImageEditorOption()); } if (privateUrl) { options.put(ImageEditorPrivateUrl.PRIVATE_URL_OPTION, true); } if (isEdits()) { @SuppressWarnings("unchecked") Map<String, Object> edits = (Map<String, Object>) item.getMetadata().get("cms.edits"); if (edits != null) { ImageEditor realEditor = editor; if (realEditor == null) { realEditor = ImageEditor.Static.getDefault(); } //rotate first Set<Map.Entry<String, Object>> entrySet = new TreeMap<String, Object>(edits).entrySet(); for (Map.Entry<String, Object> entry : entrySet) { if (entry.getKey().equals("rotate")) { item = realEditor.edit(item, entry.getKey(), null, entry.getValue()); } } for (Map.Entry<String, Object> entry : entrySet) { if (!entry.getKey().equals("rotate")) { item = realEditor.edit(item, entry.getKey(), null, entry.getValue()); } } } } // Requires at least the width and height to perform a crop if (cropWidth != null && cropHeight != null) { if (!disableHotSpotCrop && useHotSpotCrop() && standardImageSize != null && (standardImageSize.getCropOption() == null || standardImageSize.getCropOption().equals(CropOption.AUTOMATIC)) && cropX == null && cropY == null) { List<Integer> hotSpotCrop = ImageHotSpot.crop(item, cropWidth, cropHeight); if (!ObjectUtils.isBlank(hotSpotCrop) && hotSpotCrop.size() == 4) { cropX = hotSpotCrop.get(0); cropY = hotSpotCrop.get(1); cropWidth = hotSpotCrop.get(2); cropHeight = hotSpotCrop.get(3); } } item = ImageEditor.Static.crop(editor, item, options, cropX, cropY, cropWidth, cropHeight); } // Requires only one of either the width or the height to perform a resize if (width != null || height != null) { item = ImageEditor.Static.resize(editor, item, options, width, height); } String url = item.getPublicUrl(); if (url != null) { attributes.put(srcAttr != null ? srcAttr : "src", url); } Integer newWidth = findDimension(item, "width"); Integer newHeight = findDimension(item, "height"); if (newWidth != null && !hideDimensions) { attributes.put("width", String.valueOf(newWidth)); } if (newHeight != null && !hideDimensions) { attributes.put("height", String.valueOf(newHeight)); } if (this.attributes != null) { attributes.putAll(this.attributes); } } if (standardImageSize != null) { attributes.put("data-size", standardImageSize.getInternalName()); } return attributes; } } public static final class Static { private Static() { } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String getHtmlFromStandardImageSize(WebPageContext wp, StorageItem item, ImageEditor editor, StandardImageSize size, String srcAttr, Map<String, String> dynamicAttributes) { return getHtmlFromOptions(wp, item, editor, size.getWidth(), size.getHeight(), size.getCropOption(), size.getResizeOption(), srcAttr, dynamicAttributes); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String getHtmlFromOptions(WebPageContext wp, StorageItem item, ImageEditor editor, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption, String srcAttr, Map<String, String> dynamicAttributes) { return new Builder(item) .setEditor(editor) .setWidth(width) .setHeight(height) .setCropOption(cropOption) .setResizeOption(resizeOption) .setSrcAttribute(srcAttr) .addAllAttributes(dynamicAttributes) .toHtml(); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String getUrlFromStandardImageSize(WebPageContext wp, StorageItem item, ImageEditor editor, StandardImageSize size) { return getUrlFromOptions(wp, item, editor, size.getWidth(), size.getHeight(), size.getCropOption(), size.getResizeOption()); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String getUrlFromOptions(WebPageContext wp, StorageItem item, ImageEditor editor, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption) { return new Builder(item) .setEditor(editor) .setWidth(width) .setHeight(height) .setCropOption(cropOption) .setResizeOption(resizeOption) .toUrl(); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String getHtml(WebPageContext wp, Object object, String field, ImageEditor editor, StandardImageSize standardSize, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption, String srcAttr, Map<String, String> dynamicAttributes) { Map<String, String> attributes = getAttributes(wp, object, field, editor, standardSize, width, height, cropOption, resizeOption, srcAttr, dynamicAttributes); return convertAttributesToHtml(null, attributes); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String getHtml(PageContext pageContext, Object object, String field, ImageEditor editor, StandardImageSize standardSize, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption, String srcAttr, Map<String, String> dynamicAttributes) { return getHtml(new WebPageContext(pageContext), object, field, editor, standardSize, width, height, cropOption, resizeOption, srcAttr, dynamicAttributes); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String makeUrlFromStandardImageSize(WebPageContext wp, Object object, String field, ImageEditor editor, String size) { StandardImageSize standardImageSize = getStandardImageSizeByName(size); Map<String, String> attributes = getAttributes(wp, object, field, editor, standardImageSize, null, null, null, null, null, null); return attributes.get("src"); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String makeUrlFromStandardImageSize(PageContext pageContext, Object object, String field, ImageEditor editor, String size) { return makeUrlFromStandardImageSize(new WebPageContext(pageContext), object, field, editor, size); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String makeUrlFromOptions(WebPageContext wp, Object object, String field, ImageEditor editor, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption) { Map<String, String> attributes = getAttributes(wp, object, field, editor, null, width, height, cropOption, resizeOption, null, null); return attributes.get("src"); } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String makeUrlFromOptions(PageContext pageContext, Object object, String field, ImageEditor editor, Integer width, Integer height, CropOption cropOption, ResizeOption resizeOption) { return makeUrlFromOptions(new WebPageContext(pageContext), object, field, editor, width, height, cropOption, resizeOption); } } /** * @deprecated Use {@link ImageTag.Builder} instead. */ @Deprecated public static String makeUrl( PageContext pageContext, Object object, String field, String editor, String size, Integer width, Integer height) { StandardImageSize standardImageSize = getStandardImageSizeByName(size); ImageEditor imageEditor = null; if (editor != null) { imageEditor = ImageEditor.Static.getInstance(editor); } if (imageEditor == null) { imageEditor = ImageEditor.Static.getDefault(); } Map<String, String> attributes = getAttributes(new WebPageContext(pageContext), object, field, imageEditor, standardImageSize, width, height, null, null, null, null); return attributes.get("src"); } }