1 package com.atlassian.core.util.thumbnail;
2
3 import com.atlassian.core.util.ImageInfo;
4 import org.apache.commons.io.IOUtils;
5 import org.apache.log4j.Logger;
6
7 import java.awt.*;
8 import java.awt.image.AreaAveragingScaleFilter;
9 import java.awt.image.BufferedImage;
10 import java.awt.image.FilteredImageSource;
11 import java.awt.image.ImageProducer;
12 import java.awt.image.PixelGrabber;
13 import java.io.File;
14 import java.io.FileInputStream;
15 import java.io.FileNotFoundException;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.net.MalformedURLException;
19 import java.util.Arrays;
20 import java.util.Collections;
21 import java.util.List;
22 import javax.imageio.IIOException;
23 import javax.imageio.IIOImage;
24 import javax.imageio.ImageIO;
25 import javax.imageio.ImageWriteParam;
26 import javax.imageio.ImageWriter;
27 import javax.imageio.stream.FileImageOutputStream;
28 import javax.swing.*;
29
30
31
32
33 public class Thumber
34 {
35 private static final Logger log = Logger.getLogger(Thumber.class);
36 private final ImageInfo imageInfo = new ImageInfo();
37
38
39 public static final List<String> THUMBNAIL_MIME_TYPES = Collections.unmodifiableList(Arrays.asList(ImageIO.getReaderMIMETypes()));
40 public static final List<String> THUMBNAIL_FORMATS = Collections.unmodifiableList(Arrays.asList(ImageIO.getReaderFormatNames()));
41 private Thumbnail.MimeType mimeType;
42
43
44
45
46
47 public Thumber()
48 {
49 this(Thumbnail.MimeType.JPG);
50 }
51
52
53
54
55
56 public Thumber(Thumbnail.MimeType mimeType)
57 {
58 if (mimeType == null)
59 {
60 throw new IllegalArgumentException("mimeType cannot be null");
61 }
62 this.mimeType = mimeType;
63 }
64
65
66 public static List<String> getThumbnailMimeTypes()
67 {
68 return THUMBNAIL_MIME_TYPES;
69 }
70
71 public static List<String> getThumbnailFormats()
72 {
73 return THUMBNAIL_FORMATS;
74 }
75
76 private float encodingQuality = 0.80f;
77
78
79
80
81 public boolean checkToolkit()
82 {
83 try
84 {
85 Toolkit.getDefaultToolkit();
86 }
87 catch (Throwable e)
88 {
89 log.error("Unable to acquire AWT default toolkit - thumbnails will not be displayed. Check DISPLAY variable or use setting -Djava.awt.headless=true.", e);
90 return false;
91 }
92 return true;
93 }
94
95
96
97
98
99
100
101
102
103
104
105 public Thumbnail retrieveOrCreateThumbNail(File originalFile, File thumbnailFile, int maxWidth, int maxHeight, long thumbnailId)
106 throws MalformedURLException
107 {
108 FileInputStream originalFileStream = null;
109 try
110 {
111 originalFileStream = new FileInputStream(originalFile);
112 return retrieveOrCreateThumbNail(originalFileStream, originalFile.getName(), thumbnailFile, maxWidth, maxHeight, thumbnailId);
113 }
114 catch (FileNotFoundException e)
115 {
116 log.error("Unable to create thumbnail: file not found: " + originalFile.getAbsolutePath());
117 }
118 finally
119 {
120 IOUtils.closeQuietly(originalFileStream);
121 }
122
123 return null;
124 }
125
126 private void storeImageAsPng(BufferedImage image, File file) throws FileNotFoundException
127 {
128 if (image == null)
129 {
130 log.warn("Can't store a null scaledImage.");
131 return;
132 }
133 try
134 {
135 ImageIO.write(image, "png", file);
136 }
137 catch (IOException e)
138 {
139 log.error("Error encoding the thumbnail image", e);
140 }
141 }
142
143
144
145 public void storeImage(BufferedImage scaledImage, File file) throws FileNotFoundException
146 {
147 if (scaledImage == null)
148 {
149 log.warn("Can't store a null scaledImage.");
150 return;
151 }
152 FileImageOutputStream fout = null;
153 try
154 {
155 fout = new FileImageOutputStream((file));
156
157 ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
158
159 ImageWriteParam param = writer.getDefaultWriteParam();
160 param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
161 param.setCompressionQuality(encodingQuality);
162 writer.setOutput(fout);
163 writer.write(null, new IIOImage(scaledImage, null, null), param);
164 }
165 catch (IOException e)
166 {
167 log.error("Error encoding the thumbnail image", e);
168 }
169 finally
170 {
171 try
172 {
173 if (fout != null)
174 {
175 fout.close();
176 }
177 }
178 catch (IOException e)
179 {
180
181 }
182 }
183 }
184
185 private BufferedImage scaleImageFastAndGood(BufferedImage imageToScale, WidthHeightHelper newDimensions)
186 {
187 if (newDimensions.width > imageToScale.getWidth() || newDimensions.height > imageToScale.getHeight())
188 {
189 return getScaledInstance(imageToScale, newDimensions.getWidth(), newDimensions.getHeight(),
190 RenderingHints.VALUE_INTERPOLATION_BICUBIC, false);
191 }
192 else
193 {
194 return getScaledInstance(imageToScale, newDimensions.getWidth(), newDimensions.getHeight(),
195 RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
196 }
197 }
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218 private BufferedImage getScaledInstance(BufferedImage image,
219 int targetWidth,
220 int targetHeight,
221 Object hint,
222 boolean higherQuality)
223 {
224 int type = (image.getTransparency() == Transparency.OPAQUE) ?
225 BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
226 int w, h;
227 if (higherQuality)
228 {
229
230
231
232 w = image.getWidth();
233 h = image.getHeight();
234 }
235 else
236 {
237
238
239 w = targetWidth;
240 h = targetHeight;
241 }
242
243 do
244 {
245 if (higherQuality && w > targetWidth)
246 {
247 w /= 2;
248 if (w < targetWidth)
249 {
250 w = targetWidth;
251 }
252 }
253
254 if (higherQuality && h > targetHeight)
255 {
256 h /= 2;
257 if (h < targetHeight)
258 {
259 h = targetHeight;
260 }
261 }
262
263 BufferedImage tmp = new BufferedImage(w, h, type);
264 Graphics2D g2 = tmp.createGraphics();
265 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
266 g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
267 g2.setComposite(AlphaComposite.SrcOver);
268 g2.drawImage(image, 0, 0, w, h, null);
269 g2.dispose();
270
271 image = tmp;
272 }
273 while (w != targetWidth || h != targetHeight);
274
275 return image;
276 }
277
278
279
280
281
282
283
284
285
286
287
288 public BufferedImage scaleImage(Image imageToScale, WidthHeightHelper newDimensions)
289 {
290 if (imageToScale instanceof BufferedImage)
291 {
292 return scaleImageFastAndGood((BufferedImage) imageToScale, newDimensions);
293 }
294 return scaleImageFastAndGood(Pictures.toBufferedImage(imageToScale), newDimensions);
295 }
296
297
298
299
300
301
302
303
304
305
306
307 @Deprecated
308 public BufferedImage scaleImageOld(Image imageToScale, WidthHeightHelper newDimensions)
309 {
310
311
312
313
314
315
316
317
318 if (imageToScale instanceof BufferedImage)
319 {
320 BufferedImage bufferedImage = (BufferedImage) imageToScale;
321 if (!bufferedImage.getColorModel().getColorSpace().isCS_sRGB())
322 {
323 BufferedImage sRGBImage = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_INT_RGB);
324 Graphics g = sRGBImage.getGraphics();
325 g.drawImage(bufferedImage, 0, 0, null);
326 g.dispose();
327 imageToScale = sRGBImage;
328 }
329 }
330
331 AreaAveragingScaleFilter scaleFilter =
332 new AreaAveragingScaleFilter(newDimensions.getWidth(), newDimensions.getHeight());
333 ImageProducer producer = new FilteredImageSource(imageToScale.getSource(),
334 scaleFilter);
335
336 SimpleImageConsumer generator = new SimpleImageConsumer();
337 producer.startProduction(generator);
338 BufferedImage scaled = generator.getImage();
339
340
341 if (scaled == null)
342 {
343 log.warn("Unabled to create scaled image.");
344 }
345 else
346 {
347 scaled.flush();
348 }
349
350 return scaled;
351 }
352
353
354
355
356
357
358 public Thumbnail retrieveOrCreateThumbNail(InputStream originalFileStream, String fileName, File thumbnailFile, int maxWidth, int maxHeight, long thumbnailId)
359 throws MalformedURLException
360 {
361 Thumbnail thumbnail = null;
362 try
363 {
364 thumbnail = getThumbnail(thumbnailFile, fileName, thumbnailId);
365 }
366 catch (IOException e)
367 {
368 log.error("Unable to get thumbnail image for id " + thumbnailId, e);
369 return null;
370 }
371
372 if (thumbnail == null)
373 {
374 try
375 {
376 thumbnail = createThumbnail(originalFileStream, thumbnailFile, maxWidth, maxHeight, thumbnailId, fileName);
377 }
378 catch (IIOException e)
379 {
380 log.info("Unable to create thumbnail image for id " + thumbnailId, e);
381 return null;
382 }
383 catch (IOException e)
384 {
385 log.error("Unable to create thumbnail image for id " + thumbnailId, e);
386 return null;
387 }
388 }
389
390 return thumbnail;
391 }
392
393
394 private BufferedImage scaleImage(Image originalImage, int maxWidth, int maxHeight)
395 {
396 return scaleImage(originalImage, determineScaleSize(maxWidth, maxHeight, originalImage));
397 }
398
399 private WidthHeightHelper determineScaleSize(int maxWidth, int maxHeight, Image image)
400 {
401 return determineScaleSize(maxWidth, maxHeight, image.getWidth(null), image.getHeight(null));
402 }
403
404 private Thumbnail createThumbnail(InputStream originalFile, File thumbnailFile, int maxWidth, int maxHeight, long thumbId, String fileName)
405 throws IOException, FileNotFoundException
406 {
407
408 final Image originalImage = getImage(originalFile);
409
410 final BufferedImage scaledImage = scaleImage(originalImage, maxWidth, maxHeight);
411
412 final int height = scaledImage.getHeight();
413 final int width = scaledImage.getWidth();
414
415 if (mimeType == Thumbnail.MimeType.PNG)
416 {
417 storeImageAsPng(scaledImage, thumbnailFile);
418 }
419 else
420 {
421 storeImage(scaledImage, thumbnailFile);
422 }
423
424 return new Thumbnail(height, width, fileName, thumbId, mimeType);
425 }
426
427
428 private Thumbnail getThumbnail(File thumbnailFile, String filename, long thumbId) throws IOException
429 {
430 if (thumbnailFile.exists())
431 {
432 final Image thumbImage = getImage(thumbnailFile);
433 return new Thumbnail(thumbImage.getHeight(null), thumbImage.getWidth(null), filename, thumbId, mimeType);
434 }
435 return null;
436 }
437
438
439
440
441 public Image getImage(File file) throws IOException
442 {
443 return ImageIO.read(file);
444 }
445
446
447
448
449 public Image getImage(InputStream is) throws IOException
450 {
451 return ImageIO.read(is);
452 }
453
454
455
456
457 public void setEncodingQuality(float f)
458 {
459 if (f > 1.0f || f < 0.0f)
460 {
461 throw new IllegalArgumentException("Invalid quality setting '" + f + "', value must be between 0 and 1. ");
462 }
463 encodingQuality = f;
464 }
465
466 public WidthHeightHelper determineScaleSize(int maxWidth, int maxHeight, int imageWidth, int imageHeight)
467 {
468 if (maxHeight > imageHeight && maxWidth > imageWidth)
469 {
470 return new Thumber.WidthHeightHelper(imageWidth, imageHeight);
471 }
472
473
474 double thumbRatio = (double) maxWidth / (double) maxHeight;
475
476 double imageRatio = (double) imageWidth / (double) imageHeight;
477
478 if (thumbRatio < imageRatio)
479 {
480 return new Thumber.WidthHeightHelper(maxWidth, (int) Math.max(1, maxWidth / imageRatio));
481 }
482 else
483 {
484 return new Thumber.WidthHeightHelper((int) Math.max(1, maxHeight * imageRatio), maxHeight);
485 }
486 }
487
488 public boolean isFileSupportedImage(File file)
489 {
490 try
491 {
492 return isFileSupportedImage(new FileInputStream(file));
493 }
494 catch (FileNotFoundException e)
495 {
496 return false;
497 }
498 }
499
500 public boolean isFileSupportedImage(InputStream inputStream)
501 {
502 try
503 {
504 imageInfo.setInput(inputStream);
505 imageInfo.check();
506 for (String format : THUMBNAIL_FORMATS)
507 {
508 if (format.equalsIgnoreCase(imageInfo.getFormatName()))
509 {
510 return true;
511 }
512 }
513 return false;
514 }
515 finally
516 {
517 try
518 {
519 if (inputStream != null)
520 {
521 inputStream.close();
522 }
523 }
524 catch (Exception e)
525 {
526 log.error(e, e);
527 }
528 }
529 }
530
531 public static class WidthHeightHelper
532 {
533 private int width;
534 private int height;
535
536 public WidthHeightHelper(int width, int height)
537 {
538 this.width = width;
539 this.height = height;
540 }
541
542 public int getWidth()
543 {
544 return width;
545 }
546
547 public void setWidth(int width)
548 {
549 this.width = width;
550 }
551
552 public int getHeight()
553 {
554 return height;
555 }
556
557 public void setHeight(int height)
558 {
559 this.height = height;
560 }
561 }
562
563
564
565
566
567 static class Pictures
568 {
569 public static BufferedImage toBufferedImage(Image image)
570 {
571 if (image instanceof BufferedImage) {return (BufferedImage) image;}
572
573
574 image = new ImageIcon(image).getImage();
575
576
577 boolean hasAlpha = hasAlpha(image);
578
579
580 int type = hasAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
581 BufferedImage bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), type);
582
583
584 Graphics g = bimage.createGraphics();
585
586
587 g.drawImage(image, 0, 0, null);
588 g.dispose();
589
590 return bimage;
591 }
592
593 public static boolean hasAlpha(Image image)
594 {
595
596 if (image instanceof BufferedImage)
597 {
598 return ((BufferedImage) image).getColorModel().hasAlpha();
599 }
600
601
602
603 PixelGrabber pg = new PixelGrabber(image, 0, 0, 1, 1, false);
604 try
605 {
606 pg.grabPixels();
607 }
608 catch (InterruptedException ignored)
609 {
610 }
611
612
613 return pg.getColorModel().hasAlpha();
614 }
615 }
616
617 }