View Javadoc

1   package com.atlassian.core.util.thumbnail;
2   
3   import com.atlassian.core.util.ImageInfo;
4   
5   import org.apache.log4j.Category;
6   import org.apache.log4j.Logger;
7   
8   import java.awt.Graphics;
9   import java.awt.Image;
10  import java.awt.Toolkit;
11  import java.awt.image.AreaAveragingScaleFilter;
12  import java.awt.image.BufferedImage;
13  import java.awt.image.FilteredImageSource;
14  import java.awt.image.ImageProducer;
15  import java.io.File;
16  import java.io.FileInputStream;
17  import java.io.FileNotFoundException;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.net.MalformedURLException;
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Arrays;
24  import java.util.Collections;
25  
26  import javax.imageio.ImageIO;
27  
28  import javax.imageio.ImageWriter;
29  import javax.imageio.ImageWriteParam;
30  import javax.imageio.IIOImage;
31  import javax.imageio.stream.FileImageOutputStream;
32  
33  /**
34   * A class to create and retrieve thumbnail of images.
35   */
36  public class Thumber
37  {
38      private static final Logger log = Logger.getLogger(Thumber.class);
39      private ImageInfo imageInfo = new ImageInfo();
40  
41      public static final List<String> THUMBNAIL_MIME_TYPES = Collections.unmodifiableList(Arrays.asList(ImageIO.getReaderMIMETypes()));
42      public static final List<String> THUMBNAIL_FORMATS = Collections.unmodifiableList(Arrays.asList(ImageIO.getReaderFormatNames()));
43  
44      public static List<String> getThumbnailMimeTypes()
45      {
46          return THUMBNAIL_MIME_TYPES;
47      }
48  
49      public static List<String> getThumbnailFormats()
50      {
51          return THUMBNAIL_FORMATS;
52      }
53  
54      private float encodingQuality = 0.80f; // default to 0.80f, seems reasonable enough, and still provides good result.
55  
56      /**
57       * @return True if the AWT default toolkit exists, false (with an error logged) if it does not.
58       */
59      public boolean checkToolkit()
60      {
61          try
62          {
63              Toolkit.getDefaultToolkit();
64          }
65          catch (Throwable e)
66          {
67              log.error("Unable to acquire AWT default toolkit - thumbnails will not be displayed. Check DISPLAY variable or use setting -Djava.awt.headless=true.", e);
68              return false;
69          }
70          return true;
71      }
72  
73  
74      /**
75       * Retrieves an existing thumbnail, or creates a new one.
76       *
77       * @param originalFile The file which is being thumbnailed.
78       * @param thumbnailFile The location of the existing thumbnail (if it exists), or the location to create a new thumbnail.
79       * @param maxWidth The max width of the thumbnail.
80       * @param maxHeight The max height of the thumbnail.
81       * @param thumbnailId
82       * @return
83       * @throws MalformedURLException
84       */
85      public Thumbnail retrieveOrCreateThumbNail(File originalFile, File thumbnailFile, int maxWidth, int maxHeight, long thumbnailId) throws MalformedURLException
86      {
87          FileInputStream originalFileStream = null;
88          try
89          {
90              originalFileStream = new FileInputStream(originalFile);
91              return retrieveOrCreateThumbNail(originalFileStream, originalFile.getName(), thumbnailFile, maxWidth, maxHeight, thumbnailId);
92          }
93          catch (FileNotFoundException e)
94          {
95              log.error("Unable to create thumbnail: file not found: " + originalFile.getAbsolutePath());
96          }
97          finally
98          {
99              try
100             {
101                 originalFileStream.close();
102             }
103             catch (IOException e)
104             {
105                 log.warn(e, e);
106             }
107         }
108 
109         return null;
110     }
111 
112     // All thumbnail images are stored in JPEG format on disk.
113     // With CORE-101 fixed this method may be called with a null image
114     public void storeImage(BufferedImage scaledImage, File file) throws FileNotFoundException
115     {
116         if (scaledImage == null)
117         {
118             log.warn("Can't store a null scaledImage.");
119             return;
120         }
121         FileImageOutputStream fout = null;
122         try
123         {
124             fout = new FileImageOutputStream((file));
125 
126             ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
127 
128             ImageWriteParam param = writer.getDefaultWriteParam();
129             param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
130             param.setCompressionQuality(encodingQuality);
131             writer.setOutput(fout);
132             writer.write(null,new IIOImage(scaledImage, null,null), param);
133         }
134         catch (IOException e)
135         {
136             log.error("Error encoding the thumbnail image", e);
137         }
138         finally
139         {
140             try
141             {
142                 if (fout != null)
143                 {
144                     fout.close();
145                 }
146             }
147             catch (IOException e)
148             {
149                 //
150             }
151         }
152     }
153 
154     public BufferedImage scaleImage(Image imageToScale, WidthHeightHelper newDimensions)
155     {
156         // If the original image is an instance of BufferedImage, we need to make sure
157         // that it is an sRGB image. If it is not, we need to convert it before scaling it
158         // as we run into these issue otherwise:
159         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4886071
160         // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4705399
161 
162         if (imageToScale instanceof BufferedImage)
163         {
164             BufferedImage bufferedImage = (BufferedImage) imageToScale;
165             if (!bufferedImage.getColorModel().getColorSpace().isCS_sRGB()) {
166                 BufferedImage sRGBImage = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_INT_RGB);
167                 Graphics g = sRGBImage.getGraphics();
168                 g.drawImage(bufferedImage, 0, 0, null);
169                 g.dispose();
170                 imageToScale = sRGBImage;
171             }
172         }
173 
174         AreaAveragingScaleFilter scaleFilter =
175                 new AreaAveragingScaleFilter(newDimensions.getWidth(), newDimensions.getHeight());
176         ImageProducer producer = new FilteredImageSource(imageToScale.getSource(),
177                 scaleFilter);
178 
179         SimpleImageConsumer generator = new SimpleImageConsumer();
180         producer.startProduction(generator);
181         BufferedImage scaled = generator.getImage();
182 
183         // CORE-101 getImage may return null
184         if (scaled == null)
185             log.warn("Unabled to create scaled image.");
186         else
187             scaled.flush();
188 
189         return scaled;
190     }
191 
192     /**
193      * Need to pass filename in as we cannot get the filename from the stream that is passed in
194      * @param originalFileStream
195      * @param fileName
196      * @param thumbnailFile
197      * @param maxWidth
198      * @param maxHeight
199      * @param thumbnailId
200      * @return a Thumbnail instance or null if an error occured
201      * @throws MalformedURLException
202      */
203     public Thumbnail retrieveOrCreateThumbNail(InputStream originalFileStream, String fileName, File thumbnailFile, int maxWidth, int maxHeight, long thumbnailId) throws MalformedURLException
204     {
205         Thumbnail thumbnail = null;
206         try
207         {
208             thumbnail = getThumbnail(thumbnailFile, fileName, thumbnailId);
209         }
210         catch (IOException e)
211         {
212             log.error("Unable to get thumbnail image for id " + thumbnailId, e);
213             return null;
214         }
215 
216         if (thumbnail == null)
217         {
218             try
219             {
220                 thumbnail = createThumbnail(originalFileStream, thumbnailFile, maxWidth, maxHeight, thumbnailId, fileName);
221             }
222             catch (IOException e)
223             {
224                 log.error("Unable to create thumbnail image for id " + thumbnailId, e);
225                 return null;
226             }
227         }
228 
229         return thumbnail;
230     }
231 
232     // PRIVATE METHODS -------------------------------------------------------------------------------------------
233     private BufferedImage scaleImage(Image originalImage, int maxWidth, int maxHeight)
234     {
235         return scaleImage(originalImage, determineScaleSize(maxWidth, maxHeight, originalImage));
236     }
237 
238     private WidthHeightHelper determineScaleSize(int maxWidth, int maxHeight, Image image)
239     {
240         return determineScaleSize(maxWidth, maxHeight, image.getWidth(null), image.getHeight(null));
241     }
242 
243     private Thumbnail createThumbnail(InputStream originalFile, File thumbnailFile, int maxWidth, int maxHeight, long thumbId, String fileName) throws IOException, FileNotFoundException
244     {
245         // Load original image.
246         Image originalImage = getImage(originalFile);
247         // Create scaled buffered image from original image.
248         BufferedImage scaledImage = scaleImage(originalImage, maxWidth, maxHeight);
249 
250         int height = scaledImage.getHeight();
251         int width = scaledImage.getWidth();
252 
253         storeImage(scaledImage, thumbnailFile);
254 
255         return new Thumbnail(height, width, fileName, thumbId);
256     }
257 
258     private Thumbnail getThumbnail(File thumbnailFile, String filename, long thumbId) throws IOException
259     {
260         if (thumbnailFile.exists())
261         {
262             final Image thumbImage = getImage(thumbnailFile);
263             return new Thumbnail(thumbImage.getHeight(null), thumbImage.getWidth(null), filename, thumbId);
264         }
265         return null;
266     }
267 
268     /**
269      * @param file
270      * @return An Image object or null if there was no suitable ImageReader for the given data
271      * @throws IOException
272      */
273     public Image getImage(File file) throws IOException
274     {
275         return ImageIO.read(file);
276     }
277 
278     /**
279      *
280      * @param is
281      * @return An Image object or null if there was no suitable ImageReader for the given data
282      * @throws IOException
283      */
284     public Image getImage(InputStream is) throws IOException
285     {
286         return ImageIO.read(is);
287     }
288 
289     /**
290      * Set the default encoding quality used by the thumber to encode jpegs.
291      * @param f
292      */
293     public void setEncodingQuality(float f)
294     {
295         if (f > 1.0f || f < 0.0f)
296         {
297             throw new IllegalArgumentException("Invalid quality setting '"+f+"', value must be between 0 and 1. ");
298         }
299         encodingQuality = f;
300     }
301 
302     public WidthHeightHelper determineScaleSize(int maxWidth, int maxHeight, int imageWidth, int imageHeight)
303     {
304         if (maxHeight > imageHeight && maxWidth > imageWidth)
305         {
306             return new Thumber.WidthHeightHelper(imageWidth, imageHeight);
307         }
308         // Determine scale size.
309         // Retain original image proportions with scaled image.
310         double thumbRatio = (double) maxWidth / (double) maxHeight;
311 
312         double imageRatio = (double) imageWidth / (double) imageHeight;
313 
314         if (thumbRatio < imageRatio)
315         {
316             return new Thumber.WidthHeightHelper(maxWidth, (int) (maxWidth / imageRatio));
317         }
318         else
319         {
320             return new Thumber.WidthHeightHelper((int) (maxHeight * imageRatio), maxHeight);
321         }
322     }
323 
324     public boolean isFileSupportedImage(File file)
325     {
326         try
327         {
328             return isFileSupportedImage(new FileInputStream(file));
329         }
330         catch (FileNotFoundException e)
331         {
332             return false;
333         }
334     }
335 
336     public boolean isFileSupportedImage(InputStream inputStream)
337     {
338         try
339         {
340             imageInfo.setInput(inputStream);
341             imageInfo.check();
342             return THUMBNAIL_FORMATS.contains(imageInfo.getFormatName());
343         }
344         finally
345         {
346             try
347             {
348                 if (inputStream != null)
349                 {
350                     inputStream.close();
351                 }
352             } catch (Exception e)
353             {
354                 log.error(e, e);
355             }
356         }
357     }
358 
359     public static class WidthHeightHelper
360     {
361         private int width;
362         private int height;
363 
364         public WidthHeightHelper(int width, int height)
365         {
366             this.width = width;
367             this.height = height;
368         }
369 
370         public int getWidth()
371         {
372             return width;
373         }
374 
375         public void setWidth(int width)
376         {
377             this.width = width;
378         }
379 
380         public int getHeight()
381         {
382             return height;
383         }
384 
385         public void setHeight(int height)
386         {
387             this.height = height;
388         }
389     }
390 }