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.image.io;
018:
019: import java.awt.Rectangle;
020: import java.awt.image.Raster;
021: import java.awt.image.RenderedImage;
022: import java.io.IOException;
023: import java.util.logging.Level;
024: import java.util.logging.Logger;
025: import java.util.logging.LogRecord;
026: import javax.imageio.IIOImage;
027: import javax.imageio.ImageWriter;
028: import javax.imageio.ImageWriteParam;
029: import javax.imageio.ImageTypeSpecifier;
030: import javax.imageio.spi.ImageWriterSpi;
031: import javax.imageio.metadata.IIOMetadata;
032: import javax.imageio.metadata.IIOMetadataFormatImpl;
033: import javax.imageio.metadata.IIOInvalidTreeException;
034: import javax.imageio.event.IIOWriteWarningListener;
035: import javax.media.jai.iterator.RectIter;
036: import javax.media.jai.iterator.RectIterFactory;
037:
038: import org.geotools.image.ImageDimension;
039: import org.geotools.util.logging.Logging;
040: import org.geotools.resources.i18n.Locales;
041: import org.geotools.resources.i18n.Errors;
042: import org.geotools.resources.i18n.ErrorKeys;
043: import org.geotools.resources.IndexedResourceBundle;
044:
045: /**
046: * Base class for writers of geographic images.
047: *
048: * @since 2.4
049: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/GeographicImageWriter.java $
050: * @version $Id: GeographicImageWriter.java 27862 2007-11-12 19:51:19Z desruisseaux $
051: * @author Martin Desruisseaux
052: */
053: public abstract class GeographicImageWriter extends ImageWriter {
054: /**
055: * The logger to use for events related to this image writer.
056: */
057: static final Logger LOGGER = Logging
058: .getLogger("org.geotools.image.io");
059:
060: /**
061: * Index of the image in process of being written. This convenience index is reset to 0
062: * by {@link #close} method.
063: */
064: private int imageIndex = 0;
065:
066: /**
067: * Index of the ithumbnail in process of being written. This convenience index
068: * is reset to 0 by {@link #close} method.
069: */
070: private int thumbnailIndex = 0;
071:
072: /**
073: * Constructs a {@code GeographicImageWriter}.
074: *
075: * @param originatingProvider The {@code ImageWriterSpi} that
076: * is constructing this object, or {@code null}.
077: */
078: protected GeographicImageWriter(final ImageWriterSpi provider) {
079: super (provider);
080: availableLocales = Locales.getAvailableLocales();
081: }
082:
083: /**
084: * Sets the output.
085: */
086: //@Override
087: public void setOutput(final Object output) {
088: imageIndex = 0;
089: thumbnailIndex = 0;
090: super .setOutput(output);
091: }
092:
093: /**
094: * Returns the resources for formatting error messages.
095: */
096: final IndexedResourceBundle getErrorResources() {
097: return Errors.getResources(getLocale());
098: }
099:
100: /**
101: * Ensures that the specified parameter is non-null.
102: *
103: * @param parameter The parameter name, for formatting an error message if needed.
104: * @param value The value to test.
105: * @throws IllegalArgumentException if {@code value} is null.
106: */
107: private void ensureNonNull(final String parameter,
108: final Object value) throws IllegalArgumentException {
109: if (value == null) {
110: throw new IllegalArgumentException(getErrorResources()
111: .getString(ErrorKeys.NULL_ARGUMENT_$1, parameter));
112: }
113: }
114:
115: /**
116: * Returns a metadata object containing default values for encoding a stream of images.
117: * The default implementation returns {@code null}, which is appropriate for writer that
118: * do not make use of stream meta-data.
119: *
120: * @param param Parameters that will be used to encode the image (in cases where
121: * it may affect the structure of the metadata), or {@code null}.
122: * @return The metadata, or {@code null}.
123: */
124: public IIOMetadata getDefaultStreamMetadata(
125: final ImageWriteParam param) {
126: return null;
127: }
128:
129: /**
130: * Returns a metadata object containing default values for encoding an image of the given
131: * type. The default implementation returns {@code null}, which is appropriate for writer
132: * that do not make use of image meta-data.
133: *
134: * @param imageType The format of the image to be written later.
135: * @param param Parameters that will be used to encode the image (in cases where
136: * it may affect the structure of the metadata), or {@code null}.
137: * @return The metadata, or {@code null}.
138: */
139: public IIOMetadata getDefaultImageMetadata(
140: final ImageTypeSpecifier imageType,
141: final ImageWriteParam param) {
142: return null;
143: }
144:
145: /**
146: * Returns a metadata object initialized to the specified data for encoding a stream
147: * of images. The default implementation copies the specified data into a
148: * {@linkplain #getDefaultStreamMetadata default stream metadata}.
149: *
150: * @param inData Stream metadata used to initialize the state of the returned object.
151: * @param param Parameters that will be used to encode the image (in cases where
152: * it may affect the structure of the metadata), or {@code null}.
153: * @return The metadata, or {@code null}.
154: */
155: public IIOMetadata convertStreamMetadata(final IIOMetadata inData,
156: final ImageWriteParam param) {
157: ensureNonNull("inData", inData);
158: final IIOMetadata outData = getDefaultStreamMetadata(param);
159: final String format = commonMetadataFormatName(inData, outData);
160: if (format != null)
161: try {
162: outData.mergeTree(format, inData.getAsTree(format));
163: return outData;
164: } catch (IIOInvalidTreeException ex) {
165: warningOccurred("convertStreamMetadata", ex);
166: }
167: return null;
168: }
169:
170: /**
171: * Returns a metadata object initialized to the specified data for encoding an image
172: * of the given type. The default implementation copies the specified data into a
173: * {@linkplain #getDefaultImageMetadata default image metadata}.
174: *
175: * @param inData Image metadata used to initialize the state of the returned object.
176: * @param imageType The format of the image to be written later.
177: * @param param Parameters that will be used to encode the image (in cases where
178: * it may affect the structure of the metadata), or {@code null}.
179: * @return The metadata, or {@code null}.
180: */
181: public IIOMetadata convertImageMetadata(final IIOMetadata inData,
182: final ImageTypeSpecifier imageType,
183: final ImageWriteParam param) {
184: ensureNonNull("inData", inData);
185: final IIOMetadata outData = getDefaultImageMetadata(imageType,
186: param);
187: final String format = commonMetadataFormatName(inData, outData);
188: if (format != null)
189: try {
190: outData.mergeTree(format, inData.getAsTree(format));
191: return outData;
192: } catch (IIOInvalidTreeException ex) {
193: warningOccurred("convertImageMetadata", ex);
194: }
195: return null;
196: }
197:
198: /**
199: * Finds a common metadata format name between the two specified metadata objects. This
200: * method query for the {@code outData} {@linkplain #getNativeMetadataFormatName native
201: * metadata format name} first.
202: *
203: * @param inData The input metadata.
204: * @param outData The output metadata.
205: * @return A metadata format name common to {@code inData} and {@code outData},
206: * or {@code null} if none where found.
207: */
208: private static String commonMetadataFormatName(
209: final IIOMetadata inData, final IIOMetadata outData) {
210: String format = outData.getNativeMetadataFormatName();
211: if (isSupportedFormat(inData, format)) {
212: return format;
213: }
214: final String[] formats = outData.getExtraMetadataFormatNames();
215: if (formats != null) {
216: for (int i = 0; i < formats.length; i++) {
217: format = formats[i];
218: if (isSupportedFormat(inData, format)) {
219: return format;
220: }
221: }
222: }
223: if (outData.isStandardMetadataFormatSupported()
224: && inData.isStandardMetadataFormatSupported()) {
225: return IIOMetadataFormatImpl.standardMetadataFormatName;
226: }
227: return null;
228: }
229:
230: /**
231: * Returns {@code true} if the specified metadata supports the specified format.
232: *
233: * @param metadata The metadata to test.
234: * @param format The format name to test, or {@code null}.
235: * @return {@code true} if the specified metadata suports the specified format.
236: */
237: private static boolean isSupportedFormat(
238: final IIOMetadata metadata, final String format) {
239: if (format != null) {
240: if (format.equalsIgnoreCase(metadata
241: .getNativeMetadataFormatName())) {
242: return true;
243: }
244: final String[] formats = metadata
245: .getExtraMetadataFormatNames();
246: if (formats != null) {
247: for (int i = 0; i < formats.length; i++) {
248: if (format.equalsIgnoreCase(formats[i])) {
249: return true;
250: }
251: }
252: }
253: }
254: return false;
255: }
256:
257: /**
258: * Returns true if the methods that take an {@link IIOImage} parameter are capable of dealing
259: * with a {@link Raster}. The default implementation returns {@code true} since it is assumed
260: * that subclasses will fetch pixels using the iterator returned by {@link #createRectIter
261: * createRectIter}.
262: */
263: //@Override
264: public boolean canWriteRasters() {
265: return true;
266: }
267:
268: /**
269: * Returns an iterator over the pixels of the specified image, taking subsampling in account.
270: *
271: * @param image The image or raster to be written.
272: * @param parameters The write parameters, or {@code null} if the whole image will be written.
273: * @return An iterator over the pixel values of the image to be written.
274: */
275: protected static RectIter createRectIter(final IIOImage image,
276: final ImageWriteParam parameters) {
277: /*
278: * Examines the parameters for subsampling in lines, columns and bands. If a subsampling
279: * is specified, the source region will be translated by the subsampling offset (if any).
280: */
281: Rectangle bounds;
282: int[] sourceBands;
283: final int sourceXSubsampling;
284: final int sourceYSubsampling;
285: if (parameters != null) {
286: bounds = parameters.getSourceRegion(); // Needs to be a clone.
287: sourceXSubsampling = parameters.getSourceXSubsampling();
288: sourceYSubsampling = parameters.getSourceYSubsampling();
289: if (sourceXSubsampling != 1 || sourceYSubsampling != 1) {
290: if (bounds == null) {
291: if (image.hasRaster()) {
292: bounds = image.getRaster().getBounds(); // Needs to be a clone.
293: } else {
294: final RenderedImage i = image
295: .getRenderedImage();
296: bounds = new Rectangle(i.getMinX(),
297: i.getMinY(), i.getWidth(), i
298: .getHeight());
299: }
300: }
301: final int xOffset = parameters.getSubsamplingXOffset();
302: final int yOffset = parameters.getSubsamplingYOffset();
303: bounds.x += xOffset;
304: bounds.y += yOffset;
305: bounds.width -= xOffset;
306: bounds.height -= yOffset;
307: // Fits to the smallest bounding box, which is
308: // required by SubsampledRectIter implementation.
309: bounds.width -= (bounds.width - 1) % sourceXSubsampling;
310: bounds.height -= (bounds.height - 1)
311: % sourceYSubsampling;
312: }
313: sourceBands = parameters.getSourceBands();
314: } else {
315: sourceBands = null;
316: bounds = null;
317: sourceXSubsampling = 1;
318: sourceYSubsampling = 1;
319: }
320: /*
321: * Creates the JAIiterator which will iterates over all pixels in the source region.
322: * If no subsampling is specified and the source bands do not move and band, then the
323: * JAI iterator is returned directly.
324: */
325: final int numBands;
326: RectIter iterator;
327: if (image.hasRaster()) {
328: final Raster raster = image.getRaster();
329: numBands = raster.getNumBands();
330: iterator = RectIterFactory.create(raster, bounds);
331: } else {
332: final RenderedImage raster = image.getRenderedImage();
333: numBands = raster.getSampleModel().getNumBands();
334: iterator = RectIterFactory.create(raster, bounds);
335: }
336: if (sourceXSubsampling == 1 && sourceYSubsampling == 1) {
337: if (sourceBands == null) {
338: return iterator;
339: }
340: if (sourceBands.length == numBands) {
341: boolean identity = true;
342: for (int i = 0; i < numBands; i++) {
343: if (sourceBands[i] != i) {
344: identity = false;
345: break;
346: }
347: }
348: if (identity) {
349: return iterator;
350: }
351: }
352: }
353: /*
354: * A subsampling is required. Wraps the JAI iterator into a subsampler.
355: */
356: if (sourceBands == null) {
357: sourceBands = new int[numBands];
358: for (int i = 0; i < numBands; i++) {
359: sourceBands[i] = i;
360: }
361: }
362: return new SubsampledRectIter(iterator, sourceXSubsampling,
363: sourceYSubsampling, sourceBands);
364: }
365:
366: /**
367: * Computes the size of the region to be read, taking subsampling in account.
368: *
369: * @param image The image or raster to be written.
370: * @param parameters The write parameters, or {@code null} if the whole image will be written.
371: * @return dimension The dimension of the image to be written.
372: */
373: protected static ImageDimension computeSize(final IIOImage image,
374: final ImageWriteParam parameters) {
375: final ImageDimension dimension;
376: if (image.hasRaster()) {
377: dimension = new ImageDimension(image.getRaster());
378: } else {
379: dimension = new ImageDimension(image.getRenderedImage());
380: }
381: if (parameters != null) {
382: final Rectangle bounds = parameters.getSourceRegion();
383: if (bounds != null) {
384: if (bounds.width < dimension.width) {
385: dimension.width = bounds.width;
386: }
387: if (bounds.height < dimension.height) {
388: dimension.height = bounds.height;
389: }
390: }
391: dimension.width -= parameters.getSubsamplingXOffset();
392: dimension.height -= parameters.getSubsamplingYOffset();
393: }
394: return dimension;
395: }
396:
397: /**
398: * Broadcasts the start of an image write to all registered listeners. The default
399: * implementation invokes the {@linkplain #processImageStarted(int) super-class method}
400: * with an image index maintained by this writer.
401: */
402: protected void processImageStarted() {
403: processImageStarted(imageIndex);
404: }
405:
406: /**
407: * Broadcasts the completion of an image write to all registered listeners.
408: */
409: //@Override
410: protected void processImageComplete() {
411: super .processImageComplete();
412: thumbnailIndex = 0;
413: imageIndex++;
414: }
415:
416: /**
417: * Broadcasts the start of a thumbnail write to all registered listeners. The default
418: * implementation invokes the {@linkplain #processThumbnailStarted(int,int) super-class
419: * method} with an image and thumbnail index maintained by this writer.
420: */
421: protected void processThumbnailStarted() {
422: processThumbnailStarted(imageIndex, thumbnailIndex);
423: }
424:
425: /**
426: * Broadcasts the completion of a thumbnail write to all registered listeners.
427: */
428: //@Override
429: protected void processThumbnailComplete() {
430: super .processThumbnailComplete();
431: thumbnailIndex++;
432: }
433:
434: /**
435: * Broadcasts a warning message to all registered listeners. The default implementation
436: * invokes the {@linkplain #processWarningOccurred(int,String) super-class method} with
437: * an image index maintained by this writer.
438: */
439: protected void processWarningOccurred(final String warning) {
440: processWarningOccurred(imageIndex, warning);
441: }
442:
443: /**
444: * Broadcasts a warning message to all registered listeners. The default implementation
445: * invokes the {@linkplain #processWarningOccurred(int,String,String) super-class method}
446: * with an image index maintained by this writer.
447: */
448: protected void processWarningOccurred(final String baseName,
449: final String keyword) {
450: processWarningOccurred(imageIndex, baseName, keyword);
451: }
452:
453: /**
454: * Invoked when a warning occured. The default implementation make the following choice:
455: * <p>
456: * <ul>
457: * <li>If at least one {@linkplain IIOWriteWarningListener warning listener}
458: * has been {@linkplain #addIIOWriteWarningListener specified}, then the
459: * {@link IIOWriteWarningListener#warningOccurred warningOccurred} method is
460: * invoked for each of them and the log record is <strong>not</strong> logged.</li>
461: *
462: * <li>Otherwise, the log record is sent to the {@code "org.geotools.image.io"} logger.</li>
463: * </ul>
464: *
465: * Subclasses may override this method if more processing is wanted, or for
466: * throwing exception if some warnings should be considered as fatal errors.
467: */
468: public void warningOccurred(final LogRecord record) {
469: if (warningListeners == null) {
470: LOGGER.log(record);
471: } else {
472: processWarningOccurred(IndexedResourceBundle.format(record));
473: }
474: }
475:
476: /**
477: * Convenience method for logging an exception from the given method.
478: */
479: private void warningOccurred(final String method, final Exception ex) {
480: final LogRecord record = new LogRecord(Level.WARNING, ex
481: .toString());
482: record
483: .setSourceClassName(GeographicImageWriter.class
484: .getName());
485: record.setSourceMethodName(method);
486: record.setThrown(ex);
487: warningOccurred(record);
488: }
489:
490: /**
491: * To be overriden and made {@code protected} by {@link StreamImageWriter} only.
492: */
493: void close() throws IOException {
494: imageIndex = 0;
495: thumbnailIndex = 0;
496: }
497: }
|