org.jfree.chart.util.PaintAlpha Maven / Gradle / Ivy
Show all versions of jfreechart Show documentation
/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-present, by David Gilbert and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
* ---------------
* PaintAlpha.java
* ---------------
* (C) Copyright 2011-present, by DaveLaw and Contributors.
*
* Original Author: DaveLaw (dave ATT davelaw D0TT de);
* Contributor(s): David Gilbert;
*
*/
package org.jfree.chart.util;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.TexturePaint;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.util.Hashtable;
/**
* This class contains static methods for the manipulation
* of objects of type {@code Paint}
*
* The intention is to honour the alpha-channel in the process.
* {@code PaintAlpha} was originally conceived to improve the
* rendering of 3D Shapes with transparent colours and to allow
* invisible bars by making them completely transparent.
*
* Previously {@link Color#darker()} was used for this,
* which always returns an opaque colour.
*
* Additionally there are methods to control the behaviour and
* in particular a {@link PaintAlpha#cloneImage(BufferedImage) cloneImage(..)}
* method which is needed to darken objects of type {@link TexturePaint}.
*
* @author DaveLaw
*/
public class PaintAlpha {
// TODO Revert to SVN revision 2469 in JFreeChart 1.0.16
// (MultipleGradientPaint's / JDK issues)
// TODO THEN: change visibility of ALL darker(...) Methods EXCEPT
// darker(Paint) to private!
/**
* Multiplier for the {@code darker} Methods.
* (taken from {@link java.awt.Color}.FACTOR)
*/
private static final double FACTOR = 0.7;
private static boolean legacyAlpha = false;
/**
* Per default {@code PaintAlpha} will try to honour alpha-channel
* information. In the past this was not the case.
* If you wish legacy functionality for your application you can request
* this here.
*
* @param legacyAlpha boolean
*
* @return the previous setting
*/
public static boolean setLegacyAlpha(boolean legacyAlpha) {
boolean old = PaintAlpha.legacyAlpha;
PaintAlpha.legacyAlpha = legacyAlpha;
return old;
}
/**
* Create a new (if possible, darker) {@code Paint} of the same Type.
* If the Type is not supported, the original {@code Paint} is returned.
*
* @param paint a {@code Paint} implementation
* (e.g. {@link Color}, {@link GradientPaint}, {@link TexturePaint},..)
*
* @return a (usually new, see above) {@code Paint}
*/
public static Paint darker(Paint paint) {
if (paint instanceof Color) {
return darker((Color) paint);
}
if (legacyAlpha) {
/*
* Legacy? Just return the original Paint.
* (this corresponds EXACTLY to how Paints used to be darkened)
*/
return paint;
}
if (paint instanceof GradientPaint) {
return darker((GradientPaint) paint);
}
if (paint instanceof LinearGradientPaint) {
return darkerLinearGradientPaint((LinearGradientPaint) paint);
}
if (paint instanceof RadialGradientPaint) {
return darkerRadialGradientPaint((RadialGradientPaint) paint);
}
if (paint instanceof TexturePaint) {
try {
return darkerTexturePaint((TexturePaint) paint);
}
catch (Exception e) {
/*
* Lots can go wrong while fiddling with Images, Color Models
* & such! If anything at all goes awry, just return the original
* TexturePaint. (TexturePaint's are immutable anyway, so no harm
* done)
*/
return paint;
}
}
return paint;
}
/**
* Similar to {@link Color#darker()}.
*
* The essential difference is that this method
* maintains the alpha-channel unchanged
*
* @param paint a {@code Color}
*
* @return a darker version of the {@code Color}
*/
private static Color darker(Color paint) {
return new Color(
(int)(paint.getRed () * FACTOR),
(int)(paint.getGreen() * FACTOR),
(int)(paint.getBlue () * FACTOR), paint.getAlpha());
}
/**
* Create a new {@code GradientPaint} with its colors darkened.
*
* @param paint the gradient paint ({@code null} not permitted).
*
* @return a darker version of the {@code GradientPaint}
*/
private static GradientPaint darker(GradientPaint paint) {
return new GradientPaint(
paint.getPoint1(), darker(paint.getColor1()),
paint.getPoint2(), darker(paint.getColor2()),
paint.isCyclic());
}
/**
* Create a new Gradient with its colours darkened.
*
* @param paint a {@code LinearGradientPaint}
*
* @return a darker version of the {@code LinearGradientPaint}
*/
private static Paint darkerLinearGradientPaint(LinearGradientPaint paint) {
final Color[] paintColors = paint.getColors();
for (int i = 0; i < paintColors.length; i++) {
paintColors[i] = darker(paintColors[i]);
}
return new LinearGradientPaint(paint.getStartPoint(),
paint.getEndPoint(), paint.getFractions(), paintColors,
paint.getCycleMethod(), paint.getColorSpace(),
paint.getTransform());
}
/**
* Create a new Gradient with its colours darkened.
*
* @param paint a {@code RadialGradientPaint}
*
* @return a darker version of the {@code RadialGradientPaint}
*/
private static Paint darkerRadialGradientPaint(RadialGradientPaint paint) {
final Color[] paintColors = paint.getColors();
for (int i = 0; i < paintColors.length; i++) {
paintColors[i] = darker(paintColors[i]);
}
return new RadialGradientPaint(paint.getCenterPoint(),
paint.getRadius(), paint.getFocusPoint(),
paint.getFractions(), paintColors, paint.getCycleMethod(),
paint.getColorSpace(), paint.getTransform());
}
/**
* Create a new {@code TexturePaint} with its colors darkened.
*
* This entails cloning the underlying {@code BufferedImage},
* then darkening each color-pixel individually!
*
* @param paint a {@code TexturePaint}
*
* @return a darker version of the {@code TexturePaint}
*/
private static TexturePaint darkerTexturePaint(TexturePaint paint) {
/**
* Color Models with pre-multiplied Alpha tested OK without any
* special logic
*
* BufferedImage.TYPE_INT_ARGB_PRE: // Type 03: tested OK 2011.02.27
* BufferedImage.TYPE_4BYTE_ABGR_PRE: // Type 07: tested OK 2011.02.27
*/
if (paint.getImage().getColorModel().isAlphaPremultiplied()) {
/* Placeholder */
}
BufferedImage img = cloneImage(paint.getImage());
WritableRaster ras = img.copyData(null);
final int miX = ras.getMinX();
final int miY = ras.getMinY();
final int maY = ras.getMinY() + ras.getHeight();
final int wid = ras.getWidth();
/**/ int[] pix = new int[wid * img.getSampleModel().getNumBands()];
/* (pix-buffer is large enough for all pixels of one row) */
/**
* Indexed Color Models (sort of a Palette) CANNOT be simply
* multiplied (the pixel-value is just an index into the Palette).
*
* Fortunately, IndexColorModel.getComponents(..) resolves the colors.
* The resolved colors can then be multiplied by our FACTOR.
* IndexColorModel.getDataElement(..) then tries to map the computed
* color to the "nearest" in the Palette.
*
* It is quite possible that the "nearest" color is the ORIGINAL
* color! In the worst case, the returned Image will be identical to
* the original.
*
* Applies to following Image Types:
*
* BufferedImage.TYPE_BYTE_BINARY: // Type 12: tested OK 2011.02.27
* BufferedImage.TYPE_BYTE_INDEXED: // Type 13: tested OK 2011.02.27
*/
if (img.getColorModel() instanceof IndexColorModel) {
int[] nco = new int[4]; // RGB (+ optional Alpha which we leave
// unchanged)
for (int y = miY; y < maY; y++) {
pix = ras.getPixels(miX, y, wid, 1, pix);
for (int p = 0; p < pix.length; p++) {
nco = img.getColorModel().getComponents(pix[p], nco, 0);
nco[0] *= FACTOR; // Red
nco[1] *= FACTOR; // Green
nco[2] *= FACTOR; // Blue. Now map computed colour to
// nearest in Palette...
pix[p] = img.getColorModel().getDataElement(nco, 0);
}
/**/ ras.setPixels(miX, y, wid, 1, pix);
}
img.setData(ras);
return new TexturePaint(img, paint.getAnchorRect());
}
/**
* For the other 2 Color Models, java.awt.image.ComponentColorModel and
* java.awt.image.DirectColorModel, the order of subpixels returned by
* ras.getPixels(..) was observed to correspond to the following...
*/
if (img.getSampleModel().getNumBands() == 4) {
/**
* The following Image Types have an Alpha-channel which we will
* leave unchanged:
*
* BufferedImage.TYPE_INT_ARGB: // Type 02: tested OK 2011.02.27
* BufferedImage.TYPE_4BYTE_ABGR: // Type 06: tested OK 2011.02.27
*/
for (int y = miY; y < maY; y++) {
pix = ras.getPixels(miX, y, wid, 1, pix);
for (int p = 0; p < pix.length;) {
pix[p] = (int)(pix[p++] * FACTOR); // Red
pix[p] = (int)(pix[p++] * FACTOR); // Green
pix[p] = (int)(pix[p++] * FACTOR); // Blue
/* Ignore alpha-channel -> */p++;
}
/**/ ras.setPixels(miX, y, wid, 1, pix);
}
img.setData(ras);
return new TexturePaint(img, paint.getAnchorRect());
} else {
for (int y = miY; y < maY; y++) {
pix = ras.getPixels(miX, y, wid, 1, pix);
for (int p = 0; p < pix.length; p++) {
pix[p] = (int)(pix[p] * FACTOR);
}
/**/ ras.setPixels(miX, y, wid, 1, pix);
}
img.setData(ras);
return new TexturePaint(img, paint.getAnchorRect());
/**
* Above, we multiplied every pixel by our FACTOR because the
* applicable Image Types consist only of color or grey channels:
*
* BufferedImage.TYPE_INT_RGB: // Type 01: tested OK 2011.02.27
* BufferedImage.TYPE_INT_BGR: // Type 04: tested OK 2011.02.27
* BufferedImage.TYPE_3BYTE_BGR: // Type 05: tested OK 2011.02.27
* BufferedImage.TYPE_BYTE_GRAY: // Type 10: tested OK 2011.02.27
* BufferedImage.TYPE_USHORT_GRAY: // Type 11: tested OK 2011.02.27
* BufferedImage.TYPE_USHORT_565_RGB: // Type 08: tested OK 2011.02.27
* BufferedImage.TYPE_USHORT_555_RGB: // Type 09: tested OK 2011.02.27
*
* Note: as ras.getPixels(..) returned colours in the order R, G, B, A (optional)
* for both TYPE_4BYTE_ABGR & TYPE_3BYTE_BGR,
* it is assumed that TYPE_INT_BGR will behave similarly.
*/
}
}
/**
* Clone a {@link BufferedImage}.
*
* Note: when constructing the clone, the original Color Model Object is
* reused.
That keeps things simple and should not be a problem, as all
* known Color Models
* ({@link java.awt.image.IndexColorModel IndexColorModel},
* {@link java.awt.image.DirectColorModel DirectColorModel},
* {@link java.awt.image.ComponentColorModel ComponentColorModel}) are
* immutable.
*
* @param image original BufferedImage to clone
*
* @return a new BufferedImage reusing the original's Color Model and
* containing a clone of its pixels
*/
public static BufferedImage cloneImage(BufferedImage image) {
WritableRaster rin = image.getRaster();
WritableRaster ras = rin.createCompatibleWritableRaster();
/**/ ras.setRect(rin); // <- this is the code that actually COPIES the pixels
/*
* Buffered Images may have properties, but NEVER disclose them!
* Nevertheless, just in case someone implements getPropertyNames()
* one day...
*/
Hashtable props = null;
String[] propNames = image.getPropertyNames();
if (propNames != null) { // ALWAYS null
props = new Hashtable();
for (int i = 0; i < propNames.length; i++) {
props.put(propNames[i], image.getProperty(propNames[i]));
}
}
return new BufferedImage(image.getColorModel(), ras,
image.isAlphaPremultiplied(), props);
}
}