1 /******************************************************************************* 2 * Copyright (c) 2000, 2014 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 * Stephan Wahlbrink - fix for bug 341702 - incorrect mixing of images with alpha channel 14 *******************************************************************************/ 15 package org.eclipse.jface.resource; 16 17 import java.util.Objects; 18 import java.util.function.ToIntFunction; 19 20 import org.eclipse.swt.graphics.Image; 21 import org.eclipse.swt.graphics.ImageData; 22 import org.eclipse.swt.graphics.ImageDataProvider; 23 import org.eclipse.swt.graphics.PaletteData; 24 import org.eclipse.swt.graphics.Point; 25 import org.eclipse.swt.graphics.RGB; 26 27 /** 28 * Abstract base class for image descriptors that synthesize an image from other 29 * images in order to simulate the effect of custom drawing. For example, this 30 * could be used to superimpose a red bar dexter symbol across an image to 31 * indicate that something was disallowed. 32 * <p> 33 * Subclasses must implement {@link #getSize()} and {@link #drawImage(ImageDataProvider, int, int)}. 34 * Little or no work happens until the image descriptor's image is 35 * actually requested by a call to <code>createImage</code> (or to 36 * <code>getImageData</code> directly). 37 * </p> 38 * @see org.eclipse.jface.viewers.DecorationOverlayIcon 39 */ 40 public abstract class CompositeImageDescriptor extends ImageDescriptor { 41 42 /** 43 * An {@link ImageDataProvider} that caches the most recently returned 44 * {@link ImageData} object. I.e. consecutive calls to 45 * {@link #getImageData(int)} with the same zoom level are cheap. 46 * 47 * @see #createCachedImageDataProvider(Image) 48 * @see #createCachedImageDataProvider(ImageDescriptor) 49 * 50 * @since 3.13 51 * @noextend This class is not intended to be subclassed by clients. 52 */ 53 protected abstract class CachedImageDataProvider implements ImageDataProvider { 54 /** 55 * Returns the {@link ImageData#width} in points. This method must only 56 * be called within the dynamic scope of a call to 57 * {@link #drawCompositeImage(int, int)}. 58 * 59 * @return the width in points 60 */ getWidth()61 public int getWidth() { 62 return computeInPoints(imageData -> imageData.width); 63 } 64 65 /** 66 * Returns the {@link ImageData#height} in points. This method must only 67 * be called within the dynamic scope of a call to 68 * {@link #drawCompositeImage(int, int)}. 69 * 70 * @return the height in points 71 */ getHeight()72 public int getHeight() { 73 return computeInPoints(imageData -> imageData.height); 74 } 75 76 /** 77 * Returns a computed value in SWT logical points. The given function 78 * computes a value in pixels, based on information from the given 79 * ImageData, which is also in pixels. This method must only 80 * be called within the dynamic scope of a call to 81 * {@link #drawCompositeImage(int, int)}. 82 * 83 * @param function 84 * a function that takes an {@link ImageData} and computes a 85 * value in pixels 86 * @return the computed value in points 87 */ computeInPoints(ToIntFunction<ImageData> function)88 public int computeInPoints(ToIntFunction<ImageData> function) { 89 ImageData overlayData = getImageData(getZoomLevel()); 90 if (overlayData != null) { 91 int valueInPixels = function.applyAsInt(overlayData); 92 return autoScaleDown(valueInPixels); 93 } 94 overlayData = getImageData(100); 95 return function.applyAsInt(overlayData); 96 } 97 } 98 99 private final class CachedImageImageDataProvider extends CachedImageDataProvider { 100 final Image baseImage; 101 ImageData cached; 102 int cachedZoom; 103 CachedImageImageDataProvider(Image baseImage)104 private CachedImageImageDataProvider(Image baseImage) { 105 this.baseImage = Objects.requireNonNull(baseImage); 106 } 107 108 @Override getImageData(int zoom)109 public ImageData getImageData(int zoom) { 110 if (zoom == cachedZoom) { 111 return cached; 112 } 113 cached = baseImage.getImageData(zoom); 114 cachedZoom = zoom; 115 return cached; 116 } 117 } 118 119 private final class CachedDescriptorImageDataProvider extends CachedImageDataProvider { 120 final ImageDescriptor descriptor; 121 ImageData cached; 122 int cachedZoom; 123 CachedDescriptorImageDataProvider(ImageDescriptor descriptor)124 private CachedDescriptorImageDataProvider(ImageDescriptor descriptor) { 125 this.descriptor = Objects.requireNonNull(descriptor); 126 } 127 128 @Override getImageData(int zoom)129 public ImageData getImageData(int zoom) { 130 if (zoom == cachedZoom) { 131 return cached; 132 } 133 ImageData zoomed = descriptor.getImageData(zoom); 134 if (zoomed != null) { 135 cached = zoomed; 136 cachedZoom = zoom; 137 return zoomed; 138 } 139 if (zoom == 100) { 140 return ImageDescriptor.getMissingImageDescriptor().getImageData(100); 141 } 142 143 ImageData data100 = descriptor.getImageData(100); 144 if (data100 != null) { 145 // 100% is available => caller will have to scale this one 146 cached = data100; 147 cachedZoom = 100; 148 return null; 149 } 150 // 100% is not available, but requested zoom != 100 151 // => caller will have to scale missing image descriptor 152 return null; 153 } 154 } 155 156 /** 157 * The image data for this composite image. 158 */ 159 private ImageData imageData; 160 161 /** 162 * The zoom level for this composite image. Only valid within the dynamic 163 * scope of a call to {@link #drawCompositeImage(int, int)}. 164 */ 165 private int compositeZoom; 166 167 /** 168 * Constructs an uninitialized composite image. 169 */ CompositeImageDescriptor()170 protected CompositeImageDescriptor() { 171 } 172 173 /** 174 * Draw the composite images. 175 * <p> 176 * Subclasses must implement this framework method to paint images within 177 * the given bounds using one or more calls to the 178 * {@link #drawImage(ImageDataProvider, int, int)} framework method. 179 * </p> 180 * <p> 181 * Implementers that need to perform computations based on the size of 182 * another image are advised to use one of the 183 * {@link #createCachedImageDataProvider} methods to create a 184 * {@link CachedImageDataProvider} that can serve as 185 * {@link ImageDataProvider}. The {@link CachedImageDataProvider} offers 186 * other interesting methods like {@link CachedImageDataProvider#getWidth() 187 * getWidth()} or 188 * {@link CachedImageDataProvider#computeInPoints(ToIntFunction) 189 * computeInPoints(...)} that can be useful to compute values in points, 190 * based on the resolution-dependent {@link ImageData} that is applicable 191 * for the current drawing operation. 192 * </p> 193 * 194 * @param width 195 * the width 196 * @param height 197 * the height 198 * @see #drawImage(ImageDataProvider, int, int) 199 * @see #createCachedImageDataProvider(Image) 200 * @see #createCachedImageDataProvider(ImageDescriptor) 201 */ drawCompositeImage(int width, int height)202 protected abstract void drawCompositeImage(int width, int height); 203 204 /** 205 * Draws the given source image data into this composite image at the given 206 * position. 207 * <p> 208 * Call this internal framework method to superimpose another image atop 209 * this composite image. 210 * </p> 211 * 212 * @param src 213 * the source image data 214 * @param ox 215 * the x position 216 * @param oy 217 * the y position 218 * @deprecated Use {@link #drawImage(ImageDataProvider, int, int)} instead. 219 * Replace the code that created the ImageData by calls to 220 * {@link #createCachedImageDataProvider(Image)} or 221 * {@link #createCachedImageDataProvider(ImageDescriptor)} and 222 * then pass on that provider instead of ImageData objects. 223 * Replace references to {@link ImageData#width}/height by calls 224 * to {@link CachedImageDataProvider#getWidth()}/getHeight(). 225 */ 226 @Deprecated drawImage(ImageData src, int ox, int oy)227 final protected void drawImage(ImageData src, int ox, int oy) { 228 if (src == null) { // wrong hack for https://bugs.eclipse.org/372956 , kept for compatibility with broken client code 229 return; 230 } 231 drawImage(getUnzoomedImageDataProvider(src), ox, oy); 232 } 233 getUnzoomedImageDataProvider(ImageData imageData)234 private static ImageDataProvider getUnzoomedImageDataProvider(ImageData imageData) { 235 return zoom -> zoom == 100 ? imageData : null; 236 } 237 238 /** 239 * Draws the given source image data into this composite image at the given 240 * position. 241 * <p> 242 * Subclasses call this framework method to superimpose another image atop 243 * this composite image. This method must only be called within the dynamic 244 * scope of a call to {@link #drawCompositeImage(int, int)}. 245 * </p> 246 * <p> 247 * Hint: Use {@link #createCachedImageDataProvider(Image)} or 248 * {@link #createCachedImageDataProvider(ImageDescriptor)} to create an 249 * {@link ImageDataProvider}. To calculate the width and height of the image 250 * that is about to be drawn, you can use 251 * {@link CachedImageDataProvider#getWidth()}/getHeight(). These methods 252 * already return values in SWT points, so that your code doesn't have to 253 * deal with device-dependent pixel coordinates. 254 * </p> 255 * 256 * @param srcProvider 257 * the source image data provider 258 * @param ox 259 * the x position 260 * @param oy 261 * the y position 262 * @since 3.13 263 */ drawImage(ImageDataProvider srcProvider, int ox, int oy)264 final protected void drawImage(ImageDataProvider srcProvider, int ox, int oy) { 265 ImageData dst = imageData; 266 ImageData src = getZoomedImageData(srcProvider); 267 268 PaletteData srcPalette = src.palette; 269 ImageData srcMask = null; 270 int alphaMask = 0, alphaShift = 0; 271 if (src.maskData != null) { 272 srcMask = src.getTransparencyMask (); 273 if (src.depth == 32) { 274 alphaMask = ~(srcPalette.redMask | srcPalette.greenMask | srcPalette.blueMask); 275 while (alphaMask != 0 && ((alphaMask >>> alphaShift) & 1) == 0) alphaShift++; 276 } 277 } 278 for (int srcY = 0, dstY = srcY + autoScaleUp(oy); srcY < src.height; srcY++, dstY++) { 279 for (int srcX = 0, dstX = srcX + autoScaleUp(ox); srcX < src.width; srcX++, dstX++) { 280 if (!(0 <= dstX && dstX < dst.width && 0 <= dstY && dstY < dst.height)) continue; 281 int srcPixel = src.getPixel(srcX, srcY); 282 int srcAlpha = 255; 283 if (src.maskData != null) { 284 if (src.depth == 32) { 285 srcAlpha = (srcPixel & alphaMask) >>> alphaShift; 286 if (srcAlpha == 0) { 287 srcAlpha = srcMask.getPixel(srcX, srcY) != 0 ? 255 : 0; 288 } 289 } else { 290 if (srcMask.getPixel(srcX, srcY) == 0) srcAlpha = 0; 291 } 292 } else if (src.transparentPixel != -1) { 293 if (src.transparentPixel == srcPixel) srcAlpha = 0; 294 } else if (src.alpha != -1) { 295 srcAlpha = src.alpha; 296 } else if (src.alphaData != null) { 297 srcAlpha = src.getAlpha(srcX, srcY); 298 } 299 if (srcAlpha == 0) continue; 300 int srcRed, srcGreen, srcBlue; 301 if (srcPalette.isDirect) { 302 srcRed = srcPixel & srcPalette.redMask; 303 srcRed = (srcPalette.redShift < 0) ? srcRed >>> -srcPalette.redShift : srcRed << srcPalette.redShift; 304 srcGreen = srcPixel & srcPalette.greenMask; 305 srcGreen = (srcPalette.greenShift < 0) ? srcGreen >>> -srcPalette.greenShift : srcGreen << srcPalette.greenShift; 306 srcBlue = srcPixel & srcPalette.blueMask; 307 srcBlue = (srcPalette.blueShift < 0) ? srcBlue >>> -srcPalette.blueShift : srcBlue << srcPalette.blueShift; 308 } else { 309 RGB rgb = srcPalette.getRGB(srcPixel); 310 srcRed = rgb.red; 311 srcGreen = rgb.green; 312 srcBlue = rgb.blue; 313 } 314 int dstRed, dstGreen, dstBlue, dstAlpha; 315 if (srcAlpha == 255) { 316 dstRed = srcRed; 317 dstGreen = srcGreen; 318 dstBlue= srcBlue; 319 dstAlpha = srcAlpha; 320 } else { 321 int dstPixel = dst.getPixel(dstX, dstY); 322 dstAlpha = dst.getAlpha(dstX, dstY); 323 dstRed = (dstPixel & 0xFF) >>> 0; 324 dstGreen = (dstPixel & 0xFF00) >>> 8; 325 dstBlue = (dstPixel & 0xFF0000) >>> 16; 326 if (dstAlpha == 255) { // simplified calculations for performance 327 dstRed += (srcRed - dstRed) * srcAlpha / 255; 328 dstGreen += (srcGreen - dstGreen) * srcAlpha / 255; 329 dstBlue += (srcBlue - dstBlue) * srcAlpha / 255; 330 } else { 331 // See Porter T., Duff T. 1984. "Compositing Digital Images". 332 // Computer Graphics 18 (3): 253-259. 333 dstRed = srcRed * srcAlpha * 255 + dstRed * dstAlpha * (255 - srcAlpha); 334 dstGreen = srcGreen * srcAlpha * 255 + dstGreen * dstAlpha * (255 - srcAlpha); 335 dstBlue = srcBlue * srcAlpha * 255 + dstBlue * dstAlpha * (255 - srcAlpha); 336 dstAlpha = srcAlpha * 255 + dstAlpha * (255 - srcAlpha); 337 if (dstAlpha != 0) { // if both original alphas == 0, then all colors are 0 338 dstRed /= dstAlpha; 339 dstGreen /= dstAlpha; 340 dstBlue /= dstAlpha; 341 dstAlpha /= 255; 342 } 343 } 344 } 345 dst.setPixel(dstX, dstY, ((dstRed & 0xFF) << 0) | ((dstGreen & 0xFF) << 8) | ((dstBlue & 0xFF) << 16)); 346 dst.setAlpha(dstX, dstY, dstAlpha); 347 } 348 } 349 } 350 351 /** 352 * @deprecated Use {@link #getImageData(int)} instead. 353 */ 354 @Deprecated 355 @Override getImageData()356 public ImageData getImageData() { 357 return getImageData(100); 358 } 359 360 @Override getImageData(int zoom)361 public ImageData getImageData(int zoom) { 362 if (!supportsZoomLevel(zoom)) { 363 return null; 364 } 365 /* Assign before calling getSize(), just in case an implementer of 366 * getSize() already uses a CachedImageDataProvider. */ 367 compositeZoom = zoom; 368 369 Point size = getSize(); 370 371 /* Create a 24 bit image data with alpha channel */ 372 imageData = new ImageData(scaleUp(size.x, zoom), scaleUp(size.y, zoom), 24, 373 new PaletteData(0xFF, 0xFF00, 0xFF0000)); 374 imageData.alphaData = new byte[imageData.width * imageData.height]; 375 376 drawCompositeImage(size.x, size.y); 377 378 /* Detect minimum transparency */ 379 boolean transparency = false; 380 byte[] alphaData = imageData.alphaData; 381 for (byte element : alphaData) { 382 int alpha = element & 0xFF; 383 if (!(alpha == 0 || alpha == 255)) { 384 /* Full alpha channel transparency */ 385 return imageData; 386 } 387 if (!transparency && alpha == 0) transparency = true; 388 } 389 if (transparency) { 390 /* Reduce to 1-bit alpha channel transparency */ 391 PaletteData palette = new PaletteData(new RGB[]{new RGB(0, 0, 0), new RGB(255, 255, 255)}); 392 ImageData mask = new ImageData(imageData.width, imageData.height, 1, palette); 393 for (int y = 0; y < mask.height; y++) { 394 for (int x = 0; x < mask.width; x++) { 395 mask.setPixel(x, y, imageData.getAlpha(x, y) == 255 ? 1 : 0); 396 } 397 } 398 } else { 399 /* no transparency */ 400 imageData.alphaData = null; 401 } 402 return imageData; 403 } 404 405 406 /** 407 * Return the transparent pixel for the receiver. 408 * <strong>NOTE</strong> This value is not currently in use in the 409 * default implementation. 410 * @return int 411 * @since 3.3 412 */ getTransparentPixel()413 protected int getTransparentPixel() { 414 return 0; 415 } 416 417 /** 418 * Return the size of this composite image. 419 * <p> 420 * Subclasses must implement this framework method. 421 * </p> 422 * 423 * @return the x and y size of the image expressed as a point object 424 */ getSize()425 protected abstract Point getSize(); 426 427 /** 428 * Do not call this method! Behavior is unspecified. 429 * 430 * @param imageData unspecified 431 * @since 3.3 432 * @deprecated This method doesn't make sense and should never have been 433 * made API. 434 */ 435 @Deprecated setImageData(ImageData imageData)436 protected void setImageData(ImageData imageData) { 437 this.imageData = imageData; 438 } 439 440 /** 441 * Returns whether the given zoom level is supported by this 442 * CompositeImageDescriptor. 443 * 444 * @param zoom 445 * the zoom level 446 * @return whether the given zoom level is supported. Must return true for 447 * {@code zoom == 100}. 448 * @since 3.13 449 */ supportsZoomLevel(int zoom)450 protected boolean supportsZoomLevel(int zoom) { 451 // Currently only support integer zoom levels, because getZoomedImageData(..) 452 // suffers from Bug 97506: [HiDPI] ImageData.scaledTo() should use a 453 // better interpolation method. 454 return zoom > 0 && zoom % 100 == 0; 455 } 456 getZoomedImageData(ImageDataProvider srcProvider)457 private ImageData getZoomedImageData(ImageDataProvider srcProvider) { 458 ImageData src = srcProvider.getImageData(compositeZoom); 459 if (src == null) { 460 ImageData src100 = srcProvider.getImageData(100); 461 src = src100.scaledTo(autoScaleUp(src100.width), autoScaleUp(src100.height)); 462 } 463 return src; 464 } 465 466 /** 467 * Returns the current zoom level. 468 * <p> 469 * <b>Important:</b> This method must only be called within the dynamic scope of a call to 470 * {@link #drawCompositeImage(int, int)}. 471 * </p> 472 * 473 * @return The zoom level in % of the standard resolution (which is 1 474 * physical monitor pixel == 1 SWT logical point). Typically 100, 475 * 150, or 200. 476 * @since 3.13 477 */ getZoomLevel()478 protected int getZoomLevel() { 479 return compositeZoom; 480 } 481 482 /** 483 * Converts a value in high-DPI pixels to the corresponding value in SWT points. 484 * <p> 485 * This method must only be called within the dynamic 486 * scope of a call to {@link #drawCompositeImage(int, int)}. 487 * </p> 488 * 489 * @param pixels a value in high-DPI pixels 490 * @return corresponding value in SWT points 491 * @since 3.13 492 */ autoScaleDown(int pixels)493 protected int autoScaleDown(int pixels) { 494 // @see SWT's internal DPIUtil#autoScaleDown(int) 495 if (compositeZoom == 100) { 496 return pixels; 497 } 498 float scaleFactor = compositeZoom / 100f; 499 return Math.round(pixels / scaleFactor); 500 } 501 502 /** 503 * Converts a value in SWT points to the corresponding value in high-DPI pixels. 504 * <p> 505 * This method must only be called within the dynamic 506 * scope of a call to {@link #drawCompositeImage(int, int)}. 507 * </p> 508 * 509 * @param points a value in SWT points 510 * @return corresponding value in high-DPI pixels 511 * @since 3.13 512 */ autoScaleUp(int points)513 protected int autoScaleUp(int points) { 514 // @see SWT's internal DPIUtil#autoScaleUp(int) 515 return scaleUp(points, compositeZoom); 516 } 517 518 /** 519 * Creates a new {@link CachedImageDataProvider} that is backed by the given 520 * image. This method and the resulting cached image data 521 * provider are only intended to be used within the dynamic scope of a call 522 * to {@link #drawCompositeImage(int, int)}. 523 * 524 * @param image 525 * the image, must not be null 526 * @return the new cached image provider 527 * @since 3.13 528 */ createCachedImageDataProvider(Image image)529 protected CachedImageDataProvider createCachedImageDataProvider(Image image) { 530 return new CachedImageImageDataProvider(image); 531 } 532 533 /** 534 * Creates a new {@link CachedImageDataProvider} that is backed by the given 535 * image descriptor. This method and the resulting cached image data 536 * provider are only intended to be used within the dynamic scope of a call 537 * to {@link #drawCompositeImage(int, int)}. 538 * <p> 539 * The provider returns {@link ImageDescriptor#getMissingImageDescriptor()} 540 * if the image descriptor unexpectedly provides a null image data at zoom 541 * == 100. 542 * 543 * @param imageDescriptor 544 * the image descriptor, must not be null 545 * @return the new cached image provider 546 * @since 3.13 547 */ createCachedImageDataProvider(ImageDescriptor imageDescriptor)548 protected CachedImageDataProvider createCachedImageDataProvider(ImageDescriptor imageDescriptor) { 549 return new CachedDescriptorImageDataProvider(imageDescriptor); 550 } 551 scaleUp(int points, int zoom)552 private static int scaleUp(int points, int zoom) { 553 if (zoom == 100) { 554 return points; 555 } 556 float scaleFactor = zoom / 100f; 557 return Math.round(points * scaleFactor); 558 } 559 } 560