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