SimpleImageMediaMarkupBuilder.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 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.media.markup;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.jdom2.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ConsumerType;
import io.wcm.handler.commons.dom.Area;
import io.wcm.handler.commons.dom.HtmlElement;
import io.wcm.handler.commons.dom.Image;
import io.wcm.handler.commons.dom.Map;
import io.wcm.handler.commons.dom.Picture;
import io.wcm.handler.commons.dom.Source;
import io.wcm.handler.commons.dom.Span;
import io.wcm.handler.media.Asset;
import io.wcm.handler.media.Media;
import io.wcm.handler.media.MediaArgs;
import io.wcm.handler.media.MediaArgs.ImageSizes;
import io.wcm.handler.media.MediaArgs.PictureSource;
import io.wcm.handler.media.MediaArgs.WidthOption;
import io.wcm.handler.media.MediaNameConstants;
import io.wcm.handler.media.Rendition;
import io.wcm.handler.media.format.MediaFormat;
import io.wcm.handler.media.format.Ratio;
import io.wcm.handler.media.imagemap.ImageMapArea;
/**
* Basic implementation of {@link io.wcm.handler.media.spi.MediaMarkupBuilder} for images.
* <p>
* If image sizes or picture sources are set on the media handler this markup builder also
* generates markup for responsive images using <code>img</code> with <code>sizes</code> and <code>srcset</code>
* attributes or <code>picture</code> with <code>source</code> elements.
* </p>
*/
@Model(adaptables = {
SlingHttpServletRequest.class, Resource.class
})
@ConsumerType
public class SimpleImageMediaMarkupBuilder extends AbstractImageMediaMarkupBuilder {
@Override
public final boolean accepts(@NotNull Media media) {
// accept if rendition is an image rendition, and resolving was successful
Rendition rendition = media.getRendition();
return media.isValid()
&& rendition != null
&& rendition.isBrowserImage();
}
@Override
public final HtmlElement build(@NotNull Media media) {
// render media element for rendition
HtmlElement mediaElement = getMediaElement(media);
// further processing in edit or preview mode
applyWcmMarkup(mediaElement, media);
return mediaElement;
}
/**
* Create <code>img</code> or <code>picture</code> media element.
* @param media Media metadata
* @return Media element with properties or null if media metadata is invalid
*/
protected @Nullable HtmlElement getMediaElement(@NotNull Media media) {
PictureSource[] pictureSources = media.getMediaRequest().getMediaArgs().getPictureSources();
if (pictureSources != null && pictureSources.length > 0) {
return getPictureElement(media);
}
else {
return getImageElement(media);
}
}
/**
* Create an <code>img</code> element that displays the given rendition image.
* @param media Media metadata
* @return <code>img</code> element with properties or null if media metadata is invalid
*/
@SuppressWarnings("java:S3776") // ignore complexity
protected @Nullable HtmlElement getPictureElement(@NotNull Media media) {
PictureSource[] pictureSources = media.getMediaRequest().getMediaArgs().getPictureSources();
Picture picture = new Picture();
// add source elements (only if matching renditions found)
boolean foundAnySource = false;
if (pictureSources != null) {
for (PictureSource pictureSource : pictureSources) {
Source source = new Source();
if (pictureSource.getMedia() != null) {
source.setMedia(pictureSource.getMedia());
}
if (pictureSource.getSizes() != null) {
source.setSizes(pictureSource.getSizes());
}
MediaFormat mediaFormat = pictureSource.getMediaFormat();
if (mediaFormat != null) {
String srcSet = getSrcSetRenditions(media, mediaFormat, pictureSource.getWidthOptions());
if (srcSet != null) {
source.setSrcSet(srcSet);
picture.add(source);
foundAnySource = true;
}
}
}
}
// add image element
HtmlElement image = getImageElement(media);
if (image == null) {
return null;
}
if (foundAnySource) {
if (image instanceof Span) {
// if image was wrapped in span, add content of span element, not the span itself
for (Element element : List.copyOf(image.getChildren())) {
element.detach();
picture.addContent(element);
}
}
else {
picture.addContent(image);
}
return picture;
}
else {
return image;
}
}
/**
* Create an <code>img</code> element that displays the given rendition image.
* @param media Media metadata
* @return <code>img</code> element with properties or null if media metadata is invalid
*/
@SuppressWarnings({ "java:S3776", "java:S2589" }) // ignore complexity
protected @Nullable HtmlElement getImageElement(@NotNull Media media) {
Image img = null;
MediaArgs mediaArgs = media.getMediaRequest().getMediaArgs();
Asset asset = media.getAsset();
Rendition rendition = media.getRendition();
String url = null;
if (rendition != null) {
url = rendition.getUrl();
}
if (url != null) {
img = new Image(url);
// Alternative text
String altText = null;
if (asset != null) {
altText = asset.getAltText();
}
if (altText != null) {
img.setAlt(altText);
}
// set width/height
if (rendition != null
&& !rendition.isVectorImage()
&& mediaArgs.getImageSizes() == null
&& mediaArgs.getPictureSources() == null) {
long height = rendition.getHeight();
if (height > 0) {
img.setHeight(height);
}
long width = rendition.getWidth();
if (width > 0) {
img.setWidth(width);
}
}
// set image sizes/srcset
ImageSizes imageSizes = mediaArgs.getImageSizes();
if (imageSizes != null) {
MediaFormat primaryMediaFormat = getFirstMediaFormat(media);
if (primaryMediaFormat != null) {
String srcSet = getSrcSetRenditions(media, primaryMediaFormat, imageSizes.getWidthOptions());
if (srcSet != null) {
img.setSrcSet(srcSet);
img.setSizes(imageSizes.getSizes());
}
}
}
}
// set additional attributes
setAdditionalAttributes(img, media);
// apply image map markup
return applyImageMap(img, media);
}
/**
* Generate srcset list from the resolved renditions for the ratio of the given media formats and the given widths.
* Widths that have no match are ignored.
* @param media Media
* @param mediaFormat Media format
* @param widths widths
* @return srcset String or null if no matching renditions found
*/
protected @Nullable String getSrcSetRenditions(@NotNull Media media, @NotNull MediaFormat mediaFormat,
@NotNull WidthOption @Nullable... widths) {
if (widths == null) {
return null;
}
return getSrcSetRenditions(media, mediaFormat, Arrays.stream(widths)
.mapToLong(WidthOption::getWidth)
.toArray());
}
/**
* Generate srcset list from the resolved renditions for the ratio of the given media formats and the given widths.
* Widths that have no match are ignored.
* @param media Media
* @param mediaFormat Media format
* @param widths widths
* @return srcset String or null if no matching renditions found
*/
protected @Nullable String getSrcSetRenditions(@NotNull Media media, @NotNull MediaFormat mediaFormat,
long @NotNull... widths) {
StringBuilder srcset = new StringBuilder();
for (long width : widths) {
Optional<String> url = media.getRenditions().stream()
.filter(rendition -> (Ratio.matches(rendition.getRatio(), mediaFormat.getRatio())
|| Ratio.matches(mediaFormat.getRatio(), 0d))
&& rendition.getWidth() == width)
.map(Rendition::getUrl)
.findFirst();
if (url.isPresent()) {
if (srcset.length() > 0) {
srcset.append(", ");
}
srcset.append(url.get()).append(" ").append(Long.toString(width)).append("w");
}
}
if (srcset.length() > 0) {
return srcset.toString();
}
else {
return null;
}
}
/**
* Get first media format from the media formats of the media args that has a ratio set.
* @param media Media
* @return Media format or null if none found
*/
protected final @Nullable MediaFormat getFirstMediaFormatWithRatio(@NotNull Media media) {
MediaFormat[] mediaFormats = media.getMediaRequest().getMediaArgs().getMediaFormats();
if (mediaFormats != null) {
for (MediaFormat mediaFormat : mediaFormats) {
if (mediaFormat.hasRatio()) {
return mediaFormat;
}
}
}
return null;
}
/**
* Get first media format from the resolved media renditions.
* @param media Media
* @return Media format or null if none found
*/
@SuppressWarnings("null")
protected final @Nullable MediaFormat getFirstMediaFormat(@NotNull Media media) {
return media.getRenditions().stream()
.map(Rendition::getMediaFormat)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
/**
* If a image map was resolved apply map markup to given image element. As a result both image
* and map markup are wrapped in a span element.
* @param element Image Element
* @param media Media
* @return Unchanged element or wrapped element with map
*/
protected final @Nullable HtmlElement applyImageMap(@Nullable HtmlElement element, @NotNull Media media) {
List<ImageMapArea> mapData = media.getMap();
if (!(element instanceof Image) || mapData == null) {
return element;
}
// build unique name for map
String mapName = buildImageMapName(mapData, media);
// build wrapper element that will contain both image and map element
Span span = new Span();
((Image)element).setUseMap("#" + mapName);
span.addContent(element);
// build image map markup
Map map = new Map();
map.setMapName(mapName);
for (ImageMapArea areaData : mapData) {
Area area = new Area();
area.setShape(areaData.getShape());
area.setCoords(areaData.getCoordinates());
area.setHRef(areaData.getLinkUrl());
if (areaData.getLinkWindowTarget() != null) {
area.setTarget(areaData.getLinkWindowTarget());
}
if (areaData.getAltText() != null) {
area.setAlt(areaData.getAltText());
}
map.addContent(area);
}
span.addContent(map);
return span;
}
/**
* Builds an ID for the image map that is unique within the page.
* @param map Map data
* @param media Media
* @return Unique ID
*/
protected final @NotNull String buildImageMapName(@NotNull List<ImageMapArea> map, @NotNull Media media) {
HashCodeBuilder builder = new HashCodeBuilder();
for (ImageMapArea area : map) {
builder.append(area);
}
return "map-" + builder.hashCode();
}
@Override
public final boolean isValidMedia(@NotNull HtmlElement element) {
if (element instanceof Image) {
Image img = (Image)element;
return StringUtils.isNotEmpty(img.getSrc())
&& !StringUtils.contains(img.getCssClass(), MediaNameConstants.CSS_DUMMYIMAGE);
}
if (element instanceof Picture) {
Element imgChild = element.getChild("img");
if (imgChild instanceof Image) {
Image img = (Image)imgChild;
return StringUtils.isNotEmpty(img.getSrc())
&& !StringUtils.contains(element.getCssClass(), MediaNameConstants.CSS_DUMMYIMAGE);
}
}
if (element instanceof Span) {
Optional<Element> firstChild = element.getChildren().stream().findFirst();
if (firstChild.isPresent()) {
return isValidMedia((HtmlElement)firstChild.get());
}
}
return false;
}
}