001/* ======================================================
002 * JFreeChart : a chart library for the Java(tm) platform
003 * ======================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  https://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    private PaintAlpha() {
083        // no requirement to instantiate
084    }
085
086    /**
087     * Per default {@code PaintAlpha} will try to honour alpha-channel
088     * information.  In the past this was not the case.
089     * If you wish legacy functionality for your application you can request
090     * this here.
091     *
092     * @param legacyAlpha boolean
093     *
094     * @return the previous setting
095     */
096    public static boolean setLegacyAlpha(boolean legacyAlpha) {
097        boolean old = PaintAlpha.legacyAlpha;
098        PaintAlpha.legacyAlpha = legacyAlpha;
099        return old;
100    }
101
102    /**
103     * Create a new (if possible, darker) {@code Paint} of the same Type.
104     * If the Type is not supported, the original {@code Paint} is returned.
105     *
106     * @param paint a {@code Paint} implementation
107     * (e.g. {@link Color}, {@link GradientPaint}, {@link TexturePaint},..)
108     *
109     * @return a (usually new, see above) {@code Paint}
110     */
111    public static Paint darker(Paint paint) {
112
113        if (paint instanceof Color) {
114            return darker((Color) paint);
115        }
116        if (legacyAlpha) {
117            /*
118             * Legacy? Just return the original Paint.
119             * (this corresponds EXACTLY to how Paints used to be darkened)
120             */
121            return paint;
122        }
123        if (paint instanceof GradientPaint) {
124            return darker((GradientPaint) paint);
125        }
126        if (paint instanceof LinearGradientPaint) {
127            return darkerLinearGradientPaint((LinearGradientPaint) paint);
128        }
129        if (paint instanceof RadialGradientPaint) {
130            return darkerRadialGradientPaint((RadialGradientPaint) paint);
131        }
132        if (paint instanceof TexturePaint) {
133            try {
134                return darkerTexturePaint((TexturePaint) paint);
135            }
136            catch (Exception e) {
137                /*
138                 * Lots can go wrong while fiddling with Images, Color Models
139                 * & such!  If anything at all goes awry, just return the original
140                 * TexturePaint.  (TexturePaint's are immutable anyway, so no harm
141                 * done)
142                 */
143                return paint;
144            }
145        }
146        return paint;
147    }
148
149    /**
150     * Similar to {@link Color#darker()}.
151     * <p>
152     * The essential difference is that this method
153     * maintains the alpha-channel unchanged<br>
154     *
155     * @param paint a {@code Color}
156     *
157     * @return a darker version of the {@code Color}
158     */
159    private static Color darker(Color paint) {
160        return new Color(
161                (int)(paint.getRed  () * FACTOR),
162                (int)(paint.getGreen() * FACTOR),
163                (int)(paint.getBlue () * FACTOR), paint.getAlpha());
164    }
165
166    /**
167     * Create a new {@code GradientPaint} with its colors darkened.
168     *
169     * @param paint  the gradient paint ({@code null} not permitted).
170     *
171     * @return a darker version of the {@code GradientPaint}
172     */
173    private static GradientPaint darker(GradientPaint paint) {
174        return new GradientPaint(
175                paint.getPoint1(), darker(paint.getColor1()),
176                paint.getPoint2(), darker(paint.getColor2()),
177                paint.isCyclic());
178    }
179
180    /**
181     * Create a new Gradient with its colours darkened.
182     *
183     * @param paint a {@code LinearGradientPaint}
184     *
185     * @return a darker version of the {@code LinearGradientPaint}
186     */
187    private static Paint darkerLinearGradientPaint(LinearGradientPaint paint) {
188        final Color[] paintColors = paint.getColors();
189        for (int i = 0; i < paintColors.length; i++) {
190            paintColors[i] = darker(paintColors[i]);
191        }
192        return new LinearGradientPaint(paint.getStartPoint(),
193                paint.getEndPoint(), paint.getFractions(), paintColors,
194                paint.getCycleMethod(), paint.getColorSpace(), 
195                paint.getTransform());
196    }
197
198    /**
199     * Create a new Gradient with its colours darkened.
200     *
201     * @param paint a {@code RadialGradientPaint}
202     *
203     * @return a darker version of the {@code RadialGradientPaint}
204     */
205    private static Paint darkerRadialGradientPaint(RadialGradientPaint paint) {
206        final Color[] paintColors = paint.getColors();
207        for (int i = 0; i < paintColors.length; i++) {
208            paintColors[i] = darker(paintColors[i]);
209        }
210        return new RadialGradientPaint(paint.getCenterPoint(), 
211                paint.getRadius(), paint.getFocusPoint(), 
212                paint.getFractions(), paintColors, paint.getCycleMethod(),
213                paint.getColorSpace(), paint.getTransform());
214    }
215
216    /**
217     * Create a new {@code TexturePaint} with its colors darkened.
218     * <p>
219     * This entails cloning the underlying {@code BufferedImage},
220     * then darkening each color-pixel individually!
221     *
222     * @param paint a {@code TexturePaint}
223     *
224     * @return a darker version of the {@code TexturePaint}
225     */
226    private static TexturePaint darkerTexturePaint(TexturePaint paint) {
227        /*
228         * Color Models with pre-multiplied Alpha tested OK without any
229         * special logic
230         *
231         * BufferedImage.TYPE_INT_ARGB_PRE:    // Type 03: tested OK 2011.02.27
232         * BufferedImage.TYPE_4BYTE_ABGR_PRE:  // Type 07: tested OK 2011.02.27
233         */
234        if (paint.getImage().getColorModel().isAlphaPremultiplied()) {
235            /* Placeholder */
236        }
237
238        BufferedImage img = cloneImage(paint.getImage());
239
240        WritableRaster ras = img.copyData(null);
241
242        final int miX = ras.getMinX();
243        final int miY = ras.getMinY();
244        final int maY = ras.getMinY() + ras.getHeight();
245
246        final int   wid = ras.getWidth();
247
248        int[] pix = new int[wid * img.getSampleModel().getNumBands()];
249        // (pix-buffer is large enough for all pixels of one row) */
250
251        /*
252         * Indexed Color Models (sort of Palette) CANNOT be simply
253         * multiplied (the pixel-value is just an index into the Palette).
254         *
255         * Fortunately, IndexColorModel.getComponents(..) resolves the colors.
256         * The resolved colors can then be multiplied by our FACTOR.
257         * IndexColorModel.getDataElement(..) then tries to map the computed
258         * color to the "nearest" in the Palette.
259         *
260         * It is quite possible that the "nearest" color is the ORIGINAL
261         * color!  In the worst case, the returned Image will be identical to
262         * the original.
263         *
264         * Applies to following Image Types:
265         *
266         * BufferedImage.TYPE_BYTE_BINARY:    // Type 12: tested OK 2011.02.27
267         * BufferedImage.TYPE_BYTE_INDEXED:   // Type 13: tested OK 2011.02.27
268         */
269        if (img.getColorModel() instanceof IndexColorModel) {
270
271            int[] nco = new int[4]; // RGB (+ optional Alpha which we leave
272                                    // unchanged)
273
274            for (int y = miY; y < maY; y++)  {
275
276                pix = ras.getPixels(miX, y, wid, 1, pix);
277
278                for (int p = 0; p < pix.length; p++) {
279                    nco    =  img.getColorModel().getComponents(pix[p], nco, 0);
280                    nco[0] *= FACTOR; // Red
281                    nco[1] *= FACTOR; // Green
282                    nco[2] *= FACTOR; // Blue. Now map computed colour to
283                                      // nearest in Palette...
284                    pix[p] = img.getColorModel().getDataElement(nco, 0);
285                }
286                /**/ ras.setPixels(miX, y, wid, 1, pix);
287            }
288            img.setData(ras);
289
290            return new TexturePaint(img, paint.getAnchorRect());
291        }
292
293        /*
294         * For the other 2 Color Models, java.awt.image.ComponentColorModel and
295         * java.awt.image.DirectColorModel, the order of subpixels returned by
296         * ras.getPixels(..) was observed to correspond to the following...
297         */
298        if (img.getSampleModel().getNumBands() == 4) {
299            /*
300             * The following Image Types have an Alpha-channel which we will
301             * leave unchanged:
302             *
303             * BufferedImage.TYPE_INT_ARGB:   // Type 02: tested OK 2011.02.27
304             * BufferedImage.TYPE_4BYTE_ABGR: // Type 06: tested OK 2011.02.27
305             */
306            for (int y = miY; y < maY; y++)  {
307
308                pix = ras.getPixels(miX, y, wid, 1, pix);
309
310                for (int p = 0; p < pix.length;) {
311                    pix[p] = (int)(pix[p++] * FACTOR); // Red
312                    pix[p] = (int)(pix[p++] * FACTOR); // Green
313                    pix[p] = (int)(pix[p++] * FACTOR); // Blue
314                    /* Ignore alpha-channel -> */p++;
315                }
316                /**/  ras.setPixels(miX, y, wid, 1, pix);
317            }
318            img.setData(ras);
319            return new TexturePaint(img, paint.getAnchorRect());
320        } else {
321            for (int y = miY; y < maY; y++)  {
322
323                pix = ras.getPixels(miX, y, wid, 1, pix);
324
325                for (int p = 0; p < pix.length; p++) {
326                    pix[p] = (int)(pix[p] * FACTOR);
327                }
328                /**/  ras.setPixels(miX, y, wid, 1, pix);
329            }
330            img.setData(ras);
331            return new TexturePaint(img, paint.getAnchorRect());
332            /*
333             * Above, we multiplied every pixel by our FACTOR because the
334             * applicable Image Types consist only of color or grey channels:
335             *
336             * BufferedImage.TYPE_INT_RGB:        // Type 01: tested OK 2011.02.27
337             * BufferedImage.TYPE_INT_BGR:        // Type 04: tested OK 2011.02.27
338             * BufferedImage.TYPE_3BYTE_BGR:      // Type 05: tested OK 2011.02.27
339             * BufferedImage.TYPE_BYTE_GRAY:      // Type 10: tested OK 2011.02.27
340             * BufferedImage.TYPE_USHORT_GRAY:    // Type 11: tested OK 2011.02.27
341             * BufferedImage.TYPE_USHORT_565_RGB: // Type 08: tested OK 2011.02.27
342             * BufferedImage.TYPE_USHORT_555_RGB: // Type 09: tested OK 2011.02.27
343             *
344             * Note: as ras.getPixels(..) returned colours in the order R, G, B, A (optional)
345             * for both TYPE_4BYTE_ABGR & TYPE_3BYTE_BGR,
346             * it is assumed that TYPE_INT_BGR will behave similarly.
347             */
348        }
349    }
350
351    /**
352     * Clone a {@link BufferedImage}.
353     * <p>
354     * Note: when constructing the clone, the original Color Model Object is
355     * reused.<br>  That keeps things simple and should not be a problem, as all
356     * known Color Models<br>
357     * ({@link java.awt.image.IndexColorModel     IndexColorModel},
358     *  {@link java.awt.image.DirectColorModel    DirectColorModel},
359     *  {@link java.awt.image.ComponentColorModel ComponentColorModel}) are
360     * immutable.
361     *
362     * @param image original BufferedImage to clone
363     *
364     * @return a new BufferedImage reusing the original's Color Model and
365     *         containing a clone of its pixels
366     */
367    public static BufferedImage cloneImage(BufferedImage image) {
368
369        WritableRaster rin = image.getRaster();
370        WritableRaster ras = rin.createCompatibleWritableRaster();
371        /**/ ras.setRect(rin); // <- this is the code that actually COPIES the pixels
372
373        /*
374         * Buffered Images may have properties, but NEVER disclose them!
375         * Nevertheless, just in case someone implements getPropertyNames()
376         * one day...
377         */
378        Hashtable props = null;
379        String[] propNames = image.getPropertyNames();
380        if (propNames != null) { // ALWAYS null
381            props = new Hashtable();
382            for (int i = 0; i < propNames.length; i++) {
383                props.put(propNames[i], image.getProperty(propNames[i]));
384            }
385        }
386        return new BufferedImage(image.getColorModel(), ras,
387                image.isAlphaPremultiplied(), props);
388    }
389}