AEM Content Fragment with Image support

Problem Statement:

How can I get support for Core Image component within content fragment component?

Why should we use Core Image component?

Requirement:

Get support for OOTB image component within content fragment component

Introduction:

The Image Component features adaptive image selection and responsive behaviour with lazy loading for the page visitor as well as easy image placement.

The Image Component comes with robust responsive features ready right out of the box. At the page template level, the design dialog can be used to define the default widths of the image asset. The Image Component will then automatically load the correct width to display depending on the browser window’s size. As the window is resized, the Image Component dynamically loads the correct image size on the fly. There is no need for component developers to worry about defining custom media queries since the Image Component is already optimized to load your content.

In addition, the Image Component supports lazy loading to defer loading of the actual image asset until it is visible in the browser, increasing the responsiveness of your pages.

Create Custom Image content fragment component as shown below and add the following conditions into the element.html file:

Image Contentfragment Component

In the above HTL we are trying to tell if the element name contains the “image” (eg: primaryimage) keyword then don’t print it instead of that call an image model and get synthetic image resource

Note: Make sure your content fragment element name (field name) contains image word

Create a Sling mode ImageContentFragment Interface as shown below:

package com.mysite.core.models;

import org.apache.sling.api.resource.Resource;
import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface ImageContentFragment {
    /**
     * Getter for Image Synthetic Resource
     *
     * @return resource
     */
    default Resource getImageResource() {
        throw new UnsupportedOperationException();
    }
}

Create a Sling mode implementation ImageContentFragmentImpl class as shown below, this class is used to create a synthetic image resource for Adaptive Image Servlet to work:

package com.mysite.core.models.impl;

import java.util.HashMap;
import javax.annotation.PostConstruct;
import com.mysite.core.models.ImageContentFragment;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.RequestAttribute;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import com.adobe.cq.wcm.core.components.models.contentfragment.ContentFragment;
import com.adobe.cq.wcm.core.components.models.contentfragment.DAMContentFragment.DAMContentElement;
import com.adobe.granite.ui.components.ds.ValueMapResource;
import com.day.cq.commons.DownloadResource;
import lombok.Getter;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {ImageContentFragment.class}, resourceType = ImageContentFragmentImpl.RESOURCE_TYPE)
public class ImageContentFragmentImpl implements ImageContentFragment{

    public static final String RESOURCE_TYPE = "mysite/components/image";
    private static final String IMAGE = "/image";

    @SlingObject
    private ResourceResolver resourceResolver;

    @SlingObject
    protected Resource resource;

    @ValueMapValue(name = ContentFragment.PN_PATH, injectionStrategy = InjectionStrategy.OPTIONAL)
    private String fragmentPath;

    @RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
    private DAMContentElement imageElement;

    @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
    private String fileReference;

    @Getter
    private Resource imageResource;

    @PostConstruct
    private void initModel() {
        if (StringUtils.isNotEmpty(fragmentPath) && null != imageElement && null != imageElement.getValue()) {
            createSyntheticResource(imageElement.getValue().toString(), resource.getPath() + IMAGE);
        }
    }

    private void createSyntheticResource(String imagePath, String path) {
        ValueMap properties = new ValueMapDecorator(new HashMap<>());
        properties.put(DownloadResource.PN_REFERENCE, imagePath);
        imageResource = new ValueMapResource(resourceResolver, path, ImageContentFragmentImpl.RESOURCE_TYPE, properties);
    }
}

Create an ImageDelegate Lombok based delegation class as shown below, for this example, I am using Image V3 component and this class is used to modify the image srcset method to add image path into the image URL:

package com.mysite.core.models.impl;

