All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.wcm.handler.mediasource.inline.InlineRendition Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * #%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.mediasource.inline;

import io.wcm.handler.media.CropDimension;
import io.wcm.handler.media.Dimension;
import io.wcm.handler.media.Media;
import io.wcm.handler.media.MediaArgs;
import io.wcm.handler.media.Rendition;
import io.wcm.handler.media.format.MediaFormat;
import io.wcm.handler.media.format.MediaFormatHandler;
import io.wcm.handler.media.impl.ImageFileServlet;
import io.wcm.handler.media.impl.JcrBinary;
import io.wcm.handler.media.impl.MediaFileServlet;
import io.wcm.handler.url.UrlHandler;
import io.wcm.sling.commons.adapter.AdaptTo;
import io.wcm.wcm.commons.caching.ModificationDate;
import io.wcm.wcm.commons.contenttype.FileExtension;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.adapter.Adaptable;
import org.apache.sling.api.adapter.SlingAdaptable;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.image.Layer;

/**
 * {@link Rendition} implementation for inline media objects stored in a node in a content page.
 */
class InlineRendition extends SlingAdaptable implements Rendition {

  private final Adaptable adaptable;
  private final Resource resource;
  private final Media media;
  private final MediaArgs mediaArgs;
  private final String fileName;
  private final Dimension imageDimension;
  private final String url;
  private MediaFormat resolvedMediaFormat;

  /**
   * Special dimension instance that marks "scaling is required but not possible"
   */
  private static final Dimension SCALING_NOT_POSSIBLE_DIMENSION = new Dimension(-1, -1);

  /**
   * @param resource Binary resource
   * @param media Media metadata
   * @param mediaArgs Media args
   * @param fileName File name
   */
  InlineRendition(Resource resource, Media media, MediaArgs mediaArgs, String fileName, Adaptable adaptable) {
    this.resource = resource;
    this.media = media;
    this.mediaArgs = mediaArgs;
    this.adaptable = adaptable;

    // detect image dimension
    String processedFileName = fileName;

    // check if scaling is possible
    String fileExtension = StringUtils.substringAfterLast(processedFileName, ".");
    boolean isImage = FileExtension.isImage(fileExtension);

    Dimension dimension = null;
    Dimension scaledDimension = null;
    if (isImage) {
      // get dimension from image binary
      dimension = getImageDimension();

      // check if scaling is required
      scaledDimension = getScaledDimension(dimension);
      if (scaledDimension != null && !scaledDimension.equals(SCALING_NOT_POSSIBLE_DIMENSION)) {
        // overwrite image dimension of {@link Rendition} instance with scaled dimensions
        dimension = scaledDimension;
        // change extension to JPEG because scaling always produces JPEG images
        processedFileName = StringUtils.substringBeforeLast(processedFileName, ".") + "." + FileExtension.JPEG;
      }
    }
    this.fileName = processedFileName;
    this.imageDimension = dimension;

    // build media url (it is null if no rendition is available for the given media args)
    this.url = buildMediaUrl(scaledDimension);

    // set first media format as resolved format - because only the first is supported
    if (url != null && mediaArgs.getMediaFormats() != null && mediaArgs.getMediaFormats().length > 0) {
      this.resolvedMediaFormat = mediaArgs.getMediaFormats()[0];
    }

  }

  /**
   * Gets the dimension of the uploaded image (if the binary is an image file at all).
   * @return Dimension
   */
  private Dimension getImageDimension() {
    Dimension dimension = null;

    // check for cropping dimension
    if (this.media.getCropDimension() != null) {
      dimension = this.media.getCropDimension();
    }
    else {
      // if binary is image try to calculcate dimensions by loading it into a layer
      Layer layer = this.resource.adaptTo(Layer.class);
      if (layer != null) {
        dimension = new Dimension(layer.getWidth(), layer.getHeight());
      }
    }

    return dimension;
  }

