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