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