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