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