1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package com.xpn.xwiki.plugin.image

File ImagePlugin.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart7.png
64% of files have more coverage

Code metrics

32
96
13
1
425
216
45
0.47
7.38
13
3.46

Classes

Class Line # Actions
ImagePlugin 51 96 0% 45 47
0.666666766.7%
 

Contributing tests

This file is covered by 2 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package com.xpn.xwiki.plugin.image;
21   
22    import java.awt.Image;
23    import java.awt.image.RenderedImage;
24    import java.io.IOException;
25    import java.io.OutputStream;
26    import java.util.Arrays;
27   
28    import org.apache.commons.io.IOUtils;
29    import org.apache.commons.lang3.StringUtils;
30    import org.slf4j.Logger;
31    import org.slf4j.LoggerFactory;
32    import org.xwiki.cache.Cache;
33    import org.xwiki.cache.CacheException;
34    import org.xwiki.cache.CacheManager;
35    import org.xwiki.cache.config.CacheConfiguration;
36    import org.xwiki.cache.eviction.LRUEvictionConfiguration;
37   
38    import com.xpn.xwiki.XWikiContext;
39    import com.xpn.xwiki.XWikiException;
40    import com.xpn.xwiki.api.Api;
41    import com.xpn.xwiki.doc.XWikiAttachment;
42    import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
43    import com.xpn.xwiki.plugin.XWikiPluginInterface;
44    import com.xpn.xwiki.web.Utils;
45   
46    /**
47    * @version $Id: a402c7df0dfec7de94d7bc06558eb62ba1c3aad8 $
48    * @deprecated the plugin technology is deprecated, consider rewriting as components
49    */
50    @Deprecated
 
