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