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}