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