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