/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xpn.xwiki.plugin.image;
import java.awt.Image;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.cache.eviction.LRUEvictionConfiguration;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.Api;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
import com.xpn.xwiki.plugin.XWikiPluginInterface;
import com.xpn.xwiki.web.Utils;
/**
* @version $Id: 86a358d499ef1b574c019e3623a27bc8179559a5 $
* @deprecated the plugin technology is deprecated, consider rewriting as components
*/
@Deprecated
public class ImagePlugin extends XWikiDefaultPlugin
{
/**
* Logging helper object.
*/
private static final Logger LOG = LoggerFactory.getLogger(ImagePlugin.class);
/**
* The name used for retrieving this plugin from the context.
*
* @see XWikiPluginInterface#getName()
*/
private static final String PLUGIN_NAME = "image";
/**
* Cache for already served images.
*/
private Cache<XWikiAttachment> imageCache;
/**
* The size of the cache. This parameter can be configured using the key {@code xwiki.plugin.image.cache.capacity}.
*/
private int capacity = 50;
/**
* Default JPEG image quality.
*/
private float defaultQuality = 0.5f;
/**
* The object used to process images.
*/
private ImageProcessor imageProcessor;
/**
* Creates a new instance of this plugin.
*
* @param name the name of the plugin
* @param className the class name
* @param context the XWiki context
* @see XWikiDefaultPlugin#XWikiDefaultPlugin(String,String,com.xpn.xwiki.XWikiContext)
*/
public ImagePlugin(String name, String className, XWikiContext context)
{
super(name, className, context);
init(context);
}
@Override
public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
{
return new ImagePluginAPI((ImagePlugin) plugin, context);
}
@Override
public String getName()
{
return PLUGIN_NAME;
}
@Override
public void init(XWikiContext context)
{
super.init(context);
initCache(context);
String imageProcessorHint = context.getWiki().Param("xwiki.plugin.image.processorHint", "thumbnailator");
this.imageProcessor = Utils.getComponent(ImageProcessor.class, imageProcessorHint);
String defaultQualityParam = context.getWiki().Param("xwiki.plugin.image.defaultQuality");
if (!StringUtils.isBlank(defaultQualityParam)) {
try {
this.defaultQuality = Math.max(0, Math.min(1, Float.parseFloat(defaultQualityParam.trim())));
} catch (NumberFormatException e) {
LOG.warn("Failed to parse xwiki.plugin.image.defaultQuality configuration parameter. "
+ "Using {} as the default image quality.", this.defaultQuality);
}
}
}
/**
* Tries to initializes the image cache. If the initialization fails the image cache remains {@code null}.
*
* @param context the XWiki context
*/
private void initCache(XWikiContext context)
{
if (this.imageCache == null) {
CacheConfiguration configuration = new CacheConfiguration();
configuration.setConfigurationId("xwiki.plugin.image");
// Set cache constraints.
LRUEvictionConfiguration lru = new LRUEvictionConfiguration();
configuration.put(LRUEvictionConfiguration.CONFIGURATIONID, lru);
String capacityParam = context.getWiki().Param("xwiki.plugin.image.cache.capacity");
if (!StringUtils.isBlank(capacityParam) && StringUtils.isNumeric(capacityParam.trim())) {
try {
this.capacity = Integer.parseInt(capacityParam.trim());
} catch (NumberFormatException e) {
LOG.warn(String.format(
"Failed to parse xwiki.plugin.image.cache.capacity configuration parameter. "
+ "Using %s as the cache capacity.", this.capacity), e);
}
}
lru.setMaxEntries(this.capacity);
try {
this.imageCache = Utils.getComponent(CacheManager.class).createNewLocalCache(configuration);
} catch (CacheException e) {
LOG.error("Error initializing the image cache.", e);
}
}
}
@Override
public void flushCache()
{
if (this.imageCache != null) {
this.imageCache.dispose();
}
this.imageCache = null;
}
/**
* {@inheritDoc}
* <p>
* Allows to scale images server-side, in order to have real thumbnails for reduced traffic. The new image
* dimensions are passed in the request as the {@code width} and {@code height} parameters. If only one of the
* dimensions is specified, then the other one is computed to preserve the original aspect ratio of the image.
* </p>
*
* @see XWikiDefaultPlugin#downloadAttachment(XWikiAttachment, XWikiContext)
*/
@Override
public XWikiAttachment downloadAttachment(XWikiAttachment attachment, XWikiContext context)
{
if (!this.imageProcessor.isMimeTypeSupported(attachment.getMimeType(context))) {
return attachment;
}
int height = -1;
try {
height = Integer.parseInt(context.getRequest().getParameter("height"));
} catch (NumberFormatException e) {
// Ignore.
}
int width = -1;
try {
width = Integer.parseInt(context.getRequest().getParameter("width"));
} catch (NumberFormatException e) {
// Ignore.
}
float quality = -1;
try {
quality = Float.parseFloat(context.getRequest().getParameter("quality"));
} catch (NumberFormatException e) {
// Ignore.
} catch (NullPointerException e) {
// Ignore.
}
// If no scaling is needed, return the original image.
if (height <= 0 && width <= 0 && quality < 0) {
return attachment;
}
try {
// Transform the image attachment before is it downloaded.
return downloadImage(attachment, width, height, quality, context);
} catch (Exception e) {
LOG.warn("Failed to transform image attachment.", e);
return attachment;
}
}
/**
* Transforms the given image (i.e. shrinks the image and changes its quality) before it is downloaded.
*
* @param image the image to be downloaded
* @param width the desired image width; this value is taken into account only if it is greater than zero and less
* than the current image width
* @param height the desired image height; this value is taken into account only if it is greater than zero and less
* than the current image height
* @param quality the desired compression quality
* @param context the XWiki context
* @return the transformed image
* @throws Exception if transforming the image fails
*/
private XWikiAttachment downloadImage(XWikiAttachment image, int width, int height, float quality,
XWikiContext context) throws Exception
{
initCache(context);
boolean keepAspectRatio = Boolean.valueOf(context.getRequest().getParameter("keepAspectRatio"));
XWikiAttachment thumbnail = (this.imageCache == null)
? shrinkImage(image, width, height, keepAspectRatio, quality, context)
: downloadImageFromCache(image, width, height, keepAspectRatio, quality, context);
// If the image has been transformed, update the file name extension to match the image format.
String fileName = thumbnail.getFilename();
String extension = StringUtils.lowerCase(StringUtils.substringAfterLast(fileName, String.valueOf('.')));
if (thumbnail != image && !Arrays.asList("jpeg", "jpg", "png").contains(extension)) {
// The scaled image is PNG, so correct the extension in order to output the correct MIME type.
thumbnail.setFilename(StringUtils.substringBeforeLast(fileName, ".") + ".png");
}
return thumbnail;
}
/**
* Downloads the given image from cache.
*
* @param image the image to be downloaded
* @param width the desired image width; this value is taken into account only if it is greater than zero and less
* than the current image width
* @param height the desired image height; this value is taken into account only if it is greater than zero and less
* than the current image height
* @param keepAspectRatio {@code true} to preserve aspect ratio when resizing the image, {@code false} otherwise
* @param quality the desired compression quality
* @param context the XWiki context
* @return the transformed image
* @throws Exception if transforming the image fails
*/
private XWikiAttachment downloadImageFromCache(XWikiAttachment image, int width, int height,
boolean keepAspectRatio, float quality, XWikiContext context) throws Exception
{
String key =
String.format("%s;%s;%s;%s;%s;%s", image.getId(), image.getVersion(), width, height, keepAspectRatio,
quality);
XWikiAttachment thumbnail = this.imageCache.get(key);
if (thumbnail == null) {
thumbnail = shrinkImage(image, width, height, keepAspectRatio, quality, context);
this.imageCache.set(key, thumbnail);
}
return thumbnail;
}
/**
* Reduces the size (i.e. the number of bytes) of an image by scaling its width and height and by reducing its
* compression quality. This helps decreasing the time needed to download the image attachment.
*
* @param attachment the image to be shrunk
* @param requestedWidth the desired image width; this value is taken into account only if it is greater than zero
* and less than the current image width
* @param requestedHeight the desired image height; this value is taken into account only if it is greater than zero
* and less than the current image height
* @param keepAspectRatio {@code true} to preserve the image aspect ratio even when both requested dimensions are
* properly specified (in this case the image will be resized to best fit the rectangle with the
* requested width and height), {@code false} otherwise
* @param requestedQuality the desired compression quality
* @param context the XWiki context
* @return the modified image attachment
* @throws Exception if shrinking the image fails
*/
private XWikiAttachment shrinkImage(XWikiAttachment attachment, int requestedWidth, int requestedHeight,
boolean keepAspectRatio, float requestedQuality, XWikiContext context) throws Exception
{
Image image = this.imageProcessor.readImage(attachment.getContentInputStream(context));
// Compute the new image dimension.
int currentWidth = image.getWidth(null);
int currentHeight = image.getHeight(null);
int[] dimensions =
reduceImageDimensions(currentWidth, currentHeight, requestedWidth, requestedHeight, keepAspectRatio);
float quality = requestedQuality;
if (quality < 0) {
// If no scaling is needed and the quality parameter is not specified, return the original image.
if (dimensions[0] == currentWidth && dimensions[1] == currentHeight) {
return attachment;
}
quality = this.defaultQuality;
}
// Scale the image to the new dimensions.
RenderedImage shrunkImage = this.imageProcessor.scaleImage(image, dimensions[0], dimensions[1]);
// Create an image attachment for the shrunk image.
XWikiAttachment thumbnail = (XWikiAttachment) attachment.clone();
thumbnail.loadContent(context);
OutputStream acos = thumbnail.getAttachment_content().getContentOutputStream();
this.imageProcessor.writeImage(shrunkImage,
attachment.getMimeType(context),
quality,
acos);
IOUtils.closeQuietly(acos);
return thumbnail;
}
/**
* Computes the new image dimension which:
* <ul>
* <li>uses the requested width and height only if both are smaller than the current values</li>
* <li>preserves the aspect ratio when width or height is not specified.</li>
* </ul>
*
* @param currentWidth the current image width
* @param currentHeight the current image height
* @param requestedWidth the desired image width; this value is taken into account only if it is greater than zero
* and less than the current image width
* @param requestedHeight the desired image height; this value is taken into account only if it is greater than zero
* and less than the current image height
* @param keepAspectRatio {@code true} to preserve the image aspect ratio even when both requested dimensions are
* properly specified (in this case the image will be resized to best fit the rectangle with the
* requested width and height), {@code false} otherwise
* @return new width and height values
*/
private int[] reduceImageDimensions(int currentWidth, int currentHeight, int requestedWidth, int requestedHeight,
boolean keepAspectRatio)
{
double aspectRatio = (double) currentWidth / (double) currentHeight;
int width = currentWidth;
int height = currentHeight;
if (requestedWidth <= 0 || requestedWidth >= currentWidth) {
// Ignore the requested width. Check the requested height.
if (requestedHeight > 0 && requestedHeight < currentHeight) {
// Reduce the height, keeping aspect ratio.
width = (int) (requestedHeight * aspectRatio);
height = requestedHeight;
}
} else if (requestedHeight <= 0 || requestedHeight >= currentHeight) {
// Ignore the requested height. Reduce the width, keeping aspect ratio.
width = requestedWidth;
height = (int) (requestedWidth / aspectRatio);
} else if (keepAspectRatio) {
// Reduce the width and check if the corresponding height is less than the requested height.
width = requestedWidth;
height = (int) (requestedWidth / aspectRatio);
if (height > requestedHeight) {
// We have to reduce the height instead and compute the width based on it.
width = (int) (requestedHeight * aspectRatio);
height = requestedHeight;
}
} else {
// Reduce both width and height, possibly loosing aspect ratio.
width = requestedWidth;
height = requestedHeight;
}
return new int[] { width, height };
}
/**
* @param attachment an image attachment
* @param context the XWiki context
* @return the width of the specified image
* @throws IOException if reading the image from the attachment content fails
* @throws XWikiException if reading the attachment content fails
*/
public int getWidth(XWikiAttachment attachment, XWikiContext context) throws IOException, XWikiException
{
return this.imageProcessor.readImage(attachment.getContentInputStream(context)).getWidth(null);
}
/**
* @param attachment an image attachment
* @param context the XWiki context
* @return the height of the specified image
* @throws IOException if reading the image from the attachment content fails
* @throws XWikiException if reading the attachment content fails
*/
public int getHeight(XWikiAttachment attachment, XWikiContext context) throws IOException, XWikiException
{
return this.imageProcessor.readImage(attachment.getContentInputStream(context)).getHeight(null);
}
}