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 @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 #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 #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 #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 #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 * </p> 387 * 388 * @defaultValue false 389 */ 390 private final boolean preserveRatio; 391 392 /** 393 * Indicates whether to preserve the aspect ratio of the original image 394 * when scaling to fit the image within the bounding box provided by 395 * {@code width} and {@code height}. 396 * <p/> 397 * If set to {@code true}, it affects the dimensions of this {@code Image} 398 * in the following way: 399 * <ul> 400 * <li> If only {@code width} is set, height is scaled to preserve ratio 401 * <li> If only {@code height} is set, width is scaled to preserve ratio 402 * <li> If both are set, they both may be scaled to get the best fit in a 403 * width by height rectangle while preserving the original aspect ratio 404 * </ul> 405 * The reported {@code width} and {@code height} may be different from the 406 * initially set values if they needed to be adjusted to preserve aspect 407 * ratio. 408 * 409 * If unset or set to {@code false}, it affects the dimensions of this 410 * {@code ImageView} in the following way: 411 * <ul> 412 * <li> If only {@code width} is set, the image's width is scaled to 413 * match and height is unchanged; 414 * <li> If only {@code height} is set, the image's height is scaled to 415 * match and height is unchanged; 416 * <li> If both are set, the image is scaled to match both. 417 * </ul> 418 * </p> 419 * 420 * @return true if the aspect ratio of the original image is to be 421 * preserved when scaling to fit the image within the bounding 422 * box provided by {@code width} and {@code height}. 423 */ 424 public final boolean isPreserveRatio() { 425 return preserveRatio; 426 } 427 428 /** 429 * Indicates whether to use a better quality filtering algorithm or a faster 430 * one when scaling this image to fit within the 431 * bounding box provided by {@code width} and {@code height}. 432 * 433 * <p> 434 * If not initialized or set to {@code true} a better quality filtering 435 * will be used, otherwise a faster but lesser quality filtering will be 436 * used. 437 * </p> 438 * 439 * @defaultValue true 440 */ 441 private final boolean smooth; 442 443 /** 444 * Indicates whether to use a better quality filtering algorithm or a faster 445 * one when scaling this image to fit within the 446 * bounding box provided by {@code width} and {@code height}. 447 * 448 * <p> 449 * If not initialized or set to {@code true} a better quality filtering 450 * will be used, otherwise a faster but lesser quality filtering will be 451 * used. 452 * </p> 453 * 454 * @return true if a better quality (but slower) filtering algorithm 455 * is used for scaling to fit within the 456 * bounding box provided by {@code width} and {@code height}. 457 */ 458 public final boolean isSmooth() { 459 return smooth; 460 } 461 462 /** 463 * Indicates whether the image is being loaded in the background. 464 * 465 * @defaultValue false 466 */ 467 private final boolean backgroundLoading; 468 469 /** 470 * Indicates whether the image is being loaded in the background. 471 * @return true if the image is loaded in the background 472 */ 473 public final boolean isBackgroundLoading() { 474 return backgroundLoading; 475 } 476 477 /** 478 * Indicates whether an error was detected while loading an image. 479 * 480 * @defaultValue false 481 */ 482 private ReadOnlyBooleanWrapper error; 483 484 485 private void setError(boolean value) { 486 errorPropertyImpl().set(value); 487 } 488 489 public final boolean isError() { 490 return error == null ? false : error.get(); 491 } 492 493 public final ReadOnlyBooleanProperty errorProperty() { 494 return errorPropertyImpl().getReadOnlyProperty(); 495 } 496 497 private ReadOnlyBooleanWrapper errorPropertyImpl() { 498 if (error == null) { 499 error = new ReadOnlyBooleanWrapper(this, "error"); 500 } 501 return error; 502 } 503 504 /** 505 * The exception which caused image loading to fail. Contains a non-null 506 * value only if the {@code error} property is set to {@code true}. 507 * 508 * @since JavaFX 8.0 509 */ 510 private ReadOnlyObjectWrapper<Exception> exception; 511 512 private void setException(Exception value) { 513 exceptionPropertyImpl().set(value); 514 } 515 516 public final Exception getException() { 517 return exception == null ? null : exception.get(); 518 } 519 520 public final ReadOnlyObjectProperty<Exception> exceptionProperty() { 521 return exceptionPropertyImpl().getReadOnlyProperty(); 522 } 523 524 private ReadOnlyObjectWrapper<Exception> exceptionPropertyImpl() { 525 if (exception == null) { 526 exception = new ReadOnlyObjectWrapper<Exception>(this, "exception"); 527 } 528 return exception; 529 } 530 531 /** 532 * The underlying platform representation of this Image object. 533 * 534 * @defaultValue null 535 * @treatAsPrivate implementation detail 536 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 537 */ 538 private ObjectPropertyImpl<PlatformImage> platformImage; 539 540 private final Object getPlatformImage() { 541 return platformImage == null ? null : platformImage.get(); 542 } 543 544 final ReadOnlyObjectProperty<PlatformImage> acc_platformImageProperty() { 545 return platformImagePropertyImpl(); 546 } 547 548 private ObjectPropertyImpl<PlatformImage> platformImagePropertyImpl() { 549 if (platformImage == null) { 550 platformImage = new ObjectPropertyImpl<PlatformImage>("platformImage"); 551 } 552 553 return platformImage; 554 } 555 556 void pixelsDirty() { 557 platformImagePropertyImpl().fireValueChangedEvent(); 558 } 559 560 private final class ObjectPropertyImpl<T> 561 extends ReadOnlyObjectPropertyBase<T> { 562 private final String name; 563 564 private T value; 565 private boolean valid = true; 566 567 public ObjectPropertyImpl(final String name) { 568 this.name = name; 569 } 570 571 public void store(final T value) { 572 this.value = value; 573 } 574 575 public void set(final T value) { 576 if (this.value != value) { 577 this.value = value; 578 markInvalid(); 579 } 580 } 581 582 @Override 583 public void fireValueChangedEvent() { 584 super.fireValueChangedEvent(); 585 } 586 587 private void markInvalid() { 588 if (valid) { 589 valid = false; 590 fireValueChangedEvent(); 591 } 592 } 593 594 @Override 595 public T get() { 596 valid = true; 597 return value; 598 } 599 600 @Override 601 public Object getBean() { 602 return Image.this; 603 } 604 605 @Override 606 public String getName() { 607 return name; 608 } 609 } 610 611 /** 612 * Constructs an {@code Image} with content loaded from the specified 613 * url. 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 * @throws NullPointerException if URL is null 619 * @throws IllegalArgumentException if URL is invalid or unsupported 620 */ 621 public Image(@NamedArg("url") String url) { 622 this(validateUrl(url), null, 0, 0, false, false, false); 623 initialize(null); 624 } 625 626 /** 627 * Construct a new {@code Image} with the specified parameters. 628 * 629 * @param url the string representing the URL to use in fetching the pixel 630 * data 631 * @see #Image(java.lang.String, java.io.InputStream, double, double, boolean, boolean, boolean) 632 * @param backgroundLoading indicates whether the image 633 * is being loaded in the background 634 * @throws NullPointerException if URL is null 635 * @throws IllegalArgumentException if URL is invalid or unsupported 636 */ 637 public Image(@NamedArg("url") String url, @NamedArg("backgroundLoading") boolean backgroundLoading) { 638 this(validateUrl(url), null, 0, 0, false, false, backgroundLoading); 639 initialize(null); 640 } 641 642 /** 643 * Construct a new {@code Image} with the specified parameters. 644 * 645 * @param url the string representing the URL to use in fetching the pixel 646 * data 647 * @see #Image(java.lang.String, java.io.InputStream, double, double, boolean, boolean, boolean) 648 * @param requestedWidth the image's bounding box width 649 * @param requestedHeight the image's bounding box height 650 * @param preserveRatio indicates whether to preserve the aspect ratio of 651 * the original image when scaling to fit the image within the 652 * specified bounding box 653 * @param smooth indicates whether to use a better quality filtering 654 * algorithm or a faster one when scaling this image to fit within 655 * the specified bounding box 656 * @throws NullPointerException if URL is null 657 * @throws IllegalArgumentException if URL is invalid or unsupported 658 */ 659 public Image(@NamedArg("url") String url, @NamedArg("requestedWidth") double requestedWidth, @NamedArg("requestedHeight") double requestedHeight, 660 @NamedArg("preserveRatio") boolean preserveRatio, @NamedArg("smooth") boolean smooth) { 661 this(validateUrl(url), null, requestedWidth, requestedHeight, 662 preserveRatio, smooth, false); 663 initialize(null); 664 } 665 666 /** 667 * Construct a new {@code Image} with the specified parameters. 668 * 669 * The <i>url</i> without scheme is threated as relative to classpath, 670 * url with scheme is treated accordingly to the scheme using 671 * {@link URL#openStream()} 672 * 673 * @param url the string representing the URL to use in fetching the pixel 674 * data 675 * @param requestedWidth the image's bounding box width 676 * @param requestedHeight the image's bounding box height 677 * @param preserveRatio indicates whether to preserve the aspect ratio of 678 * the original image when scaling to fit the image within the 679 * specified bounding box 680 * @param smooth indicates whether to use a better quality filtering 681 * algorithm or a faster one when scaling this image to fit within 682 * the specified bounding box 683 * @param backgroundLoading indicates whether the image 684 * is being loaded in the background 685 * @throws NullPointerException if URL is null 686 * @throws IllegalArgumentException if URL is invalid or unsupported 687 */ 688 public Image( 689 @NamedArg(value="url", defaultValue="\"\"") String url, 690 @NamedArg("requestedWidth") double requestedWidth, 691 @NamedArg("requestedHeight") double requestedHeight, 692 @NamedArg("preserveRatio") boolean preserveRatio, 693 @NamedArg(value="smooth", defaultValue="true") boolean smooth, 694 @NamedArg("backgroundLoading") boolean backgroundLoading) { 695 this(validateUrl(url), null, requestedWidth, requestedHeight, 696 preserveRatio, smooth, backgroundLoading); 697 initialize(null); 698 } 699 700 /** 701 * Construct an {@code Image} with content loaded from the specified 702 * input stream. 703 * 704 * @param is the stream from which to load the image 705 * @throws NullPointerException if input stream is null 706 */ 707 public Image(@NamedArg("is") InputStream is) { 708 this(null, validateInputStream(is), 0, 0, false, false, false); 709 initialize(null); 710 } 711 712 /** 713 * Construct a new {@code Image} with the specified parameters. 714 * 715 * @param is the stream from which to load the image 716 * @param requestedWidth the image's bounding box width 717 * @param requestedHeight the image's bounding box height 718 * @param preserveRatio indicates whether to preserve the aspect ratio of 719 * the original image when scaling to fit the image within the 720 * specified bounding box 721 * @param smooth indicates whether to use a better quality filtering 722 * algorithm or a faster one when scaling this image to fit within 723 * the specified bounding box 724 * @throws NullPointerException if input stream is null 725 */ 726 public Image(@NamedArg("is") InputStream is, @NamedArg("requestedWidth") double requestedWidth, @NamedArg("requestedHeight") double requestedHeight, 727 @NamedArg("preserveRatio") boolean preserveRatio, @NamedArg("smooth") boolean smooth) { 728 this(null, validateInputStream(is), requestedWidth, requestedHeight, 729 preserveRatio, smooth, false); 730 initialize(null); 731 } 732 733 /** 734 * Package private internal constructor used only by {@link WritableImage}. 735 * The dimensions must both be positive numbers <code>(> 0)</code>. 736 * 737 * @param width the width of the empty image 738 * @param height the height of the empty image 739 * @throws IllegalArgumentException if either dimension is negative or zero. 740 */ 741 Image(int width, int height) { 742 this(null, null, width, height, false, false, false); 743 if (width <= 0 || height <= 0) { 744 throw new IllegalArgumentException("Image dimensions must be positive (w,h > 0)"); 745 } 746 initialize(Toolkit.getToolkit().createPlatformImage(width, height)); 747 } 748 749 private Image(Object externalImage) { 750 this(null, null, 0, 0, false, false, false); 751 initialize(externalImage); 752 } 753 754 private Image(String url, InputStream is, 755 double requestedWidth, double requestedHeight, 756 boolean preserveRatio, boolean smooth, 757 boolean backgroundLoading) { 758 this.url = url; 759 this.inputSource = is; 760 this.requestedWidth = requestedWidth; 761 this.requestedHeight = requestedHeight; 762 this.preserveRatio = preserveRatio; 763 this.smooth = smooth; 764 this.backgroundLoading = backgroundLoading; 765 } 766 767 /** 768 * Cancels the background loading of this image. 769 * 770 * <p>Has no effect if this image isn't loaded in background or if loading 771 * has already completed.</p> 772 */ 773 public void cancel() { 774 if (backgroundTask != null) { 775 backgroundTask.cancel(); 776 } 777 } 778 779 /** 780 * @treatAsPrivate used for testing 781 */ 782 void dispose() { 783 cancel(); 784 if (animation != null) { 785 animation.stop(); 786 } 787 } 788 789 private ImageTask backgroundTask; 790 791 private void initialize(Object externalImage) { 792 // we need to check the original values here, because setting placeholder 793 // changes platformImage, so wrong branch of if would be used 794 if (externalImage != null) { 795 // Make an image from the provided platform-specific image 796 // object (e.g. a BufferedImage in the case of the Swing profile) 797 ImageLoader loader = loadPlatformImage(externalImage); 798 finishImage(loader); 799 } else if (isBackgroundLoading() && (inputSource == null)) { 800 // Load image in the background. 801 loadInBackground(); 802 } else { 803 // Load image immediately. 804 ImageLoader loader; 805 if (inputSource != null) { 806 loader = loadImage(inputSource, getRequestedWidth(), getRequestedHeight(), 807 isPreserveRatio(), isSmooth()); 808 } else { 809 loader = loadImage(getUrl(), getRequestedWidth(), getRequestedHeight(), 810 isPreserveRatio(), isSmooth()); 811 } 812 finishImage(loader); 813 } 814 } 815 816 private void finishImage(ImageLoader loader) { 817 final Exception loadingException = loader.getException(); 818 if (loadingException != null) { 819 finishImage(loadingException); 820 return; 821 } 822 823 if (loader.getFrameCount() > 1) { 824 initializeAnimatedImage(loader); 825 } else { 826 PlatformImage pi = loader.getFrame(0); 827 double w = loader.getWidth() / pi.getPixelScale(); 828 double h = loader.getHeight() / pi.getPixelScale(); 829 setPlatformImageWH(pi, w, h); 830 } 831 setProgress(1); 832 } 833 834 private void finishImage(Exception exception) { 835 setException(exception); 836 setError(true); 837 setPlatformImageWH(null, 0, 0); 838 setProgress(1); 839 } 840 841 // Support for animated images. 842 private Animation animation; 843 // We keep the animation frames associated with the Image rather than with 844 // the animation, so most of the data can be garbage collected while 845 // the animation is still running. 846 private PlatformImage[] animFrames; 847 848 // Generates the animation Timeline for multiframe images. 849 private void initializeAnimatedImage(ImageLoader loader) { 850 final int frameCount = loader.getFrameCount(); 851 animFrames = new PlatformImage[frameCount]; 852 853 for (int i = 0; i < frameCount; ++i) { 854 animFrames[i] = loader.getFrame(i); 855 } 856 857 PlatformImage zeroFrame = loader.getFrame(0); 858 859 double w = loader.getWidth() / zeroFrame.getPixelScale(); 860 double h = loader.getHeight() / zeroFrame.getPixelScale(); 861 setPlatformImageWH(zeroFrame, w, h); 862 863 animation = new Animation(this, loader); 864 animation.start(); 865 } 866 867 private static final class Animation { 868 final WeakReference<Image> imageRef; 869 final Timeline timeline; 870 final SimpleIntegerProperty frameIndex = new SimpleIntegerProperty() { 871 @Override 872 protected void invalidated() { 873 updateImage(get()); 874 } 875 }; 876 877 public Animation(final Image image, final ImageLoader loader) { 878 imageRef = new WeakReference<Image>(image); 879 timeline = new Timeline(); 880 int loopCount = loader.getLoopCount(); 881 timeline.setCycleCount(loopCount == 0 ? Timeline.INDEFINITE : loopCount); 882 883 final int frameCount = loader.getFrameCount(); 884 int duration = 0; 885 886 for (int i = 0; i < frameCount; ++i) { 887 addKeyFrame(i, duration); 888 duration = duration + loader.getFrameDelay(i); 889 } 890 891 // Note: we need one extra frame in the timeline to define how long 892 // the last frame is shown, the wrap around is "instantaneous" 893 timeline.getKeyFrames().add(new KeyFrame(Duration.millis(duration))); 894 } 895 896 public void start() { 897 timeline.play(); 898 } 899 900 public void stop() { 901 timeline.stop(); 902 } 903 904 private void updateImage(final int frameIndex) { 905 final Image image = imageRef.get(); 906 if (image != null) { 907 image.platformImagePropertyImpl().set( 908 image.animFrames[frameIndex]); 909 } else { 910 timeline.stop(); 911 } 912 } 913 914 private void addKeyFrame(final int index, final double duration) { 915 timeline.getKeyFrames().add( 916 new KeyFrame(Duration.millis(duration), 917 new KeyValue(frameIndex, index, Interpolator.DISCRETE) 918 )); 919 } 920 } 921 922 private void cycleTasks() { 923 synchronized (pendingTasks) { 924 runningTasks--; 925 // do we have any pending tasks to run ? 926 // we can assume we are under the throttle limit because 927 // one task just completed. 928 final ImageTask nextTask = pendingTasks.poll(); 929 if (nextTask != null) { 930 runningTasks++; 931 nextTask.start(); 932 } 933 } 934 } 935 936 private void loadInBackground() { 937 backgroundTask = new ImageTask(); 938 // This is an artificial throttle on background image loading tasks. 939 // It has been shown that with large images, we can quickly use up the 940 // heap loading images, even if they result in thumbnails. 941 // The limit of MAX_RUNNING_TASKS is arbitrary, and was based on initial 942 // testing with 943 // about 60 2-6 megapixel images. 944 synchronized (pendingTasks) { 945 if (runningTasks >= MAX_RUNNING_TASKS) { 946 pendingTasks.offer(backgroundTask); 947 } else { 948 runningTasks++; 949 backgroundTask.start(); 950 } 951 } 952 } 953 954 // Used by SwingUtils.toFXImage 955 static Image fromPlatformImage(Object image) { 956 return new Image(image); 957 } 958 959 private void setPlatformImageWH(final PlatformImage newPlatformImage, 960 final double newWidth, 961 final double newHeight) { 962 if ((Toolkit.getImageAccessor().getPlatformImage(this) == newPlatformImage) 963 && (getWidth() == newWidth) 964 && (getHeight() == newHeight)) { 965 return; 966 } 967 968 final Object oldPlatformImage = Toolkit.getImageAccessor().getPlatformImage(this); 969 final double oldWidth = getWidth(); 970 final double oldHeight = getHeight(); 971 972 storePlatformImageWH(newPlatformImage, newWidth, newHeight); 973 974 if (oldPlatformImage != newPlatformImage) { 975 platformImagePropertyImpl().fireValueChangedEvent(); 976 } 977 978 if (oldWidth != newWidth) { 979 widthPropertyImpl().fireValueChangedEvent(); 980 } 981 982 if (oldHeight != newHeight) { 983 heightPropertyImpl().fireValueChangedEvent(); 984 } 985 } 986 987 private void storePlatformImageWH(final PlatformImage platformImage, 988 final double width, 989 final double height) { 990 platformImagePropertyImpl().store(platformImage); 991 widthPropertyImpl().store(width); 992 heightPropertyImpl().store(height); 993 } 994 995 void setPlatformImage(PlatformImage newPlatformImage) { 996 platformImage.set(newPlatformImage); 997 } 998 999 private static final int MAX_RUNNING_TASKS = 4; 1000 private static int runningTasks = 0; 1001 private static final Queue<ImageTask> pendingTasks = 1002 new LinkedList<ImageTask>(); 1003 1004 private final class ImageTask 1005 implements AsyncOperationListener<ImageLoader> { 1006 1007 private final AsyncOperation peer; 1008 1009 public ImageTask() { 1010 peer = constructPeer(); 1011 } 1012 1013 @Override 1014 public void onCancel() { 1015 finishImage(new CancellationException("Loading cancelled")); 1016 cycleTasks(); 1017 } 1018 1019 @Override 1020 public void onException(Exception exception) { 1021 finishImage(exception); 1022 cycleTasks(); 1023 } 1024 1025 @Override 1026 public void onCompletion(ImageLoader value) { 1027 finishImage(value); 1028 cycleTasks(); 1029 } 1030 1031 @Override 1032 public void onProgress(int cur, int max) { 1033 if (max > 0) { 1034 double curProgress = (double) cur / max; 1035 if ((curProgress < 1) && (curProgress >= (getProgress() + 0.1))) { 1036 setProgress(curProgress); 1037 } 1038 } 1039 } 1040 1041 public void start() { 1042 peer.start(); 1043 } 1044 1045 public void cancel() { 1046 peer.cancel(); 1047 } 1048 1049 private AsyncOperation constructPeer() { 1050 return loadImageAsync(this, url, 1051 requestedWidth, requestedHeight, 1052 preserveRatio, smooth); 1053 } 1054 } 1055 1056 private static ImageLoader loadImage( 1057 String url, double width, double height, 1058 boolean preserveRatio, boolean smooth) { 1059 return Toolkit.getToolkit().loadImage(url, (int) width, (int) height, 1060 preserveRatio, smooth); 1061 1062 } 1063 1064 private static ImageLoader loadImage( 1065 InputStream stream, double width, double height, 1066 boolean preserveRatio, boolean smooth) { 1067 return Toolkit.getToolkit().loadImage(stream, (int) width, (int) height, 1068 preserveRatio, smooth); 1069 1070 } 1071 1072 private static AsyncOperation loadImageAsync( 1073 AsyncOperationListener<? extends ImageLoader> listener, 1074 String url, double width, double height, 1075 boolean preserveRatio, boolean smooth) { 1076 return Toolkit.getToolkit().loadImageAsync(listener, url, 1077 (int) width, (int) height, 1078 preserveRatio, smooth); 1079 } 1080 1081 private static ImageLoader loadPlatformImage(Object platformImage) { 1082 return Toolkit.getToolkit().loadPlatformImage(platformImage); 1083 } 1084 1085 private static String validateUrl(final String url) { 1086 if (url == null) { 1087 throw new NullPointerException("URL must not be null"); 1088 } 1089 1090 if (url.trim().isEmpty()) { 1091 throw new IllegalArgumentException("URL must not be empty"); 1092 } 1093 1094 try { 1095 if (!URL_QUICKMATCH.matcher(url).matches()) { 1096 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 1097 URL resource; 1098 if (url.charAt(0) == '/') { 1099 // FIXME: JIGSAW -- use Class.getResourceAsStream if resource is in a module 1100 resource = contextClassLoader.getResource(url.substring(1)); 1101 } else { 1102 // FIXME: JIGSAW -- use Class.getResourceAsStream if resource is in a module 1103 resource = contextClassLoader.getResource(url); 1104 } 1105 if (resource == null) { 1106 throw new IllegalArgumentException("Invalid URL or resource not found"); 1107 } 1108 return resource.toString(); 1109 } 1110 // Use URL constructor for validation 1111 return new URL(url).toString(); 1112 } catch (final IllegalArgumentException e) { 1113 throw new IllegalArgumentException( 1114 constructDetailedExceptionMessage("Invalid URL", e), e); 1115 } catch (final MalformedURLException e) { 1116 throw new IllegalArgumentException( 1117 constructDetailedExceptionMessage("Invalid URL", e), e); 1118 } 1119 } 1120 1121 private static InputStream validateInputStream( 1122 final InputStream inputStream) { 1123 if (inputStream == null) { 1124 throw new NullPointerException("Input stream must not be null"); 1125 } 1126 1127 return inputStream; 1128 } 1129 1130 private static String constructDetailedExceptionMessage( 1131 final String mainMessage, 1132 final Throwable cause) { 1133 if (cause == null) { 1134 return mainMessage; 1135 } 1136 1137 final String causeMessage = cause.getMessage(); 1138 return constructDetailedExceptionMessage( 1139 (causeMessage != null) 1140 ? mainMessage + ": " + causeMessage 1141 : mainMessage, 1142 cause.getCause()); 1143 } 1144 1145 /** 1146 * Indicates whether image is animated. 1147 */ 1148 boolean isAnimation() { 1149 return animation != null; 1150 } 1151 1152 boolean pixelsReadable() { 1153 return (getProgress() >= 1.0 && !isAnimation() && !isError()); 1154 } 1155 1156 private PixelReader reader; 1157 /** 1158 * This method returns a {@code PixelReader} that provides access to 1159 * read the pixels of the image, if the image is readable. 1160 * If this method returns null then this image does not support reading 1161 * at this time. 1162 * This method will return null if the image is being loaded from a 1163 * source and is still incomplete {the progress is still < 1.0) or if 1164 * there was an error. 1165 * This method may also return null for some images in a format that 1166 * is not supported for reading and writing pixels to. 1167 * 1168 * @return the {@code PixelReader} for reading the pixel data of the image 1169 * @since JavaFX 2.2 1170 */ 1171 public final PixelReader getPixelReader() { 1172 if (!pixelsReadable()) { 1173 return null; 1174 } 1175 if (reader == null) { 1176 reader = new PixelReader() { 1177 @Override 1178 public PixelFormat getPixelFormat() { 1179 PlatformImage pimg = platformImage.get(); 1180 return pimg.getPlatformPixelFormat(); 1181 } 1182 1183 @Override 1184 public int getArgb(int x, int y) { 1185 PlatformImage pimg = platformImage.get(); 1186 return pimg.getArgb(x, y); 1187 } 1188 1189 @Override 1190 public Color getColor(int x, int y) { 1191 int argb = getArgb(x, y); 1192 int a = argb >>> 24; 1193 int r = (argb >> 16) & 0xff; 1194 int g = (argb >> 8) & 0xff; 1195 int b = (argb ) & 0xff; 1196 return Color.rgb(r, g, b, a / 255.0); 1197 } 1198 1199 @Override 1200 public <T extends Buffer> 1201 void getPixels(int x, int y, int w, int h, 1202 WritablePixelFormat<T> pixelformat, 1203 T buffer, int scanlineStride) 1204 { 1205 PlatformImage pimg = platformImage.get(); 1206 pimg.getPixels(x, y, w, h, pixelformat, 1207 buffer, scanlineStride); 1208 } 1209 1210 @Override 1211 public void getPixels(int x, int y, int w, int h, 1212 WritablePixelFormat<ByteBuffer> pixelformat, 1213 byte buffer[], int offset, int scanlineStride) 1214 { 1215 PlatformImage pimg = platformImage.get(); 1216 pimg.getPixels(x, y, w, h, pixelformat, 1217 buffer, offset, scanlineStride); 1218 } 1219 1220 @Override 1221 public void getPixels(int x, int y, int w, int h, 1222 WritablePixelFormat<IntBuffer> pixelformat, 1223 int buffer[], int offset, int scanlineStride) 1224 { 1225 PlatformImage pimg = platformImage.get(); 1226 pimg.getPixels(x, y, w, h, pixelformat, 1227 buffer, offset, scanlineStride); 1228 } 1229 }; 1230 } 1231 return reader; 1232 } 1233 1234 PlatformImage getWritablePlatformImage() { 1235 PlatformImage pimg = platformImage.get(); 1236 if (!pimg.isWritable()) { 1237 pimg = pimg.promoteToWritableImage(); 1238 // assert pimg.isWritable(); 1239 platformImage.set(pimg); 1240 } 1241 return pimg; 1242 } 1243 }