1 /* 2 * Copyright (c) 2010, 2014, 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.scene.image; 27 28 import java.io.InputStream; 29 import java.lang.ref.WeakReference; 30 import java.net.MalformedURLException; 31 import java.net.URL; 32 import java.nio.Buffer; 33 import java.nio.ByteBuffer; 34 import java.nio.IntBuffer; 35 import java.util.LinkedList; 36 import java.util.Queue; 37 import java.util.concurrent.CancellationException; 38 import java.util.regex.Pattern; 39 import javafx.animation.KeyFrame; 40 import javafx.animation.Timeline; 41 import javafx.beans.NamedArg; 42 import javafx.beans.property.ReadOnlyBooleanProperty; 43 import javafx.beans.property.ReadOnlyBooleanWrapper; 44 import javafx.beans.property.ReadOnlyDoubleProperty; 45 import javafx.beans.property.ReadOnlyDoublePropertyBase; 46 import javafx.beans.property.ReadOnlyDoubleWrapper; 47 import javafx.beans.property.ReadOnlyObjectProperty; 48 import javafx.beans.property.ReadOnlyObjectPropertyBase; 49 import javafx.beans.property.ReadOnlyObjectWrapper; 50 import javafx.event.ActionEvent; 51 import javafx.event.EventHandler; 52 import javafx.scene.paint.Color; 53 import javafx.util.Duration; 54 import com.sun.javafx.runtime.async.AsyncOperation; 55 import com.sun.javafx.runtime.async.AsyncOperationListener; 56 import com.sun.javafx.tk.ImageLoader; 57 import com.sun.javafx.tk.PlatformImage; 58 import com.sun.javafx.tk.Toolkit; 59 60 /** 61 * The {@code Image} class represents graphical images and is used for loading 62 * images from a specified URL. 63 * 64 * <p> 65 * Supported image formats are: 66 * <ul> 67 * <li><a href="http://msdn.microsoft.com/en-us/library/dd183376(v=vs.85).aspx">BMP</a></li> 68 * <li><a href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF</a></li> 69 * <li><a href="http://www.ijg.org">JPEG</a></li> 70 * <li><a href="http://www.libpng.org/pub/png/spec/">PNG</a></li> 71 * </ul> 72 * </p> 73 * 74 * <p> 75 * Images can be resized as they are loaded (for example to reduce the amount of 76 * memory consumed by the image). The application can specify the quality of 77 * filtering used when scaling, and whether or not to preserve the original 78 * image's aspect ratio. 79 * </p> 80 * 81 * <p> 82 * All URLs supported by {@link URL} can be passed to the constructor. 83 * If the passed string is not a valid URL, but a path instead, the Image is 84 * searched on the classpath in that case. 85 * </p> 86 * 87 * <p>Use {@link ImageView} for displaying images loaded with this 88 * class. The same {@code Image} instance can be displayed by multiple 89 * {@code ImageView}s.</p> 90 * 91 *<p>Example code for loading images.</p> 92 93 <PRE> 94 import javafx.scene.image.Image; 95 96 // load an image in background, displaying a placeholder while it's loading 97 // (assuming there's an ImageView node somewhere displaying this image) 98 // The image is located in default package of the classpath 99 Image image1 = new Image("/flower.png", true); 100 101 // load an image and resize it to 100x150 without preserving its original 102 // aspect ratio 103 // The image is located in my.res package of the classpath 104 Image image2 = new Image("my/res/flower.png", 100, 150, false, false); 105 106 // load an image and resize it to width of 100 while preserving its 107 // original aspect ratio, using faster filtering method 108 // The image is downloaded from the supplied URL through http protocol 109 Image image3 = new Image("http://sample.com/res/flower.png", 100, 0, false, false); 110 111 // load an image and resize it only in one dimension, to the height of 100 and 112 // the original width, without preserving original aspect ratio 113 // The image is located in the current working directory 114 Image image4 = new Image("file:flower.png", 0, 100, false, false); 115 116 </PRE> 117 * @since JavaFX 2.0 118 */ 119 public class Image { 120 121 static { 122 Toolkit.setImageAccessor(new Toolkit.ImageAccessor() { 123 124 @Override 125 public boolean isAnimation(Image image) { 126 return image.isAnimation(); 127 } 128 129 @Override 130 public ReadOnlyObjectProperty<PlatformImage> 131 getImageProperty(Image image) 132 { 133 return image.acc_platformImageProperty(); 134 } 135 136 @Override 137 public int[] getPreColors(PixelFormat<ByteBuffer> pf) { 138 return ((PixelFormat.IndexedPixelFormat) pf).getPreColors(); 139 } 140 141 @Override 142 public int[] getNonPreColors(PixelFormat<ByteBuffer> pf) { 143 return ((PixelFormat.IndexedPixelFormat) pf).getNonPreColors(); 144 } 145 }); 146 } 147 148 // Matches strings that start with a valid URI scheme 149 private static final Pattern URL_QUICKMATCH = Pattern.compile("^\\p{Alpha}[\\p{Alnum}+.-]*:.*$"); 150 /** 151 * The string representing the URL to use in fetching the pixel data. 152 * 153 * @defaultValue empty string 154 */ 155 private final String url; 156 157 /** 158 * @treatAsPrivate implementation detail 159 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 160 */ 161 // SB-dependency: RT-21216 has been filed to track this 162 @Deprecated 163 public final String impl_getUrl() { 164 return url; 165 } 166 167 /** 168 * @treatAsPrivate 169 */ 170 private final InputStream impl_source; 171 172 final InputStream getImpl_source() { 173 return impl_source; 174 } 175 176 /** 177 * The approximate percentage of image's loading that 178 * has been completed. A positive value between 0 and 1 where 0 is 0% and 1 179 * is 100%. 180 * 181 * @defaultValue 0 182 */ 183 private ReadOnlyDoubleWrapper progress; 184 185 186 /** 187 * This is package private *only* for the sake of testing. We need a way to feed fake progress 188 * values. It would be better if Image were refactored to be testable (for example, by allowing 189 * the test code to provide its own implementation of background loading), but this is a simpler 190 * and safer change for now. 191 * 192 * @param value should be 0-1. 193 */ 194 final void setProgress(double value) { 195 progressPropertyImpl().set(value); 196 } 197 198 public final double getProgress() { 199 return progress == null ? 0.0 : progress.get(); 200 } 201 202 public final ReadOnlyDoubleProperty progressProperty() { 203 return progressPropertyImpl().getReadOnlyProperty(); 204 } 205 206 private ReadOnlyDoubleWrapper progressPropertyImpl() { 207 if (progress == null) { 208 progress = new ReadOnlyDoubleWrapper(this, "progress"); 209 } 210 return progress; 211 } 212 // PENDING_DOC_REVIEW 213 /** 214 * The width of the bounding box within which the source image is 215 * resized as necessary to fit. If set to a value {@code <= 0}, then the 216 * intrinsic width of the image will be used. 217 * <p/> 218 * See {@link #preserveRatio} for information on interaction between image's 219 * {@code requestedWidth}, {@code requestedHeight} and {@code preserveRatio} 220 * attributes. 221 * 222 * @defaultValue 0 223 */ 224 private final double requestedWidth; 225 226 /** 227 * Gets the width of the bounding box within which the source image is 228 * resized as necessary to fit. If set to a value {@code <= 0}, then the 229 * intrinsic width of the image will be used. 230 * <p/> 231 * See {@link #preserveRatio} for information on interaction between image's 232 * {@code requestedWidth}, {@code requestedHeight} and {@code preserveRatio} 233 * attributes. 234 * 235 * @return The requested width 236 */ 237 public final double getRequestedWidth() { 238 return requestedWidth; 239 } 240 // PENDING_DOC_REVIEW 241 /** 242 * The height of the bounding box within which the source image is 243 * resized as necessary to fit. If set to a value {@code <= 0}, then the 244 * intrinsic height of the image will be used. 245 * <p/> 246 * See {@link #preserveRatio} for information on interaction between image's 247 * {@code requestedWidth}, {@code requestedHeight} and {@code preserveRatio} 248 * attributes. 249 * 250 * @defaultValue 0 251 */ 252 private final double requestedHeight; 253 254 /** 255 * Gets the height of the bounding box within which the source image is 256 * resized as necessary to fit. If set to a value {@code <= 0}, then the 257 * intrinsic height of the image will be used. 258 * <p/> 259 * See {@link #preserveRatio} for information on interaction between image's 260 * {@code requestedWidth}, {@code requestedHeight} and {@code preserveRatio} 261 * attributes. 262 * 263 * @return The requested height 264 */ 265 public final double getRequestedHeight() { 266 return requestedHeight; 267 } 268 // PENDING_DOC_REVIEW 269 /** 270 * The image width or {@code 0} if the image loading fails. While the image 271 * is being loaded it is set to {@code 0}. 272 */ 273 private DoublePropertyImpl width; 274 275 public final double getWidth() { 276 return width == null ? 0.0 : width.get(); 277 } 278 279 public final ReadOnlyDoubleProperty widthProperty() { 280 return widthPropertyImpl(); 281 } 282 283 private DoublePropertyImpl widthPropertyImpl() { 284 if (width == null) { 285 width = new DoublePropertyImpl("width"); 286 } 287 288 return width; 289 } 290 291 private final class DoublePropertyImpl extends ReadOnlyDoublePropertyBase { 292 private final String name; 293 294 private double value; 295 296 public DoublePropertyImpl(final String name) { 297 this.name = name; 298 } 299 300 public void store(final double value) { 301 this.value = value; 302 } 303 304 @Override 305 public void fireValueChangedEvent() { 306 super.fireValueChangedEvent(); 307 } 308 309 @Override 310 public double get() { 311 return value; 312 } 313 314 @Override 315 public Object getBean() { 316 return Image.this; 317 } 318 319 @Override 320 public String getName() { 321 return name; 322 } 323 } 324 325 // PENDING_DOC_REVIEW 326 /** 327 * The image height or {@code 0} if the image loading fails. While the image 328 * is being loaded it is set to {@code 0}. 329 */ 330 private DoublePropertyImpl height; 331 332 public final double getHeight() { 333 return height == null ? 0.0 : height.get(); 334 } 335 336 public final ReadOnlyDoubleProperty heightProperty() { 337 return heightPropertyImpl(); 338 } 339 340 private DoublePropertyImpl heightPropertyImpl() { 341 if (height == null) { 342 height = new DoublePropertyImpl("height"); 343 } 344 345 return height; 346 } 347 348 /** 349 * Indicates whether to preserve the aspect ratio of the original image 350 * when scaling to fit the image within the bounding box provided by 351 * {@code width} and {@code height}. 352 * <p/> 353 * If set to {@code true}, it affects the dimensions of this {@code Image} 354 * in the following way: 355 * <ul> 356 * <li> If only {@code width} is set, height is scaled to preserve ratio 357 * <li> If only {@code height} is set, width is scaled to preserve ratio 358 * <li> If both are set, they both may be scaled to get the best fit in a 359 * width by height rectangle while preserving the original aspect ratio 360 * </ul> 361 * The reported {@code width} and {@code height} may be different from the 362 * initially set values if they needed to be adjusted to preserve aspect 363 * ratio. 364 * 365 * If unset or set to {@code false}, it affects the dimensions of this 366 * {@code ImageView} in the following way: 367 * <ul> 368 * <li> If only {@code width} is set, the image's width is scaled to 369 * match and height is unchanged; 370 * <li> If only {@code height} is set, the image's height is scaled to 371 * match and height is unchanged; 372 * <li> If both are set, the image is scaled to match both. 373 * </ul> 374 * </p> 375 * 376 * @defaultValue false 377 */ 378 private final boolean preserveRatio; 379 380 /** 381 * Indicates whether to preserve the aspect ratio of the original image 382 * when scaling to fit the image within the bounding box provided by 383 * {@code width} and {@code height}. 384 * <p/> 385 * If set to {@code true}, it affects the dimensions of this {@code Image} 386 * in the following way: 387 * <ul> 388 * <li> If only {@code width} is set, height is scaled to preserve ratio 389 * <li> If only {@code height} is set, width is scaled to preserve ratio 390 * <li> If both are set, they both may be scaled to get the best fit in a 391 * width by height rectangle while preserving the original aspect ratio 392 * </ul> 393 * The reported {@code width} and {@code height} may be different from the 394 * initially set values if they needed to be adjusted to preserve aspect 395 * ratio. 396 * 397 * If unset or set to {@code false}, it affects the dimensions of this 398 * {@code ImageView} in the following way: 399 * <ul> 400 * <li> If only {@code width} is set, the image's width is scaled to 401 * match and height is unchanged; 402 * <li> If only {@code height} is set, the image's height is scaled to 403 * match and height is unchanged; 404 * <li> If both are set, the image is scaled to match both. 405 * </ul> 406 * </p> 407 * 408 * @return true if the aspect ratio of the original image is to be 409 * preserved when scaling to fit the image within the bounding 410 * box provided by {@code width} and {@code height}. 411 */ 412 public final boolean isPreserveRatio() { 413 return preserveRatio; 414 } 415 416 /** 417 * Indicates whether to use a better quality filtering algorithm or a faster 418 * one when scaling this image to fit within the 419 * bounding box provided by {@code width} and {@code height}. 420 * 421 * <p> 422 * If not initialized or set to {@code true} a better quality filtering 423 * will be used, otherwise a faster but lesser quality filtering will be 424 * used. 425 * </p> 426 * 427 * @defaultValue true 428 */ 429 private final boolean smooth; 430 431 /** 432 * Indicates whether to use a better quality filtering algorithm or a faster 433 * one when scaling this image to fit within the 434 * bounding box provided by {@code width} and {@code height}. 435 * 436 * <p> 437 * If not initialized or set to {@code true} a better quality filtering 438 * will be used, otherwise a faster but lesser quality filtering will be 439 * used. 440 * </p> 441 * 442 * @return true if a better quality (but slower) filtering algorithm 443 * is used for scaling to fit within the 444 * bounding box provided by {@code width} and {@code height}. 445 */ 446 public final boolean isSmooth() { 447 return smooth; 448 } 449 450 /** 451 * Indicates whether the image is being loaded in the background. 452 * 453 * @defaultValue false 454 */ 455 private final boolean backgroundLoading; 456 457 /** 458 * Indicates whether the image is being loaded in the background. 459 * @return true if the image is loaded in the background 460 */ 461 public final boolean isBackgroundLoading() { 462 return backgroundLoading; 463 } 464 465 /** 466 * Indicates whether an error was detected while loading an image. 467 * 468 * @defaultValue false 469 */ 470 private ReadOnlyBooleanWrapper error; 471 472 473 private void setError(boolean value) { 474 errorPropertyImpl().set(value); 475 } 476 477 public final boolean isError() { 478 return error == null ? false : error.get(); 479 } 480 481 public final ReadOnlyBooleanProperty errorProperty() { 482 return errorPropertyImpl().getReadOnlyProperty(); 483 } 484 485 private ReadOnlyBooleanWrapper errorPropertyImpl() { 486 if (error == null) { 487 error = new ReadOnlyBooleanWrapper(this, "error"); 488 } 489 return error; 490 } 491 492 /** 493 * The exception which caused image loading to fail. Contains a non-null 494 * value only if the {@code error} property is set to {@code true}. 495 * 496 * @since JavaFX 8.0 497 */ 498 private ReadOnlyObjectWrapper<Exception> exception; 499 500 private void setException(Exception value) { 501 exceptionPropertyImpl().set(value); 502 } 503 504 public final Exception getException() { 505 return exception == null ? null : exception.get(); 506 } 507 508 public final ReadOnlyObjectProperty<Exception> exceptionProperty() { 509 return exceptionPropertyImpl().getReadOnlyProperty(); 510 } 511 512 private ReadOnlyObjectWrapper<Exception> exceptionPropertyImpl() { 513 if (exception == null) { 514 exception = new ReadOnlyObjectWrapper<Exception>(this, "exception"); 515 } 516 return exception; 517 } 518 519 /** 520 * The underlying platform representation of this Image object. 521 * 522 * @defaultValue null 523 * @treatAsPrivate implementation detail 524 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 525 */ 526 private ObjectPropertyImpl<PlatformImage> platformImage; 527 528 /** 529 * @treatAsPrivate implementation detail 530 */ 531 // SB-dependency: RT-21219 has been filed to track this 532 // TODO: need to ensure that both SceneBuilder and JDevloper have migrated 533 // to new 2.2 public API before we remove this. 534 @Deprecated 535 public final Object impl_getPlatformImage() { 536 return platformImage == null ? null : platformImage.get(); 537 } 538 539 final ReadOnlyObjectProperty<PlatformImage> acc_platformImageProperty() { 540 return platformImagePropertyImpl(); 541 } 542 543 private ObjectPropertyImpl<PlatformImage> platformImagePropertyImpl() { 544 if (platformImage == null) { 545 platformImage = new ObjectPropertyImpl<PlatformImage>("platformImage"); 546 } 547 548 return platformImage; 549 } 550 551 void pixelsDirty() { 552 platformImagePropertyImpl().fireValueChangedEvent(); 553 } 554 555 private final class ObjectPropertyImpl<T> 556 extends ReadOnlyObjectPropertyBase<T> { 557 private final String name; 558 559 private T value; 560 561 public ObjectPropertyImpl(final String name) { 562 this.name = name; 563 } 564 565 public void store(final T value) { 566 this.value = value; 567 } 568 569 public void set(final T value) { 570 if (this.value != value) { 571 this.value = value; 572 fireValueChangedEvent(); 573 } 574 } 575 576 @Override 577 public void fireValueChangedEvent() { 578 super.fireValueChangedEvent(); 579 } 580 581 @Override 582 public T get() { 583 return value; 584 } 585 586 @Override 587 public Object getBean() { 588 return Image.this; 589 } 590 591 @Override 592 public String getName() { 593 return name; 594 } 595 } 596 597 /** 598 * Constructs an {@code Image} with content loaded from the specified 599 * url. 600 * 601 * @param url the string representing the URL to use in fetching the pixel 602 * data 603 * @see #Image(java.lang.String, java.io.InputStream, double, double, boolean, boolean, boolean) 604 * @throws NullPointerException if URL is null 605 * @throws IllegalArgumentException if URL is invalid or unsupported 606 */ 607 public Image(@NamedArg("url") String url) { 608 this(validateUrl(url), null, 0, 0, false, false, false); 609 initialize(null); 610 } 611 612 /** 613 * Construct a new {@code Image} with the specified parameters. 614 * 615 * @param url the string representing the URL to use in fetching the pixel 616 * data 617 * @see #Image(java.lang.String, java.io.InputStream, double, double, boolean, boolean, boolean) 618 * @param backgroundLoading indicates whether the image 619 * is being loaded in the background 620 * @throws NullPointerException if URL is null 621 * @throws IllegalArgumentException if URL is invalid or unsupported 622 */ 623 public Image(@NamedArg("url") String url, @NamedArg("backgroundLoading") boolean backgroundLoading) { 624 this(validateUrl(url), null, 0, 0, false, false, backgroundLoading); 625 initialize(null); 626 } 627 628 /** 629 * Construct a new {@code Image} with the specified parameters. 630 * 631 * @param url the string representing the URL to use in fetching the pixel 632 * data 633 * @see #Image(java.lang.String, java.io.InputStream, double, double, boolean, boolean, boolean) 634 * @param requestedWidth the image's bounding box width 635 * @param requestedHeight the image's bounding box height 636 * @param preserveRatio indicates whether to preserve the aspect ratio of 637 * the original image when scaling to fit the image within the 638 * specified bounding box 639 * @param smooth indicates whether to use a better quality filtering 640 * algorithm or a faster one when scaling this image to fit within 641 * the specified bounding box 642 * @throws NullPointerException if URL is null 643 * @throws IllegalArgumentException if URL is invalid or unsupported 644 */ 645 public Image(@NamedArg("url") String url, @NamedArg("requestedWidth") double requestedWidth, @NamedArg("requestedHeight") double requestedHeight, 646 @NamedArg("preserveRatio") boolean preserveRatio, @NamedArg("smooth") boolean smooth) { 647 this(validateUrl(url), null, requestedWidth, requestedHeight, 648 preserveRatio, smooth, false); 649 initialize(null); 650 } 651 652 /** 653 * Construct a new {@code Image} with the specified parameters. 654 * 655 * The <i>url</i> without scheme is threated as relative to classpath, 656 * url with scheme is treated accordingly to the scheme using 657 * {@link URL#openStream()} 658 * 659 * @param url the string representing the URL to use in fetching the pixel 660 * data 661 * @param requestedWidth the image's bounding box width 662 * @param requestedHeight the image's bounding box height 663 * @param preserveRatio indicates whether to preserve the aspect ratio of 664 * the original image when scaling to fit the image within the 665 * specified bounding box 666 * @param smooth indicates whether to use a better quality filtering 667 * algorithm or a faster one when scaling this image to fit within 668 * the specified bounding box 669 * @param backgroundLoading indicates whether the image 670 * is being loaded in the background 671 * @throws NullPointerException if URL is null 672 * @throws IllegalArgumentException if URL is invalid or unsupported 673 */ 674 public Image( 675 @NamedArg(value="url", defaultValue="\"\"") String url, 676 @NamedArg("requestedWidth") double requestedWidth, 677 @NamedArg("requestedHeight") double requestedHeight, 678 @NamedArg("preserveRatio") boolean preserveRatio, 679 @NamedArg(value="smooth", defaultValue="true") boolean smooth, 680 @NamedArg("backgroundLoading") boolean backgroundLoading) { 681 this(validateUrl(url), null, requestedWidth, requestedHeight, 682 preserveRatio, smooth, backgroundLoading); 683 initialize(null); 684 } 685 686 /** 687 * Construct an {@code Image} with content loaded from the specified 688 * input stream. 689 * 690 * @param is the stream from which to load the image 691 * @throws NullPointerException if input stream is null 692 */ 693 public Image(@NamedArg("is") InputStream is) { 694 this(null, validateInputStream(is), 0, 0, false, false, false); 695 initialize(null); 696 } 697 698 /** 699 * Construct a new {@code Image} with the specified parameters. 700 * 701 * @param is the stream from which to load the image 702 * @param requestedWidth the image's bounding box width 703 * @param requestedHeight the image's bounding box height 704 * @param preserveRatio indicates whether to preserve the aspect ratio of 705 * the original image when scaling to fit the image within the 706 * specified bounding box 707 * @param smooth indicates whether to use a better quality filtering 708 * algorithm or a faster one when scaling this image to fit within 709 * the specified bounding box 710 * @throws NullPointerException if input stream is null 711 */ 712 public Image(@NamedArg("is") InputStream is, @NamedArg("requestedWidth") double requestedWidth, @NamedArg("requestedHeight") double requestedHeight, 713 @NamedArg("preserveRatio") boolean preserveRatio, @NamedArg("smooth") boolean smooth) { 714 this(null, validateInputStream(is), requestedWidth, requestedHeight, 715 preserveRatio, smooth, false); 716 initialize(null); 717 } 718 719 /** 720 * Package private internal constructor used only by {@link WritableImage}. 721 * The dimensions must both be positive numbers <code>(> 0)</code>. 722 * 723 * @param width the width of the empty image 724 * @param height the height of the empty image 725 * @throws IllegalArgumentException if either dimension is negative or zero. 726 */ 727 Image(int width, int height) { 728 this(null, null, width, height, false, false, false); 729 if (width <= 0 || height <= 0) { 730 throw new IllegalArgumentException("Image dimensions must be positive (w,h > 0)"); 731 } 732 initialize(Toolkit.getToolkit().createPlatformImage(width, height)); 733 } 734 735 private Image(Object externalImage) { 736 this(null, null, 0, 0, false, false, false); 737 initialize(externalImage); 738 } 739 740 private Image(String url, InputStream is, 741 double requestedWidth, double requestedHeight, 742 boolean preserveRatio, boolean smooth, 743 boolean backgroundLoading) { 744 this.url = url; 745 this.impl_source = is; 746 this.requestedWidth = requestedWidth; 747 this.requestedHeight = requestedHeight; 748 this.preserveRatio = preserveRatio; 749 this.smooth = smooth; 750 this.backgroundLoading = backgroundLoading; 751 } 752 753 /** 754 * Cancels the background loading of this image. 755 * 756 * <p>Has no effect if this image isn't loaded in background or if loading 757 * has already completed.</p> 758 */ 759 public void cancel() { 760 if (backgroundTask != null) { 761 backgroundTask.cancel(); 762 } 763 } 764 765 /** 766 * @treatAsPrivate used for testing 767 */ 768 void dispose() { 769 cancel(); 770 if (animation != null) { 771 animation.stop(); 772 } 773 } 774 775 private ImageTask backgroundTask; 776 777 private void initialize(Object externalImage) { 778 // we need to check the original values here, because setting placeholder 779 // changes platformImage, so wrong branch of if would be used 780 if (externalImage != null) { 781 // Make an image from the provided platform-specific image 782 // object (e.g. a BufferedImage in the case of the Swing profile) 783 ImageLoader loader = loadPlatformImage(externalImage); 784 finishImage(loader); 785 } else if (isBackgroundLoading() && (impl_source == null)) { 786 // Load image in the background. 787 loadInBackground(); 788 } else { 789 // Load image immediately. 790 ImageLoader loader; 791 if (impl_source != null) { 792 loader = loadImage(impl_source, getRequestedWidth(), getRequestedHeight(), 793 isPreserveRatio(), isSmooth()); 794 } else { 795 loader = loadImage(impl_getUrl(), getRequestedWidth(), getRequestedHeight(), 796 isPreserveRatio(), isSmooth()); 797 } 798 finishImage(loader); 799 } 800 } 801 802 private void finishImage(ImageLoader loader) { 803 final Exception loadingException = loader.getException(); 804 if (loadingException != null) { 805 finishImage(loadingException); 806 return; 807 } 808 809 if (loader.getFrameCount() > 1) { 810 initializeAnimatedImage(loader); 811 } else { 812 PlatformImage pi = loader.getFrame(0); 813 double w = loader.getWidth() / pi.getPixelScale(); 814 double h = loader.getHeight() / pi.getPixelScale(); 815 setPlatformImageWH(pi, w, h); 816 } 817 setProgress(1); 818 } 819 820 private void finishImage(Exception exception) { 821 setException(exception); 822 setError(true); 823 setPlatformImageWH(null, 0, 0); 824 setProgress(1); 825 } 826 827 // Support for animated images. 828 private Animation animation; 829 // We keep the animation frames associated with the Image rather than with 830 // the animation, so most of the data can be garbage collected while 831 // the animation is still running. 832 private PlatformImage[] animFrames; 833 834 // Generates the animation Timeline for multiframe images. 835 private void initializeAnimatedImage(ImageLoader loader) { 836 final int frameCount = loader.getFrameCount(); 837 animFrames = new PlatformImage[frameCount]; 838 839 for (int i = 0; i < frameCount; ++i) { 840 animFrames[i] = loader.getFrame(i); 841 } 842 843 PlatformImage zeroFrame = loader.getFrame(0); 844 845 double w = loader.getWidth() / zeroFrame.getPixelScale(); 846 double h = loader.getHeight() / zeroFrame.getPixelScale(); 847 setPlatformImageWH(zeroFrame, w, h); 848 849 animation = new Animation(this, loader); 850 animation.start(); 851 } 852 853 private static final class Animation { 854 final WeakReference<Image> imageRef; 855 final Timeline timeline; 856 857 public Animation(final Image image, final ImageLoader loader) { 858 imageRef = new WeakReference<Image>(image); 859 timeline = new Timeline(); 860 int loopCount = loader.getLoopCount(); 861 timeline.setCycleCount(loopCount == 0 ? Timeline.INDEFINITE : loopCount); 862 863 final int frameCount = loader.getFrameCount(); 864 int duration = 0; 865 866 for (int i = 0; i < frameCount; ++i) { 867 addKeyFrame(i, duration); 868 duration = duration + loader.getFrameDelay(i); 869 } 870 871 // Note: we need one extra frame in the timeline to define how long 872 // the last frame is shown, the wrap around is "instantaneous" 873 timeline.getKeyFrames().add(new KeyFrame(Duration.millis(duration))); 874 } 875 876 public void start() { 877 timeline.play(); 878 } 879 880 public void stop() { 881 timeline.stop(); 882 } 883 884 private void updateImage(final int frameIndex) { 885 final Image image = imageRef.get(); 886 if (image != null) { 887 image.platformImagePropertyImpl().set( 888 image.animFrames[frameIndex]); 889 } else { 890 timeline.stop(); 891 } 892 } 893 894 private void addKeyFrame(final int index, final double duration) { 895 timeline.getKeyFrames().add( 896 new KeyFrame(Duration.millis(duration), 897 event -> updateImage(index) 898 )); 899 } 900 } 901 902 private void cycleTasks() { 903 synchronized (pendingTasks) { 904 runningTasks--; 905 // do we have any pending tasks to run ? 906 // we can assume we are under the throttle limit because 907 // one task just completed. 908 final ImageTask nextTask = pendingTasks.poll(); 909 if (nextTask != null) { 910 runningTasks++; 911 nextTask.start(); 912 } 913 } 914 } 915 916 private void loadInBackground() { 917 backgroundTask = new ImageTask(); 918 // This is an artificial throttle on background image loading tasks. 919 // It has been shown that with large images, we can quickly use up the 920 // heap loading images, even if they result in thumbnails. 921 // The limit of MAX_RUNNING_TASKS is arbitrary, and was based on initial 922 // testing with 923 // about 60 2-6 megapixel images. 924 synchronized (pendingTasks) { 925 if (runningTasks >= MAX_RUNNING_TASKS) { 926 pendingTasks.offer(backgroundTask); 927 } else { 928 runningTasks++; 929 backgroundTask.start(); 930 } 931 } 932 } 933 934 // Used by SwingUtils.toFXImage 935 /** 936 * @treatAsPrivate implementation detail 937 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 938 */ 939 // SB-dependency: RT-21217 has been filed to track this 940 // TODO: need to ensure that both SceneBuilder and JDevloper have migrated 941 // to new 2.2 public API before we remove this. 942 @Deprecated 943 public static Image impl_fromPlatformImage(Object image) { 944 return new Image(image); 945 } 946 947 private void setPlatformImageWH(final PlatformImage newPlatformImage, 948 final double newWidth, 949 final double newHeight) { 950 if ((impl_getPlatformImage() == newPlatformImage) 951 && (getWidth() == newWidth) 952 && (getHeight() == newHeight)) { 953 return; 954 } 955 956 final Object oldPlatformImage = impl_getPlatformImage(); 957 final double oldWidth = getWidth(); 958 final double oldHeight = getHeight(); 959 960 storePlatformImageWH(newPlatformImage, newWidth, newHeight); 961 962 if (oldPlatformImage != newPlatformImage) { 963 platformImagePropertyImpl().fireValueChangedEvent(); 964 } 965 966 if (oldWidth != newWidth) { 967 widthPropertyImpl().fireValueChangedEvent(); 968 } 969 970 if (oldHeight != newHeight) { 971 heightPropertyImpl().fireValueChangedEvent(); 972 } 973 } 974 975 private void storePlatformImageWH(final PlatformImage platformImage, 976 final double width, 977 final double height) { 978 platformImagePropertyImpl().store(platformImage); 979 widthPropertyImpl().store(width); 980 heightPropertyImpl().store(height); 981 } 982 983 void setPlatformImage(PlatformImage newPlatformImage) { 984 platformImage.set(newPlatformImage); 985 } 986 987 private static final int MAX_RUNNING_TASKS = 4; 988 private static int runningTasks = 0; 989 private static final Queue<ImageTask> pendingTasks = 990 new LinkedList<ImageTask>(); 991 992 private final class ImageTask 993 implements AsyncOperationListener<ImageLoader> { 994 995 private final AsyncOperation peer; 996 997 public ImageTask() { 998 peer = constructPeer(); 999 } 1000 1001 @Override 1002 public void onCancel() { 1003 finishImage(new CancellationException("Loading cancelled")); 1004 cycleTasks(); 1005 } 1006 1007 @Override 1008 public void onException(Exception exception) { 1009 finishImage(exception); 1010 cycleTasks(); 1011 } 1012 1013 @Override 1014 public void onCompletion(ImageLoader value) { 1015 finishImage(value); 1016 cycleTasks(); 1017 } 1018 1019 @Override 1020 public void onProgress(int cur, int max) { 1021 if (max > 0) { 1022 double curProgress = (double) cur / max; 1023 if ((curProgress < 1) && (curProgress >= (getProgress() + 0.1))) { 1024 setProgress(curProgress); 1025 } 1026 } 1027 } 1028 1029 public void start() { 1030 peer.start(); 1031 } 1032 1033 public void cancel() { 1034 peer.cancel(); 1035 } 1036 1037 private AsyncOperation constructPeer() { 1038 return loadImageAsync(this, url, 1039 requestedWidth, requestedHeight, 1040 preserveRatio, smooth); 1041 } 1042 } 1043 1044 private static ImageLoader loadImage( 1045 String url, double width, double height, 1046 boolean preserveRatio, boolean smooth) { 1047 return Toolkit.getToolkit().loadImage(url, (int) width, (int) height, 1048 preserveRatio, smooth); 1049 1050 } 1051 1052 private static ImageLoader loadImage( 1053 InputStream stream, double width, double height, 1054 boolean preserveRatio, boolean smooth) { 1055 return Toolkit.getToolkit().loadImage(stream, (int) width, (int) height, 1056 preserveRatio, smooth); 1057 1058 } 1059 1060 private static AsyncOperation loadImageAsync( 1061 AsyncOperationListener<? extends ImageLoader> listener, 1062 String url, double width, double height, 1063 boolean preserveRatio, boolean smooth) { 1064 return Toolkit.getToolkit().loadImageAsync(listener, url, 1065 (int) width, (int) height, 1066 preserveRatio, smooth); 1067 } 1068 1069 private static ImageLoader loadPlatformImage(Object platformImage) { 1070 return Toolkit.getToolkit().loadPlatformImage(platformImage); 1071 } 1072 1073 private static String validateUrl(final String url) { 1074 if (url == null) { 1075 throw new NullPointerException("URL must not be null"); 1076 } 1077 1078 if (url.trim().isEmpty()) { 1079 throw new IllegalArgumentException("URL must not be empty"); 1080 } 1081 1082 try { 1083 if (!URL_QUICKMATCH.matcher(url).matches()) { 1084 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 1085 URL resource; 1086 if (url.charAt(0) == '/') { 1087 resource = contextClassLoader.getResource(url.substring(1)); 1088 } else { 1089 resource = contextClassLoader.getResource(url); 1090 } 1091 if (resource == null) { 1092 throw new IllegalArgumentException("Invalid URL or resource not found"); 1093 } 1094 return resource.toString(); 1095 } 1096 // Use URL constructor for validation 1097 return new URL(url).toString(); 1098 } catch (final IllegalArgumentException e) { 1099 throw new IllegalArgumentException( 1100 constructDetailedExceptionMessage("Invalid URL", e), e); 1101 } catch (final MalformedURLException e) { 1102 throw new IllegalArgumentException( 1103 constructDetailedExceptionMessage("Invalid URL", e), e); 1104 } 1105 } 1106 1107 private static InputStream validateInputStream( 1108 final InputStream inputStream) { 1109 if (inputStream == null) { 1110 throw new NullPointerException("Input stream must not be null"); 1111 } 1112 1113 return inputStream; 1114 } 1115 1116 private static String constructDetailedExceptionMessage( 1117 final String mainMessage, 1118 final Throwable cause) { 1119 if (cause == null) { 1120 return mainMessage; 1121 } 1122 1123 final String causeMessage = cause.getMessage(); 1124 return constructDetailedExceptionMessage( 1125 (causeMessage != null) 1126 ? mainMessage + ": " + causeMessage 1127 : mainMessage, 1128 cause.getCause()); 1129 } 1130 1131 /** 1132 * Indicates whether image is animated. 1133 */ 1134 boolean isAnimation() { 1135 return animation != null; 1136 } 1137 1138 boolean pixelsReadable() { 1139 return (getProgress() >= 1.0 && !isAnimation() && !isError()); 1140 } 1141 1142 private PixelReader reader; 1143 /** 1144 * This method returns a {@code PixelReader} that provides access to 1145 * read the pixels of the image, if the image is readable. 1146 * If this method returns null then this image does not support reading 1147 * at this time. 1148 * This method will return null if the image is being loaded from a 1149 * source and is still incomplete {the progress is still < 1.0) or if 1150 * there was an error. 1151 * This method may also return null for some images in a format that 1152 * is not supported for reading and writing pixels to. 1153 * 1154 * @return the {@code PixelReader} for reading the pixel data of the image 1155 * @since JavaFX 2.2 1156 */ 1157 public final PixelReader getPixelReader() { 1158 if (!pixelsReadable()) { 1159 return null; 1160 } 1161 if (reader == null) { 1162 reader = new PixelReader() { 1163 @Override 1164 public PixelFormat getPixelFormat() { 1165 PlatformImage pimg = platformImage.get(); 1166 return pimg.getPlatformPixelFormat(); 1167 } 1168 1169 @Override 1170 public int getArgb(int x, int y) { 1171 PlatformImage pimg = platformImage.get(); 1172 return pimg.getArgb(x, y); 1173 } 1174 1175 @Override 1176 public Color getColor(int x, int y) { 1177 int argb = getArgb(x, y); 1178 int a = argb >>> 24; 1179 int r = (argb >> 16) & 0xff; 1180 int g = (argb >> 8) & 0xff; 1181 int b = (argb ) & 0xff; 1182 return Color.rgb(r, g, b, a / 255.0); 1183 } 1184 1185 @Override 1186 public <T extends Buffer> 1187 void getPixels(int x, int y, int w, int h, 1188 WritablePixelFormat<T> pixelformat, 1189 T buffer, int scanlineStride) 1190 { 1191 PlatformImage pimg = platformImage.get(); 1192 pimg.getPixels(x, y, w, h, pixelformat, 1193 buffer, scanlineStride); 1194 } 1195 1196 @Override 1197 public void getPixels(int x, int y, int w, int h, 1198 WritablePixelFormat<ByteBuffer> pixelformat, 1199 byte buffer[], int offset, int scanlineStride) 1200 { 1201 PlatformImage pimg = platformImage.get(); 1202 pimg.getPixels(x, y, w, h, pixelformat, 1203 buffer, offset, scanlineStride); 1204 } 1205 1206 @Override 1207 public void getPixels(int x, int y, int w, int h, 1208 WritablePixelFormat<IntBuffer> pixelformat, 1209 int buffer[], int offset, int scanlineStride) 1210 { 1211 PlatformImage pimg = platformImage.get(); 1212 pimg.getPixels(x, y, w, h, pixelformat, 1213 buffer, offset, scanlineStride); 1214 } 1215 }; 1216 } 1217 return reader; 1218 } 1219 1220 PlatformImage getWritablePlatformImage() { 1221 PlatformImage pimg = platformImage.get(); 1222 if (!pimg.isWritable()) { 1223 pimg = pimg.promoteToWritableImage(); 1224 // assert pimg.isWritable(); 1225 platformImage.set(pimg); 1226 } 1227 return pimg; 1228 } 1229 }