1 /* 2 * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.embed.swing; 27 28 import java.awt.AlphaComposite; 29 import java.awt.Graphics2D; 30 import java.awt.image.BufferedImage; 31 import java.nio.IntBuffer; 32 import java.util.Set; 33 import java.util.HashSet; 34 import javafx.application.Platform; 35 import javafx.scene.image.Image; 36 import javafx.scene.image.PixelFormat; 37 import javafx.scene.image.PixelReader; 38 import javafx.scene.image.PixelWriter; 39 import javafx.scene.image.WritableImage; 40 import javafx.scene.image.WritablePixelFormat; 41 import javafx.scene.paint.Color; 42 import com.sun.javafx.tk.Toolkit; 43 import javax.swing.SwingUtilities; 44 45 import jdk.swing.interop.SwingInterOpUtils; 46 47 /** 48 * This class provides utility methods for converting data types between 49 * Swing/AWT and JavaFX formats. 50 * @since JavaFX 2.2 51 */ 52 public class SwingFXUtils { 53 private SwingFXUtils() {} // no instances 54 55 /** 56 * Snapshots the specified {@link BufferedImage} and stores a copy of 57 * its pixels into a JavaFX {@link Image} object, creating a new 58 * object if needed. 59 * The returned {@code Image} will be a static snapshot of the state 60 * of the pixels in the {@code BufferedImage} at the time the method 61 * completes. Further changes to the {@code BufferedImage} will not 62 * be reflected in the {@code Image}. 63 * <p> 64 * The optional JavaFX {@link WritableImage} parameter may be reused 65 * to store the copy of the pixels. 66 * A new {@code Image} will be created if the supplied object is null, 67 * is too small or of a type which the image pixels cannot be easily 68 * converted into. 69 * 70 * @param bimg the {@code BufferedImage} object to be converted 71 * @param wimg an optional {@code WritableImage} object that can be 72 * used to store the returned pixel data 73 * @return an {@code Image} object representing a snapshot of the 74 * current pixels in the {@code BufferedImage}. 75 * @since JavaFX 2.2 76 */ 77 public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) { 78 int bw = bimg.getWidth(); 79 int bh = bimg.getHeight(); 80 switch (bimg.getType()) { 81 case BufferedImage.TYPE_INT_ARGB: 82 case BufferedImage.TYPE_INT_ARGB_PRE: 83 break; 84 default: 85 BufferedImage converted = 86 new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE); 87 Graphics2D g2d = converted.createGraphics(); 88 g2d.drawImage(bimg, 0, 0, null); 89 g2d.dispose(); 90 bimg = converted; 91 break; 92 } 93 // assert(bimg.getType == TYPE_INT_ARGB[_PRE]); 94 if (wimg != null) { 95 int iw = (int) wimg.getWidth(); 96 int ih = (int) wimg.getHeight(); 97 if (iw < bw || ih < bh) { 98 wimg = null; 99 } else if (bw < iw || bh < ih) { 100 int empty[] = new int[iw]; 101 PixelWriter pw = wimg.getPixelWriter(); 102 PixelFormat<IntBuffer> pf = PixelFormat.getIntArgbPreInstance(); 103 if (bw < iw) { 104 pw.setPixels(bw, 0, iw-bw, bh, pf, empty, 0, 0); 105 } 106 if (bh < ih) { 107 pw.setPixels(0, bh, iw, ih-bh, pf, empty, 0, 0); 108 } 109 } 110 } 111 if (wimg == null) { 112 wimg = new WritableImage(bw, bh); 113 } 114 PixelWriter pw = wimg.getPixelWriter(); 115 int data[] = SwingInterOpUtils.getData(bimg); 116 int offset = SwingInterOpUtils.getOffset(bimg); 117 int scan = SwingInterOpUtils.getScanlineStride(bimg); 118 119 PixelFormat<IntBuffer> pf = (bimg.isAlphaPremultiplied() ? 120 PixelFormat.getIntArgbPreInstance() : 121 PixelFormat.getIntArgbInstance()); 122 pw.setPixels(0, 0, bw, bh, pf, data, offset, scan); 123 return wimg; 124 } 125 126 /** 127 * Determine the optimal BufferedImage type to use for the specified 128 * {@code fxFormat} allowing for the specified {@code bimg} to be used 129 * as a potential default storage space if it is not null and is compatible. 130 * 131 * @param fxFormat the PixelFormat of the source FX Image 132 * @param bimg an optional existing {@code BufferedImage} to be used 133 * for storage if it is compatible, or null 134 * @return 135 */ 136 static int 137 getBestBufferedImageType(PixelFormat<?> fxFormat, BufferedImage bimg, 138 boolean isOpaque) 139 { 140 if (bimg != null) { 141 int bimgType = bimg.getType(); 142 if (bimgType == BufferedImage.TYPE_INT_ARGB || 143 bimgType == BufferedImage.TYPE_INT_ARGB_PRE || 144 (isOpaque && 145 (bimgType == BufferedImage.TYPE_INT_BGR || 146 bimgType == BufferedImage.TYPE_INT_RGB))) 147 { 148 // We will allow the caller to give us a BufferedImage 149 // that has an alpha channel, but we might not otherwise 150 // construct one ourselves. 151 // We will also allow them to choose their own premultiply 152 // type which may not match the image. 153 // If left to our own devices we might choose a more specific 154 // format as indicated by the choices below. 155 return bimgType; 156 } 157 } 158 switch (fxFormat.getType()) { 159 default: 160 case BYTE_BGRA_PRE: 161 case INT_ARGB_PRE: 162 return BufferedImage.TYPE_INT_ARGB_PRE; 163 case BYTE_BGRA: 164 case INT_ARGB: 165 return BufferedImage.TYPE_INT_ARGB; 166 case BYTE_RGB: 167 return BufferedImage.TYPE_INT_RGB; 168 case BYTE_INDEXED: 169 return (fxFormat.isPremultiplied() 170 ? BufferedImage.TYPE_INT_ARGB_PRE 171 : BufferedImage.TYPE_INT_ARGB); 172 } 173 } 174 175 /** 176 * Determine the appropriate {@link WritablePixelFormat} type that can 177 * be used to transfer data into the indicated BufferedImage. 178 * 179 * @param bimg the BufferedImage that will be used as a destination for 180 * a {@code PixelReader<IntBuffer>#getPixels()} operation. 181 * @return 182 */ 183 private static WritablePixelFormat<IntBuffer> 184 getAssociatedPixelFormat(BufferedImage bimg) 185 { 186 switch (bimg.getType()) { 187 // We lie here for xRGB, but we vetted that the src data was opaque 188 // so we can ignore the alpha. We use ArgbPre instead of Argb 189 // just to get a loop that does not have divides in it if the 190 // PixelReader happens to not know the data is opaque. 191 case BufferedImage.TYPE_INT_RGB: 192 case BufferedImage.TYPE_INT_ARGB_PRE: 193 return PixelFormat.getIntArgbPreInstance(); 194 case BufferedImage.TYPE_INT_ARGB: 195 return PixelFormat.getIntArgbInstance(); 196 default: 197 // Should not happen... 198 throw new InternalError("Failed to validate BufferedImage type"); 199 } 200 } 201 202 private static boolean checkFXImageOpaque(PixelReader pr, int iw, int ih) { 203 for (int x = 0; x < iw; x++) { 204 for (int y = 0; y < ih; y++) { 205 Color color = pr.getColor(x,y); 206 if (color.getOpacity() != 1.0) { 207 return false; 208 } 209 } 210 } 211 return true; 212 } 213 214 /** 215 * Snapshots the specified JavaFX {@link Image} object and stores a 216 * copy of its pixels into a {@link BufferedImage} object, creating 217 * a new object if needed. 218 * The method will only convert a JavaFX {@code Image} that is readable 219 * as per the conditions on the 220 * {@link Image#getPixelReader() Image.getPixelReader()} 221 * method. 222 * If the {@code Image} is not readable, as determined by its 223 * {@code getPixelReader()} method, then this method will return null. 224 * If the {@code Image} is a writable, or other dynamic image, then 225 * the {@code BufferedImage} will only be set to the current state of 226 * the pixels in the image as determined by its {@link PixelReader}. 227 * Further changes to the pixels of the {@code Image} will not be 228 * reflected in the returned {@code BufferedImage}. 229 * <p> 230 * The optional {@code BufferedImage} parameter may be reused to store 231 * the copy of the pixels. 232 * A new {@code BufferedImage} will be created if the supplied object 233 * is null, is too small or of a type which the image pixels cannot 234 * be easily converted into. 235 * 236 * @param img the JavaFX {@code Image} to be converted 237 * @param bimg an optional {@code BufferedImage} object that may be 238 * used to store the returned pixel data 239 * @return a {@code BufferedImage} containing a snapshot of the JavaFX 240 * {@code Image}, or null if the {@code Image} is not readable. 241 * @since JavaFX 2.2 242 */ 243 public static BufferedImage fromFXImage(Image img, BufferedImage bimg) { 244 PixelReader pr = img.getPixelReader(); 245 if (pr == null) { 246 return null; 247 } 248 int iw = (int) img.getWidth(); 249 int ih = (int) img.getHeight(); 250 PixelFormat<?> fxFormat = pr.getPixelFormat(); 251 boolean srcPixelsAreOpaque = false; 252 switch (fxFormat.getType()) { 253 case INT_ARGB_PRE: 254 case INT_ARGB: 255 case BYTE_BGRA_PRE: 256 case BYTE_BGRA: 257 // Check fx image opacity only if 258 // supplied BufferedImage is without alpha channel 259 if (bimg != null && 260 (bimg.getType() == BufferedImage.TYPE_INT_BGR || 261 bimg.getType() == BufferedImage.TYPE_INT_RGB)) { 262 srcPixelsAreOpaque = checkFXImageOpaque(pr, iw, ih); 263 } 264 break; 265 case BYTE_RGB: 266 srcPixelsAreOpaque = true; 267 break; 268 } 269 int prefBimgType = getBestBufferedImageType(pr.getPixelFormat(), bimg, srcPixelsAreOpaque); 270 if (bimg != null) { 271 int bw = bimg.getWidth(); 272 int bh = bimg.getHeight(); 273 if (bw < iw || bh < ih || bimg.getType() != prefBimgType) { 274 bimg = null; 275 } else if (iw < bw || ih < bh) { 276 Graphics2D g2d = bimg.createGraphics(); 277 g2d.setComposite(AlphaComposite.Clear); 278 g2d.fillRect(0, 0, bw, bh); 279 g2d.dispose(); 280 } 281 } 282 if (bimg == null) { 283 bimg = new BufferedImage(iw, ih, prefBimgType); 284 } 285 int offset = SwingInterOpUtils.getOffset(bimg); 286 int scan = SwingInterOpUtils.getScanlineStride(bimg); 287 int data[] = SwingInterOpUtils.getData(bimg); 288 289 WritablePixelFormat<IntBuffer> pf = getAssociatedPixelFormat(bimg); 290 pr.getPixels(0, 0, iw, ih, pf, data, offset, scan); 291 return bimg; 292 } 293 294 /** 295 * If called from the FX Application Thread 296 * invokes a runnable directly blocking the calling code 297 * Otherwise 298 * uses Platform.runLater without blocking 299 */ 300 static void runOnFxThread(Runnable runnable) { 301 if (Platform.isFxApplicationThread()) { 302 runnable.run(); 303 } else { 304 Platform.runLater(runnable); 305 } 306 } 307 308 /** 309 * If called from the event dispatch thread 310 * invokes a runnable directly blocking the calling code 311 * Otherwise 312 * uses SwingUtilities.invokeLater without blocking 313 */ 314 static void runOnEDT(final Runnable r) { 315 if (SwingUtilities.isEventDispatchThread()) { 316 r.run(); 317 } else { 318 SwingUtilities.invokeLater(r); 319 } 320 } 321 322 private static final Set<Object> eventLoopKeys = new HashSet<>(); 323 324 /** 325 * The runnable is responsible for leaving the nested event loop. 326 */ 327 static void runOnEDTAndWait(Object nestedLoopKey, Runnable r) { 328 Toolkit.getToolkit().checkFxUserThread(); 329 330 if (SwingUtilities.isEventDispatchThread()) { 331 r.run(); 332 } else { 333 eventLoopKeys.add(nestedLoopKey); 334 SwingUtilities.invokeLater(r); 335 Toolkit.getToolkit().enterNestedEventLoop(nestedLoopKey); 336 } 337 } 338 339 static void leaveFXNestedLoop(Object nestedLoopKey) { 340 if (!eventLoopKeys.contains(nestedLoopKey)) return; 341 342 if (Platform.isFxApplicationThread()) { 343 Toolkit.getToolkit().exitNestedEventLoop(nestedLoopKey, null); 344 } else { 345 Platform.runLater(() -> { 346 Toolkit.getToolkit().exitNestedEventLoop(nestedLoopKey, null); 347 }); 348 } 349 350 eventLoopKeys.remove(nestedLoopKey); 351 } 352 353 }