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