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