import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Image;
import lombok.experimental.Delegate;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {Image.class}, resourceType = ImageDelegate.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ImageDelegate implements Image {
    private static final Logger LOGGER = LoggerFactory.getLogger(ImageDelegate.class);
    public static final String RESOURCE_TYPE = "mysite/components/content/image";
    public static final String CONTENT_DAM_PATH = "/content/dam";
    public static final String HTTPS = "https://";
    public static final String PN_DISPLAY_SIZES = "sizes";
    public static final String WIDTH = "{.width}";

    @SlingObject
    private ResourceResolver resourceResolver;

    @SlingObject
    protected Resource resource;

    @Self
    @Via(type = ResourceSuperType.class)
    @Delegate(excludes = DelegationExclusion.class)
    private Image image;

    @Override
    public String getSrc() {
        return prepareSuffix(image.getSrc());
    }

    @Override
    public String getSrcset() {
        int[] widthsArray = image.getWidths();
        String srcUritemplate = image.getSrcUriTemplate();
        String[] srcsetArray = new String[widthsArray.length];
        if (widthsArray.length > 0 && srcUritemplate != null) {
            String srcUriTemplateDecoded = "";
            try {
                srcUriTemplateDecoded = prepareSuffix(URLDecoder.decode(srcUritemplate, StandardCharsets.UTF_8.name()));
            } catch (UnsupportedEncodingException e) {
                LOGGER.error("Character Decoding failed for {}", resource.getPath());
            }
            if (srcUriTemplateDecoded.contains(WIDTH)) {
                for (int i = 0; i < widthsArray.length; i++) {
                    if (srcUriTemplateDecoded.contains("="+WIDTH)) {
                        srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format("%s", widthsArray[i])) + " " + widthsArray[i] + "w";
                    } else {
                        srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format(".%s", widthsArray[i])) + " " + widthsArray[i] + "w";
                    }
                }
                return StringUtils.join(srcsetArray, ',');
            }
        }
        return null;
    }

    private String prepareSuffix(String imageSrc) {
        if(StringUtils.isNotEmpty(imageSrc) && !StringUtils.containsIgnoreCase(imageSrc, CONTENT_DAM_PATH)) {
            int endIndex = imageSrc.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
            String intermittenResult = imageSrc.substring(0, endIndex);
            endIndex = intermittenResult.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
            return intermittenResult.substring(0, endIndex)+image.getFileReference();
        }
        return imageSrc;
    }

    private interface DelegationExclusion {
        String getSrc();
        String getSrcset();
    }
}

In the above code, we are trying to prepend the URL with an image path, because we are using a synthetic image component for the content fragment

For more information on Lombok based component delegation please click on the title to check:

AEM Core Component Delegation

Create a new Adaptive Image servlet and EnhancedRendition class, this servlet is used for displaying appropriate rendition of image component based on width selected based on browser width and pixel ratio:

public class AdaptiveImageServlet  extends SlingSafeMethodsServlet {
    @Override
    protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws IOException {
        try {
            RequestPathInfo requestPathInfo = request.getRequestPathInfo();
            List<String> selectorList = selectorToList(requestPathInfo.getSelectorString());
            String suffix = requestPathInfo.getSuffix();
            String imagePath = suffix;
            String imageName = StringUtils.isNotEmpty(suffix) ? FilenameUtils.getName(suffix) : "";
            Resource component = request.getResource();
            ImageComponent imageComponent = new ImageComponent(component, imagePath);

        } catch (IllegalArgumentException e) {
            LOGGER.error("Invalid image request {}", e.getMessage());
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
    private static class ImageComponent {
        Source source = Source.NONEXISTING;
        Resource imageResource;

        ImageComponent(@NotNull Resource component, String imagePath) {
            if (StringUtils.isNotEmpty(imagePath)) {
                imageResource = component.getResourceResolver().getResource(imagePath);
                source = Source.ASSET;
            }
        }
    }
}

Adaptive Image servlet is a modified version of the Core component Adaptive Image servlet because we are using synthetic image component and Enhanced Rendition class is a support class to get the best image rendition.

Create a simple content fragment Model as shown below:

Make sure the contentreference field name contains the image

Sample Content Fragment Model

Create a new content fragment under asset path (/content/dam/{project path}) as shown below:

Content Fragment

Create a sample page and add the content fragment component and select all the fields as a multifield as shown below:

Custom Content Fragment Component Authoing

As we can see we are able to fetch Core component Image component with Image widths configured into Image component policy (design dialog)

Content Fragment with OOTB Image

You can also get the actual working code from the below link:

https://github.com/kiransg89/ImageContentFragment