| /******************************************************************************* |
| * Copyright (c) 2000, 2014 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Stephan Wahlbrink - fix for bug 341702 - incorrect mixing of images with alpha channel |
| *******************************************************************************/ |
| package org.eclipse.jface.resource; |
| |
| import java.util.Objects; |
| import java.util.function.ToIntFunction; |
| |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.ImageData; |
| import org.eclipse.swt.graphics.ImageDataProvider; |
| import org.eclipse.swt.graphics.PaletteData; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.RGB; |
| import org.eclipse.swt.graphics.Rectangle; |
| |
| /** |
| * Abstract base class for image descriptors that synthesize an image from other |
| * images in order to simulate the effect of custom drawing. For example, this |
| * could be used to superimpose a red bar dexter symbol across an image to |
| * indicate that something was disallowed. |
| * <p> |
| * Subclasses must implement {@link #getSize()} and {@link #drawImage(ImageDataProvider, int, int)}. |
| * Little or no work happens until the image descriptor's image is |
| * actually requested by a call to <code>createImage</code> (or to |
| * <code>getImageData</code> directly). |
| * </p> |
| * @see org.eclipse.jface.viewers.DecorationOverlayIcon |
| */ |
| public abstract class CompositeImageDescriptor extends ImageDescriptor { |
| |
| /** |
| * An {@link ImageDataProvider} that caches the most recently returned |
| * {@link ImageData} object. I.e. consecutive calls to |
| * {@link #getImageData(int)} with the same zoom level are cheap. |
| * |
| * @since 3.13 |
| * @noextend This class is not intended to be subclassed by clients. |
| */ |
| protected abstract class CachedImageDataProvider implements ImageDataProvider { |
| /** |
| * Returns the {@link ImageData#width} in points. This method must only |
| * be called within the dynamic scope of a call to |
| * {@link #drawCompositeImage(int, int)}. |
| * |
| * @return the width in points |
| */ |
| public int getWidth() { |
| return computeInPoints(imageData -> imageData.width); |
| } |
| |
| /** |
| * Returns the {@link ImageData#height} in points. This method must only |
| * be called within the dynamic scope of a call to |
| * {@link #drawCompositeImage(int, int)}. |
| * |
| * @return the height in points |
| */ |
| public int getHeight() { |
| return computeInPoints(imageData -> imageData.height); |
| } |
| |
| /** |
| * Returns a computed value in SWT logical points. The given function |
| * computes a value in pixels, based on information from the given |
| * ImageData, which is also in pixels. This method must only |
| * be called within the dynamic scope of a call to |
| * {@link #drawCompositeImage(int, int)}. |
| * |
| * @param function |
| * a function that takes an {@link ImageData} and computes a |
| * value in pixels |
| * @return the computed value in points |
| */ |
| public int computeInPoints(ToIntFunction<ImageData> function) { |
| ImageData overlayData = getImageData(getZoomLevel()); |
| if (overlayData != null) { |
| int valueInPixels = function.applyAsInt(overlayData); |
| return autoScaleDown(valueInPixels); |
| } |
| overlayData = getImageData(100); |
| return function.applyAsInt(overlayData); |
| } |
| } |
| |
| private final class CachedImageImageDataProvider extends CachedImageDataProvider { |
| final Image baseImage; |
| ImageData cached; |
| int cachedZoom; |
| |
| private CachedImageImageDataProvider(Image baseImage) { |
| this.baseImage = Objects.requireNonNull(baseImage); |
| } |
| |
| @Override |
| public ImageData getImageData(int zoom) { |
| if (zoom == cachedZoom) { |
| return cached; |
| } |
| // Workaround for the missing API Image#getImageData(int zoom) (bug 496409). |
| // Can't use zoom == getZoomLevel() because SWT on Cocoa asks for 100% image even on Retina screen! |
| if (zoom == 100) { |
| cached = baseImage.getImageData(); |
| } else if (isAtCurrentZoom(baseImage, zoom)) { |
| cached = baseImage.getImageDataAtCurrentZoom(); |
| } else { |
| // strange zoom value, should not happen |
| zoom = 0; |
| cached = null; |
| } |
| cachedZoom = zoom; |
| return cached; |
| } |
| |
| private /*static*/ boolean isAtCurrentZoom(Image image, int zoom) { |
| Rectangle bounds= image.getBounds(); |
| Rectangle boundsInPixels= image.getBoundsInPixels(); |
| //TODO: Probably has off-by-one problems at fractional zoom levels: |
| return bounds.width == scaleDown(boundsInPixels.width, zoom) |
| || bounds.height == scaleDown(boundsInPixels.height, zoom); |
| } |
| |
| private /*static*/ int scaleDown(int value, int zoom) { |
| // @see SWT's internal DPIUtil#autoScaleDown(int) |
| float scaleFactor = zoom / 100f; |
| return Math.round(value / scaleFactor); |
| } |
| } |
| |
| private final class CachedDescriptorImageDataProvider extends CachedImageDataProvider { |
| final ImageDescriptor descriptor; |
| ImageData cached; |
| int cachedZoom; |
| |
| private CachedDescriptorImageDataProvider(ImageDescriptor descriptor) { |
| this.descriptor = Objects.requireNonNull(descriptor); |
| } |
| |
| @Override |
| public ImageData getImageData(int zoom) { |
| if (zoom == cachedZoom) { |
| return cached; |
| } |
| ImageData zoomed = descriptor.getImageData(zoom); |
| if (zoomed != null) { |
| cached = zoomed; |
| cachedZoom = zoom; |
| return zoomed; |
| } |
| if (zoom == 100) { |
| return ImageDescriptor.getMissingImageDescriptor().getImageData(100); |
| } |
| |
| ImageData data100 = descriptor.getImageData(100); |
| if (data100 != null) { |
| // 100% is available => caller will have to scale this one |
| cached = data100; |
| cachedZoom = 100; |
| return null; |
| } |
| // 100% is not available, but requested zoom != 100 |
| // => caller will have to scale missing image descriptor |
| return null; |
| } |
| } |
| |
| /** |
| * The image data for this composite image. |
| */ |
| private ImageData imageData; |
| |
| /** |
| * The zoom level for this composite image. Only valid within the dynamic |
| * scope of a call to {@link #drawCompositeImage(int, int)}. |
| */ |
| private int compositeZoom; |
| |
| /** |
| * Constructs an uninitialized composite image. |
| */ |
| protected CompositeImageDescriptor() { |
| } |
| |
| /** |
| * Draw the composite images. |
| * <p> |
| * Subclasses must implement this framework method to paint images within |
| * the given bounds using one or more calls to the |
| * {@link #drawImage(ImageDataProvider, int, int)} framework method. |
| * </p> |
| * <p> |
| * Implementers that need to perform computations based on the size of |
| * another image are advised to use one of the |
| * {@link #createCachedImageDataProvider} methods to create a |
| * {@link CachedImageDataProvider} that can serve as |
| * {@link ImageDataProvider}. The {@link CachedImageDataProvider} offers |
| * other interesting methods like {@link CachedImageDataProvider#getWidth() |
| * getWidth()} or |
| * {@link CachedImageDataProvider#computeInPoints(ToIntFunction) |
| * computeInPoints(...)} that can be useful to compute values in points, |
| * based on the resolution-dependent {@link ImageData} that is applicable |
| * for the current drawing operation. |
| * </p> |
| * |
| * @param width |
| * the width |
| * @param height |
| * the height |
| * @see #drawImage(ImageDataProvider, int, int) |
| * @see #createCachedImageDataProvider(Image) |
| * @see #createCachedImageDataProvider(ImageDescriptor) |
| */ |
| protected abstract void drawCompositeImage(int width, int height); |
| |
| /** |
| * Draws the given source image data into this composite image at the given |
| * position. |
| * <p> |
| * Call this internal framework method to superimpose another image atop |
| * this composite image. |
| * </p> |
| * |
| * @param src |
| * the source image data |
| * @param ox |
| * the x position |
| * @param oy |
| * the y position |
| * @deprecated Use {@link #drawImage(ImageDataProvider, int, int)} instead. |
| * Replace the code that created the ImageData by calls to |
| * {@link #createCachedImageDataProvider(Image)} or |
| * {@link #createCachedImageDataProvider(ImageDescriptor)} and |
| * then pass on that provider instead of ImageData objects. |
| * Replace references to {@link ImageData#width}/height by calls |
| * to {@link CachedImageDataProvider#getWidth()}/getHeight(). |
| */ |
| @Deprecated |
| final protected void drawImage(ImageData src, int ox, int oy) { |
| if (src == null) { // wrong hack for https://bugs.eclipse.org/372956 , kept for compatibility with broken client code |
| return; |
| } |
| drawImage(getUnzoomedImageDataProvider(src), ox, oy); |
| } |
| |
| private static ImageDataProvider getUnzoomedImageDataProvider(ImageData imageData) { |
| return zoom -> zoom == 100 ? imageData : null; |
| } |
| |
| /** |
| * Draws the given source image data into this composite image at the given |
| * position. |
| * <p> |
| * Subclasses call this framework method to superimpose another image atop |
| * this composite image. This method must only be called within the dynamic |
| * scope of a call to {@link #drawCompositeImage(int, int)}. |
| * </p> |
| * |
| * @param srcProvider |
| * the source image data provider |
| * @param ox |
| * the x position |
| * @param oy |
| * the y position |
| * @since 3.13 |
| */ |
| final protected void drawImage(ImageDataProvider srcProvider, int ox, int oy) { |
| ImageData dst = imageData; |
| ImageData src = getZoomedImageData(srcProvider); |
| |
| PaletteData srcPalette = src.palette; |
| ImageData srcMask = null; |
| int alphaMask = 0, alphaShift = 0; |
| if (src.maskData != null) { |
| srcMask = src.getTransparencyMask (); |
| if (src.depth == 32) { |
| alphaMask = ~(srcPalette.redMask | srcPalette.greenMask | srcPalette.blueMask); |
| while (alphaMask != 0 && ((alphaMask >>> alphaShift) & 1) == 0) alphaShift++; |
| } |
| } |
| for (int srcY = 0, dstY = srcY + autoScaleUp(oy); srcY < src.height; srcY++, dstY++) { |
| for (int srcX = 0, dstX = srcX + autoScaleUp(ox); srcX < src.width; srcX++, dstX++) { |
| if (!(0 <= dstX && dstX < dst.width && 0 <= dstY && dstY < dst.height)) continue; |
| int srcPixel = src.getPixel(srcX, srcY); |
| int srcAlpha = 255; |
| if (src.maskData != null) { |
| if (src.depth == 32) { |
| srcAlpha = (srcPixel & alphaMask) >>> alphaShift; |
| if (srcAlpha == 0) { |
| srcAlpha = srcMask.getPixel(srcX, srcY) != 0 ? 255 : 0; |
| } |
| } else { |
| if (srcMask.getPixel(srcX, srcY) == 0) srcAlpha = 0; |
| } |
| } else if (src.transparentPixel != -1) { |
| if (src.transparentPixel == srcPixel) srcAlpha = 0; |
| } else if (src.alpha != -1) { |
| srcAlpha = src.alpha; |
| } else if (src.alphaData != null) { |
| srcAlpha = src.getAlpha(srcX, srcY); |
| } |
| if (srcAlpha == 0) continue; |
| int srcRed, srcGreen, srcBlue; |
| if (srcPalette.isDirect) { |
| srcRed = srcPixel & srcPalette.redMask; |
| srcRed = (srcPalette.redShift < 0) ? srcRed >>> -srcPalette.redShift : srcRed << srcPalette.redShift; |
| srcGreen = srcPixel & srcPalette.greenMask; |
| srcGreen = (srcPalette.greenShift < 0) ? srcGreen >>> -srcPalette.greenShift : srcGreen << srcPalette.greenShift; |
| srcBlue = srcPixel & srcPalette.blueMask; |
| srcBlue = (srcPalette.blueShift < 0) ? srcBlue >>> -srcPalette.blueShift : srcBlue << srcPalette.blueShift; |
| } else { |
| RGB rgb = srcPalette.getRGB(srcPixel); |
| srcRed = rgb.red; |
| srcGreen = rgb.green; |
| srcBlue = rgb.blue; |
| } |
| int dstRed, dstGreen, dstBlue, dstAlpha; |
| if (srcAlpha == 255) { |
| dstRed = srcRed; |
| dstGreen = srcGreen; |
| dstBlue= srcBlue; |
| dstAlpha = srcAlpha; |
| } else { |
| int dstPixel = dst.getPixel(dstX, dstY); |
| dstAlpha = dst.getAlpha(dstX, dstY); |
| dstRed = (dstPixel & 0xFF) >>> 0; |
| dstGreen = (dstPixel & 0xFF00) >>> 8; |
| dstBlue = (dstPixel & 0xFF0000) >>> 16; |
| if (dstAlpha == 255) { // simplified calculations for performance |
| dstRed += (srcRed - dstRed) * srcAlpha / 255; |
| dstGreen += (srcGreen - dstGreen) * srcAlpha / 255; |
| dstBlue += (srcBlue - dstBlue) * srcAlpha / 255; |
| } else { |
| // See Porter T., Duff T. 1984. "Compositing Digital Images". |
| // Computer Graphics 18 (3): 253-259. |
| dstRed = srcRed * srcAlpha * 255 + dstRed * dstAlpha * (255 - srcAlpha); |
| dstGreen = srcGreen * srcAlpha * 255 + dstGreen * dstAlpha * (255 - srcAlpha); |
| dstBlue = srcBlue * srcAlpha * 255 + dstBlue * dstAlpha * (255 - srcAlpha); |
| dstAlpha = srcAlpha * 255 + dstAlpha * (255 - srcAlpha); |
| if (dstAlpha != 0) { // if both original alphas == 0, then all colors are 0 |
| dstRed /= dstAlpha; |
| dstGreen /= dstAlpha; |
| dstBlue /= dstAlpha; |
| dstAlpha /= 255; |
| } |
| } |
| } |
| dst.setPixel(dstX, dstY, ((dstRed & 0xFF) << 0) | ((dstGreen & 0xFF) << 8) | ((dstBlue & 0xFF) << 16)); |
| dst.setAlpha(dstX, dstY, dstAlpha); |
| } |
| } |
| } |
| |
| /** |
| * @deprecated Use {@link #getImageData(int)} instead. |
| */ |
| @Deprecated |
| @Override |
| public ImageData getImageData() { |
| return getImageData(100); |
| } |
| |
| @Override |
| public ImageData getImageData(int zoom) { |
| if (!supportsZoomLevel(zoom)) { |
| return null; |
| } |
| Point size = getSize(); |
| |
| /* Create a 24 bit image data with alpha channel */ |
| imageData = new ImageData(scaleUp(size.x, zoom), scaleUp(size.y, zoom), 24, |
| new PaletteData(0xFF, 0xFF00, 0xFF0000)); |
| imageData.alphaData = new byte[imageData.width * imageData.height]; |
| compositeZoom = zoom; |
| |
| drawCompositeImage(size.x, size.y); |
| |
| /* Detect minimum transparency */ |
| boolean transparency = false; |
| byte[] alphaData = imageData.alphaData; |
| for (byte element : alphaData) { |
| int alpha = element & 0xFF; |
| if (!(alpha == 0 || alpha == 255)) { |
| /* Full alpha channel transparency */ |
| return imageData; |
| } |
| if (!transparency && alpha == 0) transparency = true; |
| } |
| if (transparency) { |
| /* Reduce to 1-bit alpha channel transparency */ |
| PaletteData palette = new PaletteData(new RGB[]{new RGB(0, 0, 0), new RGB(255, 255, 255)}); |
| ImageData mask = new ImageData(imageData.width, imageData.height, 1, palette); |
| for (int y = 0; y < mask.height; y++) { |
| for (int x = 0; x < mask.width; x++) { |
| mask.setPixel(x, y, imageData.getAlpha(x, y) == 255 ? 1 : 0); |
| } |
| } |
| } else { |
| /* no transparency */ |
| imageData.alphaData = null; |
| } |
| return imageData; |
| } |
| |
| |
| /** |
| * Return the transparent pixel for the receiver. |
| * <strong>NOTE</strong> This value is not currently in use in the |
| * default implementation. |
| * @return int |
| * @since 3.3 |
| */ |
| protected int getTransparentPixel() { |
| return 0; |
| } |
| |
| /** |
| * Return the size of this composite image. |
| * <p> |
| * Subclasses must implement this framework method. |
| * </p> |
| * |
| * @return the x and y size of the image expressed as a point object |
| */ |
| protected abstract Point getSize(); |
| |
| /** |
| * Do not call this method! Behavior is unspecified. |
| * |
| * @param imageData unspecified |
| * @since 3.3 |
| * @deprecated This method doesn't make sense and should never have been |
| * made API. |
| */ |
| @Deprecated |
| protected void setImageData(ImageData imageData) { |
| this.imageData = imageData; |
| } |
| |
| /** |
| * Returns whether the given zoom level is supported by this |
| * CompositeImageDescriptor. |
| * |
| * @param zoom |
| * the zoom level |
| * @return whether the given zoom level is supported. Must return true for |
| * {@code zoom == 100}. |
| * @since 3.13 |
| */ |
| protected boolean supportsZoomLevel(int zoom) { |
| // Currently only support integer zoom levels, because getZoomedImageData(..) |
| // suffers from Bug 97506: [HiDPI] ImageData.scaledTo() should use a |
| // better interpolation method. |
| return zoom > 0 && zoom % 100 == 0; |
| } |
| |
| private ImageData getZoomedImageData(ImageDataProvider srcProvider) { |
| ImageData src = srcProvider.getImageData(compositeZoom); |
| if (src == null) { |
| ImageData src100 = srcProvider.getImageData(100); |
| src = src100.scaledTo(autoScaleUp(src100.width), autoScaleUp(src100.height)); |
| } |
| return src; |
| } |
| |
| /** |
| * Returns the current zoom level. |
| * <p> |
| * <b>Important:</b> This method must only be called within the dynamic scope of a call to |
| * {@link #drawCompositeImage(int, int)}. |
| * </p> |
| * |
| * @return The zoom level in % of the standard resolution (which is 1 |
| * physical monitor pixel == 1 SWT logical point). Typically 100, |
| * 150, or 200. |
| * @since 3.13 |
| */ |
| protected int getZoomLevel() { |
| return compositeZoom; |
| } |
| |
| /** |
| * Converts a value in high-DPI pixels to the corresponding value in SWT points. |
| * <p> |
| * This method must only be called within the dynamic |
| * scope of a call to {@link #drawCompositeImage(int, int)}. |
| * </p> |
| * |
| * @param pixels a value in high-DPI pixels |
| * @return corresponding value in SWT points |
| * @since 3.13 |
| */ |
| protected int autoScaleDown(int pixels) { |
| // @see SWT's internal DPIUtil#autoScaleDown(int) |
| if (compositeZoom == 100) { |
| return pixels; |
| } |
| float scaleFactor = compositeZoom / 100f; |
| return Math.round(pixels / scaleFactor); |
| } |
| |
| /** |
| * Converts a value in SWT points to the corresponding value in high-DPI pixels. |
| * <p> |
| * This method must only be called within the dynamic |
| * scope of a call to {@link #drawCompositeImage(int, int)}. |
| * </p> |
| * |
| * @param points a value in SWT points |
| * @return corresponding value in high-DPI pixels |
| * @since 3.13 |
| */ |
| protected int autoScaleUp(int points) { |
| // @see SWT's internal DPIUtil#autoScaleUp(int) |
| return scaleUp(points, compositeZoom); |
| } |
| |
| /** |
| * Creates a new {@link CachedImageDataProvider} that is backed by the given |
| * image. This method and the resulting cached image data |
| * provider are only intended to be used within the dynamic scope of a call |
| * to {@link #drawCompositeImage(int, int)}. |
| * |
| * @param image |
| * the image, must not be null |
| * @return the new cached image provider |
| * @since 3.13 |
| */ |
| protected CachedImageDataProvider createCachedImageDataProvider(Image image) { |
| return new CachedImageImageDataProvider(image); |
| } |
| |
| /** |
| * Creates a new {@link CachedImageDataProvider} that is backed by the given |
| * image descriptor. This method and the resulting cached image data |
| * provider are only intended to be used within the dynamic scope of a call |
| * to {@link #drawCompositeImage(int, int)}. |
| * <p> |
| * The provider returns {@link ImageDescriptor#getMissingImageDescriptor()} |
| * if the image descriptor unexpectedly provides a null image data at zoom |
| * == 100. |
| * |
| * @param imageDescriptor |
| * the image descriptor, must not be null |
| * @return the new cached image provider |
| * @since 3.13 |
| */ |
| protected CachedImageDataProvider createCachedImageDataProvider(ImageDescriptor imageDescriptor) { |
| return new CachedDescriptorImageDataProvider(imageDescriptor); |
| } |
| |
| private static int scaleUp(int points, int zoom) { |
| if (zoom == 100) { |
| return points; |
| } |
| float scaleFactor = zoom / 100f; |
| return Math.round(points * scaleFactor); |
| } |
| } |