001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ---------------
028 * PaintAlpha.java
029 * ---------------
030 * (C) Copyright 2011-present, by DaveLaw and Contributors.
031 *
032 * Original Author:  DaveLaw (dave ATT davelaw D0TT de);
033 * Contributor(s):   David Gilbert;
034 *
035 */
036
037package org.jfree.chart.util;
038
039import java.awt.Color;
040import java.awt.GradientPaint;
041import java.awt.LinearGradientPaint;
042import java.awt.Paint;
043import java.awt.RadialGradientPaint;
044import java.awt.TexturePaint;
045import java.awt.image.BufferedImage;
046import java.awt.image.IndexColorModel;
047import java.awt.image.WritableRaster;
048import java.util.Hashtable;
049
050/**
051 * This class contains static methods for the manipulation
052 * of objects of type {@code Paint}
053 * <p>
054 * The intention is to honour the alpha-channel in the process.
055 * {@code PaintAlpha} was originally conceived to improve the
056 * rendering of 3D Shapes with transparent colours and to allow
057 * invisible bars by making them completely transparent.
058 * <p>
059 * Previously {@link Color#darker()} was used for this,
060 * which always returns an opaque colour.
061 * <p>
062 * Additionally there are methods to control the behaviour and
063 * in particular a {@link PaintAlpha#cloneImage(BufferedImage) cloneImage(..)}
064 * method which is needed to darken objects of type {@link TexturePaint}.
065 *
066 * @author  DaveLaw
067 */
068public class PaintAlpha {
069    // TODO Revert to SVN revision 2469 in JFreeChart 1.0.16
070    //      (MultipleGradientPaint's / JDK issues)
071    // TODO THEN: change visibility of ALL darker(...) Methods EXCEPT
072    //      darker(Paint) to private!
073
074    /**
075     * Multiplier for the {@code darker} Methods.<br>
076     * (taken from {@link java.awt.Color}.FACTOR)
077     */
078    private static final double FACTOR = 0.7;
079
080    private static boolean legacyAlpha = false;
081
082    /**
083     * Per default {@code PaintAlpha} will try to honour alpha-channel
084     * information.  In the past this was not the case.
085     * If you wish legacy functionality for your application you can request
086     * this here.
087     *
088     * @param legacyAlpha boolean
089     *
090     * @return the previous setting
091     */
092    public static boolean setLegacyAlpha(boolean legacyAlpha) {
093        boolean old = PaintAlpha.legacyAlpha;
094        PaintAlpha.legacyAlpha = legacyAlpha;
095        return old;
096    }
097
098    /**
099     * Create a new (if possible, darker) {@code Paint} of the same Type.
100     * If the Type is not supported, the original {@code Paint} is returned.
101     * <p>
102     * @param paint a {@code Paint} implementation
103     * (e.g. {@link Color}, {@link GradientPaint}, {@link TexturePaint},..)
104     * <p>
105     * @return a (usually new, see above) {@code Paint}
106     */
107    public static Paint darker(Paint paint) {
108
109        if (paint instanceof Color) {
110            return darker((Color) paint);
111        }
112        if (legacyAlpha) {
113            /*
114             * Legacy? Just return the original Paint.
115             * (this corresponds EXACTLY to how Paints used to be darkened)
116             */
117            return paint;
118        }
119        if (paint instanceof GradientPaint) {
120            return darker((GradientPaint) paint);
121        }
122        if (paint instanceof LinearGradientPaint) {
123            return darkerLinearGradientPaint((LinearGradientPaint) paint);
124        }
125        if (paint instanceof RadialGradientPaint) {
126            return darkerRadialGradientPaint((RadialGradientPaint) paint);
127        }
128        if (paint instanceof TexturePaint) {
129            try {
130                return darkerTexturePaint((TexturePaint) paint);
131            }
132            catch (Exception e) {
133                /*
134                 * Lots can go wrong while fiddling with Images, Color Models
135                 * & such!  If anything at all goes awry, just return the original
136                 * TexturePaint.  (TexturePaint's are immutable anyway, so no harm
137                 * done)
138                 */
139                return paint;
140            }
141        }
142        return paint;
143    }
144
145    /**
146     * Similar to {@link Color#darker()}.
147     * <p>
148     * The essential difference is that this method
149     * maintains the alpha-channel unchanged<br>
150     *
151     * @param paint a {@code Color}
152     *
153     * @return a darker version of the {@code Color}
154     */
155    private static Color darker(Color paint) {
156        return new Color(
157                (int)(paint.getRed  () * FACTOR),
158                (int)(paint.getGreen() * FACTOR),
159                (int)(paint.getBlue () * FACTOR), paint.getAlpha());
160    }
161
162    /**
163     * Create a new {@code GradientPaint} with its colors darkened.
164     *
165     * @param paint  the gradient paint ({@code null} not permitted).
166     *
167     * @return a darker version of the {@code GradientPaint}
168     */
169    private static GradientPaint darker(GradientPaint paint) {
170        return new GradientPaint(
171                paint.getPoint1(), darker(paint.getColor1()),
172                paint.getPoint2(), darker(paint.getColor2()),
173                paint.isCyclic());
174    }
175
176    /**
177     * Create a new Gradient with its colours darkened.
178     *
179     * @param paint a {@code LinearGradientPaint}
180     *
181     * @return a darker version of the {@code LinearGradientPaint}
182     */
183    private static Paint darkerLinearGradientPaint(LinearGradientPaint paint) {
184        final Color[] paintColors = paint.getColors();
185        for (int i = 0; i < paintColors.length; i++) {
186            paintColors[i] = darker(paintColors[i]);
187        }
188        return new LinearGradientPaint(paint.getStartPoint(),
189                paint.getEndPoint(), paint.getFractions(), paintColors,
190                paint.getCycleMethod(), paint.getColorSpace(), 
191                paint.getTransform());
192    }
193
194    /**
195     * Create a new Gradient with its colours darkened.
196     *
197     * @param paint a {@code RadialGradientPaint}
198     *
199     * @return a darker version of the {@code RadialGradientPaint}
200     */
201    private static Paint darkerRadialGradientPaint(RadialGradientPaint paint) {
202        final Color[] paintColors = paint.getColors();
203        for (int i = 0; i < paintColors.length; i++) {
204            paintColors[i] = darker(paintColors[i]);
205        }
206        return new RadialGradientPaint(paint.getCenterPoint(), 
207                paint.getRadius(), paint.getFocusPoint(), 
208                paint.getFractions(), paintColors, paint.getCycleMethod(),
209                paint.getColorSpace(), paint.getTransform());
210    }
211
212    /**
213     * Create a new {@code TexturePaint} with its colors darkened.
214     * <p>
215     * This entails cloning the underlying {@code BufferedImage},
216     * then darkening each color-pixel individually!
217     *
218     * @param paint a {@code TexturePaint}
219     *
220     * @return a darker version of the {@code TexturePaint}
221     */
222    private static TexturePaint darkerTexturePaint(TexturePaint paint) {
223        /**
224         * Color Models with pre-multiplied Alpha tested OK without any
225         * special logic
226         *
227         * BufferedImage.TYPE_INT_ARGB_PRE:    // Type 03: tested OK 2011.02.27
228         * BufferedImage.TYPE_4BYTE_ABGR_PRE:  // Type 07: tested OK 2011.02.27
229         */
230        if (paint.getImage().getColorModel().isAlphaPremultiplied()) {
231            /* Placeholder */
232        }
233
234        BufferedImage img = cloneImage(paint.getImage());
235
236        WritableRaster ras = img.copyData(null);
237
238        final int miX = ras.getMinX();
239        final int miY = ras.getMinY();
240        final int maY = ras.getMinY() + ras.getHeight();
241
242        final int   wid = ras.getWidth();
243
244        /**/  int[] pix = new int[wid * img.getSampleModel().getNumBands()];
245        /* (pix-buffer is large enough for all pixels of one row) */
246
247        /**
248         * Indexed Color Models (sort of a Palette) CANNOT be simply
249         * multiplied (the pixel-value is just an index into the Palette).
250         *
251         * Fortunately, IndexColorModel.getComponents(..) resolves the colors.
252         * The resolved colors can then be multiplied by our FACTOR.
253         * IndexColorModel.getDataElement(..) then tries to map the computed
254         * color to the "nearest" in the Palette.
255         *
256         * It is quite possible that the "nearest" color is the ORIGINAL
257         * color!  In the worst case, the returned Image will be identical to
258         * the original.
259         *
260         * Applies to following Image Types:
261         *
262         * BufferedImage.TYPE_BYTE_BINARY:    // Type 12: tested OK 2011.02.27
263         * BufferedImage.TYPE_BYTE_INDEXED:   // Type 13: tested OK 2011.02.27
264         */
265        if (img.getColorModel() instanceof IndexColorModel) {
266
267            int[] nco = new int[4]; // RGB (+ optional Alpha which we leave
268                                    // unchanged)
269
270            for (int y = miY; y < maY; y++)  {
271
272                pix = ras.getPixels(miX, y, wid, 1, pix);
273
274                for (int p = 0; p < pix.length; p++) {
275                    nco    =  img.getColorModel().getComponents(pix[p], nco, 0);
276                    nco[0] *= FACTOR; // Red
277                    nco[1] *= FACTOR; // Green
278                    nco[2] *= FACTOR; // Blue. Now map computed colour to
279                                      // nearest in Palette...
280                    pix[p] = img.getColorModel().getDataElement(nco, 0);
281                }
282                /**/ ras.setPixels(miX, y, wid, 1, pix);
283            }
284            img.setData(ras);
285
286            return new TexturePaint(img, paint.getAnchorRect());
287        }
288
289        /**
290         * For the other 2 Color Models, java.awt.image.ComponentColorModel and
291         * java.awt.image.DirectColorModel, the order of subpixels returned by
292         * ras.getPixels(..) was observed to correspond to the following...
293         */
294        if (img.getSampleModel().getNumBands() == 4) {
295            /**
296             * The following Image Types have an Alpha-channel which we will
297             * leave unchanged:
298             *
299             * BufferedImage.TYPE_INT_ARGB:   // Type 02: tested OK 2011.02.27
300             * BufferedImage.TYPE_4BYTE_ABGR: // Type 06: tested OK 2011.02.27
301             */
302            for (int y = miY; y < maY; y++)  {
303
304                pix = ras.getPixels(miX, y, wid, 1, pix);
305
306                for (int p = 0; p < pix.length;) {
307                    pix[p] = (int)(pix[p++] * FACTOR); // Red
308                    pix[p] = (int)(pix[p++] * FACTOR); // Green
309                    pix[p] = (int)(pix[p++] * FACTOR); // Blue
310                    /* Ignore alpha-channel -> */p++;
311                }
312                /**/  ras.setPixels(miX, y, wid, 1, pix);
313            }
314            img.setData(ras);
315            return new TexturePaint(img, paint.getAnchorRect());
316        } else {
317            for (int y = miY; y < maY; y++)  {
318
319                pix = ras.getPixels(miX, y, wid, 1, pix);
320
321                for (int p = 0; p < pix.length; p++) {
322                    pix[p] = (int)(pix[p] * FACTOR);
323                }
324                /**/  ras.setPixels(miX, y, wid, 1, pix);
325            }
326            img.setData(ras);
327            return new TexturePaint(img, paint.getAnchorRect());
328            /**
329             * Above, we multiplied every pixel by our FACTOR because the
330             * applicable Image Types consist only of color or grey channels:
331             *
332             * BufferedImage.TYPE_INT_RGB:        // Type 01: tested OK 2011.02.27
333             * BufferedImage.TYPE_INT_BGR:        // Type 04: tested OK 2011.02.27
334             * BufferedImage.TYPE_3BYTE_BGR:      // Type 05: tested OK 2011.02.27
335             * BufferedImage.TYPE_BYTE_GRAY:      // Type 10: tested OK 2011.02.27
336             * BufferedImage.TYPE_USHORT_GRAY:    // Type 11: tested OK 2011.02.27
337             * BufferedImage.TYPE_USHORT_565_RGB: // Type 08: tested OK 2011.02.27
338             * BufferedImage.TYPE_USHORT_555_RGB: // Type 09: tested OK 2011.02.27
339             *
340             * Note: as ras.getPixels(..) returned colours in the order R, G, B, A (optional)
341             * for both TYPE_4BYTE_ABGR & TYPE_3BYTE_BGR,
342             * it is assumed that TYPE_INT_BGR will behave similarly.
343             */
344        }
345    }
346
347    /**
348     * Clone a {@link BufferedImage}.
349     * <p>
350     * Note: when constructing the clone, the original Color Model Object is
351     * reused.<br>  That keeps things simple and should not be a problem, as all
352     * known Color Models<br>
353     * ({@link java.awt.image.IndexColorModel     IndexColorModel},
354     *  {@link java.awt.image.DirectColorModel    DirectColorModel},
355     *  {@link java.awt.image.ComponentColorModel ComponentColorModel}) are
356     * immutable.
357     *
358     * @param image original BufferedImage to clone
359     *
360     * @return a new BufferedImage reusing the original's Color Model and
361     *         containing a clone of its pixels
362     */
363    public static BufferedImage cloneImage(BufferedImage image) {
364
365        WritableRaster rin = image.getRaster();
366        WritableRaster ras = rin.createCompatibleWritableRaster();
367        /**/ ras.setRect(rin); // <- this is the code that actually COPIES the pixels
368
369        /*
370         * Buffered Images may have properties, but NEVER disclose them!
371         * Nevertheless, just in case someone implements getPropertyNames()
372         * one day...
373         */
374        Hashtable props = null;
375        String[] propNames = image.getPropertyNames();
376        if (propNames != null) { // ALWAYS null
377            props = new Hashtable();
378            for (int i = 0; i < propNames.length; i++) {
379                props.put(propNames[i], image.getProperty(propNames[i]));
380            }
381        }
382        return new BufferedImage(image.getColorModel(), ras,
383                image.isAlphaPremultiplied(), props);
384    }
385}