51    public class ImagePlugin extends XWikiDefaultPlugin
52    {
53    /**
54    * Logging helper object.
55    */
56    private static final Logger LOG = LoggerFactory.getLogger(ImagePlugin.class);
57   
58    /**
59    * The name used for retrieving this plugin from the context.
60    *
61    * @see XWikiPluginInterface#getName()
62    */
63    private static final String PLUGIN_NAME = "image";
64   
65    /**
66    * Cache for already served images.
67    */
68    private Cache<XWikiAttachment> imageCache;
69   
70    /**
71    * The size of the cache. This parameter can be configured using the key {@code xwiki.plugin.image.cache.capacity}.
72    */
73    private int capacity = 50;
74   
75    /**
76    * Default JPEG image quality.
77    */
78    private float defaultQuality = 0.5f;
79   
80    /**
81    * The object used to process images.
82    */
83    private final ImageProcessor imageProcessor = Utils.getComponent(ImageProcessor.class);
84   
85    /**
86    * Creates a new instance of this plugin.
87    *
88    * @param name the name of the plugin
89    * @param className the class name
90    * @param context the XWiki context
91    * @see XWikiDefaultPlugin#XWikiDefaultPlugin(String,String,com.xpn.xwiki.XWikiContext)
92    */
 
93  2 toggle public ImagePlugin(String name, String className, XWikiContext context)
94    {
95  2 super(name, className, context);
96   
97  2 init(context);
98    }
99   
 
100  0 toggle @Override
101    public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
102    {
103  0 return new ImagePluginAPI((ImagePlugin) plugin, context);
104    }
105   
 
106  0 toggle @Override
107    public String getName()
108    {
109  0 return PLUGIN_NAME;
110    }
111   
 
112  2 toggle @Override
113    public void init(XWikiContext context)
114    {
115  2 super.init(context);
116   
117  2 initCache(context);
118   
119  2 String defaultQualityParam = context.getWiki().Param("xwiki.plugin.image.defaultQuality");
120  2 if (!StringUtils.isBlank(defaultQualityParam)) {
121  2 try {
122  2 this.defaultQuality = Math.max(0, Math.min(1, Float.parseFloat(defaultQualityParam.trim())));
123    } catch (NumberFormatException e) {
124  0 LOG.warn("Failed to parse xwiki.plugin.image.defaultQuality configuration parameter. "
125    + "Using {} as the default image quality.", this.defaultQuality);
126    }
127    }
128    }
129   
130    /**
131    * Tries to initializes the image cache. If the initialization fails the image cache remains {@code null}.
132    *
133    * @param context the XWiki context
134    */
 
135  4 toggle private void initCache(XWikiContext context)
136    {
137  4 if (this.imageCache == null) {
138  2 CacheConfiguration configuration = new CacheConfiguration();
139   
140  2 configuration.setConfigurationId("xwiki.plugin.image");
141   
142    // Set cache constraints.
143  2 LRUEvictionConfiguration lru = new LRUEvictionConfiguration();
144  2 configuration.put(LRUEvictionConfiguration.CONFIGURATIONID, lru);
145   
146  2 String capacityParam = context.getWiki().Param("xwiki.plugin.image.cache.capacity");
147  2 if (!StringUtils.isBlank(capacityParam) && StringUtils.isNumeric(capacityParam.trim())) {
148  2 try {
149  2 this.capacity = Integer.parseInt(capacityParam.trim());
150    } catch (NumberFormatException e) {
151  0 LOG.warn(String.format(
152    "Failed to parse xwiki.plugin.image.cache.capacity configuration parameter. "
153    + "Using %s as the cache capacity.", this.capacity), e);
154    }
155    }
156  2 lru.setMaxEntries(this.capacity);
157   
158  2 try {
159  2 this.imageCache = Utils.getComponent(CacheManager.class).createNewLocalCache(configuration);
160    } catch (CacheException e) {
161  0 LOG.error("Error initializing the image cache.", e);
162    }
163    }
164    }
165   
 
166  0 toggle @Override
167    public void flushCache()
168    {
169  0 if (this.imageCache != null) {
170  0 this.imageCache.dispose();
171    }
172  0 this.imageCache = null;
173    }
174   
175    /**
176    * {@inheritDoc}
177    * <p>
178    * Allows to scale images server-side, in order to have real thumbnails for reduced traffic. The new image
179    * dimensions are passed in the request as the {@code width} and {@code height} parameters. If only one of the
180    * dimensions is specified, then the other one is computed to preserve the original aspect ratio of the image.
181    * </p>
182    *
183    * @see XWikiDefaultPlugin#downloadAttachment(XWikiAttachment, XWikiContext)
184    */
 
185  3 toggle @Override
186    public XWikiAttachment downloadAttachment(XWikiAttachment attachment, XWikiContext context)
187    {
188  3 if (!this.imageProcessor.isMimeTypeSupported(attachment.getMimeType(context))) {
189  1 return attachment;
190    }
191   
192  2 int height = -1;
193  2 try {
194  2 height = Integer.parseInt(context.getRequest().getParameter("height"));
195    } catch (NumberFormatException e) {
196    // Ignore.
197    }
198   
199  2 int width = -1;
200  2 try {
201  2 width = Integer.parseInt(context.getRequest().getParameter("width"));
202    } catch (NumberFormatException e) {
203    // Ignore.
204    }
205   
206  2 float quality = -1;
207  2 try {
208  2 quality = Float.parseFloat(context.getRequest().getParameter("quality"));
209    } catch (NumberFormatException e) {
210    // Ignore.
211    } catch (NullPointerException e) {
212    // Ignore.
213    }
214   
215    // If no scaling is needed, return the original image.
216  2 if (height <= 0 && width <= 0 && quality < 0) {
217  0 return attachment;
218    }
219   
220  2 try {
221    // Transform the image attachment before is it downloaded.
222  2 return downloadImage(attachment, width, height, quality, context);
223    } catch (Exception e) {
224  0 LOG.warn("Failed to transform image attachment.", e);
225  0 return attachment;
226    }
227    }
228   
229    /**
230    * Transforms the given image (i.e. shrinks the image and changes its quality) before it is downloaded.
231    *
232    * @param image the image to be downloaded
233    * @param width the desired image width; this value is taken into account only if it is greater than zero and less
234    * than the current image width
235    * @param height the desired image height; this value is taken into account only if it is greater than zero and less
236    * than the current image height
237    * @param quality the desired compression quality
238    * @param context the XWiki context
239    * @return the transformed image
240    * @throws Exception if transforming the image fails
241    */
 
242  2 toggle private XWikiAttachment downloadImage(XWikiAttachment image, int width, int height, float quality,
243    XWikiContext context) throws Exception
244    {
245  2 initCache(context);
246   
247  2 boolean keepAspectRatio = Boolean.valueOf(context.getRequest().getParameter("keepAspectRatio"));
248   
249  2 XWikiAttachment thumbnail = (this.imageCache == null)
250    ? shrinkImage(image, width, height, keepAspectRatio, quality, context)
251    : downloadImageFromCache(image, width, height, keepAspectRatio, quality, context);
252   
253    // If the image has been transformed, update the file name extension to match the image format.
254  2 String fileName = thumbnail.getFilename();
255  2 String extension = StringUtils.lowerCase(StringUtils.substringAfterLast(fileName, String.valueOf('.')));
256  2 if (thumbnail != image && !Arrays.asList("jpeg", "jpg", "png").contains(extension)) {
257    // The scaled image is PNG, so correct the extension in order to output the correct MIME type.
258  1 thumbnail.setFilename(StringUtils.substringBeforeLast(fileName, ".") + ".png");
259    }
260  2 return thumbnail;
261    }
262   
263    /**
264    * Downloads the given image from cache.
265    *
266    * @param image the image to be downloaded
267    * @param width the desired image width; this value is taken into account only if it is greater than zero and less
268    * than the current image width
269    * @param height the desired image height; this value is taken into account only if it is greater than zero and less
270    * than the current image height
271    * @param keepAspectRatio {@code true} to preserve aspect ratio when resizing the image, {@code false} otherwise
272    * @param quality the desired compression quality
273    * @param context the XWiki context
274    * @return the transformed image
275    * @throws Exception if transforming the image fails
276    */
 
277  2 toggle private XWikiAttachment downloadImageFromCache(XWikiAttachment image, int width, int height,
278    boolean keepAspectRatio, float quality, XWikiContext context) throws Exception
279    {
280  2 String key =
281    String.format("%s;%s;%s;%s;%s;%s", image.getId(), image.getVersion(), width, height, keepAspectRatio,
282    quality);
283   
284  2 XWikiAttachment thumbnail = this.imageCache.get(key);
285  2 if (thumbnail == null) {
286  1 thumbnail = shrinkImage(image, width, height, keepAspectRatio, quality, context);
287  1 this.imageCache.set(key, thumbnail);
288    }
289  2 return thumbnail;
290    }
291   
292    /**
293    * Reduces the size (i.e. the number of bytes) of an image by scaling its width and height and by reducing its
294    * compression quality. This helps decreasing the time needed to download the image attachment.
295    *
296    * @param attachment the image to be shrunk
297    * @param requestedWidth the desired image width; this value is taken into account only if it is greater than zero
298    * and less than the current image width
299    * @param requestedHeight the desired image height; this value is taken into account only if it is greater than zero
300    * and less than the current image height
301    * @param keepAspectRatio {@code true} to preserve the image aspect ratio even when both requested dimensions are
302    * properly specified (in this case the image will be resized to best fit the rectangle with the
303    * requested width and height), {@code false} otherwise
304    * @param requestedQuality the desired compression quality
305    * @param context the XWiki context
306    * @return the modified image attachment
307    * @throws Exception if shrinking the image fails
308    */
 
309  1 toggle private XWikiAttachment shrinkImage(XWikiAttachment attachment, int requestedWidth, int requestedHeight,
310    boolean keepAspectRatio, float requestedQuality, XWikiContext context) throws Exception
311    {
312  1 Image image = this.imageProcessor.readImage(attachment.getContentInputStream(context));
313   
314    // Compute the new image dimension.
315  1 int currentWidth = image.getWidth(null);
316  1 int currentHeight = image.getHeight(null);
317  1 int[] dimensions =
318    reduceImageDimensions(currentWidth, currentHeight, requestedWidth, requestedHeight, keepAspectRatio);
319   
320  1 float quality = requestedQuality;
321  1 if (quality < 0) {
322    // If no scaling is needed and the quality parameter is not specified, return the original image.
323  0 if (dimensions[0] == currentWidth && dimensions[1] == currentHeight) {
324  0 return attachment;
325    }
326  0 quality = this.defaultQuality;
327    }
328   
329    // Scale the image to the new dimensions.
330  1 RenderedImage shrunkImage = this.imageProcessor.scaleImage(image, dimensions[0], dimensions[1]);
331   
332    // Create an image attachment for the shrunk image.
333  1 XWikiAttachment thumbnail = (XWikiAttachment) attachment.clone();
334  1 thumbnail.loadContent(context);
335   
336  1 OutputStream acos = thumbnail.getAttachment_content().getContentOutputStream();
337  1 this.imageProcessor.writeImage(shrunkImage,
338    attachment.getMimeType(context),
339    quality,
340    acos);
341   
342  1 IOUtils.closeQuietly(acos);
343   
344  1 return thumbnail;
345    }
346   
347    /**
348    * Computes the new image dimension which:
349    * <ul>
350    * <li>uses the requested width and height only if both are smaller than the current values</li>
351    * <li>preserves the aspect ratio when width or height is not specified.</li>
352    * </ul>
353    *
354    * @param currentWidth the current image width
355    * @param currentHeight the current image height
356    * @param requestedWidth the desired image width; this value is taken into account only if it is greater than zero
357    * and less than the current image width
358    * @param requestedHeight the desired image height; this value is taken into account only if it is greater than zero
359    * and less than the current image height
360    * @param keepAspectRatio {@code true} to preserve the image aspect ratio even when both requested dimensions are
361    * properly specified (in this case the image will be resized to best fit the rectangle with the
362    * requested width and height), {@code false} otherwise
363    * @return new width and height values
364    */
 
365  1 toggle private int[] reduceImageDimensions(int currentWidth, int currentHeight, int requestedWidth, int requestedHeight,
366    boolean keepAspectRatio)
367    {
368  1 double aspectRatio = (double) currentWidth / (double) currentHeight;
369   
370  1 int width = currentWidth;
371  1 int height = currentHeight;
372   
373  1 if (requestedWidth <= 0 || requestedWidth >= currentWidth) {
374    // Ignore the requested width. Check the requested height.
375  0 if (requestedHeight > 0 && requestedHeight < currentHeight) {
376    // Reduce the height, keeping aspect ratio.
377  0 width = (int) (requestedHeight * aspectRatio);
378  0 height = requestedHeight;
379    }
380  1 } else if (requestedHeight <= 0 || requestedHeight >= currentHeight) {
381    // Ignore the requested height. Reduce the width, keeping aspect ratio.
382  0 width = requestedWidth;
383  0 height = (int) (requestedWidth / aspectRatio);
384  1 } else if (keepAspectRatio) {
385    // Reduce the width and check if the corresponding height is less than the requested height.
386  0 width = requestedWidth;
387  0 height = (int) (requestedWidth / aspectRatio);
388  0 if (height > requestedHeight) {
389    // We have to reduce the height instead and compute the width based on it.
390  0 width = (int) (requestedHeight * aspectRatio);
391  0 height = requestedHeight;
392    }
393    } else {
394    // Reduce both width and height, possibly loosing aspect ratio.
395  1 width = requestedWidth;
396  1 height = requestedHeight;
397    }
398   
399  1 return new int[] { width, height };
400    }
401   
402    /**
403    * @param attachment an image attachment
404    * @param context the XWiki context
405    * @return the width of the specified image
406    * @throws IOException if reading the image from the attachment content fails
407    * @throws XWikiException if reading the attachment content fails
408    */
 
409  0 toggle public int getWidth(XWikiAttachment attachment, XWikiContext context) throws IOException, XWikiException
410    {
411  0 return this.imageProcessor.readImage(attachment.getContentInputStream(context)).getWidth(null);
412    }
413   
414    /**
415    * @param attachment an image attachment
416    * @param context the XWiki context
417    * @return the height of the specified image
418    * @throws IOException if reading the image from the attachment content fails
419    * @throws XWikiException if reading the attachment content fails
420    */
 
421  0 toggle public int getHeight(XWikiAttachment attachment, XWikiContext context) throws IOException, XWikiException
422    {
423  0 return this.imageProcessor.readImage(attachment.getContentInputStream(context)).getHeight(null);
424    }
425    }