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