1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import com.android.ide.common.api.Rect; 20 import com.android.ide.common.rendering.api.IImageFactory; 21 import com.android.sdklib.SdkConstants; 22 23 import org.eclipse.swt.SWT; 24 import org.eclipse.swt.SWTException; 25 import org.eclipse.swt.graphics.Device; 26 import org.eclipse.swt.graphics.GC; 27 import org.eclipse.swt.graphics.Image; 28 import org.eclipse.swt.graphics.ImageData; 29 import org.eclipse.swt.graphics.PaletteData; 30 31 import java.awt.image.BufferedImage; 32 import java.awt.image.DataBufferInt; 33 import java.awt.image.WritableRaster; 34 35 /** 36 * The {@link ImageOverlay} class renders an image as an overlay. 37 */ 38 public class ImageOverlay extends Overlay implements IImageFactory { 39 /** 40 * Whether the image should be pre-scaled (scaled to the zoom level) once 41 * instead of dynamically during each paint; this is necessary on some 42 * platforms (see issue #19447) 43 */ 44 private static final boolean PRESCALE = 45 // Currently this is necessary on Linux because the "Cairo" library 46 // seems to be a bottleneck 47 SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX 48 && !(Boolean.getBoolean("adt.noprescale")); //$NON-NLS-1$ 49 50 /** Current background image. Null when there's no image. */ 51 private Image mImage; 52 53 /** A pre-scaled version of the image */ 54 private Image mPreScaledImage; 55 56 /** Current background AWT image. This is created by {@link #getImage()}, which is called 57 * by the LayoutLib. */ 58 private BufferedImage mAwtImage; 59 60 /** The associated {@link LayoutCanvas}. */ 61 private LayoutCanvas mCanvas; 62 63 /** Vertical scaling & scrollbar information. */ 64 private CanvasTransform mVScale; 65 66 /** Horizontal scaling & scrollbar information. */ 67 private CanvasTransform mHScale; 68 69 /** 70 * Constructs an {@link ImageOverlay} tied to the given canvas. 71 * 72 * @param canvas The {@link LayoutCanvas} to paint the overlay over. 73 * @param hScale The horizontal scale information. 74 * @param vScale The vertical scale information. 75 */ ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale)76 public ImageOverlay(LayoutCanvas canvas, CanvasTransform hScale, CanvasTransform vScale) { 77 this.mCanvas = canvas; 78 this.mHScale = hScale; 79 this.mVScale = vScale; 80 } 81 82 @Override create(Device device)83 public void create(Device device) { 84 super.create(device); 85 } 86 87 @Override dispose()88 public void dispose() { 89 if (mImage != null) { 90 mImage.dispose(); 91 mImage = null; 92 } 93 if (mPreScaledImage != null) { 94 mPreScaledImage.dispose(); 95 mPreScaledImage = null; 96 } 97 } 98 99 /** 100 * Sets the image to be drawn as an overlay from the passed in AWT 101 * {@link BufferedImage} (which will be converted to an SWT image). 102 * <p/> 103 * The image <b>can</b> be null, which is the case when we are dealing with 104 * an empty document. 105 * 106 * @param awtImage The AWT image to be rendered as an SWT image. 107 * @param isAlphaChannelImage whether the alpha channel of the image is relevant 108 * @return The corresponding SWT image, or null. 109 */ setImage(BufferedImage awtImage, boolean isAlphaChannelImage)110 public synchronized Image setImage(BufferedImage awtImage, boolean isAlphaChannelImage) { 111 if (awtImage != mAwtImage || awtImage == null) { 112 mAwtImage = null; 113 114 if (mImage != null) { 115 mImage.dispose(); 116 } 117 118 if (awtImage == null) { 119 mImage = null; 120 } else { 121 mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, 122 isAlphaChannelImage, -1); 123 } 124 } else { 125 assert awtImage instanceof SwtReadyBufferedImage; 126 127 if (isAlphaChannelImage) { 128 mImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), awtImage, true, -1); 129 } else { 130 mImage = ((SwtReadyBufferedImage)awtImage).getSwtImage(); 131 } 132 } 133 134 if (mPreScaledImage != null) { 135 // Force refresh on next paint 136 mPreScaledImage.dispose(); 137 mPreScaledImage = null; 138 } 139 140 return mImage; 141 } 142 143 /** 144 * Returns the currently painted image, or null if none has been set 145 * 146 * @return the currently painted image or null 147 */ getImage()148 public Image getImage() { 149 return mImage; 150 } 151 152 @Override paint(GC gc)153 public synchronized void paint(GC gc) { 154 if (mImage != null) { 155 boolean valid = mCanvas.getViewHierarchy().isValid(); 156 if (!valid) { 157 gc_setAlpha(gc, 128); // half-transparent 158 } 159 160 CanvasTransform hi = mHScale; 161 CanvasTransform vi = mVScale; 162 163 // On some platforms, dynamic image scaling is very slow (see issue #19447) so 164 // compute a pre-scaled version of the image once and render that instead. 165 // This is done lazily in paint rather than when the image changes because 166 // the image must be rescaled each time the zoom level changes, which varies 167 // independently from when the image changes. 168 if (PRESCALE && mAwtImage != null) { 169 if (mPreScaledImage == null || 170 mPreScaledImage.getImageData().width != hi.getScalledImgSize()) { 171 double xScale = hi.getScalledImgSize() / (double) mAwtImage.getWidth(); 172 double yScale = vi.getScalledImgSize() / (double) mAwtImage.getHeight(); 173 BufferedImage scaledAwtImage; 174 175 // NOTE: == comparison on floating point numbers is okay 176 // here because we normalize the scaling factor 177 // to an exact 1.0 in the zooming code when the value gets 178 // near 1.0 to make painting more efficient in the presence 179 // of rounding errors. 180 if (xScale == 1.0 && yScale == 1.0) { 181 // Scaling to 100% is easy! 182 scaledAwtImage = mAwtImage; 183 } else { 184 scaledAwtImage = ImageUtils.scale(mAwtImage, xScale, yScale); 185 } 186 assert scaledAwtImage.getWidth() == hi.getScalledImgSize(); 187 mPreScaledImage = SwtUtils.convertToSwt(mCanvas.getDisplay(), scaledAwtImage, 188 true /*transferAlpha*/, -1); 189 } 190 191 if (mPreScaledImage != null) { 192 gc.drawImage(mPreScaledImage, hi.translate(0), vi.translate(0)); 193 } 194 return; 195 } 196 197 // we only anti-alias when reducing the image size. 198 int oldAlias = -2; 199 if (hi.getScale() < 1.0) { 200 oldAlias = gc_setAntialias(gc, SWT.ON); 201 } 202 203 gc.drawImage( 204 mImage, 205 0, // srcX 206 0, // srcY 207 hi.getImgSize(), // srcWidth 208 vi.getImgSize(), // srcHeight 209 hi.translate(0), // destX 210 vi.translate(0), // destY 211 hi.getScalledImgSize(), // destWidth 212 vi.getScalledImgSize()); // destHeight 213 214 if (oldAlias != -2) { 215 gc_setAntialias(gc, oldAlias); 216 } 217 218 if (!valid) { 219 gc_setAlpha(gc, 255); // opaque 220 } 221 } 222 } 223 224 /** 225 * Sets the alpha for the given GC. 226 * <p/> 227 * Alpha may not work on all platforms and may fail with an exception, which 228 * is hidden here (false is returned in that case). 229 * 230 * @param gc the GC to change 231 * @param alpha the new alpha, 0 for transparent, 255 for opaque. 232 * @return True if the operation worked, false if it failed with an 233 * exception. 234 * @see GC#setAlpha(int) 235 */ gc_setAlpha(GC gc, int alpha)236 private boolean gc_setAlpha(GC gc, int alpha) { 237 try { 238 gc.setAlpha(alpha); 239 return true; 240 } catch (SWTException e) { 241 return false; 242 } 243 } 244 245 /** 246 * Sets the non-text antialias flag for the given GC. 247 * <p/> 248 * Antialias may not work on all platforms and may fail with an exception, 249 * which is hidden here (-2 is returned in that case). 250 * 251 * @param gc the GC to change 252 * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}. 253 * @return The previous aliasing mode if the operation worked, or -2 if it 254 * failed with an exception. 255 * @see GC#setAntialias(int) 256 */ gc_setAntialias(GC gc, int alias)257 private int gc_setAntialias(GC gc, int alias) { 258 try { 259 int old = gc.getAntialias(); 260 gc.setAntialias(alias); 261 return old; 262 } catch (SWTException e) { 263 return -2; 264 } 265 } 266 267 /** 268 * Custom {@link BufferedImage} class able to convert itself into an SWT {@link Image} 269 * efficiently. 270 * 271 * The BufferedImage also contains an instance of {@link ImageData} that's kept around 272 * and used to create new SWT {@link Image} objects in {@link #getSwtImage()}. 273 * 274 */ 275 private static final class SwtReadyBufferedImage extends BufferedImage { 276 277 private final ImageData mImageData; 278 private final Device mDevice; 279 280 /** 281 * Creates the image with a given model, raster and SWT {@link ImageData} 282 * @param model the color model 283 * @param raster the image raster 284 * @param imageData the SWT image data. 285 * @param device the {@link Device} in which the SWT image will be painted. 286 */ SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device)287 private SwtReadyBufferedImage(int width, int height, ImageData imageData, Device device) { 288 super(width, height, BufferedImage.TYPE_INT_ARGB); 289 mImageData = imageData; 290 mDevice = device; 291 } 292 293 /** 294 * Returns a new {@link Image} object initialized with the content of the BufferedImage. 295 * @return the image object. 296 */ getSwtImage()297 private Image getSwtImage() { 298 // transfer the content of the bufferedImage into the image data. 299 WritableRaster raster = getRaster(); 300 int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData(); 301 302 mImageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0); 303 304 return new Image(mDevice, mImageData); 305 } 306 307 /** 308 * Creates a new {@link SwtReadyBufferedImage}. 309 * @param w the width of the image 310 * @param h the height of the image 311 * @param device the device in which the SWT image will be painted 312 * @return a new {@link SwtReadyBufferedImage} object 313 */ createImage(int w, int h, Device device)314 private static SwtReadyBufferedImage createImage(int w, int h, Device device) { 315 ImageData imageData = new ImageData(w, h, 32, 316 new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF)); 317 318 SwtReadyBufferedImage swtReadyImage = new SwtReadyBufferedImage(w, h, 319 imageData, device); 320 321 return swtReadyImage; 322 } 323 } 324 325 /** 326 * Implementation of {@link IImageFactory#getImage(int, int)}. 327 */ getImage(int w, int h)328 public BufferedImage getImage(int w, int h) { 329 if (mAwtImage == null || 330 mAwtImage.getWidth() != w || 331 mAwtImage.getHeight() != h) { 332 333 mAwtImage = SwtReadyBufferedImage.createImage(w, h, getDevice()); 334 } 335 336 return mAwtImage; 337 } 338 339 /** 340 * Returns the bounds of the current image, or null 341 * 342 * @return the bounds of the current image, or null 343 */ getImageBounds()344 public Rect getImageBounds() { 345 if (mImage == null) { 346 return null; 347 } 348 349 return new Rect(0, 0, mImage.getImageData().width, mImage.getImageData().height); 350 } 351 } 352