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}