  /**
   * Checks if the current binary is an image and has to be scaled. In this case the destination dimension is returned.
   * @return Scaled destination or null if no scaling is required. If a destination object with both
   *         width and height set to -1 is returned, a scaling is required but not possible with the given source
   *         object.
   */
  private Dimension getScaledDimension(Dimension originalDimension) {

    // check if image has to be rescaled
    Dimension requestedDimension = getRequestedDimension();
    boolean scaleWidth = (requestedDimension.getWidth() > 0
        && requestedDimension.getWidth() != originalDimension.getWidth());
    boolean scaleHeight = (requestedDimension.getHeight() > 0
        && requestedDimension.getHeight() != originalDimension.getHeight());
    if (scaleWidth || scaleHeight) {
      long requestedWidth = requestedDimension.getWidth();
      long requestedHeight = requestedDimension.getHeight();

      // calculate missing width/height from ration if not specified
      double imageRatio = (double)originalDimension.getWidth() / (double)originalDimension.getHeight();
      if (requestedWidth == 0 && requestedHeight > 0) {
        requestedWidth = (int)Math.round(requestedHeight * imageRatio);
      }
      else if (requestedWidth > 0 && requestedHeight == 0) {
        requestedHeight = (int)Math.round(requestedWidth / imageRatio);
      }

      // calculate requested ratio
      double requestedRatio = (double)requestedWidth / (double)requestedHeight;

      // if ratio does not match, or requested width/height is larger than the original image scaling is not possible
      if ((imageRatio > requestedRatio + MediaFormatHandler.RATIO_TOLERANCE)
          || (imageRatio < requestedRatio - MediaFormatHandler.RATIO_TOLERANCE)
          || (originalDimension.getWidth() < requestedWidth)
          || (originalDimension.getHeight() < requestedHeight)) {
        return SCALING_NOT_POSSIBLE_DIMENSION;
      }
      else {
        return new Dimension(requestedWidth, requestedHeight);
      }

    }

    return null;
  }

  /**
   * Build media URL for this rendition - either "native" URL to repository or virtual url to rescaled version.
   * @return Media URL - null if no rendition is available
   */
  private String buildMediaUrl(Dimension scaledDimension) {

    // check for file extension filtering
    if (!isMatchingFileExtension()) {
      return null;
    }

    // check if image has to be rescaled
    if (scaledDimension != null) {

      // check if scaling is not possible
      if (scaledDimension.equals(SCALING_NOT_POSSIBLE_DIMENSION)) {
        return null;
      }

      // otherwise generate scaled image URL
      return buildScaledMediaUrl(scaledDimension, this.media.getCropDimension());
    }

    // if no scaling but cropping required builid scaled image URL
    if (this.media.getCropDimension() != null) {
      return buildScaledMediaUrl(this.media.getCropDimension(), this.media.getCropDimension());
    }

    if (mediaArgs.isForceDownload()) {
      // if not scaling and no cropping required but special content disposition headers required build download url
      return buildDownloadMediaUrl();
    }
    else {
      // if not scaling and no cropping required build native media URL
      return buildNativeMediaUrl();
    }
  }

  /**
   * Builds "native" URL that returns the binary data directly from the repository.
   * @return Media URL
   */
  private String buildNativeMediaUrl() {
    String path = null;

    Resource parentResource = this.resource.getParent();
    if (JcrBinary.isNtFile(parentResource)) {
      // if parent resource is nt:file and its node name equals the detected filename, directly use the nt:file node path
      if (StringUtils.equals(parentResource.getName(), getFileName())) {
        path = parentResource.getPath();
      }
      // otherwise use nt:file node path with custom filename
      else {
        path = parentResource.getPath() + "./" + getFileName();
      }
    }
    else {
      // nt:resource node does not have a nt:file parent, use its path directly
      path = this.resource.getPath() + "./" + getFileName();
    }

    // build externalized URL
    UrlHandler urlHandler = AdaptTo.notNull(this.adaptable, UrlHandler.class);
    return urlHandler.get(path).urlMode(this.mediaArgs.getUrlMode()).buildExternalResourceUrl(this.resource);
  }

  /**
   * Builds URL to rescaled version of the binary image.
   * @return Media URL
   */
  private String buildScaledMediaUrl(Dimension dimension, CropDimension cropDimension) {
    String resourcePath = this.resource.getPath();

    // if parent resource is a nt:file resource, use this one as path for scaled image
    Resource parentResource = this.resource.getParent();
    if (JcrBinary.isNtFile(parentResource)) {
      resourcePath = parentResource.getPath();
    }

    // URL to render scaled image via {@link InlineRenditionServlet}
    String path = resourcePath + "." + ImageFileServlet.SELECTOR
        + "." + dimension.getWidth() + "." + dimension.getHeight()
        + (cropDimension != null ? "." + cropDimension.getCropString() : "")
        + (this.mediaArgs.isForceDownload() ? "." + MediaFileServlet.SELECTOR_DOWNLOAD : "")
        + "." + MediaFileServlet.EXTENSION + "/"
        // replace extension based on the format supported by ImageFileServlet for rendering for this rendition
        + ImageFileServlet.getImageFileName(getFileName());

    // build externalized URL
    UrlHandler urlHandler = AdaptTo.notNull(this.adaptable, UrlHandler.class);
    return urlHandler.get(path).urlMode(this.mediaArgs.getUrlMode()).buildExternalResourceUrl(this.resource);
  }

