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}