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