001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.resources.image;
018:
019: import java.awt.Dimension;
020: import java.awt.RenderingHints;
021: import java.awt.image.IndexColorModel;
022: import java.awt.image.RenderedImage;
023: import java.io.IOException;
024: import java.util.Iterator;
025: import java.util.List;
026:
027: import javax.imageio.ImageReader;
028: import javax.imageio.spi.IIORegistry;
029: import javax.imageio.spi.ImageReaderSpi;
030: import javax.imageio.spi.ImageReaderWriterSpi;
031: import javax.imageio.spi.ImageWriterSpi;
032: import javax.media.jai.BorderExtender;
033: import javax.media.jai.BorderExtenderCopy;
034: import javax.media.jai.BorderExtenderReflect;
035: import javax.media.jai.ImageLayout;
036: import javax.media.jai.Interpolation;
037: import javax.media.jai.InterpolationNearest;
038: import javax.media.jai.JAI;
039: import javax.media.jai.OpImage;
040: import javax.media.jai.ParameterBlockJAI;
041: import javax.media.jai.RenderedOp;
042:
043: import org.geotools.resources.Utilities;
044: import org.geotools.resources.i18n.ErrorKeys;
045: import org.geotools.resources.i18n.Errors;
046:
047: import com.sun.media.jai.operator.ImageReadDescriptor;
048:
049: /**
050: * A set of static methods working on images. Some of those methods are useful, but not
051: * really rigorous. This is why they do not appear in any "official" package, but instead
052: * in this private one.
053: *
054: * <strong>Do not rely on this API!</strong>
055: *
056: * It may change in incompatible way in any future version.
057: *
058: * @since 2.0
059: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/resources/image/ImageUtilities.java $
060: * @version $Id: ImageUtilities.java 26890 2007-09-07 11:05:40Z desruisseaux $
061: * @author Martin Desruisseaux
062: * @author Simone Giannecchini
063: */
064: public final class ImageUtilities {
065: /**
066: * {@link RenderingHints} used to prevent {@link JAI} operations from expanding
067: * {@link IndexColorModel}s.
068: */
069: public final static RenderingHints DONT_REPLACE_INDEX_COLOR_MODEL = new RenderingHints(
070: JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
071:
072: /**
073: * {@link RenderingHints} used to force {@link JAI} operations to expand
074: * {@link IndexColorModel}s.
075: */
076: public final static RenderingHints REPLACE_INDEX_COLOR_MODEL = new RenderingHints(
077: JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.TRUE);
078:
079: /**
080: * {@link RenderingHints} for requesting Nearest Neighbor intepolation.
081: */
082: public static final RenderingHints NN_INTERPOLATION_HINT = new RenderingHints(
083: JAI.KEY_INTERPOLATION, new InterpolationNearest());
084:
085: /**
086: * {@link RenderingHints} for avoiding caching of {@link JAI} {@link RenderedOp}s.
087: */
088: public static final RenderingHints NOCACHE_HINT = new RenderingHints(
089: JAI.KEY_TILE_CACHE, null);
090:
091: /**
092: * Cached instance of a {@link RenderingHints} for controlling border extension on
093: * {@link JAI} operations. It contains an instance of a {@link BorderExtenderCopy}.
094: */
095: public final static RenderingHints EXTEND_BORDER_BY_COPYING = new RenderingHints(
096: JAI.KEY_BORDER_EXTENDER, BorderExtender
097: .createInstance(BorderExtender.BORDER_COPY));
098:
099: /**
100: * Cached instance of a {@link RenderingHints} for controlling border extension on
101: * {@link JAI} operations. It contains an instance of a {@link BorderExtenderReflect}.
102: */
103: public final static RenderingHints EXTEND_BORDER_BY_REFLECT = new RenderingHints(
104: JAI.KEY_BORDER_EXTENDER, BorderExtender
105: .createInstance(BorderExtender.BORDER_REFLECT));
106:
107: /**
108: * The default tile size. This default tile size can be
109: * overridden with a call to {@link JAI#setDefaultTileSize}.
110: */
111: private static final Dimension GEOTOOLS_DEFAULT_TILE_SIZE = new Dimension(
112: 512, 512);
113:
114: /**
115: * The minimum tile size.
116: */
117: private static final int GEOTOOLS_MIN_TILE_SIZE = 256;
118:
119: /**
120: * Maximum tile width or height before to consider a tile as a stripe. It tile width or height
121: * are smaller or equals than this size, then the image will be retiled. That is done because
122: * there are many formats that use stripes as an alternative to tiles, an example is tiff. A
123: * stripe can be a performance black hole, users can have stripes as large as 20000 columns x 8
124: * rows. If we just want to see a chunk of 512x512, this is a lot of uneeded data to load.
125: */
126: private static final int STRIPE_SIZE = 64;
127:
128: /**
129: * List of valid names. Note: the "Optimal" type is not
130: * implemented because currently not provided by JAI.
131: */
132: private static final String[] INTERPOLATION_NAMES = { "Nearest", // JAI name
133: "NearestNeighbor", // OpenGIS name
134: "Bilinear", "Bicubic", "Bicubic2" // Not in OpenGIS specification.
135: };
136:
137: /**
138: * Interpolation types (provided by Java Advanced Imaging) for {@link #INTERPOLATION_NAMES}.
139: */
140: private static final int[] INTERPOLATION_TYPES = {
141: Interpolation.INTERP_NEAREST, Interpolation.INTERP_NEAREST,
142: Interpolation.INTERP_BILINEAR,
143: Interpolation.INTERP_BICUBIC,
144: Interpolation.INTERP_BICUBIC_2 };
145:
146: /**
147: * Do not allow creation of instances of this class.
148: */
149: private ImageUtilities() {
150: }
151:
152: /**
153: * Suggests an {@link ImageLayout} for the specified image. All parameters are initially set
154: * equal to those of the given {@link RenderedImage}, and then the {@linkplain #toTileSize
155: * tile size is updated according the image size}. This method never returns {@code null}.
156: */
157: public static ImageLayout getImageLayout(final RenderedImage image) {
158: return getImageLayout(image, true);
159: }
160:
161: /**
162: * Returns an {@link ImageLayout} for the specified image. If {@code initToImage} is
163: * {@code true}, then all parameters are initially set equal to those of the given
164: * {@link RenderedImage} and the returned layout is never {@code null} (except if
165: * the image is null).
166: */
167: private static ImageLayout getImageLayout(
168: final RenderedImage image, final boolean initToImage) {
169: if (image == null) {
170: return null;
171: }
172: ImageLayout layout = initToImage ? new ImageLayout(image)
173: : null;
174: if ((image.getNumXTiles() == 1 || image.getTileWidth() <= STRIPE_SIZE)
175: && (image.getNumYTiles() == 1 || image.getTileHeight() <= STRIPE_SIZE)) {
176: // If the image was already tiled, reuse the same tile size.
177: // Otherwise, compute default tile size. If a default tile
178: // size can't be computed, it will be left unset.
179: if (layout != null) {
180: layout = layout.unsetTileLayout();
181: }
182: Dimension defaultSize = JAI.getDefaultTileSize();
183: if (defaultSize == null) {
184: defaultSize = GEOTOOLS_DEFAULT_TILE_SIZE;
185: }
186: int s;
187: if ((s = toTileSize(image.getWidth(), defaultSize.width)) != 0) {
188: if (layout == null) {
189: layout = new ImageLayout();
190: }
191: layout = layout.setTileWidth(s);
192: layout.setTileGridXOffset(image.getMinX());
193: }
194: if ((s = toTileSize(image.getHeight(), defaultSize.height)) != 0) {
195: if (layout == null) {
196: layout = new ImageLayout();
197: }
198: layout = layout.setTileHeight(s);
199: layout.setTileGridYOffset(image.getMinY());
200: }
201: }
202: return layout;
203: }
204:
205: /**
206: * Suggests a set of {@link RenderingHints} for the specified image.
207: * The rendering hints may include the following parameters:
208: *
209: * <ul>
210: * <li>{@link JAI#KEY_IMAGE_LAYOUT} with a proposed tile size.</li>
211: * </ul>
212: *
213: * This method may returns {@code null} if no rendering hints is proposed.
214: */
215: public static RenderingHints getRenderingHints(
216: final RenderedImage image) {
217: final ImageLayout layout = getImageLayout(image, false);
218: return (layout != null) ? new RenderingHints(
219: JAI.KEY_IMAGE_LAYOUT, layout) : null;
220: }
221:
222: /**
223: * Suggests a tile size for the specified image size. On input, {@code size} is the image's
224: * size. On output, it is the tile size. This method write the result directly in the supplied
225: * object and returns {@code size} for convenience.
226: * <p>
227: * This method it aimed to computing a tile size such that the tile grid would have overlapped
228: * the image bound in order to avoid having tiles crossing the image bounds and being therefore
229: * partially empty. This method will never returns a tile size smaller than
230: * {@value #GEOTOOLS_MIN_TILE_SIZE}. If this method can't suggest a size, then it left the corresponding
231: * {@code size} field ({@link Dimension#width width} or {@link Dimension#height height})
232: * unchanged.
233: * <p>
234: * The {@link Dimension#width width} and {@link Dimension#height height} fields are processed
235: * independently in the same way. The following discussion use the {@code width} field as an
236: * example.
237: * <p>
238: * This method inspects different tile sizes close to the {@linkplain JAI#getDefaultTileSize()
239: * default tile size}. Lets {@code width} be the default tile width. Values are tried in the
240: * following order: {@code width}, {@code width+1}, {@code width-1}, {@code width+2},
241: * {@code width-2}, {@code width+3}, {@code width-3}, <cite>etc.</cite> until one of the
242: * following happen:
243: * <p>
244: * <ul>
245: * <li>A suitable tile size is found. More specifically, a size is found which is a dividor
246: * of the specified image size, and is the closest one of the default tile size. The
247: * {@link Dimension} field ({@code width} or {@code height}) is set to this value.</li>
248: *
249: * <li>An arbitrary limit (both a minimum and a maximum tile size) is reached. In this case,
250: * this method <strong>may</strong> set the {@link Dimension} field to a value that
251: * maximize the remainder of <var>image size</var> / <var>tile size</var> (in other
252: * words, the size that left as few empty pixels as possible).</li>
253: * </ul>
254: */
255: public static Dimension toTileSize(final Dimension size) {
256: Dimension defaultSize = JAI.getDefaultTileSize();
257: if (defaultSize == null) {
258: defaultSize = GEOTOOLS_DEFAULT_TILE_SIZE;
259: }
260: int s;
261: if ((s = toTileSize(size.width, defaultSize.width)) != 0)
262: size.width = s;
263: if ((s = toTileSize(size.height, defaultSize.height)) != 0)
264: size.height = s;
265: return size;
266: }
267:
268: /**
269: * Suggests a tile size close to {@code tileSize} for the specified {@code imageSize}.
270: * This method it aimed to computing a tile size such that the tile grid would have
271: * overlapped the image bound in order to avoid having tiles crossing the image bounds
272: * and being therefore partially empty. This method will never returns a tile size smaller
273: * than {@value #GEOTOOLS_MIN_TILE_SIZE}. If this method can't suggest a size, then it returns 0.
274: *
275: * @param imageSize The image size.
276: * @param tileSize The preferred tile size, which is often {@value #GEOTOOLS_DEFAULT_TILE_SIZE}.
277: */
278: private static int toTileSize(final int imageSize,
279: final int tileSize) {
280: final int MAX_TILE_SIZE = Math.min(tileSize * 2, imageSize);
281: final int stop = Math.max(tileSize - GEOTOOLS_MIN_TILE_SIZE,
282: MAX_TILE_SIZE - tileSize);
283: int sopt = 0; // An "optimal" tile size, to be used if no exact dividor is found.
284: int rmax = 0; // The remainder of 'imageSize / sopt'. We will try to maximize this value.
285: /*
286: * Inspects all tile sizes in the range [GEOTOOLS_MIN_TILE_SIZE .. MAX_TIME_SIZE]. We will begin
287: * with a tile size equals to the specified 'tileSize'. Next we will try tile sizes of
288: * 'tileSize+1', 'tileSize-1', 'tileSize+2', 'tileSize-2', 'tileSize+3', 'tileSize-3',
289: * etc. until a tile size if found suitable.
290: *
291: * More generally, the loop below tests the 'tileSize+i' and 'tileSize-i' values. The
292: * 'stop' constant was computed assuming that MIN_TIME_SIZE < tileSize < MAX_TILE_SIZE.
293: * If a tile size is found which is a dividor of the image size, than that tile size (the
294: * closest one to 'tileSize') is returned. Otherwise, the loop continue until all values
295: * in the range [GEOTOOLS_MIN_TILE_SIZE .. MAX_TIME_SIZE] were tested. In this process, we remind
296: * the tile size that gave the greatest reminder (rmax). In other words, this is the tile
297: * size with the smallest amount of empty pixels.
298: */
299: for (int i = 0; i <= stop; i++) {
300: int s;
301: if ((s = tileSize + i) <= MAX_TILE_SIZE) {
302: final int r = imageSize % s;
303: if (r == 0) {
304: // Found a size >= to 'tileSize' which is a dividor of image size.
305: return s;
306: }
307: if (r > rmax) {
308: rmax = r;
309: sopt = s;
310: }
311: }
312: if ((s = tileSize - i) >= GEOTOOLS_MIN_TILE_SIZE) {
313: final int r = imageSize % s;
314: if (r == 0) {
315: // Found a size <= to 'tileSize' which is a dividor of image size.
316: return s;
317: }
318: if (r > rmax) {
319: rmax = r;
320: sopt = s;
321: }
322: }
323: }
324: /*
325: * No dividor were found in the range [GEOTOOLS_MIN_TILE_SIZE .. MAX_TIME_SIZE]. At this point
326: * 'sopt' is an "optimal" tile size (the one that left as few empty pixel as possible),
327: * and 'rmax' is the amount of non-empty pixels using this tile size. We will use this
328: * "optimal" tile size only if it fill at least 75% of the tile. Otherwise, we arbitrarily
329: * consider that it doesn't worth to use a "non-standard" tile size. The purpose of this
330: * arbitrary test is again to avoid too many small tiles (assuming that
331: */
332: return (rmax >= tileSize - tileSize / 4) ? sopt : 0;
333: }
334:
335: /**
336: * Computes a new {@link ImageLayout} which is the intersection of the specified
337: * {@code ImageLayout} and all {@code RenderedImage}s in the supplied list. If the
338: * {@link ImageLayout#getMinX minX}, {@link ImageLayout#getMinY minY},
339: * {@link ImageLayout#getWidth width} and {@link ImageLayout#getHeight height}
340: * properties are not defined in the {@code layout}, then they will be inherited
341: * from the <strong>first</strong> source for consistency with {@link OpImage} constructor.
342: *
343: * @param layout The original layout. This object will not be modified.
344: * @param sources The list of sources {@link RenderedImage}.
345: * @return A new {@code ImageLayout}, or the original {@code layout} if no change was needed.
346: */
347: public static ImageLayout createIntersection(
348: final ImageLayout layout, final List sources) {
349: ImageLayout result = layout;
350: if (result == null) {
351: result = new ImageLayout();
352: }
353: final int n = sources.size();
354: if (n != 0) {
355: // If layout is not set, OpImage uses the layout of the *first*
356: // source image according OpImage constructor javadoc.
357: RenderedImage source = (RenderedImage) sources.get(0);
358: int minXL = result.getMinX(source);
359: int minYL = result.getMinY(source);
360: int maxXL = result.getWidth(source) + minXL;
361: int maxYL = result.getHeight(source) + minYL;
362: for (int i = 0; i < n; i++) {
363: source = (RenderedImage) sources.get(i);
364: final int minX = source.getMinX();
365: final int minY = source.getMinY();
366: final int maxX = source.getWidth() + minX;
367: final int maxY = source.getHeight() + minY;
368: int mask = 0;
369: if (minXL < minX)
370: mask |= (1 | 4); // set minX and width
371: if (minYL < minY)
372: mask |= (2 | 8); // set minY and height
373: if (maxXL > maxX)
374: mask |= (4); // Set width
375: if (maxYL > maxY)
376: mask |= (8); // Set height
377: if (mask != 0) {
378: if (layout == result) {
379: result = (ImageLayout) layout.clone();
380: }
381: if ((mask & 1) != 0)
382: result.setMinX(minXL = minX);
383: if ((mask & 2) != 0)
384: result.setMinY(minYL = minY);
385: if ((mask & 4) != 0)
386: result.setWidth((maxXL = maxX) - minXL);
387: if ((mask & 8) != 0)
388: result.setHeight((maxYL = maxY) - minYL);
389: }
390: }
391: // If the bounds changed, adjust the tile size.
392: if (result != layout) {
393: source = (RenderedImage) sources.get(0);
394: if (result.isValid(ImageLayout.TILE_WIDTH_MASK)) {
395: final int oldSize = result.getTileWidth(source);
396: final int newSize = toTileSize(result
397: .getWidth(source), oldSize);
398: if (oldSize != newSize) {
399: result.setTileWidth(newSize);
400: }
401: }
402: if (result.isValid(ImageLayout.TILE_HEIGHT_MASK)) {
403: final int oldSize = result.getTileHeight(source);
404: final int newSize = toTileSize(result
405: .getHeight(source), oldSize);
406: if (oldSize != newSize) {
407: result.setTileHeight(newSize);
408: }
409: }
410: }
411: }
412: return result;
413: }
414:
415: /**
416: * Casts the specified object to an {@link Interpolation object}.
417: *
418: * @param type The interpolation type as an {@link Interpolation} or a {@link CharSequence}
419: * object.
420: * @return The interpolation object for the specified type.
421: * @throws IllegalArgumentException if the specified interpolation type is not a know one.
422: */
423: public static Interpolation toInterpolation(final Object type)
424: throws IllegalArgumentException {
425: if (type instanceof Interpolation) {
426: return (Interpolation) type;
427: } else if (type instanceof CharSequence) {
428: final String name = type.toString();
429: final int length = INTERPOLATION_NAMES.length;
430: for (int i = 0; i < length; i++) {
431: if (INTERPOLATION_NAMES[i].equalsIgnoreCase(name)) {
432: return Interpolation
433: .getInstance(INTERPOLATION_TYPES[i]);
434: }
435: }
436: }
437: throw new IllegalArgumentException(Errors.format(
438: ErrorKeys.UNKNOW_INTERPOLATION_$1, type));
439: }
440:
441: /**
442: * Returns the interpolation name for the specified interpolation object.
443: * This method tries to infer the name from the object's class name.
444: *
445: * @param Interpolation The interpolation object.
446: */
447: public static String getInterpolationName(final Interpolation interp) {
448: final String prefix = "Interpolation";
449: for (Class classe = interp.getClass(); classe != null; classe = classe
450: .getSuperclass()) {
451: String name = Utilities.getShortName(classe);
452: int index = name.lastIndexOf(prefix);
453: if (index >= 0) {
454: return name.substring(index + prefix.length());
455: }
456: }
457: return Utilities.getShortClassName(interp);
458: }
459:
460: /**
461: * Allows or disallows native acceleration for the specified image format. By default, the
462: * image I/O extension for JAI provides native acceleration for PNG and JPEG. Unfortunatly,
463: * those native codec has bug in their 1.0 version. Invoking this method will force the use
464: * of standard codec provided in J2SE 1.4.
465: * <p>
466: * <strong>Implementation note:</strong> the current implementation assume that JAI codec
467: * class name start with "CLib". It work for Sun's 1.0 implementation, but may change in
468: * future versions. If this method doesn't recognize the class name, it does nothing.
469: *
470: * @param format The format name (e.g. "png").
471: * @param writer {@code false} to set the reader, or {@code true} to set the writer.
472: * @param allowed {@code false} to disallow native acceleration.
473: */
474: public static synchronized void allowNativeCodec(
475: final String format, final boolean writer,
476: final boolean allowed) {
477: ImageReaderWriterSpi standard = null;
478: ImageReaderWriterSpi codeclib = null;
479: final IIORegistry registry = IIORegistry.getDefaultInstance();
480: final Class category = writer ? ImageWriterSpi.class
481: : ImageReaderSpi.class;
482: for (final Iterator it = registry.getServiceProviders(category,
483: false); it.hasNext();) {
484: final ImageReaderWriterSpi provider = (ImageReaderWriterSpi) it
485: .next();
486: final String[] formats = provider.getFormatNames();
487: for (int i = 0; i < formats.length; i++) {
488: if (formats[i].equalsIgnoreCase(format)) {
489: if (Utilities.getShortClassName(provider)
490: .startsWith("CLib")) {
491: codeclib = provider;
492: } else {
493: standard = provider;
494: }
495: break;
496: }
497: }
498: }
499: if (standard != null && codeclib != null) {
500: if (allowed) {
501: registry.setOrdering(category, codeclib, standard);
502: } else {
503: registry.setOrdering(category, standard, codeclib);
504: }
505: }
506: }
507:
508: /**
509: * Tiles the specified image.
510: *
511: * @todo Usually, the tiling doesn't need to be performed as a separated operation. The
512: * {@link ImageLayout} hint with tile information can be provided to most JAI operators.
513: * The {@link #getRenderingHints} method provides such tiling information only if the
514: * image was not already tiled, so it should not be a cause of tile size mismatch in an
515: * operation chain. The mean usage for a separated "tile" operation is to tile an image
516: * before to save it on disk in some format supporting tiling.
517: *
518: * @throws IOException If an I/O operation were required (in order to check if the image
519: * were tiled on disk) and failed.
520: *
521: * @since 2.3
522: */
523: public static RenderedOp tileImage(final RenderedOp image)
524: throws IOException {
525: // /////////////////////////////////////////////////////////////////////
526: //
527: // initialization
528: //
529: // /////////////////////////////////////////////////////////////////////
530: final int width = image.getWidth();
531: final int height = image.getHeight();
532: final int tileHeight = image.getTileHeight();
533: final int tileWidth = image.getTileWidth();
534:
535: boolean needToTile = false;
536:
537: // /////////////////////////////////////////////////////////////////////
538: //
539: // checking if the image comes directly from an image read operation
540: //
541: // /////////////////////////////////////////////////////////////////////
542: // getting the reader
543: final Object o = image
544: .getProperty(ImageReadDescriptor.PROPERTY_NAME_IMAGE_READER);
545: if (o instanceof ImageReader) {
546: final ImageReader reader = (ImageReader) o;
547: if (!reader.isImageTiled(0)) {
548: needToTile = true;
549: }
550: }
551: // /////////////////////////////////////////////////////////////////////
552: //
553: // If the original image has tileW==W &&tileH==H it is untiled.
554: //
555: // /////////////////////////////////////////////////////////////////////
556: if (!needToTile && tileWidth == width && tileHeight <= height) {
557: needToTile = true;
558: }
559: // /////////////////////////////////////////////////////////////////////
560: //
561: // tiling central.
562: //
563: // /////////////////////////////////////////////////////////////////////
564: if (needToTile) {
565:
566: // tiling the original image by providing a suitable layout
567: final ImageLayout layout = getImageLayout(image);
568: layout.unsetValid(ImageLayout.COLOR_MODEL_MASK
569: | ImageLayout.SAMPLE_MODEL_MASK);
570:
571: // changing parameters related to the tiling
572: final RenderingHints hints = new RenderingHints(
573: JAI.KEY_IMAGE_LAYOUT, layout);
574:
575: // reading the image
576: final ParameterBlockJAI pbjFormat = new ParameterBlockJAI(
577: "Format");
578: pbjFormat.addSource(image);
579: pbjFormat.setParameter("dataType", image.getSampleModel()
580: .getDataType());
581:
582: return JAI.create("Format", pbjFormat, hints);
583: }
584: return image;
585: }
586: }
|