  /**
   * Builds URL to rescaled version of the binary image.
   * @return Media URL
   */
  private String buildDownloadMediaUrl() {
    String resourcePath = this.resource.getPath();

    // if parent resource is a nt:file resource, use this one as path for scaled image
    Resource parentResource = this.resource.getParent();
    if (JcrBinary.isNtFile(parentResource)) {
      resourcePath = parentResource.getPath();
    }

    // URL to render scaled image via {@link InlineRenditionServlet}
    String path = resourcePath + "." + MediaFileServlet.SELECTOR
        + "." + MediaFileServlet.SELECTOR_DOWNLOAD
        + "." + MediaFileServlet.EXTENSION + "/" + getFileName();

    // build externalized URL
    UrlHandler urlHandler = AdaptTo.notNull(this.adaptable, UrlHandler.class);
    return urlHandler.get(path).urlMode(this.mediaArgs.getUrlMode()).buildExternalResourceUrl(this.resource);
  }

  /**
   * Checks if the file extension of the current binary matches with the requested extensions from the media args.
   * @return true if file extension matches
   */
  private boolean isMatchingFileExtension() {
    String[] fileExtensions = mediaArgs.getFileExtensions();
    if (fileExtensions == null || fileExtensions.length == 0) {
      return true;
    }
    for (String fileExtension : fileExtensions) {
      if (StringUtils.equalsIgnoreCase(fileExtension, getFileExtension())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Requested dimensions either from media format or fixed dimensions from media args.
   * @return Requested dimensions
   */
  private Dimension getRequestedDimension() {

    // check for fixed dimensions from media args
    if (mediaArgs.getFixedWidth() > 0 || mediaArgs.getFixedHeight() > 0) {
      return new Dimension(mediaArgs.getFixedWidth(), mediaArgs.getFixedHeight());
    }

    // check for dimensions from mediaformat (evaluate only first media format)
    MediaFormat[] mediaFormats = mediaArgs.getMediaFormats();
    if (mediaFormats != null && mediaFormats.length > 0) {
      Dimension dimension = mediaFormats[0].getMinDimension();
      if (dimension != null) {
        return dimension;
      }
    }

    // fallback to 0/0 - no specific dimension requested
    return new Dimension(0, 0);
  }

  @Override
  public String getUrl() {
    return this.url;
  }

  @Override
  public String getPath() {
    return this.resource.getPath();
  }

  @Override
  public String getFileName() {
    return this.fileName;
  }

  @Override
  public String getFileExtension() {
    return StringUtils.substringAfterLast(this.fileName, ".");
  }

  @Override
  public long getFileSize() {
    Node node = this.resource.adaptTo(Node.class);
    if (node != null) {
      try {
        Property data = node.getProperty(JcrConstants.JCR_DATA);
        return data.getBinary().getSize();
      }
      catch (RepositoryException ex) {
        throw new RuntimeException("Unable to detect binary file size for " + this.resource.getPath(), ex);
      }
    }
    else {
      // fallback to Sling API if JCR node is not present (inefficient - but this should happen only in unit tests)
      try {
        InputStream is = this.resource.getValueMap().get(JcrConstants.JCR_DATA, InputStream.class);
        return IOUtils.toByteArray(is).length;
      }
      catch (IOException ex) {
        throw new RuntimeException("Unable to detect binary file size for " + this.resource.getPath(), ex);
      }
    }
  }

  @Override
  public String getMimeType() {
    return JcrBinary.getMimeType(this.resource);
  }

  @Override
  public Date getModificationDate() {
    return ModificationDate.get(this.resource);
  }

  @Override
  public MediaFormat getMediaFormat() {
    return resolvedMediaFormat;
  }

  @Override
  public ValueMap getProperties() {
    return this.resource.getValueMap();
  }

  @Override
  public boolean isImage() {
    return FileExtension.isImage(getFileExtension());
  }

  @Override
  public boolean isFlash() {
    return FileExtension.isFlash(getFileExtension());
  }

  @Override
  public boolean isDownload() {
    return !isImage() && !isFlash();
  }

  @Override
  public long getWidth() {
    if (this.imageDimension != null) {
      return this.imageDimension.getWidth();
    }
    else {
      return 0;
    }
  }

  @Override
  public long getHeight() {
    if (this.imageDimension != null) {
      return this.imageDimension.getHeight();
    }
    else {
      return 0;
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public  AdapterType adaptTo(Class type) {
    if (type == Resource.class) {
      return (AdapterType)this.resource;
    }
    else if (type == Layer.class && isImage()) {
      return (AdapterType)this.resource.adaptTo(Layer.class);
    }
    else if (type == InputStream.class) {
      return (AdapterType)this.resource.adaptTo(InputStream.class);
    }
    return super.adaptTo(type);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy