0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2006, Geotools Project Managment Committee (PMC)
0005: *
0006: * This library is free software; you can redistribute it and/or
0007: * modify it under the terms of the GNU Lesser General Public
0008: * License as published by the Free Software Foundation; either
0009: * version 2.1 of the License, or (at your option) any later version.
0010: *
0011: * This library is distributed in the hope that it will be useful,
0012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0014: * Lesser General Public License for more details.
0015: */
0016: package org.geotools.image;
0017:
0018: import java.awt.Image;
0019: import java.awt.image.*;
0020: import java.awt.Color;
0021: import java.awt.Transparency;
0022: import java.awt.RenderingHints;
0023: import java.awt.HeadlessException;
0024: import java.awt.color.ColorSpace;
0025:
0026: import javax.imageio.IIOImage;
0027: import javax.imageio.ImageIO;
0028: import javax.imageio.ImageTypeSpecifier;
0029: import javax.imageio.ImageWriteParam;
0030: import javax.imageio.ImageWriter;
0031: import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
0032: import javax.imageio.spi.IIORegistry;
0033: import javax.imageio.spi.ImageWriterSpi;
0034: import javax.imageio.stream.ImageOutputStream;
0035: import javax.imageio.IIOException;
0036: import java.io.IOException;
0037: import java.io.FileNotFoundException;
0038: import java.io.File;
0039: import java.util.List;
0040: import java.util.Arrays;
0041: import java.util.ArrayList;
0042: import java.util.Iterator;
0043: import java.util.Locale;
0044: import java.util.logging.Logger;
0045: import java.lang.reflect.InvocationTargetException;
0046:
0047: import javax.media.jai.*;
0048: import javax.media.jai.operator.*;
0049: import com.sun.media.jai.util.ImageUtil;
0050:
0051: import org.geotools.factory.Hints;
0052: import org.geotools.util.logging.Logging;
0053: import org.geotools.resources.Arguments;
0054: import org.geotools.resources.i18n.Errors;
0055: import org.geotools.resources.i18n.ErrorKeys;
0056: import org.geotools.resources.image.ColorUtilities;
0057: import org.geotools.resources.image.ImageUtilities;
0058:
0059: /**
0060: * Helper methods for applying JAI operations on an image. The image is specified at
0061: * {@linkplain #ImageWorker(RenderedImage) creation time}. Sucessive operations can
0062: * be applied by invoking the methods defined in this class, and the final image can
0063: * be obtained by invoking {@link #getRenderedImage} at the end of the process.
0064: * <p>
0065: * If an exception is thrown during a method invocation, then this {@code ImageWorker}
0066: * is left in an undetermined state and should not be used anymore.
0067: *
0068: * @since 2.3
0069: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/image/ImageWorker.java $
0070: * @version $Id: ImageWorker.java 28776 2008-01-15 20:26:28Z desruisseaux $
0071: * @author Simone Giannecchini
0072: * @author Bryce Nordgren
0073: * @author Martin Desruisseaux
0074: */
0075: public class ImageWorker {
0076: /**
0077: * Workaround class for compressing PNG using the default
0078: * {@link PNGImageEncoder} shipped with the JDK.
0079: * <p>
0080: * {@link PNGImageWriter} does not support
0081: * {@link ImageWriteParam#setCompressionMode(int)} set to
0082: * {@link ImageWriteParam#MODE_EXPLICIT}, it only allows
0083: * {@link ImageWriteParam#MODE_DEFAULT}.
0084: *
0085: * @author Simone Giannecchini
0086: *
0087: * @todo Consider moving to {@link org.geotools.image.io} package.
0088: */
0089: public final static class PNGImageWriteParam extends
0090: ImageWriteParam {
0091: /**
0092: * Default constructor.
0093: */
0094: public PNGImageWriteParam() {
0095: super ();
0096: this .canWriteProgressive = true;
0097: this .canWriteCompressed = true;
0098: this .locale = Locale.getDefault();
0099: }
0100: }
0101:
0102: /**
0103: * The logger to use for this class.
0104: */
0105: private final static Logger LOGGER = Logging
0106: .getLogger("org.geotools.image");
0107:
0108: /**
0109: * If {@link Boolean#FALSE FALSE}, image operators are not allowed to
0110: * produce tiled images. The default is {@link Boolean#TRUE TRUE}. The
0111: * {@code FALSE} value is sometime useful for exporting images to some
0112: * formats that doesn't support tiling (e.g. GIF).
0113: *
0114: * @see #setRenderingHint
0115: */
0116: public static final Hints.Key TILING_ALLOWED = new Hints.Key(
0117: Boolean.class);
0118:
0119: /**
0120: * The image property name generated by {@link ExtremaDescriptor}.
0121: */
0122: private static final String EXTREMA = "extrema";
0123:
0124: /**
0125: * The image specified by the user at construction time, or last time
0126: * {@link #invalidateStatistics} were invoked. The {@link #getComputedProperty}
0127: * method will not search a property pass this point.
0128: */
0129: private RenderedImage inheritanceStopPoint;
0130:
0131: /**
0132: * The image being built.
0133: */
0134: protected RenderedImage image;
0135:
0136: /**
0137: * The region of interest, or {@code null} if none.
0138: */
0139: private ROI roi;
0140:
0141: /**
0142: * The rendering hints to provides to all image operators. Additional hints may
0143: * be set (in a separated {@link RenderingHints} object) for particular images.
0144: */
0145: private RenderingHints commonHints;
0146:
0147: /**
0148: * 0 if tile cache is enabled, any other value otherwise. This counter is
0149: * incremented everytime {@code tileCacheEnabled(false)} is invoked, and
0150: * decremented every time {@code tileCacheEnabled(true)} is invoked.
0151: */
0152: private int tileCacheDisabled = 0;
0153:
0154: /**
0155: * Creates a new uninitialized builder for an {@linkplain #load image read}.
0156: *
0157: * @see #load
0158: */
0159: public ImageWorker() {
0160: inheritanceStopPoint = this .image = null;
0161: }
0162:
0163: /**
0164: * Creates a new builder for an image read from the specified file.
0165: *
0166: * @param input The file to read.
0167: * @throws IOException if the file can't be read.
0168: */
0169: public ImageWorker(final File input) throws IOException {
0170: this (ImageIO.read(input));
0171: }
0172:
0173: /**
0174: * Creates a new builder for the specified image. The images to be computed (if any)
0175: * will save their tiles in the default {@linkplain TileCache tile cache}.
0176: *
0177: * @param image The source image.
0178: */
0179: public ImageWorker(final RenderedImage image) {
0180: inheritanceStopPoint = this .image = image;
0181: }
0182:
0183: /**
0184: * Prepare this builder for the specified image. The images to be computed (if any)
0185: * will save their tiles in the default {@linkplain TileCache tile cache}.
0186: *
0187: * @param image The source image.
0188: */
0189: public final ImageWorker setImage(final RenderedImage image) {
0190: inheritanceStopPoint = this .image = image;
0191: return this ;
0192: }
0193:
0194: /**
0195: * Creates a new image worker with the same hints but a different image.
0196: */
0197: private ImageWorker fork(final RenderedImage image) {
0198: final ImageWorker worker = new ImageWorker(image);
0199: if (commonHints != null && !commonHints.isEmpty()) {
0200: RenderingHints hints = new RenderingHints(null);
0201: hints.add(worker.commonHints);
0202: worker.commonHints = hints;
0203: }
0204: return worker;
0205: }
0206:
0207: /**
0208: * Loads an image using the provided file name and the provided hints, which
0209: * are used to control caching and layout.
0210: *
0211: * @param source
0212: * The source image.
0213: * @param hints
0214: * The hints to use.
0215: * @param imageChoice
0216: * For multipage images.
0217: * @return The loaded image.
0218: *
0219: * @deprecated Use #load instead.
0220: */
0221: public static PlanarImage loadPlanarImageImage(final String source,
0222: final RenderingHints hints, final int imageChoice,
0223: final boolean readMetadata) {
0224: final ImageWorker worker = new ImageWorker();
0225: worker.commonHints = new RenderingHints((java.util.Map) hints);
0226: worker.load(source, imageChoice, readMetadata);
0227: return worker.getPlanarImage();
0228: }
0229:
0230: /**
0231: * Loads an image using the provided file name and the
0232: * {@linkplain #getRenderingHints current hints}, which are used to control caching and layout.
0233: *
0234: * @param source
0235: * Filename of the source image to read.
0236: * @param imageChoice
0237: * Image index in multipage images.
0238: * @param readMatadata
0239: * If {@code true}, metadata will be read.
0240: */
0241: public final void load(final String source, final int imageChoice,
0242: final boolean readMetadata) {
0243: final ParameterBlockJAI pbj = new ParameterBlockJAI("ImageRead");
0244: pbj.setParameter("Input", source).setParameter("ImageChoice",
0245: new Integer(imageChoice)).setParameter("ReadMetadata",
0246: new Boolean(readMetadata)).setParameter("VerifyInput",
0247: Boolean.TRUE);
0248: image = JAI.create("ImageRead", pbj, getRenderingHints());
0249: }
0250:
0251: ///////////////////////////////////////////////////////////////////////////////////////
0252: //////// ////////
0253: //////// IMAGE, PROPERTIES AND RENDERING HINTS ACCESSORS ////////
0254: //////// ////////
0255: ///////////////////////////////////////////////////////////////////////////////////////
0256:
0257: /**
0258: * Returns the current image.
0259: *
0260: * @see #getPlanarImage
0261: * @see #getRenderedOperation
0262: * @see #getImageAsROI
0263: */
0264: public final RenderedImage getRenderedImage() {
0265: return image;
0266: }
0267:
0268: /**
0269: * Returns the {@linkplain #getRenderedImage rendered image} as a planar image.
0270: *
0271: * @see #getRenderedImage
0272: * @see #getRenderedOperation
0273: * @see #getImageAsROI
0274: */
0275: public final PlanarImage getPlanarImage() {
0276: return PlanarImage.wrapRenderedImage(getRenderedImage());
0277: }
0278:
0279: /**
0280: * Returns the {@linkplain #getRenderedImage rendered image} as a rendered operation.
0281: *
0282: * @see #getRenderedImage
0283: * @see #getPlanarImage
0284: * @see #getImageAsROI
0285: */
0286: public final RenderedOp getRenderedOperation() {
0287: final RenderedImage image = getRenderedImage();
0288: if (image instanceof RenderedOp) {
0289: return (RenderedOp) image;
0290: }
0291: return NullDescriptor.create(image, getRenderingHints());
0292: }
0293:
0294: /**
0295: * Returns a {@linkplain ROI Region Of Interest} built from the current
0296: * {@linkplain #getRenderedImage image}. If the image is multi-bands, then this method first
0297: * computes an estimation of its {@linkplain #intensity intensity}. Next, this method
0298: * {@linkplain #binarize() binarize} the image and constructs a {@link ROI} from the result.
0299: *
0300: * @see #getRenderedImage
0301: * @see #getPlanarImage
0302: * @see #getRenderedOperation
0303: */
0304: public final ROI getImageAsROI() {
0305: binarize();
0306: return new ROI(getRenderedImage());
0307: }
0308:
0309: /**
0310: * Returns the <cite>region of interest</cite> currently set, or {@code null} if none.
0311: * The default value is {@code null}.
0312: *
0313: * @see #getMinimums
0314: * @see #getMaximums
0315: */
0316: public final ROI getROI() {
0317: return roi;
0318: }
0319:
0320: /**
0321: * Set the <cite>region of interest</cite> (ROI). A {@code null} set the ROI to the whole
0322: * {@linkplain #image}. The ROI is used by statistical methods like {@link #getMinimums}
0323: * and {@link #getMaximums}.
0324: *
0325: * @return This ImageWorker
0326: *
0327: * @see #getMinimums
0328: * @see #getMaximums
0329: */
0330: public final ImageWorker setROI(final ROI roi) {
0331: this .roi = roi;
0332: invalidateStatistics();
0333: return this ;
0334: }
0335:
0336: /**
0337: * Returns the rendering hint for the specified key, or {@code null} if none.
0338: */
0339: public final Object getRenderingHint(final RenderingHints.Key key) {
0340: return (commonHints != null) ? commonHints.get(key) : null;
0341: }
0342:
0343: /**
0344: * Sets a rendering hint tile to use for all images to be computed by this class. This method
0345: * applies only to the next images to be computed; images already computed before this method
0346: * call (if any) will not be affected.
0347: * <p>
0348: * Some common examples:
0349: * <p>
0350: * <ul>
0351: * <li><code>setRenderingHint({@linkplain JAI#KEY_TILE_CACHE}, null)</code>
0352: * disables completly the tile cache.</li>
0353: * <li><code>setRenderingHint({@linkplain #TILING_ALLOWED}, Boolean.FALSE)</code>
0354: * forces all operators to produce untiled images.</li>
0355: * </ul>
0356: *
0357: * @return This ImageWorker
0358: */
0359: public final ImageWorker setRenderingHint(
0360: final RenderingHints.Key key, final Object value) {
0361: if (commonHints == null) {
0362: commonHints = new RenderingHints(null);
0363: }
0364: commonHints.add(new RenderingHints(key, value));
0365: return this ;
0366: }
0367:
0368: /**
0369: * Removes a rendering hint. Note that invoking this method is <strong>not</strong> the same
0370: * than invoking <code>{@linkplain #setRenderingHint setRenderingHint}(key, null)</code>.
0371: * This is especially true for the {@linkplain javax.media.jai.TileCache tile cache} hint:
0372: * <p>
0373: * <ul>
0374: * <li><code>{@linkplain #setRenderingHint setRenderingHint}({@linkplain JAI#KEY_TILE_CACHE},
0375: * null)</code> disables the use of any tile cache. In other words, this method call do
0376: * request a tile cache, which happen to be the "null" cache.</li>
0377: *
0378: * <li><code>removeRenderingHint({@linkplain JAI#KEY_TILE_CACHE})</code> unsets any tile cache
0379: * specified by a previous rendering hint. All images to be computed after this method
0380: * call will save their tiles in the {@linkplain JAI#getTileCache JAI default tile
0381: * cache}.</li>
0382: * </ul>
0383: *
0384: * @return This ImageWorker
0385: */
0386: public final ImageWorker removeRenderingHint(
0387: final RenderingHints.Key key) {
0388: if (commonHints != null) {
0389: commonHints.remove(key);
0390: }
0391: return this ;
0392: }
0393:
0394: /**
0395: * Returns the rendering hints for an image to be computed by this class.
0396: * The default implementation returns the following hints:
0397: * <p>
0398: * <ul>
0399: * <li>An {@linkplain ImageLayout image layout} with tiles size computed automatically
0400: * from the current {@linkplain #image} size.</li>
0401: * <li>Any additional hints specified through the {@link #setRenderingHint} method. If the
0402: * user provided explicitly a {@link JAI#KEY_IMAGE_LAYOUT}, then the user layout has
0403: * precedence over the automatic layout computed in previous step.</li>
0404: * </ul>
0405: *
0406: * @return The rendering hints to use for image computation (never {@code null}).
0407: */
0408: public final RenderingHints getRenderingHints() {
0409: RenderingHints hints = ImageUtilities.getRenderingHints(image);
0410: if (hints == null) {
0411: hints = new RenderingHints(null);
0412: if (commonHints != null) {
0413: hints.add(commonHints);
0414: }
0415: } else if (commonHints != null) {
0416: hints.putAll(commonHints);
0417: }
0418: if (Boolean.FALSE.equals(hints.get(TILING_ALLOWED))) {
0419: final ImageLayout layout = getImageLayout(hints);
0420: if (commonHints == null
0421: || layout != commonHints.get(JAI.KEY_IMAGE_LAYOUT)) {
0422: // Set the layout only if it is not a user-supplied object.
0423: layout.setTileWidth(image.getWidth());
0424: layout.setTileHeight(image.getHeight());
0425: layout.setTileGridXOffset(image.getMinX());
0426: layout.setTileGridYOffset(image.getMinY());
0427: hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
0428: }
0429: }
0430: if (tileCacheDisabled != 0
0431: && (commonHints != null && !commonHints
0432: .containsKey(JAI.KEY_TILE_CACHE))) {
0433: hints.add(new RenderingHints(JAI.KEY_TILE_CACHE, null));
0434: }
0435: return hints;
0436: }
0437:
0438: /**
0439: * Returns the {@linkplain #getRenderingHints rendering hints}, but with a
0440: * {@linkplain ComponentColorModel component color model} of the specified
0441: * data type. The data type is changed only if no color model was explicitly
0442: * specified by the user through {@link #getRenderingHints()}.
0443: *
0444: * @param type The data type (typically {@link DataBuffer#TYPE_BYTE}).
0445: */
0446: private final RenderingHints getRenderingHints(final int type) {
0447: /*
0448: * Gets the default hints, which usually contains only informations about tiling.
0449: * If the user overridden the rendering hints with an explict color model, keep
0450: * the user's choice.
0451: */
0452: final RenderingHints hints = getRenderingHints();
0453: final ImageLayout layout = getImageLayout(hints);
0454: if (layout.isValid(ImageLayout.COLOR_MODEL_MASK)) {
0455: return hints;
0456: }
0457: /*
0458: * Creates the new color model.
0459: */
0460: final ColorModel oldCm = image.getColorModel();
0461: final ColorModel newCm = new ComponentColorModel(oldCm
0462: .getColorSpace(), oldCm.hasAlpha(), // If true, supports transparency.
0463: oldCm.isAlphaPremultiplied(), // If true, alpha is premultiplied.
0464: oldCm.getTransparency(), // What alpha values can be represented.
0465: type); // Type of primitive array used to represent pixel.
0466: /*
0467: * Creating the final image layout which should allow us to change color model.
0468: */
0469: layout.setColorModel(newCm);
0470: layout.setSampleModel(newCm.createCompatibleSampleModel(image
0471: .getWidth(), image.getHeight()));
0472: hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
0473: return hints;
0474: }
0475:
0476: /**
0477: * Gets the image layout from the specified rendering hints, creating a new one if needed.
0478: * This method do not modify the specified hints. If the caller modifies the image layout,
0479: * it should invoke {@code hints.put(JAI.KEY_IMAGE_LAYOUT, layout)} explicitly.
0480: */
0481: private static ImageLayout getImageLayout(final RenderingHints hints) {
0482: final Object candidate = hints.get(JAI.KEY_IMAGE_LAYOUT);
0483: if (candidate instanceof ImageLayout) {
0484: return (ImageLayout) candidate;
0485: }
0486: return new ImageLayout();
0487: }
0488:
0489: /**
0490: * If {@code false}, disables the tile cache. Invoking this method with value {@code true}
0491: * cancel the last invocation with value {@code false}. If this method was invoking many
0492: * time with value {@code false}, then this method must be invoked the same amount of time
0493: * with the value {@code true} for reenabling the cache.
0494: * <p>
0495: * <strong>Note:</strong> This method name doesn't contain the usual {@code set} prefix
0496: * because it doesn't really set a flag. Instead it increments or decrements a counter.
0497: *
0498: * @return This ImageWorker
0499: */
0500: public final ImageWorker tileCacheEnabled(final boolean status) {
0501: if (status) {
0502: if (tileCacheDisabled != 0) {
0503: tileCacheDisabled--;
0504: } else {
0505: throw new IllegalStateException();
0506: }
0507: } else {
0508: tileCacheDisabled++;
0509: }
0510: return this ;
0511: }
0512:
0513: /**
0514: * Returns the number of bands in the {@linkplain #image}.
0515: *
0516: * @see #retainBands
0517: * @see #retainFirstBand
0518: * @see SampleModel#getNumBands
0519: */
0520: public final int getNumBands() {
0521: return image.getSampleModel().getNumBands();
0522: }
0523:
0524: /**
0525: * Returns the transparent pixel value, or -1 if none.
0526: */
0527: public final int getTransparentPixel() {
0528: final ColorModel cm = image.getColorModel();
0529: return (cm instanceof IndexColorModel) ? ((IndexColorModel) cm)
0530: .getTransparentPixel() : -1;
0531: }
0532:
0533: /**
0534: * Gets a property from the property set of the {@linkplain #image}. If the property name
0535: * is not recognized, then {@link Image#UndefinedProperty} will be returned. This method
0536: * do <strong>not</strong> inherits properties from the image specified at
0537: * {@linkplain #ImageWorker(RenderedImage) construction time} - only properties generated
0538: * by this class are returned.
0539: */
0540: private Object getComputedProperty(final String name) {
0541: final Object value = image.getProperty(name);
0542: return (value == inheritanceStopPoint.getProperty(name)) ? Image.UndefinedProperty
0543: : value;
0544: }
0545:
0546: /**
0547: * Returns the minimums and maximums values found in the image. Those extremas are
0548: * returned as an array of the form {@code double[2][#bands]}.
0549: */
0550: private double[][] getExtremas() {
0551: Object extrema = getComputedProperty(EXTREMA);
0552: if (!(extrema instanceof double[][])) {
0553: final Integer ONE = new Integer(1);
0554: image = ExtremaDescriptor.create(image, // The source image.
0555: roi, // The region of the image to scan. Default to all.
0556: ONE, // The horizontal sampling rate. Default to 1.
0557: ONE, // The vertical sampling rate. Default to 1.
0558: null, // Whether to store extrema locations. Default to false.
0559: ONE, // Maximum number of run length codes to store. Default to 1.
0560: getRenderingHints());
0561: extrema = getComputedProperty(EXTREMA);
0562: }
0563: return (double[][]) extrema;
0564: }
0565:
0566: /**
0567: * Tells this builder that all statistics on pixel values (e.g. the "extrema" property
0568: * in the {@linkplain #image}) should not be inherited from the source images (if any).
0569: * This method should be invoked every time an operation changed the pixel values.
0570: *
0571: * @return This ImageWorker
0572: */
0573: private ImageWorker invalidateStatistics() {
0574: inheritanceStopPoint = image;
0575: return this ;
0576: }
0577:
0578: /**
0579: * Returns the minimal values found in every {@linkplain #image} bands. If a
0580: * {@linkplain #getROI region of interest} is defined, then the statistics
0581: * will be computed only over that region.
0582: *
0583: * @see #getMaximums
0584: * @see #setROI
0585: */
0586: public final double[] getMinimums() {
0587: return getExtremas()[0];
0588: }
0589:
0590: /**
0591: * Returns the maximal values found in every {@linkplain #image} bands. If a
0592: * {@linkplain #getROI region of interest} is defined, then the statistics
0593: * will be computed only over that region.
0594: *
0595: * @see #getMinimums
0596: * @see #setROI
0597: */
0598: public final double[] getMaximums() {
0599: return getExtremas()[1];
0600: }
0601:
0602: ///////////////////////////////////////////////////////////////////////////////////////
0603: //////// ////////
0604: //////// KIND OF IMAGE (BYTES, BINARY, INDEXED, RGB...) ////////
0605: //////// ////////
0606: ///////////////////////////////////////////////////////////////////////////////////////
0607:
0608: /**
0609: * Returns {@code true} if the {@linkplain #image} stores its pixel values in 8 bits.
0610: *
0611: * @see #rescaleToBytes
0612: */
0613: public final boolean isBytes() {
0614: return image.getSampleModel().getDataType() == DataBuffer.TYPE_BYTE;
0615: }
0616:
0617: /**
0618: * Returns {@code true} if the {@linkplain #image} is binary. Such image usually contains
0619: * only two values: 0 and 1.
0620: *
0621: * @see #binarize()
0622: * @see #binarize(double)
0623: * @see #binarize(int,int)
0624: */
0625: public final boolean isBinary() {
0626: return ImageUtil.isBinary(image.getSampleModel());
0627: }
0628:
0629: /**
0630: * Returns {@code true} if the {@linkplain #image} uses an {@linkplain IndexColorModel
0631: * index color model}.
0632: *
0633: * @see #forceIndexColorModel
0634: * @see #forceBitmaskIndexColorModel
0635: * @see #forceIndexColorModelForGIF
0636: */
0637: public final boolean isIndexed() {
0638: return image.getColorModel() instanceof IndexColorModel;
0639: }
0640:
0641: /**
0642: * Returns {@code true} if the {@linkplain #image} uses a RGB {@linkplain ColorSpace color
0643: * space}. Note that a RGB color space doesn't mean that pixel values are directly stored
0644: * as RGB components. The image may be {@linkplain #isIndexed indexed} as well.
0645: *
0646: * @see #forceColorSpaceRGB
0647: */
0648: public final boolean isColorSpaceRGB() {
0649: return image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB;
0650: }
0651:
0652: /**
0653: * Returns {@code true} if the {@linkplain #image} uses a GrayScale
0654: * {@linkplain ColorSpace color space}. Note that a GrayScale color space
0655: * doesn't mean that pixel values are directly stored as GrayScale
0656: * component. The image may be {@linkplain #isIndexed indexed} as well.
0657: *
0658: * @see #forceColorSpaceGRAYScale
0659: */
0660: public final boolean isColorSpaceGRAYScale() {
0661: return image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY;
0662: }
0663:
0664: /**
0665: * Returns {@code true} if the {@linkplain #image} is
0666: * {@linkplain Transparency#TRANSLUCENT translucent}.
0667: *
0668: * @see #forceBitmaskIndexColorModel
0669: */
0670: public final boolean isTranslucent() {
0671: return image.getColorModel().getTransparency() == Transparency.TRANSLUCENT;
0672: }
0673:
0674: ///////////////////////////////////////////////////////////////////////////////////////
0675: //////// ////////
0676: //////// IMAGE OPERATORS ////////
0677: //////// ////////
0678: ///////////////////////////////////////////////////////////////////////////////////////
0679:
0680: /**
0681: * Rescales the {@linkplain #image} such that it uses 8 bits. If the image already uses 8 bits,
0682: * then this method does nothing. Otherwise this method computes the minimum and maximum values
0683: * for each band, {@linkplain RescaleDescriptor rescale} them in the range {@code [0 .. 255]}
0684: * and force the resulting image to {@link DataBuffer#TYPE_BYTE TYPE_BYTE}.
0685: *
0686: * @return This ImageWorker
0687: *
0688: * @see #isBytes
0689: * @see RescaleDescriptor
0690: */
0691: public final ImageWorker rescaleToBytes() {
0692: if (isBytes()) {
0693: // Already using bytes - nothing to do.
0694: return this ;
0695: }
0696: if (isIndexed()) {
0697: throw new UnsupportedOperationException(
0698: "Rescaling not yet implemented for IndexColorModel.");
0699: }
0700: final double[][] extrema = getExtremas();
0701: final int length = extrema[0].length;
0702: final double[] scale = new double[length];
0703: final double[] offset = new double[length];
0704: for (int i = 0; i < length; i++) {
0705: final double delta = extrema[1][i] - extrema[0][i];
0706: scale[i] = 255 / delta;
0707: offset[i] = -scale[i] * extrema[0][i];
0708: }
0709: final RenderingHints hints = getRenderingHints(DataBuffer.TYPE_BYTE);
0710: image = RescaleDescriptor.create(image, // The source image.
0711: scale, // The per-band constants to multiply by.
0712: offset, // The per-band offsets to be added.
0713: hints); // The rendering hints.
0714: invalidateStatistics(); // Extremas are no longer valid.
0715:
0716: // All post conditions for this method contract.
0717: assert isBytes();
0718: return this ;
0719: }
0720:
0721: /**
0722: * Reduces the color model to {@linkplain IndexColorModel index color model}.
0723: * If the current {@linkplain #image} already uses an
0724: * {@linkplain IndexColorModel index color model}, then this method do
0725: * nothing. Otherwise, the current implementation performs a ditering on the
0726: * original color model. Note that this operation loose the alpha channel.
0727: * <p>
0728: * This for the moment should work only with opaque images, with non opaque
0729: * images we just remove the alpha band in order to build an
0730: * {@link IndexColorModel}. This is one because in general it could be very
0731: * difficult to decide the final transparency for each pixel given the
0732: * complexity if the algorithms for obtaining an {@link IndexColorModel}.
0733: * <p>
0734: * If an {@link IndexColorModel} with a single transparency index is enough
0735: * for you, we advise you to take a look at
0736: * {@link #forceIndexColorModelForGIF(boolean)} methdo.
0737: *
0738: * @see #isIndexed
0739: * @see #forceBitmaskIndexColorModel
0740: * @see #forceIndexColorModelForGIF
0741: * @see OrderedDitherDescriptor
0742: */
0743: public final ImageWorker forceIndexColorModel(final boolean error) {
0744: final ColorModel cm = image.getColorModel();
0745: if (cm instanceof IndexColorModel) {
0746: // Already an index color model - nothing to do.
0747: return this ;
0748: }
0749: tileCacheEnabled(false);
0750: final int numBands = getNumBands();
0751: if ((numBands & 1) == 0) {
0752: retainBands(numBands - 1);
0753: }
0754: forceColorSpaceRGB();
0755: final RenderingHints hints = getRenderingHints();
0756: if (error) {
0757: if (false) {
0758: // color quantization, disabled for now.
0759: final RenderedOp temp = ColorQuantizerDescriptor
0760: .create(image,
0761: ColorQuantizerDescriptor.MEDIANCUT,
0762: new Integer(254), new Integer(200),
0763: null, new Integer(1), new Integer(1),
0764: hints);
0765: final ImageLayout layout = new ImageLayout();
0766: layout.setColorModel(temp.getColorModel());
0767: hints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT,
0768: layout));
0769: LookupTableJAI lookup = (LookupTableJAI) temp
0770: .getProperty("JAI.LookupTable");
0771: }
0772: // error diffusion
0773: final KernelJAI ditherMask = KernelJAI.ERROR_FILTER_FLOYD_STEINBERG;
0774: final LookupTableJAI colorMap = ColorCube.BYTE_496;
0775: image = ErrorDiffusionDescriptor.create(image, colorMap,
0776: ditherMask, hints);
0777: } else {
0778: // ordered dither
0779: final KernelJAI[] ditherMask = KernelJAI.DITHER_MASK_443;
0780: final ColorCube colorMap = ColorCube.BYTE_496;
0781: image = OrderedDitherDescriptor.create(image, colorMap,
0782: ditherMask, hints);
0783: }
0784: tileCacheEnabled(true);
0785: invalidateStatistics();
0786:
0787: // All post conditions for this method contract.
0788: assert isIndexed();
0789: return this ;
0790: }
0791:
0792: /**
0793: * Reduces the color model to {@linkplain IndexColorModel index color model}
0794: * with {@linkplain Transparency#OPAQUE opaque} or
0795: * {@linkplain Transparency#BITMASK bitmask} transparency. If the current
0796: * {@linkplain #image} already uses a suitable color model, then this method
0797: * do nothing.
0798: *
0799: * @return this {@link ImageWorker}.
0800: *
0801: * @see #isIndexed
0802: * @see #isTranslucent
0803: * @see #forceIndexColorModel
0804: * @see #forceIndexColorModelForGIF
0805: */
0806: public final ImageWorker forceBitmaskIndexColorModel() {
0807: forceBitmaskIndexColorModel(getTransparentPixel(), true);
0808: return this ;
0809: }
0810:
0811: /**
0812: * Reduces the color model to {@linkplain IndexColorModel index color model}
0813: * with {@linkplain Transparency#OPAQUE opaque} or
0814: * {@linkplain Transparency#BITMASK bitmask} transparency. If the current
0815: * {@linkplain #image} already uses a suitable color model, then this method
0816: * do nothing.
0817: *
0818: * @param transparent
0819: * A pixel value to define as the transparent pixel. *
0820: * @param errorDiffusion
0821: * Tells if I should use {@link ErrorDiffusionDescriptor} or
0822: * {@link OrderedDitherDescriptor} JAi operations. errorDiffusion
0823: * @return this {@link ImageWorker}.
0824: *
0825: * @see #isIndexed
0826: * @see #isTranslucent
0827: * @see #forceIndexColorModel
0828: * @see #forceIndexColorModelForGIF
0829: */
0830: public final ImageWorker forceBitmaskIndexColorModel(
0831: int transparent, final boolean errorDiffusion) {
0832: final ColorModel cm = image.getColorModel();
0833: if (cm instanceof IndexColorModel) {
0834: final IndexColorModel oldCM = (IndexColorModel) cm;
0835: switch (oldCM.getTransparency()) {
0836: case Transparency.OPAQUE: {
0837: // Suitable color model. There is nothing to do.
0838: return this ;
0839: }
0840: case Transparency.BITMASK: {
0841: if (oldCM.getTransparentPixel() == transparent) {
0842: // Suitable color model. There is nothing to do.
0843: return this ;
0844: }
0845: break;
0846: }
0847: default: {
0848: break;
0849: }
0850: }
0851: /*
0852: * The index color model need to be replaced. Creates a lookup table
0853: * mapping from the old pixel values to new pixels values, with
0854: * transparent colors mapped to the new transparent pixel value. The
0855: * lookup table uses TYPE_BYTE or TYPE_USHORT, which are the two
0856: * only types supported by IndexColorModel.
0857: */
0858: final int pixelSize = oldCM.getPixelSize();
0859: transparent &= (1 << pixelSize) - 1;
0860: final int mapSize = oldCM.getMapSize();
0861: final int newSize = Math.max(mapSize, transparent + 1);
0862: final LookupTableJAI lookupTable;
0863: if (newSize <= 0xFF) {
0864: final byte[] table = new byte[mapSize];
0865: for (int i = 0; i < mapSize; i++) {
0866: table[i] = (byte) ((oldCM.getAlpha(i) == 0) ? transparent
0867: : i);
0868: }
0869: lookupTable = new LookupTableJAI(table);
0870: } else if (newSize <= 0xFFFF) {
0871: final short[] table = new short[mapSize];
0872: for (int i = 0; i < mapSize; i++) {
0873: table[i] = (short) ((oldCM.getAlpha(i) == 0) ? transparent
0874: : i);
0875: }
0876: lookupTable = new LookupTableJAI(table, true);
0877: } else {
0878: throw new AssertionError(mapSize); // Should never happen.
0879: }
0880: /*
0881: * Now we need to perform the look up transformation. First of all
0882: * we create the new color model with a bitmask transparency using
0883: * the transparency index specified to this method. Then we perform
0884: * the lookup operation in order to prepare for the gif image.
0885: */
0886: final byte[][] rgb = new byte[3][newSize];
0887: oldCM.getReds(rgb[0]);
0888: oldCM.getGreens(rgb[1]);
0889: oldCM.getBlues(rgb[2]);
0890: final IndexColorModel newCM = new IndexColorModel(
0891: pixelSize, newSize, rgb[0], rgb[1], rgb[2],
0892: transparent);
0893: final RenderingHints hints = getRenderingHints();
0894: final ImageLayout layout = getImageLayout(hints);
0895: layout.setColorModel(newCM);
0896: hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
0897: hints.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
0898: image = LookupDescriptor.create(image, lookupTable, hints);
0899: } else {
0900: /*
0901: * The image is not indexed. Getting the alpha channel.
0902: */
0903: RenderedImage alphaChannel = null;
0904: if (cm.hasAlpha()) {
0905: tileCacheEnabled(false);
0906: int numBands = getNumBands();
0907: final RenderingHints hints = getRenderingHints();
0908: alphaChannel = BandSelectDescriptor.create(image,
0909: new int[] { --numBands }, hints);
0910: retainBands(numBands);
0911: forceIndexColorModel(errorDiffusion);
0912: tileCacheEnabled(true);
0913: }
0914: /*
0915: * Adding transparency if needed, which means using the alpha
0916: * channel to build a new color model. The method call below implies
0917: * 'forceColorSpaceRGB()' and 'forceIndexColorModel()' method calls.
0918: */
0919: addTransparencyToIndexColorModel(alphaChannel, false,
0920: transparent, errorDiffusion);
0921: }
0922: // All post conditions for this method contract.
0923: assert isIndexed();
0924: assert !isTranslucent();
0925: return this ;
0926: }
0927:
0928: /**
0929: * Converts the image to a GIF-compliant image. This method has been created
0930: * in order to convert the input image to a form that is compatible with the
0931: * GIF model. It first remove the information about transparency since the
0932: * error diffusion and the error dither operations are unable to process
0933: * images with more than 3 bands. Afterwards the image is processed with an
0934: * error diffusion operator in order to reduce the number of bands from 3 to
0935: * 1 and the number of color to 216. A suitable layout is used for the final
0936: * image via the {@linkplain #getRenderingHints rendering hints} in order to
0937: * take into account the different layout model for the final image.
0938: * <p>
0939: * <strong>Tip:</strong> For optimizing writing GIF, we need to create the
0940: * image untiled. This can be done by invoking
0941: * <code>{@linkplain #setRenderingHint setRenderingHint}({@linkplain
0942: * #TILING_ALLOWED}, Boolean.FALSE)</code>
0943: * first.
0944: *
0945: * @param errorDiffusion
0946: * Tells if I should use {@link ErrorDiffusionDescriptor} or
0947: * {@link OrderedDitherDescriptor} JAi operations.
0948: *
0949: * @return this {@link ImageWorker}.
0950: *
0951: * @see #isIndexed
0952: * @see #forceIndexColorModel
0953: * @see #forceBitmaskIndexColorModel
0954: */
0955: public final ImageWorker forceIndexColorModelForGIF(
0956: final boolean errorDiffusion) {
0957: /*
0958: * Checking the color model to see if we need to convert it back to
0959: * color model. We might also need to reformat the image in order to get
0960: * it to 8 bits samples.
0961: */
0962: tileCacheEnabled(false);
0963: if (image.getColorModel() instanceof PackedColorModel) {
0964: forceComponentColorModel();
0965: }
0966: rescaleToBytes();
0967: tileCacheEnabled(true);
0968: /*
0969: * Getting the alpha channel and separating from the others bands. If
0970: * the initial image had no alpha channel (more specifically, if it is
0971: * either opaque or a bitmask) we proceed without doing anything since
0972: * it seems that GIF encoder in such a case works fine. If we need to
0973: * create a bitmask, we will use the last index value allowed (255) as
0974: * the transparent pixel value.
0975: */
0976: if (isTranslucent()) {
0977: forceBitmaskIndexColorModel(255, errorDiffusion);
0978: } else {
0979: forceIndexColorModel(errorDiffusion);
0980: }
0981: // All post conditions for this method contract.
0982: assert isBytes();
0983: assert isIndexed();
0984: assert !isTranslucent();
0985: return this ;
0986: }
0987:
0988: /**
0989: * Reformats the {@linkplain ColorModel color model} to a
0990: * {@linkplain ComponentColorModel component color model} preserving
0991: * transparency. This is used especially in order to go from
0992: * {@link PackedColorModel} to {@link ComponentColorModel}, which seems to
0993: * be well accepted from {@code PNGEncoder} and {@code TIFFEncoder}.
0994: * <p>
0995: * This code is adapted from jai-interests mailing list archive.
0996: *
0997: * @return this {@link ImageWorker}.
0998: *
0999: * @see FormatDescriptor
1000: */
1001: public final ImageWorker forceComponentColorModel() {
1002: return forceComponentColorModel(false);
1003: }
1004:
1005: /**
1006: * Reformats the {@linkplain ColorModel color model} to a
1007: * {@linkplain ComponentColorModel component color model} preserving
1008: * transparency. This is used especially in order to go from
1009: * {@link PackedColorModel} to {@link ComponentColorModel}, which seems to
1010: * be well accepted from {@code PNGEncoder} and {@code TIFFEncoder}.
1011: * <p>
1012: * This code is adapted from jai-interests mailing list archive.
1013: *
1014: * @param checkTransparent
1015: * tells this method to not consider fully transparent pixels
1016: * when optimizing grayscale palettes.
1017: *
1018: * @return this {@link ImageWorker}.
1019: *
1020: * @todo Make this function work on 16 bits indexed images which means
1021: * changing the lookup code.
1022: * @see FormatDescriptor
1023: */
1024: public final ImageWorker forceComponentColorModel(
1025: boolean checkTransparent) {
1026: final ColorModel cm = image.getColorModel();
1027: if (cm instanceof ComponentColorModel) {
1028: // Already an component color model - nothing to do.
1029: return this ;
1030: }
1031: // shortcut for index color model
1032: if (cm instanceof IndexColorModel) {
1033: final IndexColorModel icm = (IndexColorModel) cm;
1034: boolean gray = ColorUtilities.isGrayPalette(icm,
1035: checkTransparent);
1036: boolean alpha = icm.hasAlpha();
1037: final int numBands = icm.getNumComponents();
1038: byte data[][] = new byte[cm.getNumComponents()][icm
1039: .getMapSize()];
1040: icm.getReds(data[0]);
1041: icm.getGreens(data[1]);
1042: icm.getBlues(data[2]);
1043: if (numBands == 4) {
1044: icm.getAlphas(data[3]);
1045: }
1046: final LookupTableJAI lut = new LookupTableJAI(data);
1047: /*
1048: * Get the default hints, which usually contains only informations
1049: * about tiling. If the user overrode the rendering hints with an
1050: * explict color model, keep the user's choice.
1051: */
1052: final RenderingHints hints = getRenderingHints();
1053: image = JAI.create("Lookup", image, lut, hints);
1054: /*
1055: * If the image is grayscale, retain only the first band.
1056: */
1057: if (gray && !alpha) {
1058: retainBands(1);
1059: } else if (gray && alpha) {
1060: retainBands(new int[] { 0, 3 });
1061: }
1062: } else {
1063: // Most of the code adapted from jai-interests is in 'getRenderingHints(int)'.
1064: final int type = (cm instanceof DirectColorModel) ? DataBuffer.TYPE_BYTE
1065: : image.getSampleModel().getTransferType();
1066: final RenderingHints hints = getRenderingHints(type);
1067: image = FormatDescriptor.create(image, new Integer(type),
1068: hints);
1069: }
1070: invalidateStatistics();
1071:
1072: // All post conditions for this method contract.
1073: assert image.getColorModel() instanceof ComponentColorModel;
1074: return this ;
1075: }
1076:
1077: /**
1078: * Forces the {@linkplain #image} color model to the
1079: * {@linkplain ColorSpace#CS_sRGB RGB color space}. If the current color
1080: * space is already of {@linkplain ColorSpace#TYPE_RGB RGB type}, then this
1081: * method does nothing. This operation may loose the alpha channel.
1082: *
1083: * @return this {@link ImageWorker}.
1084: *
1085: * @see #isColorSpaceRGB
1086: * @see ColorConvertDescriptor
1087: */
1088: public final ImageWorker forceColorSpaceRGB() {
1089: if (!isColorSpaceRGB()) {
1090: final ColorModel cm = new ComponentColorModel(ColorSpace
1091: .getInstance(ColorSpace.CS_sRGB), false, false,
1092: Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
1093: image = ColorConvertDescriptor.create(image, cm,
1094: getRenderingHints());
1095: invalidateStatistics();
1096: }
1097: // All post conditions for this method contract.
1098: assert isColorSpaceRGB();
1099: return this ;
1100: }
1101:
1102: /**
1103: * Forces the {@linkplain #image} color model to the
1104: * {@linkplain ColorSpace#CS_GRAY GRAYScale color space}. If the current
1105: * color space is already of {@linkplain ColorSpace#TYPE_GRAY type}, then
1106: * this method does nothing.
1107: *
1108: * @return this {@link ImageWorker}.
1109: *
1110: * @see #isColorSpaceGRAYScale
1111: * @see ColorConvertDescriptor
1112: */
1113: public final ImageWorker forceColorSpaceGRAYScale() {
1114: if (!isColorSpaceRGB()) {
1115: final ColorModel cm = new ComponentColorModel(ColorSpace
1116: .getInstance(ColorSpace.CS_GRAY), false, false,
1117: Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
1118: image = ColorConvertDescriptor.create(image, cm,
1119: getRenderingHints());
1120: invalidateStatistics();
1121: }
1122: // All post conditions for this method contract.
1123: assert isColorSpaceGRAYScale();
1124: return this ;
1125: }
1126:
1127: /**
1128: * Creates an image which represents approximatively the intensity of
1129: * {@linkplain #image}. The result is always a single-banded image. If the
1130: * image uses an {@linkplain IHSColorSpace IHS color space}, then this
1131: * method just {@linkplain #retainFirstBand retain the first band} without
1132: * any further processing. Otherwise, this method performs a simple
1133: * {@linkplain BandCombineDescriptor band combine} operation on the
1134: * {@linkplain #image} in order to come up with a simple estimation of the
1135: * intensity of the image based on the average value of the color
1136: * components. It is worthwhile to note that the alpha band is stripped from
1137: * the image.
1138: *
1139: * @return this {@link ImageWorker}.
1140: *
1141: * @see BandCombineDescriptor
1142: */
1143: public final ImageWorker intensity() {
1144: /*
1145: * If the color model already uses a IHS color space or a Gray color
1146: * space, keep only the intensity band. Otherwise, we need a component
1147: * color model to be sure to understand what we are doing.
1148: */
1149: ColorModel cm = image.getColorModel();
1150: final ColorSpace cs = cm.getColorSpace();
1151: if (cs.getType() == ColorSpace.TYPE_GRAY
1152: || cs instanceof IHSColorSpace) {
1153: retainFirstBand();
1154: return this ;
1155: }
1156: if (cm instanceof IndexColorModel) {
1157: forceComponentColorModel();
1158: cm = image.getColorModel();
1159: }
1160:
1161: // Number of color componenents
1162: final int numBands = cm.getNumComponents();
1163: final int numColorBands = cm.getNumColorComponents();
1164: final boolean hasAlpha = cm.hasAlpha();
1165:
1166: // One band, nothing to combine.
1167: if (numBands == 1) {
1168: return this ;
1169: }
1170: // One band plus alpha, let's remove alpha.
1171: if (numColorBands == 1 && hasAlpha) {
1172: retainFirstBand();
1173: return this ;
1174: }
1175: /*
1176: * We have more than one band. Note that there is no need to remove the
1177: * alpha band before to apply the "bandCombine" operation - it is
1178: * suffisient to let the coefficient for the alpha band to the 0 value.
1179: */
1180: final double[][] coeff = new double[1][numBands + 1];
1181: Arrays.fill(coeff[0], 0, numColorBands, 1.0 / numColorBands);
1182: image = BandCombineDescriptor.create(image, coeff,
1183: getRenderingHints());
1184: invalidateStatistics();
1185:
1186: // All post conditions for this method contract.
1187: assert getNumBands() == 1;
1188: return this ;
1189: }
1190:
1191: /**
1192: * Retains inconditionnaly the first band of {@linkplain #image}. All other
1193: * bands (if any) are discarted without any further processing.
1194: *
1195: * @return this {@link ImageWorker}.
1196: *
1197: * @see #getNumBands
1198: * @see #retainBands
1199: * @see BandSelectDescriptor
1200: */
1201: public final ImageWorker retainFirstBand() {
1202: retainBands(1);
1203:
1204: // All post conditions for this method contract.
1205: assert getNumBands() == 1;
1206: return this ;
1207: }
1208:
1209: /**
1210: * Retains inconditionnaly the last band of {@linkplain #image}. All other
1211: * bands (if any) are discarted without any further processing.
1212: *
1213: * @return this {@link ImageWorker}.
1214: *
1215: * @see #getNumBands
1216: * @see #retainBands
1217: * @see BandSelectDescriptor
1218: */
1219: public final ImageWorker retainLastBand() {
1220: retainBands(new int[] { getNumBands() - 1 });
1221:
1222: // All post conditions for this method contract.
1223: assert getNumBands() == 1;
1224: return this ;
1225: }
1226:
1227: /**
1228: * Retains inconditionnaly the first {@code numBands} of {@linkplain #image}.
1229: * All other bands (if any) are discarted without any further processing.
1230: * This method does nothing if the current {@linkplain #image} does not have
1231: * a greater amount of bands than {@code numBands}.
1232: *
1233: * @param numBands
1234: * the number of bands to retain.
1235: * @return this {@link ImageWorker}.
1236: *
1237: * @see #getNumBands
1238: * @see #retainFirstBand
1239: * @see BandSelectDescriptor
1240: */
1241: public final ImageWorker retainBands(final int numBands) {
1242: if (numBands <= 0) {
1243: throw new IndexOutOfBoundsException(Errors.format(
1244: ErrorKeys.ILLEGAL_ARGUMENT_$2, "numBands",
1245: new Integer(numBands)));
1246: }
1247: if (getNumBands() > numBands) {
1248: final int[] bands = new int[numBands];
1249: for (int i = 0; i < bands.length; i++) {
1250: bands[i] = i;
1251: }
1252: image = BandSelectDescriptor.create(image, bands,
1253: getRenderingHints());
1254: }
1255:
1256: // All post conditions for this method contract.
1257: assert getNumBands() <= numBands;
1258: return this ;
1259: }
1260:
1261: /**
1262: * Retains inconditionnaly certain bands of {@linkplain #image}. All other
1263: * bands (if any) are discarded without any further processing.
1264: *
1265: * @param bands
1266: * the bands to retain.
1267: * @return this {@link ImageWorker}.
1268: *
1269: * @see #getNumBands
1270: * @see #retainFirstBand
1271: * @see BandSelectDescriptor
1272: */
1273: public final ImageWorker retainBands(final int[] bands) {
1274: image = BandSelectDescriptor.create(image, bands,
1275: getRenderingHints());
1276: return this ;
1277: }
1278:
1279: /**
1280: * Formats the underlying image to the provided data type.
1281: *
1282: * @param dataType
1283: * to be used for this {@link FormatDescriptor} operation.
1284: * @return this {@link ImageWorker}
1285: */
1286: public final ImageWorker format(final int dataType) {
1287: image = FormatDescriptor.create(image, new Integer(dataType),
1288: getRenderingHints());
1289:
1290: // All post conditions for this method contract.
1291: assert image.getSampleModel().getDataType() == dataType;
1292: return this ;
1293: }
1294:
1295: /**
1296: * Binarizes the {@linkplain #image}. If the image is multi-bands, then
1297: * this method first computes an estimation of its
1298: * {@linkplain #intensity intensity}. Then, the threshold value is set
1299: * halfway between the minimal and maximal values found in the image.
1300: *
1301: * @return this {@link ImageWorker}.
1302: *
1303: * @see #isBinary
1304: * @see #binarize(double)
1305: * @see #binarize(int,int)
1306: * @see BinarizeDescriptor
1307: */
1308: public final ImageWorker binarize() {
1309: binarize(Double.NaN);
1310:
1311: // All post conditions for this method contract.
1312: assert isBinary();
1313: return this ;
1314: }
1315:
1316: /**
1317: * Binarizes the {@linkplain #image}. If the image is already binarized,
1318: * then this method does nothing.
1319: *
1320: * @param threshold
1321: * The threshold value.
1322: * @return this {@link ImageWorker}.
1323: *
1324: * @see #isBinary
1325: * @see #binarize()
1326: * @see #binarize(int,int)
1327: * @see BinarizeDescriptor
1328: */
1329: public final ImageWorker binarize(double threshold) {
1330: // If the image is already binary and the threshold is >=1 then there is no work to do.
1331: if (!isBinary()) {
1332: if (Double.isNaN(threshold)) {
1333: if (getNumBands() != 1) {
1334: tileCacheEnabled(false);
1335: intensity();
1336: tileCacheEnabled(true);
1337: }
1338: final double[][] extremas = getExtremas();
1339: threshold = 0.5 * (extremas[0][0] + extremas[1][0]);
1340: }
1341: final RenderingHints hints = getRenderingHints();
1342: image = BinarizeDescriptor.create(image, new Double(
1343: threshold), hints);
1344: invalidateStatistics();
1345: }
1346: // All post conditions for this method contract.
1347: assert isBinary();
1348: return this ;
1349: }
1350:
1351: /**
1352: * Binarizes the {@linkplain #image} (if not already done) and replace all 0
1353: * values by {@code value0} and all 1 values by {@code value1}. If the
1354: * image should be binarized using a custom threshold value (instead of the
1355: * automatic one), invoke {@link #binarize(double)} explicitly before this
1356: * method.
1357: *
1358: * @return this {@link ImageWorker}.
1359: * @see #isBinary
1360: * @see #binarize()
1361: * @see #binarize(double)
1362: * @see BinarizeDescriptor
1363: * @see LookupDescriptor
1364: */
1365: public final ImageWorker binarize(final int value0, final int value1) {
1366: tileCacheEnabled(false);
1367: binarize();
1368: tileCacheEnabled(true);
1369: final LookupTableJAI table;
1370: final int min = Math.min(value0, value1);
1371: if (min >= 0) {
1372: final int max = Math.max(value0, value1);
1373: if (max < 256) {
1374: table = new LookupTableJAI(new byte[] { (byte) value0,
1375: (byte) value1 });
1376: } else if (max < 65536) {
1377: table = new LookupTableJAI(new short[] {
1378: (short) value0, (short) value1 }, true);
1379: } else {
1380: table = new LookupTableJAI(new int[] { value0, value1 });
1381: }
1382: } else {
1383: table = new LookupTableJAI(new int[] { value0, value1 });
1384: }
1385: image = LookupDescriptor.create(image, table,
1386: getRenderingHints());
1387: invalidateStatistics();
1388: return this ;
1389: }
1390:
1391: /**
1392: * Replaces all occurences of the given color (usually opaque) by a fully transparent color.
1393: * Currents implementation supports image backed by any {@link IndexColorModel}, or by
1394: * {@link ComponentColorModel} with {@link DataBuffer#TYPE_BYTE TYPE_BYTE}. More types
1395: * may be added in future GeoTools versions.
1396: *
1397: * @param transparentColor The color to make transparent.
1398: * @return this image worker.
1399: *
1400: * @throws IllegalStateException if the current {@linkplain #image} has an unsupported color
1401: * model.
1402: */
1403: public final ImageWorker makeColorTransparent(
1404: final Color transparentColor) throws IllegalStateException {
1405: if (transparentColor == null) {
1406: throw new IllegalArgumentException(Errors.format(
1407: ErrorKeys.NULL_ARGUMENT_$1, "transparentColor"));
1408: }
1409: final ColorModel cm = image.getColorModel();
1410: if (cm instanceof IndexColorModel) {
1411: return maskIndexColorModelByte(transparentColor);
1412: } else if (cm instanceof ComponentColorModel) {
1413: switch (image.getSampleModel().getDataType()) {
1414: case DataBuffer.TYPE_BYTE: {
1415: return maskComponentColorModelByte(transparentColor);
1416: }
1417: // Add other types here if we support them...
1418: }
1419: }
1420: throw new IllegalStateException(Errors
1421: .format(ErrorKeys.UNSUPPORTED_DATA_TYPE));
1422: }
1423:
1424: /**
1425: * For an image backed by an {@link IndexColorModel}, replaces all occurences of the given
1426: * color (usually opaque) by a fully transparent color.
1427: *
1428: * @param transparentColor The color to make transparent.
1429: * @return this image worker.
1430: *
1431: * @deprecated This method will be private (and maybe replaced) in a future version.
1432: * Use {@link #makeColorTransparent} instead.
1433: */
1434: public final ImageWorker maskIndexColorModelByte(
1435: final Color transparentColor) {
1436: assert image.getColorModel() instanceof IndexColorModel;
1437:
1438: // Gets informations about the provided images.
1439: IndexColorModel cm = (IndexColorModel) image.getColorModel();
1440: final int numComponents = cm.getNumComponents();
1441: int transparency = cm.getTransparency();
1442: int transparencyIndex = cm.getTransparentPixel();
1443: final int mapSize = cm.getMapSize();
1444: final int transparentRGB = transparentColor.getRGB() & 0xFFFFFF;
1445: /*
1446: * Optimization in case of Transparency.BITMASK.
1447: * If the color we want to use as the fully transparent one is the same
1448: * that is actually used as the transparent color, we leave doing nothing.
1449: */
1450: if (transparency == Transparency.BITMASK
1451: && transparencyIndex != -1) {
1452: int transpColor = cm.getRGB(transparencyIndex) & 0xFFFFFF;
1453: if (transpColor == transparentRGB) {
1454: return this ;
1455: }
1456: }
1457: /*
1458: * Find the index of the specified color. Most of the time, the color should appears only
1459: * once, which will leads us to a BITMASK image. However we allows more occurences, which
1460: * will leads us to a TRANSLUCENT image.
1461: */
1462: final List transparentPixelsIndexes = new ArrayList();
1463: for (int i = 0; i < mapSize; i++) {
1464: // Gets the color for this pixel removing the alpha information.
1465: final int color = cm.getRGB(i) & 0xFFFFFF;
1466: if (transparentRGB == color) {
1467: transparentPixelsIndexes.add(new Integer(i));
1468: if (Transparency.BITMASK == transparency) {
1469: break;
1470: }
1471: }
1472: }
1473: final int found = transparentPixelsIndexes.size();
1474: if (found == 1) {
1475: // Transparent color found.
1476: transparencyIndex = ((Integer) transparentPixelsIndexes
1477: .get(0)).intValue();
1478: transparency = Transparency.BITMASK;
1479: } else if (found == 0) {
1480: return this ;
1481: } else {
1482: transparencyIndex = -1;
1483: transparency = Transparency.TRANSLUCENT;
1484: }
1485:
1486: // Prepare the new ColorModel.
1487: // Get the old map and update it as needed.
1488: final byte rgb[][] = new byte[4][mapSize];
1489: Arrays.fill(rgb[3], (byte) 255);
1490: cm.getReds(rgb[0]);
1491: cm.getGreens(rgb[1]);
1492: cm.getBlues(rgb[2]);
1493: if (numComponents == 4) {
1494: cm.getAlphas(rgb[3]);
1495: }
1496: if (transparency != Transparency.TRANSLUCENT) {
1497: cm = new IndexColorModel(cm.getPixelSize(), mapSize,
1498: rgb[0], rgb[1], rgb[2], transparencyIndex);
1499: } else {
1500: for (int k = 0; k < found; k++) {
1501: rgb[3][((Integer) transparentPixelsIndexes.get(k))
1502: .intValue()] = (byte) 0;
1503: }
1504: cm = new IndexColorModel(cm.getPixelSize(), mapSize,
1505: rgb[0], rgb[1], rgb[2], rgb[3]);
1506: }
1507:
1508: // Format the input image.
1509: final ImageLayout layout = new ImageLayout(image);
1510: layout.setColorModel(cm);
1511: final RenderingHints hints = getRenderingHints();
1512: hints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
1513: hints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL,
1514: Boolean.FALSE));
1515: image = FormatDescriptor.create(image, new Integer(image
1516: .getSampleModel().getDataType()), hints);
1517: invalidateStatistics();
1518: return this ;
1519: }
1520:
1521: /**
1522: * For an image backed by an {@link ComponentColorModel}, replaces all occurences
1523: * of the given color (usually opaque) by a fully transparent color.
1524: *
1525: * @param transparentColor The color to make transparent.
1526: * @return this image worker.
1527: *
1528: * @deprecated This method will be private (and maybe replaced) in a future version.
1529: * Use {@link #makeColorTransparent} instead.
1530: *
1531: * Current implementation invokes a lot of JAI operations:
1532: *
1533: * "BandSelect" --> "Lookup" --> "BandCombine" --> "Extrema" --> "Binarize" -->
1534: * "Format" --> "BandSelect" (one more time) --> "Multiply" --> "BandMerge".
1535: *
1536: * I would expect more speed and memory efficiency by writting our own JAI operation (PointOp
1537: * subclass) doing that in one step. It would also be more determinist (our "binarize" method
1538: * depends on statistics on pixel values) and avoid unwanted side-effect like turning black
1539: * color (RGB = 0,0,0) to transparent one. It would also be easier to maintain I believe.
1540: */
1541: public final ImageWorker maskComponentColorModelByte(
1542: final Color transparentColor) {
1543: assert image.getColorModel() instanceof ComponentColorModel;
1544: assert image.getSampleModel().getDataType() == DataBuffer.TYPE_BYTE;
1545: /*
1546: * Prepares the look up table for the source image.
1547: * Remember what follows which is taken from the JAI programming guide.
1548: *
1549: * "The lookup operation performs a general table lookup on a rendered or renderable
1550: * image. The destination image is obtained by passing the source image through the
1551: * lookup table. The source image may be single- or multi-banded of data types byte,
1552: * ushort, short, or int. The lookup table may be single- or multi-banded of any JAI-
1553: * supported data types.
1554: *
1555: * The destination image must have the same data type as the lookup table, and its
1556: * number of bands is determined based on the number of bands of the source and the
1557: * table. If the source is single-banded, the destination has the same number of bands
1558: * as the lookup table; otherwise, the destination has the same number of bands as the
1559: * source.
1560: *
1561: * If either the source or the table is single-banded and the other one is multibanded,
1562: * the single band is applied to every band of the multi-banded object. If both are
1563: * multi-banded, their corresponding bands are matched up."
1564: *
1565: * A final annotation, if we have an input image with transparency we just DROP it since
1566: * we want to re-add it using the supplied color as the mask for transparency.
1567: */
1568:
1569: /*
1570: * In case of a gray color model we can do everything in one step by expanding
1571: * the color model to get one more band directly which is the alpha band itself.
1572: *
1573: * For a multiband image the lookup is applied to each band separately.
1574: * This means that we cannot control directly the image as a whole but
1575: * we need first to interact with the single bands then to combine the
1576: * result into a single band that will provide us with the alpha band.
1577: */
1578: final int numBands = image.getSampleModel().getNumBands();
1579: final int numColorBands = image.getColorModel()
1580: .getNumColorComponents();
1581: final RenderingHints hints = getRenderingHints();
1582: RenderedImage transparentBand = null;
1583: if (numColorBands != numBands) {
1584: // Typically, numColorBands will be equals to numBands-1.
1585: transparentBand = BandSelectDescriptor.create(image,
1586: new int[] { numColorBands }, hints);
1587: final int[] opaqueBands = new int[numColorBands];
1588: for (int i = 0; i < opaqueBands.length; i++) {
1589: opaqueBands[i] = i;
1590: }
1591: image = BandSelectDescriptor.create(image, opaqueBands,
1592: hints);
1593: }
1594: final byte[][] tableData = new byte[numColorBands][256];
1595: final boolean singleStep = (numColorBands == 1);
1596: if (singleStep) {
1597: final byte[] data = tableData[0];
1598: Arrays.fill(data, (byte) 255);
1599: data[transparentColor.getRed()] = 0;
1600: } else {
1601: for (int j = 0; j < numColorBands; j++) {
1602: final byte[] data = tableData[j];
1603: for (int i = 0; i < data.length; i++) {
1604: data[i] = (byte) i;
1605: }
1606: }
1607: if (true) {
1608: // TODO: BUG ???????????????
1609: // The previous code was written in an other way with an end result as below, which
1610: // sound like a bug to me. But I'm dont understand well enough what the code tries
1611: // to do, so I reproduce here what the old code did.
1612: Arrays.fill(tableData[1], (byte) 0);
1613: Arrays.fill(tableData[2], (byte) 255);
1614: }
1615: switch (numColorBands) {
1616: case 3:
1617: tableData[2][transparentColor.getBlue()] = 0; // fall through
1618: case 2:
1619: tableData[1][transparentColor.getGreen()] = 0; // fall through
1620: case 1:
1621: tableData[0][transparentColor.getRed()] = 0; // fall through
1622: case 0:
1623: break;
1624: }
1625: }
1626: // Create a LookupTableJAI object to be used with the "lookup" operator.
1627: LookupTableJAI table = new LookupTableJAI(tableData);
1628: // Do the lookup operation.
1629: PlanarImage luImage = LookupDescriptor.create(image, table,
1630: hints);
1631: /*
1632: * Now that we have performed the lookup operation we have to remember
1633: * what we stated here above.
1634: *
1635: * If the input image is multibanded we will get a multiband image as
1636: * the output of the lookup operation hence we need to perform some form
1637: * of band combination to get the alpha band out of the lookup image.
1638: *
1639: * The way we wanted things to be done is by exploiting the clamping
1640: * behaviour that kicks in when we do sums and the like on pixels and
1641: * we overcome the maximum value allowed by the DataBufer DataType.
1642: */
1643: if (!singleStep) {
1644: // We simply add the three generated bands together in order to get the right.
1645: final double[][] matrix = new double[1][4];
1646: // Values at index 0,1,2 are set to 1.0, value at index 3 is left to 0.
1647: Arrays.fill(matrix[0], 0, 3, 1.0);
1648: luImage = BandCombineDescriptor.create(luImage, matrix,
1649: hints);
1650: if (transparentBand != null) {
1651: luImage = fork(luImage).binarize(254)
1652: .forceComponentColorModel().retainFirstBand()
1653: .getPlanarImage();
1654: luImage = MultiplyDescriptor.create(transparentBand,
1655: luImage, hints);
1656: }
1657: }
1658: image = BandMergeDescriptor.create(image, luImage, hints);
1659: invalidateStatistics();
1660: return this ;
1661: }
1662:
1663: /**
1664: * Inverts the pixel values of the {@linkplain #image}.
1665: *
1666: * @see InvertDescriptor
1667: */
1668: public final ImageWorker invert() {
1669: image = InvertDescriptor.create(image, getRenderingHints());
1670: invalidateStatistics();
1671: return this ;
1672: }
1673:
1674: /**
1675: * Applies the specified mask over the current {@linkplain #image}. The mask should be
1676: * {@linkplain #binarize() binarized} - if it is not, this method will do it itself.
1677: * Then, for every pixels in the mask with value equals to {@code maskValue}, the
1678: * corresponding pixel in the {@linkplain #image} will be set to the specified
1679: * {@code newValue}.
1680: * <p>
1681: * <strong>Note:</strong> current implementation force the color model to an
1682: * {@linkplain IndexColorModel indexed} one. Future versions may avoid this change.
1683: *
1684: * @param mask
1685: * The mask to apply, as a {@linkplain #binarize() binarized} image.
1686: * @param maskValue
1687: * The mask value to search for ({@code false} for 0 or {@code true} for 1).
1688: * @param newValue
1689: * The new value for every pixels in {@linkplain #image}
1690: * corresponding to {@code maskValue} in the mask.
1691: *
1692: * @return this {@link ImageWorker}.
1693: *
1694: * @todo This now should work only if {@code newValue} is 255
1695: * and {@code maskValue} is {@code false}.
1696: */
1697: public final ImageWorker mask(RenderedImage mask,
1698: final boolean maskValue, int newValue) {
1699: /*
1700: * Makes sure that the underlying image is indexed.
1701: */
1702: tileCacheEnabled(false);
1703: forceIndexColorModel(true);
1704: final RenderingHints hints = new RenderingHints(
1705: JAI.KEY_TILE_CACHE, null);
1706: /*
1707: * special case for newValue == 255 && !maskValue.
1708: */
1709: if (newValue == 255 && !maskValue) {
1710: /*
1711: * Build a lookup table in order to make the transparent pixels
1712: * equal to 255 and all the others equal to 0.
1713: */
1714: final byte[] lutData = new byte[256]; // Initially filled to 0.
1715: lutData[0] = (byte) 255; // for transparent pixels.
1716: final LookupTableJAI lut = new LookupTableJAI(lutData);
1717: mask = LookupDescriptor.create(mask, lut, hints);
1718: /*
1719: * Adding to the other image exploiting the implict clamping.
1720: */
1721: image = AddDescriptor.create(image, mask,
1722: getRenderingHints());
1723: tileCacheEnabled(true);
1724: invalidateStatistics();
1725: } else {
1726: // General case. It has to be binary
1727: if (!isBinary()) {
1728: binarize();
1729: }
1730: // Now if we mask with 1 we have to invert the mask.
1731: if (maskValue) {
1732: mask = NotDescriptor.create(mask, new RenderingHints(
1733: JAI.KEY_REPLACE_INDEX_COLOR_MODEL,
1734: Boolean.FALSE));
1735: }
1736: // And with the image to zero the interested pixels.
1737: tileCacheEnabled(false);
1738: image = AndDescriptor.create(mask, image,
1739: getRenderingHints());
1740:
1741: // Add the new value to the mask.
1742: mask = AddConstDescriptor.create(mask,
1743: new double[] { newValue }, new RenderingHints(
1744: JAI.KEY_REPLACE_INDEX_COLOR_MODEL,
1745: Boolean.FALSE));
1746:
1747: // Add the mask to the image to mask with the new value
1748: image = AddDescriptor.create(mask, image,
1749: getRenderingHints());
1750: tileCacheEnabled(true);
1751: invalidateStatistics();
1752: }
1753: return this ;
1754: }
1755:
1756: /**
1757: * Takes two rendered or renderable source images, and adds every pair of pixels, one from
1758: * each source image of the corresponding position and band. See JAI {@link AddDescriptor}
1759: * for details.
1760: *
1761: * @param renderedImage
1762: * the {@link RenderedImage} to be added to this {@link ImageWorker}.
1763: * @return this {@link ImageWorker}.
1764: *
1765: * @see AddDescriptor
1766: */
1767: public final ImageWorker addImage(final RenderedImage renderedImage) {
1768: image = AddDescriptor.create(image, renderedImage,
1769: getRenderingHints());
1770: invalidateStatistics();
1771: return this ;
1772: }
1773:
1774: /**
1775: * Takes one rendered or renderable image and an array of double constants, and multiplies
1776: * every pixel of the same band of the source by the constant from the corresponding array
1777: * entry. See JAI {@link MultiplyConstDescriptor} for details.
1778: *
1779: * @param inValues
1780: * The constants to be multiplied.
1781: * @return this {@link ImageWorker}.
1782: *
1783: * @see MultiplyConstDescriptor
1784: */
1785: public final ImageWorker multiplyConst(double[] inValues) {
1786: image = MultiplyConstDescriptor.create(image, inValues,
1787: getRenderingHints());
1788: invalidateStatistics();
1789: return this ;
1790: }
1791:
1792: /**
1793: * Takes one rendered or renderable image and an array of integer constants, and performs a
1794: * bit-wise logical "xor" between every pixel in the same band of the source and the constant
1795: * from the corresponding array entry. See JAI {@link XorConstDescriptor} for details.
1796: *
1797: * @see XorConstDescriptor
1798: */
1799: public final ImageWorker xorConst(int[] values) {
1800: image = XorConstDescriptor.create(image, values,
1801: getRenderingHints());
1802: invalidateStatistics();
1803: return this ;
1804: }
1805:
1806: /**
1807: * Adds transparency to a preexisting image whose color model is
1808: * {@linkplain IndexColorModel index color model}. For all pixels with the
1809: * value {@code false} in the specified transparency mask, the corresponding
1810: * pixel in the {@linkplain #image} is set to the transparent pixel value.
1811: * All other pixels are left unchanged.
1812: *
1813: * @param alphaChannel
1814: * The mask to apply as a {@linkplain #binarize() binarized} image.
1815: * @param errorDiffusion
1816: * Tells if I should use {@link ErrorDiffusionDescriptor} or
1817: * {@link OrderedDitherDescriptor} JAi operations.
1818: * @return this {@link ImageWorker}.
1819: *
1820: * @see #isTranslucent
1821: * @see #forceBitmaskIndexColorModel
1822: */
1823: public ImageWorker addTransparencyToIndexColorModel(
1824: final RenderedImage alphaChannel,
1825: final boolean errorDiffusion) {
1826: addTransparencyToIndexColorModel(alphaChannel, true,
1827: getTransparentPixel(), errorDiffusion);
1828: return this ;
1829: }
1830:
1831: /**
1832: * Adds transparency to a preexisting image whose color model is {@linkplain IndexColorModel
1833: * index color model}. First, this method creates a new index color model with the specified
1834: * {@code transparent} pixel, if needed (this method may skip this step if the specified pixel
1835: * is already transparent. Then for all pixels with the value {@code false} in the specified
1836: * transparency mask, the corresponding pixel in the {@linkplain #image} is set to that
1837: * transparent value. All other pixels are left unchanged.
1838: *
1839: * @param alphaChannel
1840: * The mask to apply as a {@linkplain #binarize() binarized} image.
1841: * @param translucent
1842: * {@code true} if {@linkplain Transparency#TRANSLUCENT translucent} images are
1843: * allowed, or {@code false} if the resulting images must be a
1844: * {@linkplain Transparency#BITMASK bitmask}.
1845: * @param transparent
1846: * The value for transparent pixels, to be given to every pixels in the
1847: * {@linkplain #image} corresponding to {@code false} in the mask. The
1848: * special value {@code -1} maps to the last pixel value allowed for the
1849: * {@linkplain IndexedColorModel indexed color model}.
1850: * @param errorDiffusion
1851: * Tells if I should use {@link ErrorDiffusionDescriptor} or
1852: * {@link OrderedDitherDescriptor} JAi operations.
1853: *
1854: * @return this {@link ImageWorker}.
1855: */
1856: public final ImageWorker addTransparencyToIndexColorModel(
1857: final RenderedImage alphaChannel,
1858: final boolean translucent, int transparent,
1859: final boolean errorDiffusion) {
1860: tileCacheEnabled(false);
1861: forceIndexColorModel(errorDiffusion);
1862: tileCacheEnabled(true);
1863: /*
1864: * Prepares hints and layout to use for mask operations. A color model
1865: * hint will be set only if the block below is executed.
1866: */
1867: final ImageWorker worker = fork(image);
1868: final RenderingHints hints = worker.getRenderingHints();
1869: /*
1870: * Gets the index color model. If the specified 'transparent' value is not fully
1871: * transparent, replaces the color model by a new one with the transparent pixel
1872: * defined. NOTE: the "transparent &= (1 << pixelSize) - 1" instruction below is
1873: * a safety for making sure that the transparent index value can hold in the amount
1874: * of bits allowed for this color model (the mapSize value may not use all bits).
1875: * It works as expected with the -1 special value. It also make sure that
1876: * "transparent + 1" do not exeed the maximum map size allowed.
1877: */
1878: final boolean forceBitmask;
1879: final IndexColorModel oldCM = (IndexColorModel) image
1880: .getColorModel();
1881: final int pixelSize = oldCM.getPixelSize();
1882: transparent &= (1 << pixelSize) - 1;
1883: forceBitmask = !translucent
1884: && oldCM.getTransparency() == Transparency.TRANSLUCENT;
1885: if (forceBitmask || oldCM.getTransparentPixel() != transparent) {
1886: final int mapSize = Math.max(oldCM.getMapSize(),
1887: transparent + 1);
1888: final byte[][] RGBA = new byte[translucent ? 4 : 3][mapSize];
1889: // Note: we might use less that 256 values.
1890: oldCM.getReds(RGBA[0]);
1891: oldCM.getGreens(RGBA[1]);
1892: oldCM.getBlues(RGBA[2]);
1893: final IndexColorModel newCM;
1894: if (translucent) {
1895: oldCM.getAlphas(RGBA[3]);
1896: RGBA[3][transparent] = 0;
1897: newCM = new IndexColorModel(pixelSize, mapSize,
1898: RGBA[0], RGBA[1], RGBA[2], RGBA[3]);
1899: } else {
1900: newCM = new IndexColorModel(pixelSize, mapSize,
1901: RGBA[0], RGBA[1], RGBA[2], transparent);
1902: }
1903: /*
1904: * Set the color model hint.
1905: */
1906: final ImageLayout layout = getImageLayout(hints);
1907: layout.setColorModel(newCM);
1908: worker.setRenderingHint(JAI.KEY_IMAGE_LAYOUT, layout);
1909: }
1910: /*
1911: * Applies the mask, maybe with a color model change.
1912: */
1913: worker.setRenderingHint(JAI.KEY_REPLACE_INDEX_COLOR_MODEL,
1914: Boolean.FALSE);
1915: worker.mask(alphaChannel, false, transparent);
1916: image = worker.image;
1917: invalidateStatistics();
1918:
1919: // All post conditions for this method contract.
1920: assert isIndexed();
1921: assert translucent || !isTranslucent() : translucent;
1922: assert ((IndexColorModel) image.getColorModel())
1923: .getAlpha(transparent) == 0;
1924: return this ;
1925: }
1926:
1927: /**
1928: * If the was not already tiled, tile it. Note that no tiling will be done
1929: * if 'getRenderingHints()' failed to suggest a tile size. This method is
1930: * for internal use by {@link #write} methods only.
1931: *
1932: * @return this {@link ImageWorker}.
1933: */
1934: public final ImageWorker tile() {
1935: final RenderingHints hints = getRenderingHints();
1936: final ImageLayout layout = getImageLayout(hints);
1937: if (layout.isValid(ImageLayout.TILE_WIDTH_MASK)
1938: || layout.isValid(ImageLayout.TILE_HEIGHT_MASK)) {
1939: final int type = image.getSampleModel().getDataType();
1940: image = FormatDescriptor.create(image, new Integer(type),
1941: hints);
1942: }
1943: return this ;
1944: }
1945:
1946: /**
1947: * Writes the {@linkplain #image} to the specified file. This method differs
1948: * from {@link ImageIO#write(String,File)} in a number of ways:
1949: * <p>
1950: * <ul>
1951: * <li>The {@linkplain ImageWriter image writer} to use is inferred from the file
1952: * extension.</li>
1953: * <li>If the image writer accepts {@link File} objects as input, then the {@code file}
1954: * argument is given directly without creating an {@link ImageOutputStream} object.
1955: * This is important for some formats like HDF, which work <em>only</em> with files.</li>
1956: * <li>If the {@linkplain #image} is not tiled, then it is tiled prior to be written.</li>
1957: * <li>If some special processing is needed for a given format, then the corresponding method
1958: * is invoked. Example: {@link #forceIndexColorModelForGIF}.</li>
1959: * </ul>
1960: *
1961: * @return this {@link ImageWorker}.
1962: */
1963: public final ImageWorker write(final File output)
1964: throws IOException {
1965: final String filename = output.getName();
1966: final int dot = filename.lastIndexOf('.');
1967: if (dot < 0) {
1968: throw new IIOException(Errors
1969: .format(ErrorKeys.NO_IMAGE_WRITER));
1970: }
1971: final String extension = filename.substring(dot + 1).trim();
1972: write(output, ImageIO.getImageWritersBySuffix(extension));
1973: return this ;
1974: }
1975:
1976: /**
1977: * Writes outs the image contained into this {@link ImageWorker} as a PNG
1978: * using the provided destination, compression and compression rate.
1979: * <p>
1980: * The destination object can be anything providing that we have an
1981: * {@link ImageOutputStreamSpi} that recognizes it.
1982: *
1983: * @param destination
1984: * where to write the internal {@link #image} as a PNG.
1985: * @param compression
1986: * algorithm.
1987: * @param compressionRate
1988: * percentage of compression.
1989: * @param nativeAcc
1990: * should we use native acceleration.
1991: * @param paletted
1992: * should we write the png as 8 bits?
1993: * @return this {@link ImageWorker}.
1994: * @throws IOException
1995: * In case an error occurs during the search for an
1996: * {@link ImageOutputStream} or during the eoncding process.
1997: *
1998: * @todo Current code doesn't check if the writer already accepts the provided destination.
1999: * It wraps it in a {@link ImageOutputStream} inconditionnaly.
2000: */
2001: public final void writePNG(final Object destination,
2002: final String compression, final float compressionRate,
2003: final boolean nativeAcc, final boolean paletted)
2004: throws IOException {
2005: // Reformatting this image for PNG.
2006: tileCacheEnabled(false);
2007: if (!paletted) {
2008: forceComponentColorModel();
2009: } else {
2010: forceIndexColorModelForGIF(true);
2011: }
2012: LOGGER.finer("Encoded input image for png writer");
2013:
2014: // Getting a writer.
2015: LOGGER.finer("Getting a writer");
2016: final Iterator it = ImageIO.getImageWritersByFormatName("PNG");
2017: if (!it.hasNext()) {
2018: throw new IllegalStateException(Errors
2019: .format(ErrorKeys.NO_IMAGE_WRITER));
2020: }
2021: ImageWriter writer = (ImageWriter) it.next();
2022:
2023: // Getting a stream.
2024: LOGGER.finer("Setting write parameters for this writer");
2025: ImageWriteParam iwp = null;
2026: final ImageOutputStream memOutStream = ImageIO
2027: .createImageOutputStream(destination);
2028: if (nativeAcc
2029: && writer
2030: .getClass()
2031: .getName()
2032: .equals(
2033: "com.sun.media.imageioimpl.plugins.png.CLibPNGImageWriter")) {
2034: // Compressing with native.
2035: LOGGER.finer("Writer is native");
2036: iwp = writer.getDefaultWriteParam();
2037: // Define compression mode
2038: iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
2039: // best compression
2040: iwp.setCompressionType(compression);
2041: // we can control quality here
2042: iwp.setCompressionQuality(compressionRate);
2043: // destination image type
2044: iwp.setDestinationType(new ImageTypeSpecifier(image
2045: .getColorModel(), image.getSampleModel()));
2046: } else {
2047: // Compressing with pure Java.
2048: // pure java from native
2049: if (nativeAcc
2050: && it.hasNext()
2051: && writer
2052: .getClass()
2053: .getName()
2054: .equals(
2055: "com.sun.media.imageioimpl.plugins.png.CLibPNGImageWriter")) {
2056: writer = (ImageWriter) it.next();
2057: }
2058: LOGGER.finer("Writer is NOT native");
2059: // Instantiating PNGImageWriteParam
2060: iwp = new PNGImageWriteParam();
2061: // Define compression mode
2062: iwp.setCompressionMode(ImageWriteParam.MODE_DEFAULT);
2063: }
2064: LOGGER.finer("About to write png image");
2065: writer.setOutput(memOutStream);
2066: writer.write(null, new IIOImage(image, null, null), iwp);
2067: tileCacheEnabled(true);
2068: memOutStream.flush();
2069: writer.dispose();
2070: memOutStream.close();
2071: }
2072:
2073: /**
2074: * Writes outs the image contained into this {@link ImageWorker} as a GIF
2075: * using the provided destination, compression and compression rate.
2076: * <p>
2077: * It is worth to point out that the only compressions algorithm availaible
2078: * with the jdk {@link GIFImageWriter} is "LZW" while the compression rates
2079: * have to be confined between 0 and 1. AN acceptable values is usally 0.75f.
2080: * <p>
2081: * The destination object can be anything providing that we have an
2082: * {@link ImageOutputStreamSpi} that recognizes it.
2083: *
2084: * @param destination
2085: * where to write the internal {@link #image} as a gif.
2086: * @param compression
2087: * The name of compression algorithm.
2088: * @param compressionRate
2089: * percentage of compression, as a number between 0 and 1.
2090: * @return this {@link ImageWorker}.
2091: * @throws IOException
2092: * In case an error occurs during the search for an
2093: * {@link ImageOutputStream} or during the eoncding process.
2094: *
2095: * @see #forceIndexColorModelForGIF(boolean)
2096: */
2097: public final ImageWorker writeGIF(final Object destination,
2098: final String compression, final float compressionRate)
2099: throws IOException {
2100: tileCacheEnabled(false);
2101: forceIndexColorModelForGIF(true);
2102: tileCacheEnabled(true);
2103: final IIORegistry registry = IIORegistry.getDefaultInstance();
2104: Iterator it = registry.getServiceProviders(
2105: ImageWriterSpi.class, true);
2106: ImageWriterSpi spi = null;
2107: while (it.hasNext()) {
2108: final ImageWriterSpi candidate = (ImageWriterSpi) it.next();
2109: if (containsFormatName(candidate.getFormatNames(), "gif")) {
2110: if (spi == null) {
2111: spi = candidate;
2112: } else {
2113: final String name = candidate.getClass().getName();
2114: if (name
2115: .equals("com.sun.media.imageioimpl.plugins.gif.GIFImageWriterSpi")) {
2116: spi = candidate;
2117: break;
2118: }
2119: }
2120: }
2121: }
2122: if (spi == null) {
2123: throw new IIOException(Errors
2124: .format(ErrorKeys.NO_IMAGE_WRITER));
2125: }
2126: final ImageOutputStream stream = ImageIO
2127: .createImageOutputStream(destination);
2128: final ImageWriter writer = spi.createWriterInstance();
2129: final ImageWriteParam param = writer.getDefaultWriteParam();
2130: param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
2131: param.setCompressionType(compression);
2132: param.setCompressionQuality(compressionRate);
2133:
2134: writer.setOutput(stream);
2135: writer.write(null, new IIOImage(image, null, null), param);
2136: stream.close();
2137: writer.dispose();
2138: return this ;
2139: }
2140:
2141: /**
2142: * Writes outs the image contained into this {@link ImageWorker} as a JPEG
2143: * using the provided destination , compression and compression rate.
2144: * <p>
2145: * The destination object can be anything providing that we have an
2146: * {@link ImageOutputStreamSpi} that recognizes it.
2147: *
2148: * @param destination
2149: * where to write the internal {@link #image} as a JPEG.
2150: * @param compression
2151: * algorithm.
2152: * @param compressionRate
2153: * percentage of compression.
2154: * @param nativeAcc
2155: * should we use native acceleration.
2156: * @return this {@link ImageWorker}.
2157: * @throws IOException
2158: * In case an error occurs during the search for an
2159: * {@link ImageOutputStream} or during the eoncding process.
2160: */
2161: public final void writeJPEG(final Object destination,
2162: final String compression, final float compressionRate,
2163: final boolean nativeAcc) throws IOException {
2164: // Reformatting this image for jpeg.
2165: LOGGER.finer("Encoding input image to write out as JPEG.");
2166: tileCacheEnabled(false);
2167: final ColorModel cm = image.getColorModel();
2168: final boolean indexColorModel = image.getColorModel() instanceof IndexColorModel;
2169: final int numBands = image.getSampleModel().getNumBands();
2170: final boolean hasAlpha = cm.hasAlpha();
2171: if (indexColorModel || hasAlpha) {
2172: if (indexColorModel) {
2173: forceComponentColorModel();
2174: }
2175: if (hasAlpha) {
2176: retainBands(numBands - 1);
2177: }
2178: }
2179: tileCacheEnabled(true);
2180:
2181: // Getting a writer.
2182: LOGGER.finer("Getting a JPEG writer and configuring it.");
2183: final Iterator it = ImageIO.getImageWritersByFormatName("JPEG");
2184: if (!it.hasNext()) {
2185: throw new IllegalStateException(Errors
2186: .format(ErrorKeys.NO_IMAGE_WRITER));
2187: }
2188: ImageWriter writer = (ImageWriter) it.next();
2189: if (!nativeAcc
2190: && writer
2191: .getClass()
2192: .getName()
2193: .equals(
2194: "com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriter")) {
2195: writer = (ImageWriter) it.next();
2196: }
2197:
2198: // Compression is available on both lib
2199: final ImageWriteParam iwp = writer.getDefaultWriteParam();
2200: final ImageOutputStream outStream = ImageIO
2201: .createImageOutputStream(destination);
2202: iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
2203: iwp.setCompressionType(compression); // Lossy compression.
2204: iwp.setCompressionQuality(compressionRate); // We can control quality here.
2205: writer.setOutput(outStream);
2206: if (iwp instanceof JPEGImageWriteParam) {
2207: final JPEGImageWriteParam param = (JPEGImageWriteParam) iwp;
2208: param.setOptimizeHuffmanTables(true);
2209: try {
2210: param
2211: .setProgressiveMode(JPEGImageWriteParam.MODE_DEFAULT);
2212: } catch (UnsupportedOperationException e) {
2213: throw (IOException) new IOException().initCause(e);
2214: // TODO: inline cause when we will be allowed to target Java 6.
2215: }
2216: }
2217: LOGGER.finer("Writing out...");
2218: writer.write(null, new IIOImage(image, null, null), iwp);
2219: outStream.flush();
2220: writer.dispose();
2221: outStream.close();
2222: }
2223:
2224: /**
2225: * Writes the {@linkplain #image} to the specified output, trying all
2226: * encoders in the specified iterator in the iteration order.
2227: *
2228: * @return this {@link ImageWorker}.
2229: */
2230: private ImageWorker write(final Object output,
2231: final Iterator encoders) throws IOException {
2232: if (encoders != null) {
2233: while (encoders.hasNext()) {
2234: final ImageWriter writer = (ImageWriter) encoders
2235: .next();
2236: final ImageWriterSpi spi = writer
2237: .getOriginatingProvider();
2238: final Class[] outputTypes;
2239: if (spi == null) {
2240: outputTypes = ImageWriterSpi.STANDARD_OUTPUT_TYPE;
2241: } else {
2242: /*
2243: * If the encoder is for some format handled in a special way (e.g. GIF), apply
2244: * the required operation. Note that invoking the same method many time (e.g.
2245: * "forceIndexColorModelForGIF", which could occurs if there is more than one
2246: * GIF encoder registered) should not hurt - all method invocation after the
2247: * first one should be no-op.
2248: */
2249: final String[] formats = spi.getFormatNames();
2250: if (containsFormatName(formats, "gif")) {
2251: forceIndexColorModelForGIF(true);
2252: } else {
2253: tile();
2254: }
2255: if (!spi.canEncodeImage(image)) {
2256: continue;
2257: }
2258: outputTypes = spi.getOutputTypes();
2259: }
2260: /*
2261: * Now try to set the output directly (if possible), or as an ImageOutputStream if
2262: * the encoder doesn't accept directly the specified output. Note that some formats
2263: * like HDF may not support ImageOutputStream.
2264: */
2265: final ImageOutputStream stream;
2266: if (acceptInputType(outputTypes, output.getClass())) {
2267: writer.setOutput(output);
2268: stream = null;
2269: } else if (acceptInputType(outputTypes,
2270: ImageOutputStream.class)) {
2271: stream = ImageIO.createImageOutputStream(output);
2272: writer.setOutput(stream);
2273: } else {
2274: continue;
2275: }
2276: /*
2277: * Now saves the image.
2278: */
2279: writer.write(image);
2280: writer.dispose();
2281: if (stream != null) {
2282: stream.close();
2283: }
2284: return this ;
2285: }
2286: }
2287: throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_WRITER));
2288: }
2289:
2290: /**
2291: * Returns {@code true} if the specified array contains the specified type.
2292: */
2293: private static boolean acceptInputType(final Class[] types,
2294: final Class searchFor) {
2295: for (int i = types.length; --i >= 0;) {
2296: if (searchFor.isAssignableFrom(types[i])) {
2297: return true;
2298: }
2299: }
2300: return false;
2301: }
2302:
2303: /**
2304: * Returns {@code true} if the specified array contains the specified string.
2305: */
2306: private static boolean containsFormatName(final String[] formats,
2307: final String searchFor) {
2308: for (int i = formats.length; --i >= 0;) {
2309: if (searchFor.equalsIgnoreCase(formats[i])) {
2310: return true;
2311: }
2312: }
2313: return false;
2314: }
2315:
2316: ///////////////////////////////////////////////////////////////////////////////////////
2317: //////// ////////
2318: //////// DEBUGING HELP ////////
2319: //////// ////////
2320: ///////////////////////////////////////////////////////////////////////////////////////
2321:
2322: /**
2323: * Shows the current {@linkplain #image} in a window together with the operation chain as a
2324: * {@linkplain javax.swing.JTree tree}. This method is provided mostly for debugging purpose.
2325: * This method requires the {@code gt2-widgets-swing.jar} file in the classpath.
2326: *
2327: * @throws HeadlessException
2328: * if {@code gt2-widgets-swing.jar} is not on the classpath, or
2329: * if AWT can't create the window components.
2330: * @return this {@link ImageWorker}.
2331: *
2332: * @see org.geotools.gui.swing.image.OperationTreeBrowser#show(RenderedImage)
2333: */
2334: public final ImageWorker show() throws HeadlessException {
2335: /*
2336: * Uses reflection because the "gt2-widgets-swing.jar" dependency is optional and may not
2337: * be available in the classpath. All the complicated stuff below is simply doing this call:
2338: *
2339: * OperationTreeBrowser.show(image);
2340: *
2341: * Tip: The @see tag in the above javadoc can be used as a check for the existence
2342: * of class and method referenced below. Check for the javadoc warnings.
2343: */
2344: final Class c;
2345: try {
2346: c = Class
2347: .forName("org.geotools.gui.swing.image.OperationTreeBrowser");
2348: } catch (ClassNotFoundException cause) {
2349: final HeadlessException e;
2350: e = new HeadlessException(
2351: "The \"gt2-widgets-swing.jar\" file is required.");
2352: e.initCause(cause);
2353: throw e;
2354: }
2355: try {
2356: c.getMethod("show", new Class[] { RenderedImage.class })
2357: .invoke(null, new Object[] { image });
2358: } catch (InvocationTargetException e) {
2359: final Throwable cause = e.getCause();
2360: if (cause instanceof RuntimeException) {
2361: throw (RuntimeException) cause;
2362: }
2363: if (cause instanceof Error) {
2364: throw (Error) cause;
2365: }
2366: throw new AssertionError(e);
2367: } catch (Exception e) {
2368: /*
2369: * ClassNotFoundException may be expected, but all other kinds of
2370: * checked exceptions (and they are numerous...) are errors.
2371: */
2372: throw new AssertionError(e);
2373: }
2374: return this ;
2375: }
2376:
2377: /**
2378: * Loads the image from the specified file, and {@linkplain #show display}
2379: * it in a window. This method is mostly as a convenient way to test
2380: * operation chains. This method can be invoked from the command line. If an
2381: * optional {@code -operation} argument is provided, the Java method (one of
2382: * the image operations provided in this class) immediately following it is
2383: * executed. Example:
2384: *
2385: * <blockquote><pre>
2386: * java org.geotools.image.ImageWorker -operation binarize <var><filename></var>
2387: * </pre></blockquote>
2388: */
2389: public static void main(String[] args) {
2390: final Arguments arguments = new Arguments(args);
2391: final String operation = arguments
2392: .getOptionalString("-operation");
2393: args = arguments.getRemainingArguments(1);
2394: if (args.length != 0)
2395: try {
2396: final ImageWorker worker = new ImageWorker(new File(
2397: args[0]));
2398: // Force usage of tile cache for every operations, including intermediate steps.
2399: worker.setRenderingHint(JAI.KEY_TILE_CACHE, JAI
2400: .getDefaultInstance().getTileCache());
2401: if (operation != null) {
2402: worker.getClass().getMethod(operation,
2403: (Class[]) null).invoke(worker,
2404: (Object[]) null);
2405: }
2406: /*
2407: * TIP: Tests operations here (before the call to 'show()'), if wanted.
2408: */
2409: worker.show();
2410: } catch (FileNotFoundException e) {
2411: arguments.printSummary(e);
2412: } catch (NoSuchMethodException e) {
2413: arguments.printSummary(e);
2414: } catch (Exception e) {
2415: e.printStackTrace(arguments.err);
2416: }
2417: }
2418: }
|