1 | // Jomic - a viewer for comic book archives. |
2 | // Copyright (C) 2004-2011 Thomas Aglassinger |
3 | // |
4 | // This program is free software: you can redistribute it and/or modify |
5 | // it under the terms of the GNU General Public License as published by |
6 | // the Free Software Foundation, either version 3 of the License, or |
7 | // (at your option) any later version. |
8 | // |
9 | // This program is distributed in the hope that it will be useful, |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | // GNU General Public License for more details. |
13 | // |
14 | // You should have received a copy of the GNU General Public License |
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | package net.sf.jomic.tools; |
17 | |
18 | import java.awt.BasicStroke; |
19 | import java.awt.Color; |
20 | import java.awt.Dimension; |
21 | import java.awt.Graphics2D; |
22 | import java.awt.RenderingHints; |
23 | import java.awt.color.ColorSpace; |
24 | import java.awt.geom.AffineTransform; |
25 | import java.awt.geom.Ellipse2D; |
26 | import java.awt.image.BufferedImage; |
27 | import java.awt.image.BufferedImageOp; |
28 | import java.awt.image.ColorModel; |
29 | import java.awt.image.RenderedImage; |
30 | import java.io.File; |
31 | import java.io.FileNotFoundException; |
32 | import java.io.IOException; |
33 | import java.util.Arrays; |
34 | import java.util.Collections; |
35 | import java.util.Iterator; |
36 | import java.util.LinkedList; |
37 | import java.util.List; |
38 | import java.util.Map; |
39 | import java.util.NoSuchElementException; |
40 | import java.util.TreeMap; |
41 | |
42 | import javax.imageio.IIOException; |
43 | import javax.imageio.ImageIO; |
44 | import javax.imageio.ImageReadParam; |
45 | import javax.imageio.ImageReader; |
46 | import javax.imageio.ImageTypeSpecifier; |
47 | import javax.imageio.spi.ImageReaderSpi; |
48 | import javax.imageio.stream.ImageInputStream; |
49 | import javax.swing.ImageIcon; |
50 | |
51 | import net.sf.jomic.common.Settings; |
52 | |
53 | import org.apache.commons.logging.Log; |
54 | import org.apache.commons.logging.LogFactory; |
55 | |
56 | import com.jhlabs.image.GaussianFilter; |
57 | import com.jhlabs.image.SmartBlurFilter; |
58 | |
59 | /** |
60 | * Utility methods for working with images. |
61 | * |
62 | * @author Thomas Aglassinger |
63 | */ |
64 | public final class ImageTools |
65 | { |
66 | public static final int DISTINCT_ROTATIONS_COUNT = 4; |
67 | public static final String GAUSSIAN_BLUR = "GaussianBlur"; |
68 | public static final int ROTATE_CLOCKWISE = 1; |
69 | public static final int ROTATE_COUNTERCLOCKWISE = -1; |
70 | public static final int ROTATE_NONE = 0; |
71 | public static final int ROTATE_UPSIDE_DOWN = 2; |
72 | public static final String SCALE_ACTUAL = "actual"; |
73 | public static final String SCALE_FIT = "fit"; |
74 | public static final String SCALE_HEIGHT = "fitHeight"; |
75 | public static final String SCALE_WIDTH = "fitWidth"; |
76 | public static final String THRESHOLD_BLUR = "ThresholdBlur"; |
77 | |
78 | /** |
79 | * Index of brightness in HSB arrays. |
80 | */ |
81 | private static final int BRIGHTNESS_INDEX = 2; |
82 | private static final int BROKEN_IMAGE_STROKE_INSET_DIVISOR = 6; |
83 | private static final int BUSY_IMAGE_STROKE_INSET_DIVISOR = 3; |
84 | private static final int ROTATION_ANGLE_CLOCKWISE = 90; |
85 | private static final int ROTATION_ANGLE_COUNTER_CLOCKWISE = -90; |
86 | private static final int ROTATION_ANGLE_UPSIDE_DOWN = 180; |
87 | private static final int STROKE_WIDTH_DIVISOR = 10; |
88 | |
89 | private static ImageTools instance; |
90 | private String[] compressedImageFormats; |
91 | |
92 | private FileTools fileTools; |
93 | private Map imageProviderMap; |
94 | private List imageSuffixList; |
95 | private Map imageSuffixMap; |
96 | private LocaleTools localeTools; |
97 | private Log logger; |
98 | private String[] possibleBlurModes; |
99 | private RenderingHints renderHints; |
100 | private StringTools stringTools; |
101 | |
102 | private ImageTools() { |
103 | logger = LogFactory.getLog(ImageTools.class); |
104 | fileTools = FileTools.instance(); |
105 | localeTools = LocaleTools.instance(); |
106 | stringTools = StringTools.instance(); |
107 | imageSuffixMap = new TreeMap(); |
108 | imageProviderMap = new TreeMap(); |
109 | |
110 | compressedImageFormats = new String[]{"gif", "jpeg", "png"}; |
111 | Arrays.sort(compressedImageFormats); |
112 | |
113 | possibleBlurModes = new String[]{ |
114 | ImageTools.GAUSSIAN_BLUR, ImageTools.THRESHOLD_BLUR}; |
115 | Arrays.sort(possibleBlurModes); |
116 | |
117 | // HACK: Force scanning for plug ins. This should not be necessary, but |
118 | // otherwise Java Web Start on Mac OS X doesn't seem to find them. |
119 | ImageIO.scanForPlugins(); |
120 | |
121 | String[] formats = ImageIO.getReaderFormatNames(); |
122 | |
123 | for (int i = 0; i < formats.length; i += 1) { |
124 | String format = formats[i].toLowerCase(); |
125 | Iterator rider = ImageIO.getImageReadersByFormatName(format); |
126 | |
127 | assert rider.hasNext() : "no reader for format \"" + format; |
128 | ImageReader reader = (ImageReader) rider.next(); |
129 | ImageReaderSpi provider = reader.getOriginatingProvider(); |
130 | |
131 | imageSuffixMap.put(format, format); |
132 | imageProviderMap.put(format, provider); |
133 | } |
134 | renderHints = new RenderingHints(null); |
135 | renderHints.add(new RenderingHints( |
136 | RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)); |
137 | renderHints.add(new RenderingHints( |
138 | RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)); |
139 | renderHints.add(new RenderingHints( |
140 | RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY)); |
141 | renderHints.add(new RenderingHints( |
142 | RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE)); |
143 | renderHints.add(new RenderingHints( |
144 | RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)); |
145 | renderHints.add(new RenderingHints( |
146 | RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); |
147 | // HACK: work-around for missing suffix in Apple's TIFF reader. |
148 | possiblyAddImageFormatCopy("tiff", "tif"); |
149 | possiblyAddImageFormatCopy("jpeg 2000", "jp2"); |
150 | imageSuffixList = new LinkedList(imageSuffixMap.values()); |
151 | Collections.sort(imageSuffixList); |
152 | |
153 | if (logger.isInfoEnabled()) { |
154 | String suffixes = ""; |
155 | Iterator suffixRider = imageSuffixList.iterator(); |
156 | |
157 | while (suffixRider.hasNext()) { |
158 | String suffix = (String) suffixRider.next(); |
159 | |
160 | if (suffixes.length() > 0) { |
161 | suffixes += ", "; |
162 | } |
163 | suffixes += suffix; |
164 | } |
165 | logger.info("supported image file suffixes: " + imageSuffixList); |
166 | logger.info("supported image providers:"); |
167 | |
168 | Iterator rider = imageProviderMap.entrySet().iterator(); |
169 | |
170 | while (rider.hasNext()) { |
171 | Map.Entry entry = (Map.Entry) rider.next(); |
172 | ImageReaderSpi provider = (ImageReaderSpi) entry.getValue(); |
173 | |
174 | logger.info(" " + entry.getKey() + ": " + provider.getVendorName() + ", v" + provider.getVersion()); |
175 | } |
176 | |
177 | for (int i = 0; i < formats.length; i += 1) { |
178 | String format = formats[i].toLowerCase(); |
179 | |
180 | rider = ImageIO.getImageReadersByFormatName(format); |
181 | |
182 | ImageReader reader = (ImageReader) rider.next(); |
183 | |
184 | while (rider.hasNext()) { |
185 | reader = (ImageReader) rider.next(); |
186 | |
187 | ImageReaderSpi provider = reader.getOriginatingProvider(); |
188 | |
189 | logger.info(" " + format + ": " + provider.getVendorName() + ", v" + provider.getVersion()); |
190 | } |
191 | } |
192 | } |
193 | |
194 | assert imageSuffixList.size() > 0; |
195 | assert imageProviderMap.size() > 0; |
196 | } |
197 | |
198 | /** |
199 | * Convert <code>image</code> to <code>BufferedImage</code>. This is slow. |
200 | */ |
201 | public BufferedImage getAsBufferedImage(RenderedImage image) { |
202 | return getAsQuickBufferedImage(image); |
203 | } |
204 | |
205 | /** |
206 | * Convert <code>image</code> to <code>ImageIcon</code>. This is slow. |
207 | */ |
208 | public ImageIcon getAsImageIcon(RenderedImage image) { |
209 | assert image != null; |
210 | BufferedImage buffered = getAsBufferedImage(image); |
211 | ImageIcon result = new ImageIcon(buffered); |
212 | |
213 | return result; |
214 | } |
215 | |
216 | /** |
217 | * Get image in a form that can be processed quickly by most Java rendering operations. |
218 | */ |
219 | public BufferedImage getAsQuickBufferedImage(RenderedImage image) { |
220 | // TODO: Consolidate this with getAsBufferedImage(). |
221 | BufferedImage result; |
222 | int targetType = BufferedImage.TYPE_INT_RGB; |
223 | |
224 | if ((image instanceof BufferedImage) && ((BufferedImage) image).getType() == targetType) { |
225 | result = (BufferedImage) image; |
226 | } else { |
227 | result = new BufferedImage(image.getWidth(), image.getHeight(), targetType); |
228 | |
229 | Graphics2D g2d = result.createGraphics(); |
230 | |
231 | try { |
232 | AffineTransform identityTransformation = new AffineTransform(); |
233 | |
234 | g2d.drawRenderedImage(image, identityTransformation); |
235 | } finally { |
236 | g2d.dispose(); |
237 | } |
238 | } |
239 | return result; |
240 | } |
241 | |
242 | public RenderedImage getBluredImage(RenderedImage image, String blurMode) { |
243 | assert isValidBlurMode(blurMode) : "blurMode=" + blurMode; |
244 | BufferedImage result; |
245 | BufferedImage bufferedSourceImage = getAsBufferedImage(image); |
246 | Settings settings = Settings.instance(); |
247 | int radius = settings.getBlurRadius(); |
248 | BufferedImageOp blur; |
249 | |
250 | if (blurMode.equals(THRESHOLD_BLUR)) { |
251 | int threshold = settings.getBlurThreshold(); |
252 | |
253 | blur = new SmartBlurFilter(radius, threshold); |
254 | } else { |
255 | if (!blurMode.equals(GAUSSIAN_BLUR)) { |
256 | logger.warn("unknown blur mode \"" + blurMode + "\", using \"" + GAUSSIAN_BLUR + "\""); |
257 | } |
258 | blur = new GaussianFilter(radius); |
259 | } |
260 | |
261 | result = blur.filter(bufferedSourceImage, null); |
262 | return result; |
263 | } |
264 | |
265 | /** |
266 | * Get relative Rotation needed to go from absolute rotation <code>oldRotation</code> to <code>newRotation</code> |
267 | * . |
268 | */ |
269 | public int getDeltaRotation(int oldRotation, int newRotation) { |
270 | assertIsValidRotation(oldRotation); |
271 | assertIsValidRotation(newRotation); |
272 | |
273 | int result = getFixedRotation(-(oldRotation - newRotation)); |
274 | |
275 | assertIsValidRotation(result); |
276 | return result; |
277 | } |
278 | |
279 | /** |
280 | * Get the image dimension of <code>imageStream</code> or <code>null</code> if no reader can be |
281 | * found. |
282 | */ |
283 | public Dimension getImageDimension(ImageInputStream imageStream) |
284 | throws IOException { |
285 | |
286 | Dimension result; |
287 | |
288 | try { |
289 | ImageReader reader = getImageReader(imageStream); |
290 | |
291 | try { |
292 | reader.setInput(imageStream); |
293 | |
294 | int height = reader.getHeight(0); |
295 | int width = reader.getWidth(0); |
296 | |
297 | result = new Dimension(width, height); |
298 | if (logger.isDebugEnabled()) { |
299 | logger.debug("image dimension: " + result.width + "x" + result.height); |
300 | } |
301 | } finally { |
302 | reader.dispose(); |
303 | } |
304 | } catch (NoSuchElementException error) { |
305 | result = null; |
306 | } |
307 | return result; |
308 | } |
309 | |
310 | public Dimension getImageDimension(File imageFile) |
311 | throws IOException { |
312 | assert imageFile != null; |
313 | |
314 | Dimension result; |
315 | ImageInputStream imageStream = createImageInputStream(imageFile); |
316 | |
317 | try { |
318 | result = getImageDimension(imageStream); |
319 | } finally { |
320 | imageStream.close(); |
321 | } |
322 | return result; |
323 | } |
324 | |
325 | /** |
326 | * Get the format of <code>imageStream</code> or <code>null</code> if no image stream or reader |
327 | * is available. |
328 | * |
329 | * @see ImageIO#getImageReaders(java.lang.Object) |
330 | * @see ImageIO#createImageInputStream(Object) |
331 | * @see ImageReader#getFormatName() |
332 | */ |
333 | public String getImageFormat(ImageInputStream imageStream) |
334 | throws IOException { |
335 | String result = null; |
336 | |
337 | if (imageStream != null) { |
338 | Iterator readerRider = ImageIO.getImageReaders(imageStream); |
339 | |
340 | while ((result == null) && readerRider.hasNext()) { |
341 | ImageReader reader = (ImageReader) readerRider.next(); |
342 | |
343 | result = reader.getFormatName(); |
344 | assert result != null; |
345 | result = result.toLowerCase(); |
346 | if (logger.isDebugEnabled()) { |
347 | logger.debug("image format=" + result); |
348 | } |
349 | } |
350 | } |
351 | return result; |
352 | } |
353 | |
354 | /** |
355 | * Get the format of <code>imageFile</code> or <code>null</code> if no image reader is |
356 | * available for it. |
357 | * |
358 | * @see ImageIO#getImageReaders(java.lang.Object) |
359 | * @see ImageReader#getFormatName() |
360 | */ |
361 | public String getImageFormat(File imageFile) |
362 | throws IOException { |
363 | String result = null; |
364 | ImageInputStream imageStream = ImageIO.createImageInputStream(imageFile); |
365 | |
366 | if (imageStream != null) { |
367 | try { |
368 | result = getImageFormat(imageStream); |
369 | } finally { |
370 | imageStream.close(); |
371 | } |
372 | } |
373 | return result; |
374 | } |
375 | |
376 | /** |
377 | * Get a map with the keys being an image file suffix, and the value being the name of the |
378 | * provider handling it. |
379 | */ |
380 | public Map getImageProviderMap() { |
381 | return imageProviderMap; |
382 | } |
383 | |
384 | /** |
385 | * Get an reader for in the image in <code>imageStream</code>. |
386 | */ |
387 | public ImageReader getImageReader(ImageInputStream imageStream) { |
388 | return (ImageReader) ImageIO.getImageReaders(imageStream).next(); |
389 | } |
390 | |
391 | public int getLeftRotation(int rotation) { |
392 | assertIsValidRotation(rotation); |
393 | |
394 | int result = getFixedRotation(rotation - 1); |
395 | |
396 | return result; |
397 | } |
398 | |
399 | /** |
400 | * Like JAI's <code>PlanarImage.getNumBands()</code>, but also works on <code>RenderedImage</code> |
401 | * . |
402 | */ |
403 | public int getNumBands(RenderedImage image) { |
404 | assert image != null; |
405 | return image.getColorModel().getColorSpace().getNumComponents(); |
406 | } |
407 | |
408 | /** |
409 | * @see #getBluredImage(RenderedImage, String) |
410 | */ |
411 | public String[] getPossibleBlurModes() { |
412 | return possibleBlurModes; |
413 | } |
414 | |
415 | /** |
416 | * RenderingHints used by all render operations within ImageTools. |
417 | */ |
418 | public RenderingHints getRenderingHints() { |
419 | return renderHints; |
420 | } |
421 | |
422 | public int getRightRotation(int rotation) { |
423 | assertIsValidRotation(rotation); |
424 | |
425 | int result = getFixedRotation(rotation + 1); |
426 | |
427 | return result; |
428 | } |
429 | |
430 | public RenderedImage getRotatedImage(RenderedImage image, double angle) { |
431 | double sinusOfAngle = Math.abs(Math.sin(angle)); |
432 | double cosinusOfAngle = Math.abs(Math.cos(angle)); |
433 | int imageWidth = image.getWidth(); |
434 | int imageHeight = image.getHeight(); |
435 | int rotatedImageWidth = (int) Math.floor(imageWidth * cosinusOfAngle + imageHeight * sinusOfAngle); |
436 | int rotatedImageHeight = (int) Math.floor(imageHeight * cosinusOfAngle + imageWidth * sinusOfAngle); |
437 | BufferedImage result = createBufferedImage(rotatedImageWidth, rotatedImageHeight); |
438 | Graphics2D g2d = result.createGraphics(); |
439 | |
440 | try { |
441 | g2d.translate((rotatedImageWidth - imageWidth) / 2, (rotatedImageHeight - imageHeight) / 2); |
442 | g2d.rotate(angle, imageWidth / 2, imageHeight / 2); |
443 | g2d.drawRenderedImage(image, null); |
444 | } finally { |
445 | g2d.dispose(); |
446 | } |
447 | return result; |
448 | } |
449 | |
450 | /** |
451 | * Rotate image in steps of 90 degrees. |
452 | * |
453 | * @param rotation -1=counter clockwise, 0=no rotation, 1=clockwise, 2=upside down |
454 | */ |
455 | public RenderedImage getRotatedImage(RenderedImage image, int rotation) { |
456 | assertIsValidRotation(rotation); |
457 | |
458 | double radians = 0; |
459 | |
460 | if (rotation == ROTATE_CLOCKWISE) { |
461 | radians = Math.toRadians(ROTATION_ANGLE_CLOCKWISE); |
462 | } else if (rotation == ROTATE_NONE) { |
463 | radians = 0; |
464 | } else if (rotation == ROTATE_UPSIDE_DOWN) { |
465 | radians = Math.toRadians(ROTATION_ANGLE_UPSIDE_DOWN); |
466 | } else { |
467 | assert rotation == ROTATE_COUNTERCLOCKWISE; |
468 | radians = Math.toRadians(ROTATION_ANGLE_COUNTER_CLOCKWISE); |
469 | } |
470 | |
471 | return getRotatedImage(image, radians); |
472 | } |
473 | |
474 | /** |
475 | * Compute scale to squeeze into an area of size <code>areaWidth</code>x<code>areaHeight</code> |
476 | * an image of size <code>sourceWidth</code>x<code>sourceHeight</code>. |
477 | */ |
478 | public double getScaleToSqueeze( |
479 | int areaWidth, int areaHeight, int sourceWidth, int sourceHeight, String scaleMode) { |
480 | double result; |
481 | |
482 | if (scaleMode.equals(SCALE_FIT)) { |
483 | double sourceRatio = ((double) sourceWidth) / sourceHeight; |
484 | double areaRatio = ((double) areaWidth) / areaHeight; |
485 | |
486 | if (logger.isDebugEnabled()) { |
487 | logger.debug("area: " + areaWidth + " / " + areaHeight + " = " + areaRatio); |
488 | logger.debug("source: " + sourceWidth + " / " + sourceHeight + " = " + sourceRatio); |
489 | } |
490 | |
491 | if (sourceRatio < areaRatio) { |
492 | result = ((double) areaHeight) / sourceHeight; |
493 | } else { |
494 | result = ((double) areaWidth) / sourceWidth; |
495 | } |
496 | } else if (scaleMode.equals(SCALE_HEIGHT)) { |
497 | result = ((double) areaHeight) / sourceHeight; |
498 | } else if (scaleMode.equals(SCALE_WIDTH)) { |
499 | result = ((double) areaWidth) / sourceWidth; |
500 | } else { |
501 | assert scaleMode.equals(SCALE_ACTUAL) : "scaleMode=" + scaleMode; |
502 | result = 1.0; |
503 | } |
504 | |
505 | if (logger.isDebugEnabled()) { |
506 | logger.debug("-> scale = " + result); |
507 | } |
508 | return result; |
509 | } |
510 | |
511 | /** |
512 | * Get a rescaled version of <code>source</code> that depending on <code>scaleMode</code> |
513 | * possibly fits <code>width</code> and/or <code>height</code> while keeping proportions. |
514 | * |
515 | * @param scaleMode one of: SCALE_ACTUAL, SCALE_FIT, SCALE_WIDTH, SCALE_HEIGHT |
516 | */ |
517 | public RenderedImage getSqueezed( |
518 | final RenderedImage source, |
519 | final int areaWidth, |
520 | final int areaHeight, |
521 | final String scaleMode) { |
522 | assert source != null; |
523 | assert areaWidth > 0; |
524 | assert areaHeight > 0; |
525 | assertIsValidScaleMode(scaleMode); |
526 | |
527 | RenderedImage result; |
528 | int sourceWidth = source.getWidth(); |
529 | int sourceHeight = source.getHeight(); |
530 | |
531 | if (scaleMode.equals(SCALE_ACTUAL)) { |
532 | result = source; |
533 | } else { |
534 | double scale = getScaleToSqueeze(areaWidth, areaHeight, sourceWidth, sourceHeight, scaleMode); |
535 | |
536 | if (scale == 1.0) { |
537 | result = source; |
538 | } else { |
539 | // create the scaled image |
540 | int targetWidth = (int) Math.round(scale * sourceWidth); |
541 | int targetHeight = (int) Math.round(scale * sourceHeight); |
542 | Graphics2D g2d; |
543 | |
544 | result = createBufferedImage(targetWidth, targetHeight); |
545 | g2d = ((BufferedImage) result).createGraphics(); |
546 | try { |
547 | g2d.setRenderingHints(renderHints); |
548 | g2d.drawRenderedImage(source, AffineTransform.getScaleInstance(scale, scale)); |
549 | } finally { |
550 | g2d.dispose(); |
551 | } |
552 | } |
553 | } |
554 | |
555 | return result; |
556 | } |
557 | |
558 | /** |
559 | * Get the size of the actual area used by a sourceWidth x sourceHeight image squeezed in an |
560 | * areaWidth x areaHeight area. |
561 | */ |
562 | public Dimension getSqueezedDimension( |
563 | int areaWidth, int areaHeight, int sourceWidth, int sourceHeight, String scaleMode) { |
564 | double scale = getScaleToSqueeze(areaWidth, areaHeight, sourceWidth, sourceHeight, scaleMode); |
565 | int squeezedWidth = Math.min(areaWidth, (int) Math.round(scale * sourceWidth)); |
566 | int squeezedHeight = Math.min(areaHeight, (int) Math.round(scale * sourceHeight)); |
567 | Dimension result = new Dimension(squeezedWidth, squeezedHeight); |
568 | |
569 | return result; |
570 | } |
571 | |
572 | /** |
573 | * Yield <code>true</code> if <code>imageFormat</code> indicates a compressed reformat. Images |
574 | * in such formats will not shrink significantly by compressing them using for example ZIP. |
575 | * |
576 | * @see #getImageFormat(ImageInputStream) |
577 | */ |
578 | public boolean isCompressedImageFormat(String imageFormat) { |
579 | boolean result = Arrays.binarySearch(compressedImageFormats, imageFormat) >= 0; |
580 | |
581 | return result; |
582 | } |
583 | |
584 | /** |
585 | * Yield <code>true</code> if <code>filePathToCheck</code> indicates an image file. The main |
586 | * indicator is the suffix, additionally file names starting with "." are not considered images |
587 | * even if the suffix would match. The latter ensures that files storing the resource fork in |
588 | * ZIP archives created by Mac OS X's "compress" are ignored. |
589 | */ |
590 | public boolean isImageFile(String filePathToCheck) { |
591 | assert filePathToCheck != null; |
592 | boolean result; |
593 | |
594 | if (isImageSuffix(fileTools.getSuffix(filePathToCheck))) { |
595 | File imageFile = new File(filePathToCheck); |
596 | String imageName = imageFile.getName(); |
597 | |
598 | result = !imageName.startsWith("."); |
599 | } else { |
600 | result = false; |
601 | } |
602 | return result; |
603 | } |
604 | |
605 | public boolean isImageFile(File file) { |
606 | assert file != null; |
607 | return isImageSuffix(fileTools.getSuffix(file)); |
608 | } |
609 | |
610 | public boolean isImageSuffix(String suffix) { |
611 | assert suffix != null; |
612 | return Collections.binarySearch(imageSuffixList, suffix.toLowerCase()) >= 0; |
613 | } |
614 | |
615 | public boolean isLandscape(int width, int height) { |
616 | return (width > height); |
617 | } |
618 | |
619 | public boolean isLandscape(Dimension size) { |
620 | return isLandscape(size.width, size.height); |
621 | } |
622 | |
623 | public boolean isLandscape(RenderedImage image) { |
624 | return isLandscape(image.getWidth(), image.getHeight()); |
625 | } |
626 | |
627 | public boolean isValidBlurMode(String some) { |
628 | return stringTools.equalsAnyOf(possibleBlurModes, some); |
629 | } |
630 | |
631 | public boolean isValidRotation(int rotation) { |
632 | return (rotation == ROTATE_NONE) |
633 | || (rotation == ROTATE_CLOCKWISE) |
634 | || (rotation == ROTATE_UPSIDE_DOWN) |
635 | || (rotation == ROTATE_COUNTERCLOCKWISE); |
636 | } |
637 | |
638 | public boolean isValidScaleMode(String mode) { |
639 | return (mode != null) && (mode.equals(ImageTools.SCALE_ACTUAL) |
640 | || mode.equals(ImageTools.SCALE_FIT) |
641 | || mode.equals(ImageTools.SCALE_HEIGHT) |
642 | || mode.equals(ImageTools.SCALE_WIDTH)); |
643 | } |
644 | |
645 | /** |
646 | * Get adjusted rotation so that it fits in valid range. |
647 | */ |
648 | int getFixedRotation(int possiblyInvalidRotation) { |
649 | int result = ((possiblyInvalidRotation + 1) % DISTINCT_ROTATIONS_COUNT) - 1; |
650 | |
651 | if (result < -1) { |
652 | result += DISTINCT_ROTATIONS_COUNT; |
653 | } |
654 | assertIsValidRotation(result); |
655 | return result; |
656 | } |
657 | |
658 | |
659 | /** |
660 | * Get accessor to unique instance. |
661 | */ |
662 | public static synchronized ImageTools instance() { |
663 | if (instance == null) { |
664 | instance = new ImageTools(); |
665 | } |
666 | return instance; |
667 | } |
668 | |
669 | public void assertIsValidRotation(int rotation) { |
670 | assert (rotation >= -1) && (rotation <= 2) : "rotation = " + rotation; |
671 | } |
672 | |
673 | public void assertIsValidScaleMode(String scaleMode) { |
674 | assert isValidScaleMode(scaleMode) : "scaleMode = " + scaleMode; |
675 | } |
676 | |
677 | /** |
678 | * Create image to represent a broken item. |
679 | */ |
680 | public RenderedImage createBrokenImage(int width, int height, Color background, Color foreground) { |
681 | assert width > 0; |
682 | assert height > 0; |
683 | assert background != null; |
684 | assert foreground != null; |
685 | |
686 | BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
687 | Graphics2D g = result.createGraphics(); |
688 | |
689 | try { |
690 | float strokeWidth = Math.max(1f, ((float) Math.min(width, height)) / STROKE_WIDTH_DIVISOR); |
691 | int insetX = width / BROKEN_IMAGE_STROKE_INSET_DIVISOR; |
692 | int insetY = height / BROKEN_IMAGE_STROKE_INSET_DIVISOR; |
693 | |
694 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
695 | g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); |
696 | g.setColor(background); |
697 | g.fillRect(0, 0, width, height); |
698 | g.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
699 | g.setColor(foreground); |
700 | g.drawLine(insetX, insetY, width - insetX, height - insetY); |
701 | g.drawLine(width - insetX, insetY, insetX, height - insetY); |
702 | } finally { |
703 | g.dispose(); |
704 | } |
705 | return result; |
706 | } |
707 | |
708 | /** |
709 | * Create image to represent a busy image that is in process of being rendered. |
710 | */ |
711 | public RenderedImage createBusyImage(int width, int height, Color background, Color foreground) { |
712 | assert width > 0; |
713 | assert height > 0; |
714 | assert background != null; |
715 | assert foreground != null; |
716 | |
717 | BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
718 | Graphics2D g = result.createGraphics(); |
719 | |
720 | try { |
721 | float strokeWidth = Math.max(1f, ((float) Math.min(width, height)) / STROKE_WIDTH_DIVISOR); |
722 | float baseDiameter = Math.min(width, height); |
723 | int inset = Math.min(width / BUSY_IMAGE_STROKE_INSET_DIVISOR, |
724 | height / BUSY_IMAGE_STROKE_INSET_DIVISOR); |
725 | float radius = baseDiameter - 2 * inset; |
726 | Ellipse2D.Double ellipse = new Ellipse2D.Double( |
727 | (width - radius) / 2, (height - radius) / 2, radius, radius); |
728 | |
729 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
730 | g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); |
731 | g.setColor(background); |
732 | g.fillRect(0, 0, width, height); |
733 | g.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
734 | g.setColor(foreground); |
735 | g.draw(ellipse); |
736 | } finally { |
737 | g.dispose(); |
738 | } |
739 | return result; |
740 | } |
741 | |
742 | /** |
743 | * Create a rectangle of a certain color. |
744 | */ |
745 | public BufferedImage createColorBox(int width, int height, Color color) { |
746 | assert width > 0; |
747 | assert height > 0; |
748 | assert color != null; |
749 | BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
750 | Graphics2D g = result.createGraphics(); |
751 | |
752 | try { |
753 | g.setColor(color); |
754 | g.fillRect(0, 0, width, height); |
755 | g.setColor(Color.BLACK); |
756 | g.drawRect(0, 0, width - 1, height - 1); |
757 | } finally { |
758 | g.dispose(); |
759 | } |
760 | return result; |
761 | } |
762 | |
763 | /** |
764 | * Same as ImageIO.createImageInputStream, but throws a <code>FileNotFoundException</code> if |
765 | * <code>imageFile</code> cannot be found (instead of returning <code>null</code>). Why the |
766 | * original does not work that way already is beyond me.<p> |
767 | * |
768 | * Note that a non-image (such as a text file) still returns proper stream. |
769 | */ |
770 | public ImageInputStream createImageInputStream(File imageFile) |
771 | throws IOException { |
772 | assert imageFile != null; |
773 | ImageInputStream result = ImageIO.createImageInputStream(imageFile); |
774 | |
775 | if (result == null) { |
776 | String message = localeTools.getMessage("errors.cannotFindImageFile", imageFile); |
777 | |
778 | throw new FileNotFoundException(message); |
779 | } |
780 | return result; |
781 | } |
782 | |
783 | /** |
784 | * Compute fill values for <code>color</code> when it should be used by the "border" operator |
785 | * on an image with a ColorModel of <code>colorModel</code>. |
786 | */ |
787 | public double[] fillColorValues(Color color, ColorModel colorModel) { |
788 | assert color != null; |
789 | assert colorModel != null; |
790 | |
791 | double[] result; |
792 | ColorSpace colorSpace = colorModel.getColorSpace(); |
793 | boolean isGray = colorSpace.getType() == ColorSpace.TYPE_GRAY; |
794 | float[] fillValuesFloat; |
795 | |
796 | if (isGray) { |
797 | // In case of grayscale images, use brightness instead of color |
798 | float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); |
799 | float brightness = hsb[BRIGHTNESS_INDEX]; |
800 | |
801 | assert colorSpace.getNumComponents() == 1; |
802 | float minValue = colorSpace.getMinValue(0); |
803 | float maxValue = colorSpace.getMaxValue(0); |
804 | float range = maxValue - minValue; |
805 | |
806 | fillValuesFloat = new float[]{brightness * range + minValue}; |
807 | } else { |
808 | // Otherwise, just use the color components |
809 | fillValuesFloat = color.getColorComponents(colorSpace, null); |
810 | } |
811 | |
812 | // Convert float[] to double[] |
813 | result = new double[fillValuesFloat.length]; |
814 | for (int i = 0; i < fillValuesFloat.length; i += 1) { |
815 | result[i] = ((1 << colorModel.getComponentSize(i)) - 1) * fillValuesFloat[i]; |
816 | } |
817 | if (logger.isInfoEnabled()) { |
818 | String colorText = stringTools.colorString(color.getRGB()); |
819 | |
820 | logger.info("fillValues for " + colorText + ": " + stringTools.arrayToString(result)); |
821 | } |
822 | return result; |
823 | } |
824 | |
825 | /** |
826 | * Read the specified <code>imageFile</code>. |
827 | */ |
828 | public RenderedImage readImage(File imageFile) |
829 | throws IOException { |
830 | assert imageFile != null; |
831 | RenderedImage result; |
832 | Exception errorCause = null; |
833 | ImageInputStream in = createImageInputStream(imageFile); |
834 | |
835 | try { |
836 | ImageReader reader = getImageReader(in); |
837 | |
838 | try { |
839 | reader.setInput(in); |
840 | try { |
841 | ImageReadParam readParameters = reader.getDefaultReadParam(); |
842 | |
843 | readParameters.setDestinationType( |
844 | ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB)); |
845 | result = reader.read(0); |
846 | } finally { |
847 | reader.dispose(); |
848 | } |
849 | } finally { |
850 | in.close(); |
851 | } |
852 | } catch (Exception error) { |
853 | result = null; |
854 | errorCause = error; |
855 | } |
856 | if (result == null) { |
857 | String message = localeTools.getMessage("errors.cannotReadImageFile", imageFile); |
858 | |
859 | throw new IIOException(message, errorCause); |
860 | } |
861 | result = getAsQuickBufferedImage(result); |
862 | return result; |
863 | } |
864 | |
865 | private BufferedImage createBufferedImage(int width, int height) { |
866 | assert width >= 0; |
867 | assert height >= 0; |
868 | return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); |
869 | } |
870 | |
871 | /** |
872 | * If there is a provider for suffix <code>original</code> but none for suffix <code>copy</code> |
873 | * , add the same provider for it. |
874 | */ |
875 | private void possiblyAddImageFormatCopy(String original, String copy) { |
876 | assert original != null; |
877 | assert copy != null; |
878 | assert original.equals(original.toLowerCase()); |
879 | assert copy.equals(copy.toLowerCase()); |
880 | assert !original.equals(copy); |
881 | |
882 | Object originalProvider = imageProviderMap.get(original); |
883 | |
884 | if (originalProvider != null) { |
885 | Object copyProvider = imageProviderMap.get(copy); |
886 | |
887 | if (copyProvider == null) { |
888 | logger.warn("adding suffix " + stringTools.sourced(copy) |
889 | + " to broken provider for suffix " + stringTools.sourced(original)); |
890 | imageProviderMap.put(copy, originalProvider); |
891 | imageSuffixMap.put(copy, copy); |
892 | } |
893 | } |
894 | } |
895 | } |