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