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}